@arbor-education/design-system.components 0.4.2 → 0.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/CHANGELOG.md +10 -0
- package/dist/components/datePicker/DatePicker.d.ts +12 -0
- package/dist/components/datePicker/DatePicker.d.ts.map +1 -0
- package/dist/components/datePicker/DatePicker.js +60 -0
- package/dist/components/datePicker/DatePicker.js.map +1 -0
- package/dist/components/datePicker/DatePicker.stories.d.ts +31 -0
- package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -0
- package/dist/components/datePicker/DatePicker.stories.js +41 -0
- package/dist/components/datePicker/DatePicker.stories.js.map +1 -0
- package/dist/components/datePicker/DatePicker.test.d.ts +2 -0
- package/dist/components/datePicker/DatePicker.test.d.ts.map +1 -0
- package/dist/components/datePicker/DatePicker.test.js +158 -0
- package/dist/components/datePicker/DatePicker.test.js.map +1 -0
- package/dist/components/formField/FormField.d.ts +4 -0
- package/dist/components/formField/FormField.d.ts.map +1 -1
- package/dist/components/formField/FormField.js +2 -1
- package/dist/components/formField/FormField.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +2 -2
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/FormField.test.js +5 -0
- package/dist/components/formField/FormField.test.js.map +1 -1
- package/dist/components/slideover/Slideover.d.ts +1 -1
- package/dist/components/slideover/Slideover.d.ts.map +1 -1
- package/dist/components/slideover/Slideover.js +10 -2
- package/dist/components/slideover/Slideover.js.map +1 -1
- package/dist/index.css +454 -0
- 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/package.json +3 -1
- package/src/components/datePicker/DatePicker.stories.tsx +47 -0
- package/src/components/datePicker/DatePicker.test.tsx +189 -0
- package/src/components/datePicker/DatePicker.tsx +144 -0
- package/src/components/datePicker/datePicker.scss +37 -0
- package/src/components/formField/FormField.stories.tsx +7 -1
- package/src/components/formField/FormField.test.tsx +6 -0
- package/src/components/formField/FormField.tsx +5 -0
- package/src/components/slideover/Slideover.tsx +22 -3
- package/src/components/slideover/slideover.scss +5 -0
- package/src/index.scss +2 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { render, screen, within, fireEvent } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
import { format } from 'date-fns';
|
|
6
|
+
import { DatePicker } from './DatePicker';
|
|
7
|
+
|
|
8
|
+
describe('DatePicker', () => {
|
|
9
|
+
test('renders a text input', () => {
|
|
10
|
+
render(<DatePicker />);
|
|
11
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('renders a calendar toggle button', () => {
|
|
15
|
+
render(<DatePicker />);
|
|
16
|
+
// Button has an icon with screenReaderText="date" (from iconLeftName fallback in Button/Icon)
|
|
17
|
+
expect(screen.getByRole('button', { name: 'Open date picker' })).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('day picker is hidden by default', () => {
|
|
21
|
+
render(<DatePicker />);
|
|
22
|
+
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('calendar toggle', () => {
|
|
26
|
+
test('clicking the button opens the day picker', async () => {
|
|
27
|
+
render(<DatePicker />);
|
|
28
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
29
|
+
expect(screen.getByRole('application')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('clicking the button again closes the day picker', async () => {
|
|
33
|
+
render(<DatePicker />);
|
|
34
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
35
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
36
|
+
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('text input', () => {
|
|
41
|
+
// Use fireEvent.change rather than userEvent.type — the component's useEffect overwrites
|
|
42
|
+
// inputValue state between individual keystrokes, which conflicts with userEvent's tracking.
|
|
43
|
+
|
|
44
|
+
test('entering a valid date updates the input value', () => {
|
|
45
|
+
render(<DatePicker />);
|
|
46
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '15/06/2024' } });
|
|
47
|
+
expect(screen.getByRole('textbox')).toHaveValue('15/06/2024');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('entering a valid date calls onChange with the parsed Date', () => {
|
|
51
|
+
const onChange = vi.fn();
|
|
52
|
+
render(<DatePicker onChange={onChange} />);
|
|
53
|
+
onChange.mockClear(); // ignore the initial mount call with undefined
|
|
54
|
+
|
|
55
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '15/06/2024' } });
|
|
56
|
+
|
|
57
|
+
expect(onChange).toHaveBeenLastCalledWith(expect.any(Date));
|
|
58
|
+
const lastDate: Date = onChange.mock.lastCall![0];
|
|
59
|
+
expect(lastDate.getFullYear()).toBe(2024);
|
|
60
|
+
expect(lastDate.getMonth()).toBe(5); // June is month index 5
|
|
61
|
+
expect(lastDate.getDate()).toBe(15);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('entering an invalid string after a valid date calls onChange with undefined', () => {
|
|
65
|
+
// onChange is only fired by the useEffect when selectedDate actually changes.
|
|
66
|
+
// So we first need a valid date to put selectedDate into a non-undefined state.
|
|
67
|
+
const onChange = vi.fn();
|
|
68
|
+
render(<DatePicker onChange={onChange} />);
|
|
69
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '15/06/2024' } });
|
|
70
|
+
onChange.mockClear();
|
|
71
|
+
|
|
72
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'not-a-date' } });
|
|
73
|
+
|
|
74
|
+
expect(onChange).toHaveBeenLastCalledWith(undefined);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('day picker selection', () => {
|
|
79
|
+
// Avoid fake timers (incompatible with userEvent) by navigating the picker to a
|
|
80
|
+
// known month via the text input before opening the calendar.
|
|
81
|
+
|
|
82
|
+
test('selecting a date from the picker updates the input', async () => {
|
|
83
|
+
render(<DatePicker />);
|
|
84
|
+
// Navigate to June 2024 by entering a different day in that month
|
|
85
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '01/06/2024' } });
|
|
86
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
87
|
+
// Day aria-labels use date-fns PPPP format e.g. "Thursday, June 20, 2024"
|
|
88
|
+
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
89
|
+
expect(screen.getByRole('textbox')).toHaveValue('20/06/2024');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('selecting a date from the picker closes the picker', async () => {
|
|
93
|
+
render(<DatePicker />);
|
|
94
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '01/06/2024' } });
|
|
95
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
96
|
+
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
97
|
+
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('selecting a date from the picker calls onChange with the selected date', async () => {
|
|
101
|
+
const onChange = vi.fn();
|
|
102
|
+
render(<DatePicker onChange={onChange} />);
|
|
103
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '01/06/2024' } });
|
|
104
|
+
onChange.mockClear();
|
|
105
|
+
|
|
106
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
107
|
+
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
108
|
+
|
|
109
|
+
expect(onChange).toHaveBeenCalledWith(expect.any(Date));
|
|
110
|
+
const lastDate: Date = onChange.mock.lastCall![0];
|
|
111
|
+
expect(lastDate.getFullYear()).toBe(2024);
|
|
112
|
+
expect(lastDate.getMonth()).toBe(5); // June is month index 5
|
|
113
|
+
expect(lastDate.getDate()).toBe(20);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('clicking the Today button selects today\'s date', async () => {
|
|
117
|
+
render(<DatePicker />);
|
|
118
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
119
|
+
await userEvent.click(screen.getByRole('button', { name: 'Today' }));
|
|
120
|
+
expect(screen.getByRole('textbox')).toHaveValue(format(new Date(), 'dd/MM/yyyy'));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('clicking the Today button closes the picker', async () => {
|
|
124
|
+
render(<DatePicker />);
|
|
125
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
126
|
+
await userEvent.click(screen.getByRole('button', { name: 'Today' }));
|
|
127
|
+
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('controlled value prop', () => {
|
|
132
|
+
test('renders with the initial value', () => {
|
|
133
|
+
render(<DatePicker value={new Date(2024, 5, 15)} />);
|
|
134
|
+
expect(screen.getByRole('textbox')).toHaveValue('15/06/2024');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('updates the input when value prop changes', () => {
|
|
138
|
+
const { rerender } = render(<DatePicker value={new Date(2024, 5, 15)} />);
|
|
139
|
+
rerender(<DatePicker value={new Date(2024, 11, 25)} />);
|
|
140
|
+
expect(screen.getByRole('textbox')).toHaveValue('25/12/2024');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('does not call onChange when value prop changes externally', () => {
|
|
144
|
+
const onChange = vi.fn();
|
|
145
|
+
const { rerender } = render(<DatePicker value={new Date(2024, 5, 15)} onChange={onChange} />);
|
|
146
|
+
onChange.mockClear();
|
|
147
|
+
|
|
148
|
+
rerender(<DatePicker value={new Date(2024, 11, 25)} onChange={onChange} />);
|
|
149
|
+
|
|
150
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('still calls onChange when user interacts after a controlled value is set', async () => {
|
|
154
|
+
const onChange = vi.fn();
|
|
155
|
+
render(<DatePicker value={new Date(2024, 5, 15)} onChange={onChange} />);
|
|
156
|
+
onChange.mockClear();
|
|
157
|
+
|
|
158
|
+
// Navigate to June 2024 (already set via value) and pick a different day
|
|
159
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
160
|
+
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
161
|
+
|
|
162
|
+
expect(onChange).toHaveBeenCalledWith(expect.any(Date));
|
|
163
|
+
const lastDate: Date = onChange.mock.lastCall![0];
|
|
164
|
+
expect(lastDate.getDate()).toBe(20);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('custom dateFormat prop', () => {
|
|
169
|
+
test('formats the value according to the custom format', () => {
|
|
170
|
+
render(<DatePicker dateFormat="yyyy-MM-dd" />);
|
|
171
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '2024-06-15' } });
|
|
172
|
+
expect(screen.getByRole('textbox')).toHaveValue('2024-06-15');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('calls onChange with the correctly parsed date for a custom format', () => {
|
|
176
|
+
const onChange = vi.fn();
|
|
177
|
+
render(<DatePicker dateFormat="MM/dd/yyyy" onChange={onChange} />);
|
|
178
|
+
onChange.mockClear();
|
|
179
|
+
|
|
180
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '06/15/2024' } });
|
|
181
|
+
|
|
182
|
+
expect(onChange).toHaveBeenLastCalledWith(expect.any(Date));
|
|
183
|
+
const lastDate: Date = onChange.mock.lastCall![0];
|
|
184
|
+
expect(lastDate.getFullYear()).toBe(2024);
|
|
185
|
+
expect(lastDate.getMonth()).toBe(5); // June
|
|
186
|
+
expect(lastDate.getDate()).toBe(15);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { Button } from 'Components/button/Button';
|
|
3
|
+
import { TextInput } from 'Components/formField/inputs/text/TextInput';
|
|
4
|
+
import { format, isValid, parse } from 'date-fns';
|
|
5
|
+
import { Popover } from 'radix-ui';
|
|
6
|
+
import { useContext, useRef, useState, type ChangeEvent } from 'react';
|
|
7
|
+
import { DayPicker } from 'react-day-picker';
|
|
8
|
+
import { useComponentDidUpdate } from 'Utils/hooks/useComponentDidUpdate';
|
|
9
|
+
import { PopupParentContext } from 'Utils/PopupParentContext';
|
|
10
|
+
|
|
11
|
+
export type DatePickerProps = {
|
|
12
|
+
'className'?: string;
|
|
13
|
+
'dateFormat'?: string;
|
|
14
|
+
'onChange'?: (newDate?: Date) => void;
|
|
15
|
+
'id'?: string;
|
|
16
|
+
'hasError'?: boolean;
|
|
17
|
+
'aria-describedby'?: string;
|
|
18
|
+
'aria-invalid'?: boolean;
|
|
19
|
+
'value'?: Date;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const DatePicker = (props: DatePickerProps) => {
|
|
23
|
+
const {
|
|
24
|
+
className,
|
|
25
|
+
dateFormat = 'dd/MM/yyyy',
|
|
26
|
+
onChange,
|
|
27
|
+
id,
|
|
28
|
+
hasError,
|
|
29
|
+
'aria-describedby': ariaDescribedBy,
|
|
30
|
+
'aria-invalid': ariaInvalid,
|
|
31
|
+
value,
|
|
32
|
+
} = props;
|
|
33
|
+
|
|
34
|
+
const [month, setMonth] = useState(value ?? new Date());
|
|
35
|
+
|
|
36
|
+
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value);
|
|
37
|
+
|
|
38
|
+
const [inputValue, setInputValue] = useState(value ? format(value, dateFormat) : '');
|
|
39
|
+
|
|
40
|
+
const isExternalSyncRef = useRef(false);
|
|
41
|
+
|
|
42
|
+
useComponentDidUpdate(() => {
|
|
43
|
+
if (value && value.getTime() !== selectedDate?.getTime()) {
|
|
44
|
+
isExternalSyncRef.current = true;
|
|
45
|
+
setSelectedDate(value);
|
|
46
|
+
setMonth(value);
|
|
47
|
+
}
|
|
48
|
+
}, [value]);
|
|
49
|
+
|
|
50
|
+
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
|
51
|
+
|
|
52
|
+
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
53
|
+
setInputValue(e.target.value);
|
|
54
|
+
|
|
55
|
+
const parsedDate = parse(e.target.value, dateFormat, new Date());
|
|
56
|
+
|
|
57
|
+
if (isValid(parsedDate)) {
|
|
58
|
+
setSelectedDate(parsedDate);
|
|
59
|
+
setMonth(parsedDate);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
setSelectedDate(undefined);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const onDayPickerSelect = (date?: Date) => {
|
|
67
|
+
setSelectedDate(date);
|
|
68
|
+
setIsPickerOpen(false);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useComponentDidUpdate(() => {
|
|
72
|
+
if (selectedDate) {
|
|
73
|
+
setInputValue(format(selectedDate, dateFormat));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
setInputValue('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isExternalSyncRef.current && onChange) {
|
|
80
|
+
onChange(selectedDate);
|
|
81
|
+
}
|
|
82
|
+
isExternalSyncRef.current = false;
|
|
83
|
+
}, [selectedDate, dateFormat, onChange]);
|
|
84
|
+
|
|
85
|
+
const popupParentRef = useContext(PopupParentContext);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
className={classNames(
|
|
90
|
+
'ds-date-picker',
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<TextInput
|
|
95
|
+
value={inputValue}
|
|
96
|
+
onChange={onInputChange}
|
|
97
|
+
id={id}
|
|
98
|
+
hasError={hasError}
|
|
99
|
+
aria-describedby={ariaDescribedBy}
|
|
100
|
+
aria-invalid={ariaInvalid}
|
|
101
|
+
/>
|
|
102
|
+
<Popover.Root open={isPickerOpen} onOpenChange={open => setIsPickerOpen(open)}>
|
|
103
|
+
<Popover.Trigger asChild>
|
|
104
|
+
<Button
|
|
105
|
+
onClick={() => {
|
|
106
|
+
setIsPickerOpen(!isPickerOpen);
|
|
107
|
+
}}
|
|
108
|
+
className="ds-date-picker__button"
|
|
109
|
+
variant="text-link"
|
|
110
|
+
iconLeftName="date"
|
|
111
|
+
iconLeftScreenReaderText="Open date picker"
|
|
112
|
+
/>
|
|
113
|
+
</Popover.Trigger>
|
|
114
|
+
<Popover.Portal container={popupParentRef.current}>
|
|
115
|
+
<Popover.Content align="end" sideOffset={5}>
|
|
116
|
+
<DayPicker
|
|
117
|
+
className="ds-date-picker__popup"
|
|
118
|
+
month={month}
|
|
119
|
+
onMonthChange={setMonth}
|
|
120
|
+
autoFocus
|
|
121
|
+
role="application"
|
|
122
|
+
mode="single"
|
|
123
|
+
selected={selectedDate}
|
|
124
|
+
onSelect={onDayPickerSelect}
|
|
125
|
+
captionLayout="dropdown"
|
|
126
|
+
footer={(
|
|
127
|
+
<Button
|
|
128
|
+
variant="text-link"
|
|
129
|
+
onClick={() => {
|
|
130
|
+
setSelectedDate(new Date());
|
|
131
|
+
setIsPickerOpen(false);
|
|
132
|
+
}}
|
|
133
|
+
className="ds-date-picker__today-button"
|
|
134
|
+
>
|
|
135
|
+
Today
|
|
136
|
+
</Button>
|
|
137
|
+
)}
|
|
138
|
+
/>
|
|
139
|
+
</Popover.Content>
|
|
140
|
+
</Popover.Portal>
|
|
141
|
+
</Popover.Root>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.ds-date-picker {
|
|
2
|
+
position: relative;
|
|
3
|
+
|
|
4
|
+
.ds-date-picker__button {
|
|
5
|
+
position: absolute;
|
|
6
|
+
right: 0;
|
|
7
|
+
top: 0;
|
|
8
|
+
|
|
9
|
+
svg {
|
|
10
|
+
stroke: var(--form-field-icon-default-color-icon);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.ds-date-picker__popup {
|
|
16
|
+
box-shadow: var(--shadow-small);
|
|
17
|
+
padding: var(--date-picker-spacing-vertical) var(--date-picker-spacing-horizontal);
|
|
18
|
+
border-radius: var(--date-picker-radius);
|
|
19
|
+
background-color: var(--date-picker-color-background);
|
|
20
|
+
|
|
21
|
+
--rdp-accent-color: var(--date-picker-date-cell-today-default-color-text);
|
|
22
|
+
--rdp-accent-background-color: var(--date-picker-date-cell-today-default-color-background);
|
|
23
|
+
|
|
24
|
+
.rdp-selected {
|
|
25
|
+
font-size: unset;
|
|
26
|
+
|
|
27
|
+
.rdp-day_button{
|
|
28
|
+
background-color: var(--rdp-accent-background-color);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.rdp-footer {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: flex-end;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -41,7 +41,7 @@ export const Default = {
|
|
|
41
41
|
},
|
|
42
42
|
'inputType': {
|
|
43
43
|
control: 'select',
|
|
44
|
-
options: ['text', 'textarea', 'number'],
|
|
44
|
+
options: ['text', 'textarea', 'number', 'datePicker'],
|
|
45
45
|
description: 'Input type',
|
|
46
46
|
},
|
|
47
47
|
'inputProps.size': {
|
|
@@ -130,6 +130,12 @@ export const FormExample: Story = {
|
|
|
130
130
|
onSelectionChange: fn(),
|
|
131
131
|
}}
|
|
132
132
|
/>
|
|
133
|
+
<FormField
|
|
134
|
+
id="date-of-birth"
|
|
135
|
+
label="Date of Birth"
|
|
136
|
+
inputType="datePicker"
|
|
137
|
+
inputProps={{ onChange: fn() }}
|
|
138
|
+
/>
|
|
133
139
|
</div>
|
|
134
140
|
),
|
|
135
141
|
};
|
|
@@ -56,4 +56,10 @@ describe('FormField component', () => {
|
|
|
56
56
|
const input = screen.getByRole('button');
|
|
57
57
|
expect(input.textContent).toContain('Select');
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
test('renders a date picker when inputType is datePicker', () => {
|
|
61
|
+
render(<FormField id="niceid" inputType="datePicker" />);
|
|
62
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByRole('button', { name: 'Open date picker' })).toBeInTheDocument();
|
|
64
|
+
});
|
|
59
65
|
});
|
|
@@ -6,6 +6,7 @@ import { TextArea, type TextAreaProps } from './inputs/textArea/TextArea';
|
|
|
6
6
|
import { NumberInput, type NumberInputProps } from './inputs/number/NumberInput';
|
|
7
7
|
import { ColourPickerDropdown, type ColourPickerDropdownProps } from './inputs/colourPickerDropdown/ColourPickerDropdown';
|
|
8
8
|
import { SelectDropdown, type SelectDropdownInputProps } from './inputs/selectDropdown/SelectDropdown';
|
|
9
|
+
import { DatePicker, type DatePickerProps } from 'Components/datePicker/DatePicker';
|
|
9
10
|
|
|
10
11
|
type FormFieldProps = {
|
|
11
12
|
className?: string;
|
|
@@ -21,6 +22,7 @@ type FormFieldProps = {
|
|
|
21
22
|
| { inputType?: 'number'; inputProps?: NumberInputProps }
|
|
22
23
|
| { inputType?: 'colourPicker'; inputProps?: ColourPickerDropdownProps }
|
|
23
24
|
| { inputType?: 'selectDropdown'; inputProps?: SelectDropdownInputProps }
|
|
25
|
+
| { inputType?: 'datePicker'; inputProps?: DatePickerProps }
|
|
24
26
|
);
|
|
25
27
|
|
|
26
28
|
export const FormField = (props: FormFieldProps) => {
|
|
@@ -66,6 +68,9 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
66
68
|
{inputType === 'selectDropdown' && (
|
|
67
69
|
<SelectDropdown {...sharedProps} {...(inputProps as SelectDropdownInputProps)} />
|
|
68
70
|
)}
|
|
71
|
+
{inputType === 'datePicker' && (
|
|
72
|
+
<DatePicker {...sharedProps} {...(inputProps as DatePickerProps)} />
|
|
73
|
+
)}
|
|
69
74
|
{((helperLinkText && helperLinkUrl) || errorText) && (
|
|
70
75
|
<div className="ds-form-field__message">
|
|
71
76
|
{errorText && (
|
|
@@ -3,7 +3,8 @@ 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 type
|
|
6
|
+
import { useState, type ReactNode } from 'react';
|
|
7
|
+
import { useComponentDidMount } from 'Utils/hooks/useComponentDidMount';
|
|
7
8
|
import { SlideoverUtils } from 'Utils/SlideoverUtils';
|
|
8
9
|
|
|
9
10
|
export type SlideoverProps = {
|
|
@@ -16,10 +17,28 @@ export type SlideoverProps = {
|
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export const Slideover = (props: SlideoverProps) => {
|
|
19
|
-
const {
|
|
20
|
+
const {
|
|
21
|
+
title,
|
|
22
|
+
children,
|
|
23
|
+
footerContents,
|
|
24
|
+
headerIcon,
|
|
25
|
+
centerHeaderText = true,
|
|
26
|
+
hideBackButton,
|
|
27
|
+
} = props;
|
|
28
|
+
|
|
29
|
+
const [isFirstRender, setIsFirstRender] = useState(true);
|
|
30
|
+
|
|
31
|
+
useComponentDidMount(() => {
|
|
32
|
+
setIsFirstRender(false);
|
|
33
|
+
});
|
|
20
34
|
|
|
21
35
|
return (
|
|
22
|
-
<aside className=
|
|
36
|
+
<aside className={classNames('ds-slideover',
|
|
37
|
+
{
|
|
38
|
+
'ds-slideover--initial': isFirstRender,
|
|
39
|
+
},
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
23
42
|
<div className={classNames('ds-slideover__header', { 'ds-slideover__header--center': centerHeaderText })}>
|
|
24
43
|
{
|
|
25
44
|
!hideBackButton && (
|
package/src/index.scss
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@use "../node_modules/react-day-picker/src/style.css";
|
|
1
2
|
@use "./tokens.scss";
|
|
2
3
|
@use "./global.scss";
|
|
3
4
|
@use "components/button/button.scss";
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
@use "components/editableText/editableText.scss";
|
|
33
34
|
@use "components/progress/progress.scss";
|
|
34
35
|
@use "components/toast/toast.scss";
|
|
36
|
+
@use "components/datePicker/datePicker.scss";
|
|
35
37
|
@use "components/avatar/avatar.scss";
|
|
36
38
|
@use "components/userDropdown/userDropdown.scss";
|
|
37
39
|
@import "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap";
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { Banner, type BannerProps, BANNER_LEVEL } from 'Components/banner/Banner
|
|
|
30
30
|
export { EditableText } from 'Components/editableText/EditableText';
|
|
31
31
|
export { Progress } from 'Components/progress/Progress';
|
|
32
32
|
export { Toast } from 'Components/toast/Toast';
|
|
33
|
+
export { DatePicker } from 'Components/datePicker/DatePicker';
|
|
33
34
|
export { Avatar } from 'Components/avatar/Avatar';
|
|
34
35
|
export { UserDropdown } from 'Components/userDropdown/UserDropdown';
|
|
35
36
|
export type { UserDropdownUserInfoAction } from 'Components/userDropdown/UserDropdown';
|