@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.
- package/.storybook/preview.ts +10 -0
- package/CHANGELOG.md +16 -0
- package/dist/components/datePicker/DatePicker.stories.d.ts +5 -0
- package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.stories.js +211 -2
- package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
- package/dist/components/datePicker/DatePickerPopover.d.ts +11 -0
- package/dist/components/datePicker/DatePickerPopover.d.ts.map +1 -0
- package/dist/components/datePicker/DatePickerPopover.js +33 -0
- package/dist/components/datePicker/DatePickerPopover.js.map +1 -0
- package/dist/components/datePicker/DatePickerPopover.test.d.ts +2 -0
- package/dist/components/datePicker/DatePickerPopover.test.d.ts.map +1 -0
- package/dist/components/datePicker/DatePickerPopover.test.js +101 -0
- package/dist/components/datePicker/DatePickerPopover.test.js.map +1 -0
- package/dist/components/slideover/Slideover.d.ts +7 -2
- package/dist/components/slideover/Slideover.d.ts.map +1 -1
- package/dist/components/slideover/Slideover.js +31 -4
- package/dist/components/slideover/Slideover.js.map +1 -1
- package/dist/components/slideover/Slideover.test.js +116 -2
- package/dist/components/slideover/Slideover.test.js.map +1 -1
- package/dist/components/slideoverManager/SlideoverManager.stories.d.ts +3 -0
- package/dist/components/slideoverManager/SlideoverManager.stories.d.ts.map +1 -1
- package/dist/components/slideoverManager/SlideoverManager.stories.js +74 -4
- package/dist/components/slideoverManager/SlideoverManager.stories.js.map +1 -1
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/SlideoverUtils.d.ts +3 -1
- package/dist/utils/SlideoverUtils.d.ts.map +1 -1
- package/dist/utils/SlideoverUtils.js.map +1 -1
- package/package.json +1 -1
- package/src/components/datePicker/DatePicker.stories.tsx +293 -0
- package/src/components/datePicker/DatePickerPopover.test.tsx +144 -0
- package/src/components/datePicker/DatePickerPopover.tsx +72 -0
- package/src/components/slideover/Slideover.test.tsx +152 -2
- package/src/components/slideover/Slideover.tsx +56 -9
- package/src/components/slideover/slideover.scss +8 -1
- package/src/components/slideoverManager/SlideoverManager.stories.tsx +147 -3
- package/src/docs/Contributing.mdx +2 -0
- package/src/index.ts +1 -0
- 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
|
|
37
|
-
{
|
|
38
|
-
|
|
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={
|
|
91
|
+
<Button variant="tertiary" onClick={() => onBack()}>
|
|
46
92
|
<Icon name="chevrons-left" />
|
|
47
|
-
|
|
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
|
|
122
|
-
'- The
|
|
123
|
-
'-
|
|
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:
|
|
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
|
};
|