From 215ac5a31aeb3bfbeed88064f1d5a18126a18169 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:26:14 +0200 Subject: [PATCH 1/4] feat(print): introduce PrintingProvider + context-based usePrint #99 --- .storybook/preview.tsx | 3 + .../accordion-item-content.tsx | 2 +- src/community/components/table/table.tsx | 2 +- src/community/components/tabs/tabs/tabs.tsx | 2 +- .../components/buttons/collapse/collapse.tsx | 3 +- src/tedi/components/misc/print/printing.mdx | 119 ++++++++++++++---- src/tedi/helpers/hooks/use-print.ts | 7 ++ src/tedi/helpers/index.ts | 1 - src/tedi/index.ts | 1 + .../printing-provider/printing-provider.tsx | 38 ++++++ 10 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 src/tedi/providers/printing-provider/printing-provider.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 08ba23034..1a7d9d5d6 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -8,6 +8,7 @@ import '../src/community/styles/index.scss'; import '../node_modules/@tedi-design-system/core/tedi-storybook-styles.scss'; import '../src/community/styles/storybook.scss'; +import { PrintingProvider } from '../src/tedi/providers/printing-provider/printing-provider'; import { ThemeProvider } from '../src/tedi/providers/theme-provider/theme-provider'; import { useEffect } from 'react'; @@ -59,6 +60,7 @@ export const decorators: Preview['decorators'] = [ return ( + {context.componentId === 'components-labelprovider' ? ( ) : ( @@ -66,6 +68,7 @@ export const decorators: Preview['decorators'] = [ )} + ); }, diff --git a/src/community/components/accordion/accordion-item-content/accordion-item-content.tsx b/src/community/components/accordion/accordion-item-content/accordion-item-content.tsx index 04d4988d6..3f99dab13 100644 --- a/src/community/components/accordion/accordion-item-content/accordion-item-content.tsx +++ b/src/community/components/accordion/accordion-item-content/accordion-item-content.tsx @@ -2,7 +2,7 @@ import cn from 'classnames'; import React from 'react'; import AnimateHeight from 'react-animate-height'; -import { usePrint } from '../../../../tedi/helpers'; +import { usePrint } from '../../../../tedi/providers/printing-provider/printing-provider'; import { CardContent, CardContentProps } from '../../card'; import { AccordionContext } from '../accordion'; import styles from '../accordion.module.scss'; diff --git a/src/community/components/table/table.tsx b/src/community/components/table/table.tsx index 3e770cc82..fc14750ba 100644 --- a/src/community/components/table/table.tsx +++ b/src/community/components/table/table.tsx @@ -18,8 +18,8 @@ import { import cn from 'classnames'; import React from 'react'; -import { usePrint } from '../../../tedi/helpers'; import { useLabels } from '../../../tedi/providers/label-provider'; +import { usePrint } from '../../../tedi/providers/printing-provider/printing-provider'; import { IntentionalAny } from '../../types'; import { Card, CardContent } from '../card'; import { PlaceholderProps } from '../placeholder/placeholder'; diff --git a/src/community/components/tabs/tabs/tabs.tsx b/src/community/components/tabs/tabs/tabs.tsx index 717d6006d..2270b4b96 100644 --- a/src/community/components/tabs/tabs/tabs.tsx +++ b/src/community/components/tabs/tabs/tabs.tsx @@ -2,7 +2,7 @@ import cn from 'classnames'; import React from 'react'; import Print, { PrintProps } from '../../../../tedi/components/misc/print/print'; -import { usePrint } from '../../../../tedi/helpers'; +import { usePrint } from '../../../../tedi/providers/printing-provider/printing-provider'; import { TabsContext } from '../tabs-context'; import TabsItem, { TabsItemProps } from '../tabs-item/tabs-item'; import TabsNav from '../tabs-nav/tabs-nav'; diff --git a/src/tedi/components/buttons/collapse/collapse.tsx b/src/tedi/components/buttons/collapse/collapse.tsx index 28ae27f68..276fb205b 100644 --- a/src/tedi/components/buttons/collapse/collapse.tsx +++ b/src/tedi/components/buttons/collapse/collapse.tsx @@ -2,8 +2,9 @@ import cn from 'classnames'; import React from 'react'; import AnimateHeight from 'react-animate-height'; -import { BreakpointSupport, useBreakpointProps, usePrint } from '../../../helpers'; +import { BreakpointSupport, useBreakpointProps } from '../../../helpers'; import { useLabels } from '../../../providers/label-provider'; +import { usePrint } from '../../../providers/printing-provider/printing-provider'; import { Icon } from '../../base/icon/icon'; import { Text } from '../../base/typography/text/text'; import { Col, Row, RowProps } from '../../layout/grid'; diff --git a/src/tedi/components/misc/print/printing.mdx b/src/tedi/components/misc/print/printing.mdx index 087ff8cd0..62ee70b35 100644 --- a/src/tedi/components/misc/print/printing.mdx +++ b/src/tedi/components/misc/print/printing.mdx @@ -2,37 +2,108 @@ import { Meta, Title, Subtitle, Unstyled } from '@storybook/blocks'; -# Printing +# Printing in TEDI-Ready -## Components have built-in styles for printing +Components and layouts include built-in optimizations for print media (via `@media print` styles). This ensures clean, readable output when users trigger print preview or print to PDF. -- Text colors and some components are converted to grayscale. -- Most background colors are set to white. -- The base font size is 14px, with other font sizes and spacing adjusted accordingly. -- Some components are hidden by default, including Buttons, Anchors, Header, Footer, Breadcrumbs, and Sidenav. -- Expandable components are open by default, such as Accordion, Collapse, and Table subRow/subComponent. -- Page breaks are prevented in certain components (e.g., Placeholder, Table rows). Other page break locations should be defined in the application using the `` component. - _Note: Page breaks do not work in Firefox. See the following issues for more details:_ - [Bug 1](https://bugzilla.mozilla.org/show_bug.cgi?id=1807406), [Bug 2](https://bugzilla.mozilla.org/show_bug.cgi?id=1695475), [Bug 3](https://bugzilla.mozilla.org/show_bug.cgi?id=939897) -- Tables are printed by default, but each table should be reviewed on a case-by-case basis. If a table does not fit horizontally, it will include a scrollbar. Tables with many columns may not fit well in the print view. +## Global Print Behaviors (Applied Automatically) -**The UI can be adjusted using the following methods:** +- Text colors and many components are converted to grayscale for better ink economy and readability. +- Most background colors are forced to white (or transparent) to reduce ink usage. +- Base font size is set to **14px**, with proportional scaling for headings, spacing, and line heights. +- Interactive or non-essential elements are **hidden by default** in print: + - Buttons + - Links/Anchors (unless overridden) + - Header, Footer + - Breadcrumbs + - Sidenav / Navigation +- Expandable components are **expanded by default**: + - Accordion + - Collapse + - Table sub-rows / sub-components +- Tables are printed by default, but: + - Very wide tables may show horizontal scrollbars if content overflows. + - Review large or multi-column tables individually — consider hiding columns or switching to a simplified print layout when needed. +- Some components prevent page breaks internally (e.g. `break-inside: avoid` on table rows, Placeholder content), but most page-break decisions should be made explicitly using the tools below. -### `` component +**Firefox & Page Breaks Note** +Firefox has supported the modern `break-before`, `break-after`, and `break-inside` properties since version **65** (January 2019). Legacy `page-break-*` properties are aliased for compatibility. +Many fragmentation issues — especially with flex containers and `avoid` values — were resolved around Firefox 122 (late 2023). Complex flexbox layouts may still encounter minor pagination quirks in edge cases. +Always test in Firefox print preview. Relevant historical bugs (mostly fixed): -Preferred method. [More info here](/docs/tedi-ready-components-helpers-print--docs). +- [Meta: Flex fragmentation/print issues](https://bugzilla.mozilla.org/show_bug.cgi?id=939897) +- Related fixes: [1744363](https://bugzilla.mozilla.org/show_bug.cgi?id=1744363), [1695475](https://bugzilla.mozilla.org/show_bug.cgi?id=1695475), [1807406](https://bugzilla.mozilla.org/show_bug.cgi?id=1807406) -### Hooks +## How to Control Print Output -- `const isPrinting = usePrint()` - This hook checks if `window.matchMedia('print')` matches. It is useful for conditionally re-rendering the UI during printing. +Use these tools — in recommended order: -### Utility classes +### 1. Preferred: `` Component -**Note:** Use utility classes only when the `` component cannot be used. +Wrap content to apply print-specific visibility or page-break behavior via cloned `className`. -- `no-print` - Hides a component from the print view. -- `show-print` - Shows components in the print view that are hidden by default. -- Determines where page breaks occur during printing. Accepted values include `auto`, `avoid`, `avoid-column`, `avoid-page`, `avoid-region` - - `break-before-{value}` - Controls how page, column, or region breaks behave **before** a generated box. - - `break-after-{value}` - Controls how page, column, or region breaks behave **after** a generated box. - - `break-inside-{value}` - Controls how page, column, or region breaks behave **inside** a generated box. +```tsx +import { Print } from '@tedi-design-system/react/tedi'; + +// Hide during print + + This toolbar / interactive section disappears when printing + + +// Show only during print + + This message or simplified content is only visible in print preview / on paper + + +// Control page breaks + + + +``` + +- `visibility`: `'show'` or `'hide'` → adds `show-print` or `no-print` +- `breakBefore` / `breakAfter` / `breakInside`: `'auto'`, `'avoid'`, `'avoid-page'`, `'avoid-column'`, `'avoid-region'` +- Full props & stories → [Print component docs](/docs/tedi-ready-components-helpers-print--docs) + +### 2. Conditional Rendering: `usePrint()` Hook + `PrintingProvider` + +For completely different markup, structure, or logic during print (e.g. no charts, simplified tables, no filters). + +```tsx +import { usePrint } from '@tedi-design-system/react/tedi'; + +function ReportPage({ data }) { + const isPrinting = usePrint(); + + return isPrinting ? ( + // Lightweight, print-friendly version + + ) : ( + // Rich screen version + <> + + + + + ); +} +``` + +**Setup (already handled globally):** + +```tsx +// In your App.tsx +import { PrintingProvider } from '@tedi-design-system/react/tedi'; + + + +; +``` + +- Returns `true` during browser print preview / printing +- Consistent value across all nested components (no race conditions) +- Uses `beforeprint` / `afterprint` events (more reliable than `matchMedia('print')` alone) diff --git a/src/tedi/helpers/hooks/use-print.ts b/src/tedi/helpers/hooks/use-print.ts index e05cfee16..99696d7e0 100644 --- a/src/tedi/helpers/hooks/use-print.ts +++ b/src/tedi/helpers/hooks/use-print.ts @@ -1,6 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; +/** + * @deprecated Use the context-based version instead: import { usePrint } from './printing-provider' + * and wrap your app with . + * The standalone hook adds duplicate event listeners per usage and can cause + * inconsistent state in nested components. + * Will be removed in the next major version. + */ export const usePrint = (): boolean => { const isServerSide = typeof window === 'undefined'; const [isPrinting, setIsPrinting] = React.useState(isServerSide ? false : window.matchMedia('print').matches); diff --git a/src/tedi/helpers/index.ts b/src/tedi/helpers/index.ts index c239ce85e..78c03a0fb 100644 --- a/src/tedi/helpers/index.ts +++ b/src/tedi/helpers/index.ts @@ -1,7 +1,6 @@ export * from './hooks/use-is-mounted'; export * from './hooks/use-breakpoint'; export * from './hooks/use-breakpoint-props'; -export * from './hooks/use-print'; export * from './hooks/use-element-size'; export * from './hooks/use-scroll'; export * from './hooks/use-is-touch-device'; diff --git a/src/tedi/index.ts b/src/tedi/index.ts index 2f778ed5e..39d953124 100644 --- a/src/tedi/index.ts +++ b/src/tedi/index.ts @@ -53,3 +53,4 @@ export * from './providers/style-provider/style-provider'; export * from './providers/accessibility-provider/accessibility-provider'; export * from './providers/accessibility-provider/use-declare-loader'; export * from './providers/theme-provider/theme-provider'; +export * from './providers/printing-provider/printing-provider'; diff --git a/src/tedi/providers/printing-provider/printing-provider.tsx b/src/tedi/providers/printing-provider/printing-provider.tsx new file mode 100644 index 000000000..bd27b7868 --- /dev/null +++ b/src/tedi/providers/printing-provider/printing-provider.tsx @@ -0,0 +1,38 @@ +import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; + +interface PrintingProviderProps { + children: ReactNode; +} + +const PrintingContext = createContext(undefined); + +export const PrintingProvider = ({ children }: PrintingProviderProps) => { + const [isPrinting, setIsPrinting] = useState(false); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const handleBeforePrint = () => setIsPrinting(true); + const handleAfterPrint = () => setIsPrinting(false); + + window.addEventListener('beforeprint', handleBeforePrint); + window.addEventListener('afterprint', handleAfterPrint); + + setIsPrinting(window.matchMedia('print').matches); + + return () => { + window.removeEventListener('beforeprint', handleBeforePrint); + window.removeEventListener('afterprint', handleAfterPrint); + }; + }, []); + + return {children}; +}; + +export const usePrint = (): boolean => { + const context = useContext(PrintingContext); + if (context === undefined) { + throw new Error('usePrint must be used within a PrintingProvider'); + } + return context; +}; From aade05634b175e885f9a730a75a4275bb2e82ccf Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:40:21 +0200 Subject: [PATCH 2/4] feat(print): add test for PrintingProvider #99 --- .../printing-provider.spec.tsx | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/tedi/providers/printing-provider/printing-provider.spec.tsx diff --git a/src/tedi/providers/printing-provider/printing-provider.spec.tsx b/src/tedi/providers/printing-provider/printing-provider.spec.tsx new file mode 100644 index 000000000..21724ea4d --- /dev/null +++ b/src/tedi/providers/printing-provider/printing-provider.spec.tsx @@ -0,0 +1,152 @@ +import { act, render, screen } from '@testing-library/react'; + +import { PrintingProvider, usePrint } from './printing-provider'; + +import '@testing-library/jest-dom'; + +describe('PrintingProvider', () => { + let beforePrintHandler: EventListener | null = null; + let afterPrintHandler: EventListener | null = null; + + beforeEach(() => { + beforePrintHandler = null; + afterPrintHandler = null; + + jest + .spyOn(window, 'addEventListener') + .mockImplementation((event: string, handler: EventListenerOrEventListenerObject) => { + if (event === 'beforeprint') { + beforePrintHandler = handler as EventListener; + } + if (event === 'afterprint') { + afterPrintHandler = handler as EventListener; + } + }); + + jest + .spyOn(window, 'removeEventListener') + .mockImplementation((event: string, handler: EventListenerOrEventListenerObject) => { + if (event === 'beforeprint' && handler === beforePrintHandler) { + beforePrintHandler = null; + } + if (event === 'afterprint' && handler === afterPrintHandler) { + afterPrintHandler = null; + } + }); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const TestConsumer = () => { + const isPrinting = usePrint(); + return ( +
+

