@arbor-education/design-system.components 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.storybook/preview.ts +10 -0
  2. package/CHANGELOG.md +16 -0
  3. package/dist/components/datePicker/DatePicker.stories.d.ts +5 -0
  4. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  5. package/dist/components/datePicker/DatePicker.stories.js +211 -2
  6. package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
  7. package/dist/components/datePicker/DatePickerPopover.d.ts +11 -0
  8. package/dist/components/datePicker/DatePickerPopover.d.ts.map +1 -0
  9. package/dist/components/datePicker/DatePickerPopover.js +33 -0
  10. package/dist/components/datePicker/DatePickerPopover.js.map +1 -0
  11. package/dist/components/datePicker/DatePickerPopover.test.d.ts +2 -0
  12. package/dist/components/datePicker/DatePickerPopover.test.d.ts.map +1 -0
  13. package/dist/components/datePicker/DatePickerPopover.test.js +101 -0
  14. package/dist/components/datePicker/DatePickerPopover.test.js.map +1 -0
  15. package/dist/components/slideover/Slideover.d.ts +7 -2
  16. package/dist/components/slideover/Slideover.d.ts.map +1 -1
  17. package/dist/components/slideover/Slideover.js +31 -4
  18. package/dist/components/slideover/Slideover.js.map +1 -1
  19. package/dist/components/slideover/Slideover.test.js +116 -2
  20. package/dist/components/slideover/Slideover.test.js.map +1 -1
  21. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts +3 -0
  22. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts.map +1 -1
  23. package/dist/components/slideoverManager/SlideoverManager.stories.js +74 -4
  24. package/dist/components/slideoverManager/SlideoverManager.stories.js.map +1 -1
  25. package/dist/index.css +6 -1
  26. package/dist/index.css.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/utils/SlideoverUtils.d.ts +3 -1
  32. package/dist/utils/SlideoverUtils.d.ts.map +1 -1
  33. package/dist/utils/SlideoverUtils.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/datePicker/DatePicker.stories.tsx +293 -0
  36. package/src/components/datePicker/DatePickerPopover.test.tsx +144 -0
  37. package/src/components/datePicker/DatePickerPopover.tsx +72 -0
  38. package/src/components/slideover/Slideover.test.tsx +152 -2
  39. package/src/components/slideover/Slideover.tsx +56 -9
  40. package/src/components/slideover/slideover.scss +8 -1
  41. package/src/components/slideoverManager/SlideoverManager.stories.tsx +147 -3
  42. package/src/docs/Contributing.mdx +2 -0
  43. package/src/index.ts +1 -0
  44. package/src/utils/SlideoverUtils.ts +4 -1
@@ -1,5 +1,5 @@
1
- import { expect, test, describe, vi } from 'vitest';
2
- import { render, screen, fireEvent } from '@testing-library/react';
1
+ import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
2
+ import { render, screen, fireEvent, createEvent, act } from '@testing-library/react';
3
3
  import { Slideover } from './Slideover.js';
4
4
  import '@testing-library/jest-dom/vitest';
5
5
  import { SlideoverUtils } from 'Utils/SlideoverUtils';
@@ -10,7 +10,17 @@ vi.mock('Utils/SlideoverUtils', () => ({
10
10
  },
11
11
  }));
12
12
 