{isPrinting ? 'printing' : 'not-printing'}

+
+ ); + }; + + it('provides false by default (not printing)', () => { + render( + + + + ); + + expect(screen.getByTestId('print-status')).toHaveTextContent('not-printing'); + }); + + it('updates to true when beforeprint fires', () => { + render( + + + + ); + + expect(screen.getByTestId('print-status')).toHaveTextContent('not-printing'); + + act(() => { + beforePrintHandler?.(new Event('beforeprint')); + }); + + expect(screen.getByTestId('print-status')).toHaveTextContent('printing'); + }); + + it('reverts to false when afterprint fires', () => { + render( + + + + ); + + act(() => { + beforePrintHandler?.(new Event('beforeprint')); + }); + + expect(screen.getByTestId('print-status')).toHaveTextContent('printing'); + + act(() => { + afterPrintHandler?.(new Event('afterprint')); + }); + + expect(screen.getByTestId('print-status')).toHaveTextContent('not-printing'); + }); + + it('cleans up event listeners on unmount', () => { + const { unmount } = render( + + + + ); + + expect(window.addEventListener).toHaveBeenCalledTimes(2); + expect(window.addEventListener).toHaveBeenCalledWith('beforeprint', expect.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith('afterprint', expect.any(Function)); + + unmount(); + + expect(window.removeEventListener).toHaveBeenCalledTimes(2); + expect(window.removeEventListener).toHaveBeenCalledWith('beforeprint', expect.any(Function)); + expect(window.removeEventListener).toHaveBeenCalledWith('afterprint', expect.any(Function)); + }); + + it('keeps state consistent across nested consumers', () => { + const NestedConsumer = () => { + const isPrinting = usePrint(); + return {isPrinting ? 'yes' : 'no'}; + }; + + render( + + + + + ); + + expect(screen.getByTestId('print-status')).toHaveTextContent('not-printing'); + expect(screen.getByTestId('nested')).toHaveTextContent('no'); + + act(() => { + beforePrintHandler?.(new Event('beforeprint')); + }); + + expect(screen.getByTestId('print-status')).toHaveTextContent('printing'); + expect(screen.getByTestId('nested')).toHaveTextContent('yes'); + }); +}); From 5aa3ab41c8e236c8c5ced4cbcf834b69e07a6e51 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:36:42 +0200 Subject: [PATCH 3/4] chore: fix tests that were using usePrint #99 --- .../buttons/collapse/collapse.spec.tsx | 27 +++++++---- .../sidenav-item/sidenav-item.spec.tsx | 47 ++++++++++++------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/tedi/components/buttons/collapse/collapse.spec.tsx b/src/tedi/components/buttons/collapse/collapse.spec.tsx index 724fa8950..0be3b34f1 100644 --- a/src/tedi/components/buttons/collapse/collapse.spec.tsx +++ b/src/tedi/components/buttons/collapse/collapse.spec.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { useBreakpointProps, usePrint } from '../../../helpers'; +import { useBreakpointProps } from '../../../helpers'; +import { PrintingProvider, usePrint } from '../../../providers/printing-provider/printing-provider'; import { Heading } from '../../base/typography/heading/heading'; import Collapse, { CollapseProps } from './collapse'; @@ -8,20 +9,26 @@ import '@testing-library/jest-dom'; jest.mock('../../../helpers', () => ({ useBreakpointProps: jest.fn(), +})); + +jest.mock('../../../providers/printing-provider/printing-provider', () => ({ + PrintingProvider: ({ children }: { children: React.ReactNode }) => <>{children}, usePrint: jest.fn(), })); const getComponent = (props?: Partial) => render( - Heading} - openText="Näita rohkem" - closeText="Näita vähem" - {...props} - > - Collapse content - + + Heading} + openText="Näita rohkem" + closeText="Näita vähem" + {...props} + > + Collapse content + + ); describe('Collapse component with breakpoint support', () => { diff --git a/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.spec.tsx b/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.spec.tsx index b4084cc83..4c6c7c08c 100644 --- a/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.spec.tsx +++ b/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.spec.tsx @@ -1,8 +1,16 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { PrintingProvider, usePrint } from '../../../../../providers/printing-provider/printing-provider'; import { SideNavItem } from './sidenav-item'; +jest.mock('../../../../../providers/printing-provider/printing-provider', () => ({ + PrintingProvider: ({ children }: { children: React.ReactNode }) => <>{children}, + usePrint: jest.fn(), +})); + +const renderWithProviders = (ui: React.ReactElement) => render({ui}); + describe('SideNavItem', () => { const defaultProps = { children: 'Test Item', @@ -15,11 +23,12 @@ describe('SideNavItem', () => { beforeEach(() => { jest.clearAllMocks(); + (usePrint as jest.Mock).mockReturnValue(false); }); describe('basic item (no children)', () => { test('renders correctly', () => { - render(); + renderWithProviders(); expect(screen.getByText('Test Item')).toBeInTheDocument(); expect(screen.getByRole('menuitem')).toBeInTheDocument(); }); @@ -35,26 +44,28 @@ describe('SideNavItem', () => { const user = userEvent.setup(); const onItemClick = jest.fn(); - render(); + renderWithProviders( + + ); await user.click(screen.getByText('Test Item')); expect(onItemClick).not.toHaveBeenCalled(); }); test('applies active styles when isActive=true', () => { - render(); + renderWithProviders(); expect(screen.getByRole('menuitem').parentElement).toHaveClass('tedi-sidenav__item--current'); }); }); describe('collapsed mode (isCollapsed = true)', () => { test('uses aria-label when collapsed', () => { - render(); + renderWithProviders(); expect(screen.getByRole('menuitem')).toHaveAttribute('aria-label', 'Test Item'); }); test('renders SideNavDropdown when has children & collapsed', () => { - render(); + renderWithProviders(); expect(screen.getByText('Test Item')).toBeInTheDocument(); expect(screen.getByText(/expand_more/i)).toBeInTheDocument(); }); @@ -62,13 +73,13 @@ describe('SideNavItem', () => { describe('items with children', () => { test('opens subitems when isDefaultOpen=true', () => { - render(); + renderWithProviders(); expect(screen.getByText('Visible Child')).toBeInTheDocument(); }); test('renders subItemGroups with heading', () => { - render( + renderWithProviders( { }); test('uses clickable Link + separate Collapse when href exists (level 1)', () => { - render(); + renderWithProviders( + + ); const link = screen.getByRole('menuitem', { name: /Test Item/ }); expect(link).toHaveAttribute('href', '/dashboard'); @@ -96,7 +109,7 @@ describe('SideNavItem', () => { test('keyboard toggle works on Collapse button (Enter/Space)', async () => { const user = userEvent.setup(); - render(); + renderWithProviders(); const collapseButton = screen.getByRole('button', { name: 'sidenav.toggleSubmenuChildren', @@ -112,7 +125,7 @@ describe('SideNavItem', () => { test('keyboard toggle works with Space key on non-linked parent', async () => { const user = userEvent.setup(); - render(); + renderWithProviders(); await user.tab(); await user.keyboard(' '); @@ -123,7 +136,7 @@ describe('SideNavItem', () => { describe('level > 1 (nested)', () => { test('renders bullet instead of chevron on nested parents', () => { - render( + renderWithProviders( Nested Parent @@ -134,7 +147,7 @@ describe('SideNavItem', () => { }); test('handles accessibility attributes', () => { - render( + renderWithProviders( Active Item @@ -146,7 +159,7 @@ describe('SideNavItem', () => { }); test('handles accessibility attributes', () => { - render( + renderWithProviders( Active Item @@ -165,7 +178,7 @@ describe('SideNavItem', () => { subItems: [{ children: 'Deep Item', icon: 'deep-icon' }], }, ]; - render(); + renderWithProviders(); expect(screen.getByText('Deep Item')).toBeInTheDocument(); const nestedItem = screen.getByText('Deep Item').closest('li'); const icon = nestedItem?.querySelector('span[data-name="icon"]'); @@ -175,7 +188,7 @@ describe('SideNavItem', () => { test('does not toggle on unrelated keys', async () => { const user = userEvent.setup(); - render(); + renderWithProviders(); expect(screen.getByText('Hidden Child')).toBeInTheDocument(); await user.tab(); @@ -185,7 +198,7 @@ describe('SideNavItem', () => { }); test('sets aria attributes for linked parent with children', () => { - render(); + renderWithProviders(); const link = screen.getByRole('menuitem', { name: /test item/i }); expect(link).toHaveAttribute('aria-haspopup', 'true'); @@ -196,7 +209,7 @@ describe('SideNavItem', () => { test('updates dropdown open state when SideNavDropdown opens', async () => { const user = userEvent.setup(); - render(); + renderWithProviders(); await user.click(screen.getByText('Test Item')); }); From 6518fcdc723e11de109448299499551c1cc1aa6b Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:43:44 +0200 Subject: [PATCH 4/4] fix(print): remove old usePrint hook #99 BREAKING CHANGE: usePrint hook removed. Replace with usePrint from the new PrintingProvider context. --- src/tedi/helpers/hooks/use-print.ts | 41 ----------------------------- 1 file changed, 41 deletions(-) delete mode 100644 src/tedi/helpers/hooks/use-print.ts diff --git a/src/tedi/helpers/hooks/use-print.ts b/src/tedi/helpers/hooks/use-print.ts deleted file mode 100644 index 99696d7e0..000000000 --- a/src/tedi/helpers/hooks/use-print.ts +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -/** - * @deprecated Use the context-based version instead: import { usePrint } from './printing-provider' - * and wrap your app with . - * The standalone hook adds duplicate event listeners per usage and can cause - * inconsistent state in nested components. - * Will be removed in the next major version. - */ -export const usePrint = (): boolean => { - const isServerSide = typeof window === 'undefined'; - const [isPrinting, setIsPrinting] = React.useState(isServerSide ? false : window.matchMedia('print').matches); - - const handleBeforePrint = () => { - // https://github.com/facebook/react/issues/11876#issuecomment-352421144 - ReactDOM.flushSync(() => { - setIsPrinting(true); - }); - }; - - const handleAfterPrint = () => { - setIsPrinting(false); - }; - - React.useEffect(() => { - // we have to use beforeprint and afterprint instead of matchMedia('print') due to chrome not detecting correct page count. - // when UI changes then it might not fit on the same number of pages - window.addEventListener('beforeprint', handleBeforePrint); - window.addEventListener('afterprint', handleAfterPrint); - - return () => { - window.removeEventListener('beforeprint', handleBeforePrint); - window.removeEventListener('afterprint', handleAfterPrint); - }; - }, []); - - return isPrinting; -}; - -export default usePrint;