13
+ const fireTransitionEnd = (element: Element, propertyName: string) => {
14
+ const event = createEvent.transitionEnd(element);
15
+ Object.defineProperty(event, 'propertyName', { value: propertyName });
16
+ fireEvent(element, event);
17
+ };
18
+
13
19
  describe('Slideover', () => {
20
+ beforeEach(() => {
21
+ vi.mocked(SlideoverUtils.removeSlideover).mockClear();
22
+ });
23
+
14
24
  test('renders the title and children', () => {
15
25
  render(<Slideover title="My Test Slideover"><div>My Content</div></Slideover>);
16
26
  expect(screen.getByText('My Test Slideover')).toBeInTheDocument();
@@ -33,4 +43,144 @@ describe('Slideover', () => {
33
43
  fireEvent.click(backButton);
34
44
  expect(SlideoverUtils.removeSlideover).toHaveBeenCalled();
35
45
  });
46
+
47
+ describe('onBack', () => {
48
+ test('calls onBack instead of removeSlideover when provided', () => {
49
+ const onBack = vi.fn();
50
+ render(<Slideover onBack={onBack} />);
51
+
52
+ fireEvent.click(screen.getByRole('button', { name: /Back/i }));
53
+
54
+ expect(onBack).toHaveBeenCalledTimes(1);
55
+ expect(SlideoverUtils.removeSlideover).not.toHaveBeenCalled();
56
+ });
57
+ });
58
+
59
+ describe('backButtonText', () => {
60
+ test('labels the back button with backButtonText', () => {
61
+ render(<Slideover backButtonText="Yn ôl" />);
62
+ expect(screen.getByRole('button', { name: 'Yn ôl' })).toBeInTheDocument();
63
+ expect(screen.queryByRole('button', { name: 'Back' })).not.toBeInTheDocument();
64
+ });
65
+
66
+ test('defaults the label to Back', () => {
67
+ render(<Slideover />);
68
+ expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument();
69
+ });
70
+ });
71
+
72
+ describe('headerActions', () => {
73
+ test('renders header actions in the header', () => {
74
+ render(<Slideover title="Student" headerActions={<button type="button">Edit</button>} />);
75
+ const header = screen.getByRole('heading', { name: 'Student' }).parentElement;
76
+ expect(header).toContainElement(screen.getByRole('button', { name: 'Edit' }));
77
+ });
78
+
79
+ test('renders no header actions container when headerActions is not provided', () => {
80
+ const { container } = render(<Slideover />);
81
+ expect(container.querySelector('.ds-slideover__header-actions')).not.toBeInTheDocument();
82
+ });
83
+ });
84
+
85
+ describe('closing', () => {
86
+ test('is not marked as closing by default', () => {
87
+ const { container } = render(<Slideover />);
88
+ expect(container.querySelector('.ds-slideover')).not.toHaveClass('ds-slideover--closing');
89
+ });
90
+
91
+ test('is marked as closing when isClosing is set', () => {
92
+ const { container } = render(<Slideover isClosing />);
93
+ expect(container.querySelector('.ds-slideover')).toHaveClass('ds-slideover--closing');
94
+ });
95
+
96
+ test('calls onCloseTransitionEnd when the closing slide-out finishes', () => {
97
+ const onCloseTransitionEnd = vi.fn();
98
+ const { container } = render(<Slideover isClosing onCloseTransitionEnd={onCloseTransitionEnd} />);
99
+
100
+ fireTransitionEnd(container.querySelector('.ds-slideover')!, 'right');
101
+
102
+ expect(onCloseTransitionEnd).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ test('does not call onCloseTransitionEnd when the enter slide-in finishes', () => {
106
+ const onCloseTransitionEnd = vi.fn();
107
+ const { container } = render(<Slideover onCloseTransitionEnd={onCloseTransitionEnd} />);
108
+
109
+ fireTransitionEnd(container.querySelector('.ds-slideover')!, 'right');
110
+
111
+ expect(onCloseTransitionEnd).not.toHaveBeenCalled();
112
+ });
113
+
114
+ describe('when the slide-out transition never runs', () => {
115
+ beforeEach(() => {
116
+ vi.useFakeTimers();
117
+ });
118
+
119
+ afterEach(() => {
120
+ vi.useRealTimers();
121
+ });
122
+
123
+ test('still calls onCloseTransitionEnd once the transition time has passed', () => {
124
+ const onCloseTransitionEnd = vi.fn();
125
+ render(<Slideover isClosing onCloseTransitionEnd={onCloseTransitionEnd} />);
126
+
127
+ act(() => {
128
+ vi.advanceTimersByTime(299);
129
+ });
130
+ expect(onCloseTransitionEnd).not.toHaveBeenCalled();
131
+
132
+ act(() => {
133
+ vi.advanceTimersByTime(200);
134
+ });
135
+ expect(onCloseTransitionEnd).toHaveBeenCalledTimes(1);
136
+ });
137
+
138
+ test('calls onCloseTransitionEnd once when the transition does finish', () => {
139
+ const onCloseTransitionEnd = vi.fn();
140
+ const { container } = render(<Slideover isClosing onCloseTransitionEnd={onCloseTransitionEnd} />);
141
+
142
+ fireTransitionEnd(container.querySelector('.ds-slideover')!, 'right');
143
+ act(() => {
144
+ vi.advanceTimersByTime(1000);
145
+ });
146
+
147
+ expect(onCloseTransitionEnd).toHaveBeenCalledTimes(1);
148
+ });
149
+
150
+ test('does not call onCloseTransitionEnd if closing is cancelled first', () => {
151
+ const onCloseTransitionEnd = vi.fn();
152
+ const { rerender } = render(<Slideover isClosing onCloseTransitionEnd={onCloseTransitionEnd} />);
153
+
154
+ rerender(<Slideover onCloseTransitionEnd={onCloseTransitionEnd} />);
155
+ act(() => {
156
+ vi.advanceTimersByTime(1000);
157
+ });
158
+
159
+ expect(onCloseTransitionEnd).not.toHaveBeenCalled();
160
+ });
161
+ });
162
+
163
+ test('ignores transitions of other properties and of child elements', () => {
164
+ const onCloseTransitionEnd = vi.fn();
165
+ const { container } = render(
166
+ <Slideover isClosing onCloseTransitionEnd={onCloseTransitionEnd}>
167
+ <div data-testid="child" />
168
+ </Slideover>,
169
+ );
170
+
171
+ fireTransitionEnd(container.querySelector('.ds-slideover')!, 'opacity');
172
+ fireTransitionEnd(screen.getByTestId('child'), 'right');
173
+
174
+ expect(onCloseTransitionEnd).not.toHaveBeenCalled();
175
+ });
176
+ });
177
+
178
+ describe('accessibility attributes', () => {
179
+ test('forwards role and aria attributes to the panel', () => {
180
+ render(<Slideover title="Student" role="dialog" aria-label="Student details" aria-modal />);
181
+ const panel = screen.getByRole('dialog', { name: 'Student details' });
182
+ expect(panel).toHaveAttribute('aria-modal', 'true');
183
+ expect(panel).toHaveClass('ds-slideover');
184
+ });
185
+ });
36
186
  });
@@ -3,18 +3,26 @@ import { Button } from 'Components/button/Button';
3
3
  import { Heading } from 'Components/heading/Heading';
4
4
  import type { IconName } from 'Components/icon/allowedIcons';
5
5
  import { Icon } from 'Components/icon/Icon';
6
- import { useState, type ReactNode } from 'react';
6
+ import { useEffect, useRef, useState, type HTMLAttributes, type ReactNode, type TransitionEvent } from 'react';
7
7
  import { useComponentDidMount } from 'Utils/hooks/useComponentDidMount';
8
8
  import { SlideoverUtils } from 'Utils/SlideoverUtils';
9
9
 
10
+ // just over the 300ms transition in slideover.scss
11
+ const CLOSE_TRANSITION_FALLBACK_MS = 400;
12
+
10
13
  export type SlideoverProps = {
11
14
  title?: string;
12
15
  children?: ReactNode;
13
16
  footerContents?: ReactNode;
14
17
  headerIcon?: IconName;
18
+ headerActions?: ReactNode;
15
19
  centerHeaderText?: boolean;
16
20
  hideBackButton?: boolean;
17
- };
21
+ backButtonText?: string;
22
+ onBack?: () => void;
23
+ isClosing?: boolean;
24
+ onCloseTransitionEnd?: () => void;
25
+ } & Omit<HTMLAttributes<HTMLElement>, 'title' | 'children' | 'className' | 'onTransitionEnd'>;
18
26
 
19
27
  export const Slideover = (props: SlideoverProps) => {
20
28
  const {
@@ -22,8 +30,14 @@ export const Slideover = (props: SlideoverProps) => {
22
30
  children,
23
31
  footerContents,
24
32
  headerIcon,
33
+ headerActions,
25
34
  centerHeaderText = true,
26
35
  hideBackButton,
36
+ backButtonText = 'Back',
37
+ onBack = SlideoverUtils.removeSlideover,
38
+ isClosing = false,
39
+ onCloseTransitionEnd,
40
+ ...rest
27
41
  } = props;
28
42
 
29
43
  const [isFirstRender, setIsFirstRender] = useState(true);
@@ -32,19 +46,51 @@ export const Slideover = (props: SlideoverProps) => {
32
46
  setIsFirstRender(false);
33
47
  });
34
48
 
49
+ const hasFinishedClosingRef = useRef(false);
50
+ const onCloseTransitionEndRef = useRef(onCloseTransitionEnd);
51
+ onCloseTransitionEndRef.current = onCloseTransitionEnd;
52
+
53
+ const finishClosing = () => {
54
+ if (hasFinishedClosingRef.current) {
55
+ return;
56
+ }
57
+ hasFinishedClosingRef.current = true;
58
+ onCloseTransitionEndRef.current?.();
59
+ };
60
+
61
+ // transitionend does not fire if the slide-out never runs, so also finish on a timer
62
+ useEffect(() => {
63
+ if (!isClosing) {
64
+ hasFinishedClosingRef.current = false;
65
+ return;
66
+ }
67
+ const timeout = setTimeout(finishClosing, CLOSE_TRANSITION_FALLBACK_MS);
68
+ return () => clearTimeout(timeout);
69
+ }, [isClosing]);
70
+
71
+ const handleTransitionEnd = (event: TransitionEvent<HTMLElement>) => {
72
+ if (isClosing && event.target === event.currentTarget && event.propertyName === 'right') {
73
+ finishClosing();
74
+ }
75
+ };
76
+
35
77
  return (
36
- <aside className={classNames('ds-slideover',
37
- {
38
- 'ds-slideover--initial': isFirstRender,
39
- },
40
- )}
78
+ <aside
79
+ {...rest}
80
+ className={classNames('ds-slideover',
81
+ {
82
+ 'ds-slideover--initial': isFirstRender,
83
+ 'ds-slideover--closing': isClosing,
84
+ },
85
+ )}
86
+ onTransitionEnd={handleTransitionEnd}
41
87
  >
42
88
  <div className={classNames('ds-slideover__header', { 'ds-slideover__header--center': centerHeaderText })}>
43
89
  {
44
90
  !hideBackButton && (
45
- <Button variant="tertiary" onClick={SlideoverUtils.removeSlideover}>
91
+ <Button variant="tertiary" onClick={() => onBack()}>
46
92
  <Icon name="chevrons-left" />
47
- Back
93
+ {backButtonText}
48
94
  </Button>
49
95
  )
50
96
  }
@@ -52,6 +98,7 @@ export const Slideover = (props: SlideoverProps) => {
52
98
  {title}
53
99
  {headerIcon && <Icon name={headerIcon} />}
54
100
  </Heading>
101
+ {headerActions && <div className="ds-slideover__header-actions">{headerActions}</div>}
55
102
  </div>
56
103
 
57
104
  <div className="ds-slideover__contents">{children}</div>
@@ -9,7 +9,8 @@
9
9
  align-items: stretch;
10
10
  transition: right 300ms ease;
11
11
 
12
- &--initial {
12
+ &--initial,
13
+ &--closing {
13
14
  right: -580px;
14
15
  }
15
16
 
@@ -34,6 +35,12 @@
34
35
  }
35
36
  }
36
37
 
38
+ &__header-actions {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: var(--slideover-header-spacing-padding);
42
+ }
43
+
37
44
  &__contents {
38
45
  flex-grow: 1;
39
46
  overflow-y: scroll;
@@ -11,6 +11,10 @@ import { SlideoverUtils } from 'Utils/SlideoverUtils';
11
11
  import { Button } from 'Components/button/Button';
12
12
  import { FormField } from 'Components/formField/FormField';
13
13
  import { Section } from 'Components/section/Section';
14
+ import { Slideover } from 'Components/slideover/Slideover';
15
+ import { Icon } from 'Components/icon/Icon';
16
+ import { useState } from 'react';
17
+ import { fn } from 'storybook/test';
14
18
 
15
19
  // ---------------------------------------------------------------------------
16
20
  // Docs page content
@@ -49,8 +53,15 @@ const USAGE_GUIDANCE = [
49
53
  '| `children` | `ReactNode` | — | Main scrollable content |',
50
54
  '| `footerContents` | `ReactNode` | — | Sticky footer — use for primary actions (Save / Cancel) |',
51
55
  '| `headerIcon` | `IconName` | — | Icon rendered next to the title |',
56
+ '| `headerActions` | `ReactNode` | — | Actions rendered at the end of the header, e.g. an Edit button |',
52
57
  '| `centerHeaderText` | `boolean` | `true` | Whether the header text is centred |',
53
58
  '| `hideBackButton` | `boolean` | — | Hides the Back chevron button in the header |',
59
+ '| `backButtonText` | `string` | `\'Back\'` | Label for the Back button — pass a translated string |',
60
+ '| `onBack` | `() => void` | `SlideoverUtils.removeSlideover` | Replaces what the Back button does, e.g. to confirm unsaved changes before closing |',
61
+ '| `isClosing` | `boolean` | `false` | Plays the slide-out transition. Keep the panel mounted until `onCloseTransitionEnd` fires |',
62
+ '| `onCloseTransitionEnd` | `() => void` | — | Called once the slide-out transition started by `isClosing` has finished |',
63
+ '',
64
+ 'Any other HTML attribute (`role`, `aria-label`, `aria-modal`, `id`, …) is forwarded to the panel\'s `<aside>` element.',
54
65
  '',
55
66
  '---',
56
67
  '',
@@ -118,9 +129,22 @@ const DEVELOPER_NOTES = [
118
129
  '',
119
130
  '### Accessibility',
120
131
  '',
121
- '- The overlay dims the background and traps focus inside the active slideover panel.',
122
- '- The Back button is keyboard accessible and labelled.',
123
- '- Ensure the `title` prop is always set — it is the visible and accessible heading for the panel.',
132
+ '- The overlay dims the background. It does **not** trap focus, move focus into the panel on open, or close on Escape — consumers that need modal behaviour must provide it.',
133
+ '- The panel renders as an `<aside>`. Pass `role="dialog"`, `aria-modal` and an `aria-label` when the slideover is used as a dialog.',
134
+ '- The Back button is keyboard accessible and labelled. Pass `backButtonText` to translate it.',
135
+ '- Ensure the `title` prop is always set — it is the visible heading for the panel.',
136
+ ].join('\n');
137
+
138
+ const CUSTOM_BEHAVIOUR_NOTES = [
139
+ '### Intercepting Back',
140
+ '',
141
+ 'By default the Back button pops the top panel. Pass `onBack` to run your own logic first — for example, asking the user to confirm before unsaved changes are lost — and call `SlideoverUtils.removeSlideover()` yourself when it is safe to close.',
142
+ '',
143
+ '### Animating a panel out',
144
+ '',
145
+ 'Panels slide in on mount. To slide one out, set `isClosing` and remove it from your stack in `onCloseTransitionEnd`. This is for consumers rendering `Slideover` from their own stack; `SlideoverUtils.addSlideover` does not accept these props, because `SlideoverManager` cannot update a panel once it is added.',
146
+ '',
147
+ '`onCloseTransitionEnd` fires when the slide-out finishes, or after a short fallback delay if no transition runs (for example when the panel is hidden or motion is reduced), so a closing panel is never left mounted.',
124
148
  ].join('\n');
125
149
 
126
150
  const RELATED_COMPONENTS = [
@@ -143,6 +167,7 @@ function SlideoverManagerDocsPage() {
143
167
  <Markdown>{USAGE_GUIDANCE}</Markdown>
144
168
  <DocHeading>Developer notes</DocHeading>
145
169
  <Markdown>{DEVELOPER_NOTES}</Markdown>
170
+ <Markdown>{CUSTOM_BEHAVIOUR_NOTES}</Markdown>
146
171
  <DocHeading>Examples</DocHeading>
147
172
  <Stories title="" />
148
173
  <Markdown>{RELATED_COMPONENTS}</Markdown>
@@ -384,6 +409,89 @@ const HideBackButtonTemplate = () => (
384
409
  </PageBackground>
385
410
  );
386
411
 
412
+ const ConfirmOnBackTemplate = () => (
413
+ <PageBackground>
414
+ <SlideoverManager slideovers={[]} />
415
+ <Button
416
+ onClick={() =>
417
+ SlideoverUtils.addSlideover({
418
+ title: 'Edit Pupil Details',
419
+ onBack: () => {
420
+ if (window.confirm('You have unsaved changes. Leave without saving?')) {
421
+ SlideoverUtils.removeSlideover();
422
+ }
423
+ },
424
+ children: (
425
+ <FormField
426
+ id="pupil-name-confirm"
427
+ label="Full name"
428
+ inputType="text"
429
+ inputProps={{ defaultValue: 'Alex Johnson' }}
430
+ />
431
+ ),
432
+ })}
433
+ >
434
+ Edit pupil details
435
+ </Button>
436
+ </PageBackground>
437
+ );
438
+
439
+ const handleEditClick = fn().mockName('headerActions Edit onClick');
440
+
441
+ const HeaderActionsTemplate = () => (
442
+ <PageBackground>
443
+ <SlideoverManager slideovers={[]} />
444
+ <Button
445
+ onClick={() =>
446
+ SlideoverUtils.addSlideover({
447
+ title: 'Pupil: Alex Johnson',
448
+ headerActions: (
449
+ <Button variant="secondary" size="S" onClick={handleEditClick}>
450
+ <Icon name="pencil" />
451
+ Edit
452
+ </Button>
453
+ ),
454
+ children: <p>Year 5 · Registration group 5B</p>,
455
+ })}
456
+ >
457
+ View pupil record
458
+ </Button>
459
+ </PageBackground>
460
+ );
461
+
462
+ const ClosingTransitionTemplate = () => {
463
+ const [isOpen, setIsOpen] = useState(false);
464
+ const [isClosing, setIsClosing] = useState(false);
465
+
466
+ return (
467
+ <PageBackground>
468
+ <Button
469
+ onClick={() => {
470
+ setIsClosing(false);
471
+ setIsOpen(true);
472
+ }}
473
+ >
474
+ Open slideover
475
+ </Button>
476
+ {isOpen && (
477
+ <div className="ds-slideover-manager ds-slideover-manager--active">
478
+ <Slideover
479
+ title="Pupil Record"
480
+ onBack={() => setIsClosing(true)}
481
+ isClosing={isClosing}
482
+ onCloseTransitionEnd={() => setIsOpen(false)}
483
+ role="dialog"
484
+ aria-modal
485
+ aria-label="Pupil Record"
486
+ >
487
+ <p>Press Back to watch the panel slide out before it is removed.</p>
488
+ </Slideover>
489
+ </div>
490
+ )}
491
+ </PageBackground>
492
+ );
493
+ };
494
+
387
495
  // ---------------------------------------------------------------------------
388
496
  // Stories
389
497
  // ---------------------------------------------------------------------------
@@ -602,3 +710,39 @@ export default RemovePupilButton;
602
710
  'Without a Back button, the user must interact with the footer actions — reducing accidental dismissal.',
603
711
  ].join(' '),
604
712
  );
713
+
714
+ export const ConfirmOnBack: Story = withDescription(
715
+ {
716
+ render: ConfirmOnBackTemplate,
717
+ parameters: { controls: { disable: true } },
718
+ },
719
+ [
720
+ 'Pass `onBack` to replace what the Back button does.',
721
+ 'Here it asks for confirmation before closing, the pattern for protecting unsaved changes.',
722
+ 'Call `SlideoverUtils.removeSlideover()` yourself once it is safe to close.',
723
+ ].join(' '),
724
+ );
725
+
726
+ export const HeaderActions: Story = withDescription(
727
+ {
728
+ render: HeaderActionsTemplate,
729
+ parameters: { controls: { disable: true } },
730
+ },
731
+ [
732
+ 'Use `headerActions` to place actions such as Edit at the end of the header.',
733
+ 'Clicking Edit here logs to the Actions panel.',
734
+ 'Pass `backButtonText` to replace the Back label, for example with a translated string.',
735
+ ].join(' '),
736
+ );
737
+
738
+ export const ClosingTransition: Story = withDescription(
739
+ {
740
+ render: ClosingTransitionTemplate,
741
+ parameters: { controls: { disable: true } },
742
+ },
743
+ [
744
+ 'Set `isClosing` to slide a panel out, and unmount it in `onCloseTransitionEnd`.',
745
+ 'This example renders `Slideover` from its own state rather than through `SlideoverManager`,',
746
+ 'and forwards `role`, `aria-modal` and `aria-label` to use the panel as a dialog.',
747
+ ].join(' '),
748
+ );
@@ -225,6 +225,8 @@ Both are defined in `vitest.config.ts` as named Vitest projects (`unit` and `sto
225
225
 
226
226
  The Storybook project needs Playwright's Chromium browser installed locally the first time: `yarn playwright install chromium`. You can also run these tests interactively right here — click **Run Tests** in the bottom-left panel of this Storybook.
227
227
 
228
+ Every story also runs axe-core accessibility checks scoped to WCAG 2.2 A/AA. Every story and component should meet WCAG 2.2 standards — violations surface as warnings in the test output, so address them when you touch the affected component rather than leaving them for someone else to find.
229
+
228
230
  ---
229
231
 
230
232
  ## Quality gates
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export { Card, type CardProps } from 'Components/card/Card';
10
10
  export { Combobox } from 'Components/combobox/Combobox';
11
11
  export { DataViewCard, type DataViewCardProps } from 'Components/dataViewCard/DataViewCard';
12
12
  export { DatePicker } from 'Components/datePicker/DatePicker';
13
+ export { DatePickerPopover } from 'Components/datePicker/DatePickerPopover';
13
14
  export { DateTimePicker } from 'Components/dateTimePicker/DateTimePicker';
14
15
  export { Dot } from 'Components/dot/Dot';
15
16
  export { Dropdown } from 'Components/dropdown/Dropdown';
@@ -2,8 +2,11 @@ import type { SlideoverProps } from 'Components/slideover/Slideover';
2
2
  import { SLIDEOVER } from './Constants.js';
3
3
  import PubSub from './PubSub.js';
4
4
 
5
+ // SlideoverManager cannot update a panel once added, so closing props would do nothing
6
+ type ManagedSlideoverProps = Omit<SlideoverProps, 'isClosing' | 'onCloseTransitionEnd'>;
7
+
5
8
  export const SlideoverUtils = {
6
- addSlideover: (slideoverConfig: SlideoverProps) => PubSub.publish(SLIDEOVER.ADD_SLIDEOVER, slideoverConfig),
9
+ addSlideover: (slideoverConfig: ManagedSlideoverProps) => PubSub.publish(SLIDEOVER.ADD_SLIDEOVER, slideoverConfig),
7
10
  removeSlideover: () => PubSub.publish(SLIDEOVER.REMOVE_SLIDEOVER),
8
11
  removeAllSlideovers: () => PubSub.publish(SLIDEOVER.REMOVE_ALL_SLIDEOVERS),
9
12
  };