@arbor-education/design-system.components 4.9.2 → 5.0.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 +20 -0
- package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.js +2 -1
- package/dist/components/datePicker/DatePicker.js.map +1 -1
- package/dist/components/datePicker/DatePicker.test.js +32 -0
- package/dist/components/datePicker/DatePicker.test.js.map +1 -1
- package/dist/components/datePicker/dateInputUtils.d.ts +2 -1
- package/dist/components/datePicker/dateInputUtils.d.ts.map +1 -1
- package/dist/components/datePicker/dateInputUtils.js +20 -0
- package/dist/components/datePicker/dateInputUtils.js.map +1 -1
- package/dist/components/datePicker/dateInputUtils.test.d.ts +2 -0
- package/dist/components/datePicker/dateInputUtils.test.d.ts.map +1 -0
- package/dist/components/datePicker/dateInputUtils.test.js +28 -0
- package/dist/components/datePicker/dateInputUtils.test.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -1
- package/dist/components/dateTimePicker/DateTimePicker.js +34 -10
- package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -1
- package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts.map +1 -1
- package/dist/components/dateTimePicker/DateTimePicker.stories.js +5 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.js.map +1 -1
- package/dist/components/dateTimePicker/DateTimePicker.test.js +136 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +3 -4
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/FormField.test.js +1 -1
- package/dist/components/formField/FormField.test.js.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.d.ts +4 -1
- package/dist/components/formField/inputs/textArea/TextArea.d.ts.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.js +33 -14
- package/dist/components/formField/inputs/textArea/TextArea.js.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +6 -1
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.stories.js +33 -13
- package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.test.d.ts.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.test.js +76 -2
- package/dist/components/formField/inputs/textArea/TextArea.test.js.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.d.ts.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.js +24 -8
- package/dist/components/formField/inputs/time/TimeInput.js.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +0 -1
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.stories.js +38 -67
- package/dist/components/formField/inputs/time/TimeInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.test.js +51 -6
- package/dist/components/formField/inputs/time/TimeInput.test.js.map +1 -1
- package/dist/components/kvpList/KVPList.d.ts +2 -1
- package/dist/components/kvpList/KVPList.d.ts.map +1 -1
- package/dist/components/kvpList/KVPList.js +1 -1
- package/dist/components/kvpList/KVPList.js.map +1 -1
- package/dist/components/kvpList/KVPList.stories.d.ts +2 -1
- package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -1
- package/dist/components/kvpList/KVPList.stories.js +35 -0
- package/dist/components/kvpList/KVPList.stories.js.map +1 -1
- package/dist/components/kvpList/KVPList.test.js +16 -0
- package/dist/components/kvpList/KVPList.test.js.map +1 -1
- package/dist/components/row/Row.d.ts +1 -0
- package/dist/components/row/Row.d.ts.map +1 -1
- package/dist/components/row/Row.js +3 -2
- package/dist/components/row/Row.js.map +1 -1
- package/dist/components/row/Row.stories.d.ts +13 -0
- package/dist/components/row/Row.stories.d.ts.map +1 -1
- package/dist/components/row/Row.stories.js +53 -0
- package/dist/components/row/Row.stories.js.map +1 -1
- package/dist/components/row/Row.test.js +40 -0
- package/dist/components/row/Row.test.js.map +1 -1
- package/dist/index.css +10 -5
- package/dist/index.css.map +1 -1
- package/dist/utils/hooks/useIsControlledOnceSet.d.ts +6 -0
- package/dist/utils/hooks/useIsControlledOnceSet.d.ts.map +1 -0
- package/dist/utils/hooks/useIsControlledOnceSet.js +13 -0
- package/dist/utils/hooks/useIsControlledOnceSet.js.map +1 -0
- package/dist/utils/hooks/useIsControlledOnceSet.test.d.ts +2 -0
- package/dist/utils/hooks/useIsControlledOnceSet.test.d.ts.map +1 -0
- package/dist/utils/hooks/useIsControlledOnceSet.test.js +25 -0
- package/dist/utils/hooks/useIsControlledOnceSet.test.js.map +1 -0
- package/package.json +1 -1
- package/src/components/banner/banner.scss +4 -0
- package/src/components/datePicker/DatePicker.test.tsx +48 -0
- package/src/components/datePicker/DatePicker.tsx +2 -1
- package/src/components/datePicker/dateInputUtils.test.ts +29 -0
- package/src/components/datePicker/dateInputUtils.ts +24 -0
- package/src/components/dateTimePicker/DateTimePicker.stories.tsx +5 -0
- package/src/components/dateTimePicker/DateTimePicker.test.tsx +186 -0
- package/src/components/dateTimePicker/DateTimePicker.tsx +38 -8
- package/src/components/formField/FormField.stories.tsx +3 -4
- package/src/components/formField/FormField.test.tsx +1 -1
- package/src/components/formField/inputs/textArea/TextArea.stories.tsx +61 -13
- package/src/components/formField/inputs/textArea/TextArea.test.tsx +94 -2
- package/src/components/formField/inputs/textArea/TextArea.tsx +39 -13
- package/src/components/formField/inputs/time/TimeInput.stories.tsx +38 -87
- package/src/components/formField/inputs/time/TimeInput.test.tsx +75 -6
- package/src/components/formField/inputs/time/TimeInput.tsx +29 -10
- package/src/components/formField/inputs/time/timeInput.scss +0 -6
- package/src/components/kvpList/KVPList.stories.tsx +54 -0
- package/src/components/kvpList/KVPList.test.tsx +52 -0
- package/src/components/kvpList/KVPList.tsx +10 -1
- package/src/components/kvpList/kvpList.scss +4 -0
- package/src/components/row/Row.stories.tsx +63 -0
- package/src/components/row/Row.test.tsx +59 -0
- package/src/components/row/Row.tsx +4 -0
- package/src/components/row/row.scss +5 -2
- package/src/utils/hooks/useIsControlledOnceSet.test.ts +44 -0
- package/src/utils/hooks/useIsControlledOnceSet.ts +13 -0
|
@@ -77,6 +77,11 @@ const DEVELOPER_NOTES = [
|
|
|
77
77
|
'**`timeOptions` switches the embedded time input to combobox mode.** Instead of a free-form',
|
|
78
78
|
'time input, the user picks from a predefined list. Use for fixed appointment slots.',
|
|
79
79
|
'',
|
|
80
|
+
'**Typed times commit on blur, Enter or closing the popup.** In free-form mode the embedded time',
|
|
81
|
+
'input holds what the user types, and the date changes only once a complete, in-range time is',
|
|
82
|
+
'committed. Partial or out-of-range text never changes the date. In combobox mode, choosing an',
|
|
83
|
+
'option commits immediately.',
|
|
84
|
+
'',
|
|
80
85
|
'**`hasError` is visual-only when used standalone.** It applies the red border but does NOT',
|
|
81
86
|
'set `aria-invalid`. Always pair both when outside `<FormField>`:',
|
|
82
87
|
'',
|
|
@@ -188,6 +188,142 @@ describe('DateTimePicker', () => {
|
|
|
188
188
|
expect(lastDate.getMinutes()).toBe(30);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
test('partial text typed into the popup time field does not change the date', async () => {
|
|
192
|
+
const user = userEvent.setup();
|
|
193
|
+
const onChange = vi.fn();
|
|
194
|
+
|
|
195
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 14, 30)} onChange={onChange} />);
|
|
196
|
+
onChange.mockClear();
|
|
197
|
+
|
|
198
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
199
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
200
|
+
await user.clear(timeInput);
|
|
201
|
+
await user.type(timeInput, '14');
|
|
202
|
+
|
|
203
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('out-of-range text in the popup time field does not change the date on Enter', async () => {
|
|
207
|
+
const user = userEvent.setup();
|
|
208
|
+
const onChange = vi.fn();
|
|
209
|
+
|
|
210
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 14, 30)} onChange={onChange} />);
|
|
211
|
+
onChange.mockClear();
|
|
212
|
+
|
|
213
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
214
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
215
|
+
await user.clear(timeInput);
|
|
216
|
+
await user.type(timeInput, '25:00{Enter}');
|
|
217
|
+
|
|
218
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('typing in the popup time field does not emit until the time is committed', async () => {
|
|
222
|
+
const user = userEvent.setup();
|
|
223
|
+
const onChange = vi.fn();
|
|
224
|
+
|
|
225
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 9, 0)} onChange={onChange} />);
|
|
226
|
+
onChange.mockClear();
|
|
227
|
+
|
|
228
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
229
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
230
|
+
await user.clear(timeInput);
|
|
231
|
+
await user.type(timeInput, '14:30');
|
|
232
|
+
|
|
233
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
234
|
+
expect(timeInput).toHaveValue('14:30');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('Enter in the popup time field commits the typed time once', async () => {
|
|
238
|
+
const user = userEvent.setup();
|
|
239
|
+
const onChange = vi.fn();
|
|
240
|
+
|
|
241
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 9, 0)} onChange={onChange} />);
|
|
242
|
+
onChange.mockClear();
|
|
243
|
+
|
|
244
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
245
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
246
|
+
await user.clear(timeInput);
|
|
247
|
+
await user.type(timeInput, '14:30{Enter}');
|
|
248
|
+
|
|
249
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
250
|
+
const emitted: Date = onChange.mock.lastCall![0];
|
|
251
|
+
expect(emitted.getDate()).toBe(1);
|
|
252
|
+
expect(emitted.getHours()).toBe(14);
|
|
253
|
+
expect(emitted.getMinutes()).toBe(30);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('leaving the popup time field commits the typed time', async () => {
|
|
257
|
+
const user = userEvent.setup();
|
|
258
|
+
const onChange = vi.fn();
|
|
259
|
+
|
|
260
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 9, 0)} onChange={onChange} />);
|
|
261
|
+
onChange.mockClear();
|
|
262
|
+
|
|
263
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
264
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
265
|
+
await user.clear(timeInput);
|
|
266
|
+
await user.type(timeInput, '14:30');
|
|
267
|
+
await user.tab();
|
|
268
|
+
|
|
269
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
270
|
+
const emitted: Date = onChange.mock.lastCall![0];
|
|
271
|
+
expect(emitted.getHours()).toBe(14);
|
|
272
|
+
expect(emitted.getMinutes()).toBe(30);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('closing the popup with Escape commits the typed time', async () => {
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
const onChange = vi.fn();
|
|
278
|
+
|
|
279
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 9, 0)} onChange={onChange} />);
|
|
280
|
+
onChange.mockClear();
|
|
281
|
+
|
|
282
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
283
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
284
|
+
await user.clear(timeInput);
|
|
285
|
+
await user.type(timeInput, '14:30');
|
|
286
|
+
await user.keyboard('{Escape}');
|
|
287
|
+
|
|
288
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
289
|
+
const emitted: Date = onChange.mock.lastCall![0];
|
|
290
|
+
expect(emitted.getHours()).toBe(14);
|
|
291
|
+
expect(emitted.getMinutes()).toBe(30);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('a partial time that fails to commit is replaced with the selected time on blur', async () => {
|
|
295
|
+
const user = userEvent.setup();
|
|
296
|
+
|
|
297
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 14, 30)} />);
|
|
298
|
+
|
|
299
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
300
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
301
|
+
await user.clear(timeInput);
|
|
302
|
+
await user.type(timeInput, '14');
|
|
303
|
+
await user.tab();
|
|
304
|
+
|
|
305
|
+
expect(timeInput).toHaveValue('14:30');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('picking a day keeps the selected time when the popup time text is partial', async () => {
|
|
309
|
+
const user = userEvent.setup();
|
|
310
|
+
const onChange = vi.fn();
|
|
311
|
+
|
|
312
|
+
render(<DateTimePicker value={new Date(2024, 5, 1, 9, 15)} onChange={onChange} />);
|
|
313
|
+
onChange.mockClear();
|
|
314
|
+
|
|
315
|
+
await user.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
316
|
+
const timeInput = screen.getByLabelText('Select time', { selector: 'input' });
|
|
317
|
+
await user.clear(timeInput);
|
|
318
|
+
await user.type(timeInput, '14');
|
|
319
|
+
await user.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
320
|
+
|
|
321
|
+
const emitted: Date = onChange.mock.lastCall![0];
|
|
322
|
+
expect(emitted.getDate()).toBe(20);
|
|
323
|
+
expect(emitted.getHours()).toBe(9);
|
|
324
|
+
expect(emitted.getMinutes()).toBe(15);
|
|
325
|
+
});
|
|
326
|
+
|
|
191
327
|
test('opening the picker after typing a valid combined value syncs month, year, and time controls', async () => {
|
|
192
328
|
const user = userEvent.setup();
|
|
193
329
|
const { container } = render(<DateTimePicker />);
|
|
@@ -245,6 +381,17 @@ describe('DateTimePicker', () => {
|
|
|
245
381
|
render(<DateTimePicker defaultValue={new Date(2024, 5, 15, 14, 30)} onChange={onChange} />);
|
|
246
382
|
expect(onChange).not.toHaveBeenCalled();
|
|
247
383
|
});
|
|
384
|
+
|
|
385
|
+
test('keeps the user-entered value across re-renders', () => {
|
|
386
|
+
const { rerender } = render(
|
|
387
|
+
<DateTimePicker aria-label="Start" defaultValue={new Date(2024, 5, 15, 14, 30)} />,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
fireEvent.change(screen.getByLabelText('Start'), { target: { value: '2024-12-25T09:45' } });
|
|
391
|
+
rerender(<DateTimePicker aria-label="Start" defaultValue={new Date(2024, 5, 15, 14, 30)} />);
|
|
392
|
+
|
|
393
|
+
expect(screen.getByLabelText('Start')).toHaveValue('2024-12-25T09:45');
|
|
394
|
+
});
|
|
248
395
|
});
|
|
249
396
|
|
|
250
397
|
describe('onChange call-count correctness', () => {
|
|
@@ -289,6 +436,45 @@ describe('DateTimePicker', () => {
|
|
|
289
436
|
expect(onChange).not.toHaveBeenCalled();
|
|
290
437
|
});
|
|
291
438
|
|
|
439
|
+
test('clearing a controlled value empties the date, time and calendar selection without firing onChange', () => {
|
|
440
|
+
const onChange = vi.fn();
|
|
441
|
+
const { rerender } = render(
|
|
442
|
+
<DateTimePicker aria-label="Start" value={new Date(2026, 3, 14, 14, 27)} onChange={onChange} />,
|
|
443
|
+
);
|
|
444
|
+
fireEvent.click(screen.getByRole('button', { name: 'Open date and time picker' }));
|
|
445
|
+
const calendar = screen.getByRole('application');
|
|
446
|
+
expect(within(calendar).getByRole('gridcell', { selected: true })).toBeInTheDocument();
|
|
447
|
+
onChange.mockClear();
|
|
448
|
+
|
|
449
|
+
rerender(<DateTimePicker aria-label="Start" value={undefined} onChange={onChange} />);
|
|
450
|
+
|
|
451
|
+
expect(screen.getByLabelText('Start')).toHaveValue('');
|
|
452
|
+
expect(screen.getByLabelText('Select time', { selector: 'input' })).toHaveValue('');
|
|
453
|
+
expect(within(calendar).queryByRole('gridcell', { selected: true })).not.toBeInTheDocument();
|
|
454
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('a cleared controlled picker still accepts a new date from the user', () => {
|
|
458
|
+
const onChange = vi.fn();
|
|
459
|
+
const { rerender } = render(
|
|
460
|
+
<DateTimePicker aria-label="Start" value={new Date(2026, 3, 14, 14, 27)} onChange={onChange} />,
|
|
461
|
+
);
|
|
462
|
+
rerender(<DateTimePicker aria-label="Start" value={undefined} onChange={onChange} />);
|
|
463
|
+
|
|
464
|
+
fireEvent.change(screen.getByLabelText('Start'), { target: { value: '2026-05-01T08:15' } });
|
|
465
|
+
|
|
466
|
+
expect(screen.getByLabelText('Start')).toHaveValue('2026-05-01T08:15');
|
|
467
|
+
expect(onChange).toHaveBeenLastCalledWith(new Date(2026, 4, 1, 8, 15));
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('a controlled value that starts empty displays a value supplied later', () => {
|
|
471
|
+
const { rerender } = render(<DateTimePicker aria-label="Start" value={undefined} />);
|
|
472
|
+
|
|
473
|
+
rerender(<DateTimePicker aria-label="Start" value={new Date(2026, 3, 14, 14, 27)} />);
|
|
474
|
+
|
|
475
|
+
expect(screen.getByLabelText('Start')).toHaveValue('2026-04-14T14:27');
|
|
476
|
+
});
|
|
477
|
+
|
|
292
478
|
test('Today preserves the current time and closes the picker', async () => {
|
|
293
479
|
const user = userEvent.setup();
|
|
294
480
|
const onChange = vi.fn();
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getDateTimePlaceholder,
|
|
10
10
|
getNativeDateTimePlaceholder,
|
|
11
11
|
mergeDateAndTime,
|
|
12
|
+
parseCompleteTime,
|
|
12
13
|
parseDateTimeInputValue,
|
|
13
14
|
parseNativeDateTimeInputValue,
|
|
14
15
|
type DateDisplayFormat,
|
|
@@ -30,8 +31,10 @@ import {
|
|
|
30
31
|
useRef,
|
|
31
32
|
useState,
|
|
32
33
|
type ChangeEvent,
|
|
34
|
+
type KeyboardEvent,
|
|
33
35
|
type PointerEvent,
|
|
34
36
|
} from 'react';
|
|
37
|
+
import { useIsControlledOnceSet } from 'Utils/hooks/useIsControlledOnceSet';
|
|
35
38
|
import { PopupParentContext } from 'Utils/popupParent/PopupParentContext';
|
|
36
39
|
|
|
37
40
|
export type DateTimePickerProps = {
|
|
@@ -109,7 +112,7 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
|
109
112
|
));
|
|
110
113
|
|
|
111
114
|
const initialValue = value ?? defaultValue;
|
|
112
|
-
const isControlled = value
|
|
115
|
+
const isControlled = useIsControlledOnceSet(value);
|
|
113
116
|
const [month, setMonth] = useState(initialValue ?? new Date());
|
|
114
117
|
const [selectedDateTime, setSelectedDateTime] = useState<Date | undefined>(
|
|
115
118
|
initialValue,
|
|
@@ -189,16 +192,23 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
|
189
192
|
}
|
|
190
193
|
};
|
|
191
194
|
|
|
192
|
-
const
|
|
193
|
-
setTimeInputValue(nextTimeValue);
|
|
195
|
+
const selectedTimeText = selectedDateTime ? formatTimeInputValue(selectedDateTime, granularity) : '';
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
const commitTimeInput = (nextTimeValue: string) => {
|
|
198
|
+
const completeTime = parseCompleteTime(nextTimeValue, granularity);
|
|
199
|
+
if (completeTime === null) {
|
|
200
|
+
if (selectedDateTime) {
|
|
201
|
+
setTimeInputValue(selectedTimeText);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!selectedDateTime || completeTime === selectedTimeText) {
|
|
196
206
|
return;
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
const nextDateTime = mergeDateAndTime(
|
|
200
210
|
selectedDateTime,
|
|
201
|
-
|
|
211
|
+
completeTime,
|
|
202
212
|
granularity,
|
|
203
213
|
);
|
|
204
214
|
setSelectedDateTime(nextDateTime);
|
|
@@ -206,6 +216,19 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
|
206
216
|
onChange?.(nextDateTime);
|
|
207
217
|
};
|
|
208
218
|
|
|
219
|
+
const handleTimeChange = (nextTimeValue: string) => {
|
|
220
|
+
setTimeInputValue(nextTimeValue);
|
|
221
|
+
if (timeOptions) {
|
|
222
|
+
commitTimeInput(nextTimeValue);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const handleTimeInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
227
|
+
if (event.key === 'Enter') {
|
|
228
|
+
commitTimeInput(timeInputValue);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
209
232
|
const handleDayPickerSelect = (nextDate?: Date) => {
|
|
210
233
|
if (!nextDate) {
|
|
211
234
|
setSelectedDateTime(undefined);
|
|
@@ -217,7 +240,7 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
|
217
240
|
|
|
218
241
|
const nextDateTime = mergeDateAndTime(
|
|
219
242
|
nextDate,
|
|
220
|
-
timeInputValue,
|
|
243
|
+
parseCompleteTime(timeInputValue, granularity) ?? selectedTimeText,
|
|
221
244
|
granularity,
|
|
222
245
|
);
|
|
223
246
|
setSelectedDateTime(nextDateTime);
|
|
@@ -230,7 +253,7 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
|
230
253
|
};
|
|
231
254
|
|
|
232
255
|
const handleTodayClick = () => {
|
|
233
|
-
const nextDate = mergeDateAndTime(new Date(), timeInputValue, granularity);
|
|
256
|
+
const nextDate = mergeDateAndTime(new Date(), parseCompleteTime(timeInputValue, granularity) ?? selectedTimeText, granularity);
|
|
234
257
|
setSelectedDateTime(nextDate);
|
|
235
258
|
setMonth(nextDate);
|
|
236
259
|
setInputValue(formatInputValue(nextDate));
|
|
@@ -276,7 +299,12 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
|
276
299
|
</div>
|
|
277
300
|
<Popover.Root
|
|
278
301
|
open={isPickerOpen}
|
|
279
|
-
onOpenChange={open =>
|
|
302
|
+
onOpenChange={(open) => {
|
|
303
|
+
if (!open) {
|
|
304
|
+
commitTimeInput(timeInputValue);
|
|
305
|
+
}
|
|
306
|
+
setIsPickerOpen(open);
|
|
307
|
+
}}
|
|
280
308
|
>
|
|
281
309
|
<Popover.Trigger asChild>
|
|
282
310
|
<Button
|
|
@@ -303,6 +331,8 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
|
303
331
|
aria-label="Select time"
|
|
304
332
|
value={timeInputValue as TimeValue | ''}
|
|
305
333
|
onValueChange={handleTimeChange}
|
|
334
|
+
onBlur={() => commitTimeInput(timeInputValue)}
|
|
335
|
+
onKeyDown={handleTimeInputKeyDown}
|
|
306
336
|
granularity={granularity}
|
|
307
337
|
options={timeOptions}
|
|
308
338
|
searchType={searchType}
|
|
@@ -815,13 +815,12 @@ export default TextAreaExample;
|
|
|
815
815
|
),
|
|
816
816
|
},
|
|
817
817
|
[
|
|
818
|
-
'`inputType="textarea"` renders a `TextArea` — a multi-line input that
|
|
819
|
-
'
|
|
820
|
-
'`rows` prop in `inputProps`.',
|
|
818
|
+
'`inputType="textarea"` renders a `TextArea` — a multi-line input that sizes itself to its content',
|
|
819
|
+
'(`autoSize={true}` is the default). The `rows` prop in `inputProps` sets the minimum starting height.',
|
|
821
820
|
'',
|
|
822
821
|
'**Use for:** pastoral notes, medical conditions, free-text feedback, long descriptions.',
|
|
823
822
|
'',
|
|
824
|
-
'**Auto-sizing:** the textarea
|
|
823
|
+
'**Auto-sizing:** the textarea fits its content on mount, when the controlled `value` changes, and as the user types — it does not scroll.',
|
|
825
824
|
'To opt out, pass `autoSize: false` in `inputProps`.',
|
|
826
825
|
'',
|
|
827
826
|
'**All shared wiring still applies:** `errorText`, `fieldDescription`, and `id` work identically.',
|
|
@@ -48,7 +48,7 @@ describe('FormField component', () => {
|
|
|
48
48
|
test('renders a time input when inputType is time', () => {
|
|
49
49
|
render(<FormField id="niceid" inputType="time" inputProps={{ 'aria-label': 'Start time' }} />);
|
|
50
50
|
const input = screen.getByLabelText('Start time');
|
|
51
|
-
expect(input).
|
|
51
|
+
expect(input).toHaveClass('ds-time-input__input');
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test('renders a colour picker dropdown when inputType is colourPicker', () => {
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
Title,
|
|
10
10
|
} from '@storybook/addon-docs/blocks';
|
|
11
11
|
import { useState } from 'react';
|
|
12
|
-
import { fn } from 'storybook/test';
|
|
12
|
+
import { expect, fn, within } from 'storybook/test';
|
|
13
|
+
import { Button } from 'Components/button/Button';
|
|
13
14
|
import { FormField } from 'Components/formField/FormField';
|
|
14
15
|
import { TextArea } from './TextArea.js';
|
|
15
16
|
|
|
@@ -57,11 +58,11 @@ const DEVELOPER_NOTES = [
|
|
|
57
58
|
'### Critical gotchas',
|
|
58
59
|
'',
|
|
59
60
|
'#### 1. `autoSize` mechanics',
|
|
60
|
-
'When `autoSize={true}` (the default), the component reads `scrollHeight`
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
61
|
+
'When `autoSize={true}` (the default), the component reads `scrollHeight` and sets',
|
|
62
|
+
'`height = scrollHeight + 4px` (vertical padding compensation).',
|
|
63
|
+
'It does this on mount, whenever the controlled `value` changes, and on every `onChange` event,',
|
|
64
|
+
'so a field pre-populated through `value` or `defaultValue` opens fitted to its content.',
|
|
65
|
+
'`rows` is the minimum starting height for empty or short content.',
|
|
65
66
|
'',
|
|
66
67
|
'#### 2. CSS `resize` is NOT controlled by this component',
|
|
67
68
|
'The browser default (`resize: both` or `resize: vertical` depending on browser) applies.',
|
|
@@ -75,11 +76,9 @@ const DEVELOPER_NOTES = [
|
|
|
75
76
|
'<TextArea className="my-fixed-textarea" />',
|
|
76
77
|
'```',
|
|
77
78
|
'',
|
|
78
|
-
'#### 3.
|
|
79
|
-
'TextArea
|
|
80
|
-
'
|
|
81
|
-
'If you need imperative focus control, wrap the TextArea and use a separate DOM query,',
|
|
82
|
-
'or request `forwardRef` support as a library enhancement.',
|
|
79
|
+
'#### 3. `ref` resolves to the native `<textarea>`',
|
|
80
|
+
'TextArea is a `forwardRef` component. A `ref` you pass resolves to the underlying `HTMLTextAreaElement`,',
|
|
81
|
+
'for example to call `focus()` or `select()`. Attaching a ref does not interfere with `autoSize`.',
|
|
83
82
|
'',
|
|
84
83
|
'#### 4. `size` prop is intentionally omitted',
|
|
85
84
|
'The HTML `size` attribute is omitted from the props (the TypeScript type uses `Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "size">`).',
|
|
@@ -277,8 +276,7 @@ const meta = {
|
|
|
277
276
|
description: [
|
|
278
277
|
'Uncontrolled initial value. The textarea manages its own state after mount.',
|
|
279
278
|
'Do not combine with `value` — use one or the other.',
|
|
280
|
-
'
|
|
281
|
-
'The textarea starts at the height set by `rows` regardless of the initial value length.',
|
|
279
|
+
'With `autoSize` enabled, the textarea grows on mount to fit a long `defaultValue`.',
|
|
282
280
|
].join(' '),
|
|
283
281
|
table: {
|
|
284
282
|
type: { summary: 'string' },
|
|
@@ -642,6 +640,56 @@ export const ReadOnly: Story = withDescription(
|
|
|
642
640
|
].join(' '),
|
|
643
641
|
);
|
|
644
642
|
|
|
643
|
+
const PRE_POPULATED_NOTES = 'Pupil has a documented nut allergy (anaphylaxis risk). EpiPen is held in the school office and a second pen travels with the pupil on trips. All catering staff have been briefed. Parents have supplied an updated care plan, which is reviewed each term with the school nurse.';
|
|
644
|
+
|
|
645
|
+
export const PrePopulated: Story = withDescription(
|
|
646
|
+
{
|
|
647
|
+
render: () => (
|
|
648
|
+
<div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
649
|
+
<label htmlFor="medical-notes-prepopulated" style={{ color: 'var(--color-grey-600)' }}>
|
|
650
|
+
Medical notes
|
|
651
|
+
</label>
|
|
652
|
+
<TextArea id="medical-notes-prepopulated" rows={2} defaultValue={PRE_POPULATED_NOTES} />
|
|
653
|
+
</div>
|
|
654
|
+
),
|
|
655
|
+
play: async ({ canvasElement }) => {
|
|
656
|
+
const canvas = within(canvasElement);
|
|
657
|
+
const textArea = canvas.getByRole('textbox', { name: 'Medical notes' });
|
|
658
|
+
|
|
659
|
+
await expect(textArea.scrollHeight).toBeLessThanOrEqual(textArea.clientHeight);
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
[
|
|
663
|
+
'A field opened with stored content longer than its `rows` height.',
|
|
664
|
+
'With `autoSize` enabled (the default) the textarea fits the content on mount, so nothing is hidden behind an internal scrollbar.',
|
|
665
|
+
].join(' '),
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
const ExternallyUpdatedValueTemplate = () => {
|
|
669
|
+
const [notes, setNotes] = useState('Allergy reviewed.');
|
|
670
|
+
return (
|
|
671
|
+
<div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
672
|
+
<label htmlFor="care-plan-notes" style={{ color: 'var(--color-grey-600)' }}>
|
|
673
|
+
Care plan notes
|
|
674
|
+
</label>
|
|
675
|
+
<TextArea id="care-plan-notes" rows={2} value={notes} onChange={e => setNotes(e.currentTarget.value)} />
|
|
676
|
+
<Button variant="secondary" size="S" onClick={() => setNotes(PRE_POPULATED_NOTES)}>
|
|
677
|
+
Load care plan template
|
|
678
|
+
</Button>
|
|
679
|
+
</div>
|
|
680
|
+
);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
export const ExternallyUpdatedValue: Story = withDescription(
|
|
684
|
+
{
|
|
685
|
+
render: () => <ExternallyUpdatedValueTemplate />,
|
|
686
|
+
},
|
|
687
|
+
[
|
|
688
|
+
'A controlled textarea whose `value` is replaced from outside, here by loading a template.',
|
|
689
|
+
'With `autoSize` enabled the height refits to the new content without the user typing.',
|
|
690
|
+
].join(' '),
|
|
691
|
+
);
|
|
692
|
+
|
|
645
693
|
export const WithFormField: Story = withDescription(
|
|
646
694
|
{
|
|
647
695
|
render: () => (
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import { expect, describe, test, vi } from 'vitest';
|
|
2
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, expect, describe, test, vi } from 'vitest';
|
|
2
|
+
import { createRef } from 'react';
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
4
|
import { TextArea } from './TextArea.js';
|
|
4
5
|
import '@testing-library/jest-dom/vitest';
|
|
5
6
|
|
|
7
|
+
const SHORT_TEXT = 'Short note';
|
|
8
|
+
const LONG_TEXT = 'Pupil receives 1:1 literacy support three times per week. Extra time applies in all timed assessments.';
|
|
9
|
+
const SHORT_SCROLL_HEIGHT = 40;
|
|
10
|
+
const LONG_SCROLL_HEIGHT = 120;
|
|
11
|
+
const VERTICAL_PADDING = 4;
|
|
12
|
+
|
|
6
13
|
describe('TextArea component', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.spyOn(HTMLTextAreaElement.prototype, 'scrollHeight', 'get').mockImplementation(function (this: HTMLTextAreaElement) {
|
|
16
|
+
return this.value.length > SHORT_TEXT.length ? LONG_SCROLL_HEIGHT : SHORT_SCROLL_HEIGHT;
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
7
24
|
test('TextArea says hello', () => {
|
|
8
25
|
render(<TextArea placeholder="Hello I'm a TextArea!" />);
|
|
9
26
|
expect(screen.getByPlaceholderText("Hello I'm a TextArea!")).toBeInTheDocument();
|
|
@@ -16,4 +33,79 @@ describe('TextArea component', () => {
|
|
|
16
33
|
expect(onChange).toHaveBeenCalled();
|
|
17
34
|
expect((screen.getByPlaceholderText('Enter text') as HTMLInputElement).value).toBe('Hello');
|
|
18
35
|
});
|
|
36
|
+
|
|
37
|
+
test('fits its height to a long defaultValue on mount', () => {
|
|
38
|
+
render(<TextArea aria-label="Notes" rows={2} defaultValue={LONG_TEXT} />);
|
|
39
|
+
expect(screen.getByRole('textbox', { name: 'Notes' })).toHaveStyle({ height: `${LONG_SCROLL_HEIGHT + VERTICAL_PADDING}px` });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('fits its height to a long controlled value on mount', () => {
|
|
43
|
+
render(<TextArea aria-label="Notes" rows={2} value={LONG_TEXT} onChange={vi.fn()} />);
|
|
44
|
+
expect(screen.getByRole('textbox', { name: 'Notes' })).toHaveStyle({ height: `${LONG_SCROLL_HEIGHT + VERTICAL_PADDING}px` });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('refits its height when the controlled value changes externally', () => {
|
|
48
|
+
const { rerender } = render(<TextArea aria-label="Notes" value={SHORT_TEXT} onChange={vi.fn()} />);
|
|
49
|
+
expect(screen.getByRole('textbox', { name: 'Notes' })).toHaveStyle({ height: `${SHORT_SCROLL_HEIGHT + VERTICAL_PADDING}px` });
|
|
50
|
+
|
|
51
|
+
rerender(<TextArea aria-label="Notes" value={LONG_TEXT} onChange={vi.fn()} />);
|
|
52
|
+
expect(screen.getByRole('textbox', { name: 'Notes' })).toHaveStyle({ height: `${LONG_SCROLL_HEIGHT + VERTICAL_PADDING}px` });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('keeps its rows height on mount when autoSize is false', () => {
|
|
56
|
+
render(<TextArea aria-label="Notes" rows={2} autoSize={false} defaultValue={LONG_TEXT} />);
|
|
57
|
+
expect(screen.getByRole('textbox', { name: 'Notes' }).style.height).toBe('');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('resizes while typing and still calls onChange', () => {
|
|
61
|
+
const onChange = vi.fn();
|
|
62
|
+
render(<TextArea aria-label="Notes" onChange={onChange} />);
|
|
63
|
+
const textArea = screen.getByRole('textbox', { name: 'Notes' });
|
|
64
|
+
|
|
65
|
+
fireEvent.change(textArea, { target: { value: LONG_TEXT } });
|
|
66
|
+
|
|
67
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(textArea).toHaveStyle({ height: `${LONG_SCROLL_HEIGHT + VERTICAL_PADDING}px` });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('forwards a ref to the underlying textarea element', () => {
|
|
72
|
+
const textAreaRef = createRef<HTMLTextAreaElement>();
|
|
73
|
+
render(<TextArea aria-label="Notes" ref={textAreaRef} />);
|
|
74
|
+
expect(textAreaRef.current).toBe(screen.getByRole('textbox', { name: 'Notes' }));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('keeps its rows height when mounted while not rendered', () => {
|
|
78
|
+
vi.spyOn(HTMLTextAreaElement.prototype, 'scrollHeight', 'get').mockReturnValue(0);
|
|
79
|
+
render(<TextArea aria-label="Notes" rows={2} defaultValue={LONG_TEXT} />);
|
|
80
|
+
expect(screen.getByRole('textbox', { name: 'Notes' })).toHaveStyle({ height: 'auto' });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('refits its height once web fonts have loaded', async () => {
|
|
84
|
+
let resolveFontsReady: () => void = () => {};
|
|
85
|
+
const fontsReady = new Promise<void>((resolve) => {
|
|
86
|
+
resolveFontsReady = resolve;
|
|
87
|
+
});
|
|
88
|
+
Object.defineProperty(document, 'fonts', { value: { ready: fontsReady }, configurable: true });
|
|
89
|
+
const scrollHeightSpy = vi.spyOn(HTMLTextAreaElement.prototype, 'scrollHeight', 'get').mockReturnValue(LONG_SCROLL_HEIGHT);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
render(<TextArea aria-label="Notes" defaultValue={LONG_TEXT} />);
|
|
93
|
+
const textArea = screen.getByRole('textbox', { name: 'Notes' });
|
|
94
|
+
expect(textArea).toHaveStyle({ height: `${LONG_SCROLL_HEIGHT + VERTICAL_PADDING}px` });
|
|
95
|
+
|
|
96
|
+
scrollHeightSpy.mockReturnValue(SHORT_SCROLL_HEIGHT);
|
|
97
|
+
resolveFontsReady();
|
|
98
|
+
|
|
99
|
+
await waitFor(() => expect(textArea).toHaveStyle({ height: `${SHORT_SCROLL_HEIGHT + VERTICAL_PADDING}px` }));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
Reflect.deleteProperty(document, 'fonts');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('still autosizes when a consumer passes a ref', () => {
|
|
107
|
+
const textAreaRef = createRef<HTMLTextAreaElement>();
|
|
108
|
+
render(<TextArea aria-label="Notes" ref={textAreaRef} defaultValue={LONG_TEXT} />);
|
|
109
|
+
expect(screen.getByRole('textbox', { name: 'Notes' })).toHaveStyle({ height: `${LONG_SCROLL_HEIGHT + VERTICAL_PADDING}px` });
|
|
110
|
+
});
|
|
19
111
|
});
|
|
@@ -1,24 +1,56 @@
|
|
|
1
|
-
import { useRef } from 'react';
|
|
1
|
+
import { forwardRef, useImperativeHandle, useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import type { ChangeEvent, TextareaHTMLAttributes } from 'react';
|
|
4
|
+
import { useComponentDidMount } from 'Utils/hooks/useComponentDidMount';
|
|
4
5
|
|
|
5
6
|
export type TextAreaProps = {
|
|
6
7
|
hasError?: boolean;
|
|
7
8
|
autoSize?: boolean;
|
|
8
9
|
} & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'>;
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const VERTICAL_PADDING = 4;
|
|
12
|
+
|
|
13
|
+
const autosizeTextArea = (textArea: HTMLTextAreaElement | null) => {
|
|
14
|
+
if (!textArea) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
textArea.style.height = 'auto';
|
|
18
|
+
const contentHeight = textArea.scrollHeight;
|
|
19
|
+
// A textarea that is not rendered (e.g. inside a hidden tab) measures 0; keep its rows height instead of collapsing it
|
|
20
|
+
if (contentHeight > 0) {
|
|
21
|
+
textArea.style.height = `${contentHeight + VERTICAL_PADDING}px`;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>((props, ref) => {
|
|
11
26
|
const {
|
|
12
27
|
hasError,
|
|
13
28
|
className = '',
|
|
14
29
|
disabled = false,
|
|
15
30
|
autoSize = true,
|
|
16
31
|
onChange,
|
|
32
|
+
value,
|
|
17
33
|
...rest
|
|
18
34
|
} = props;
|
|
19
35
|
|
|
20
36
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
21
37
|
|
|
38
|
+
useImperativeHandle(ref, () => inputRef.current as HTMLTextAreaElement, []);
|
|
39
|
+
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
if (autoSize) {
|
|
42
|
+
autosizeTextArea(inputRef.current);
|
|
43
|
+
}
|
|
44
|
+
}, [autoSize, value]);
|
|
45
|
+
|
|
46
|
+
// Text measured before web fonts load wraps differently, so measure again once they are ready
|
|
47
|
+
useComponentDidMount(() => {
|
|
48
|
+
if (!autoSize || !document.fonts) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
void document.fonts.ready.then(() => autosizeTextArea(inputRef.current));
|
|
52
|
+
});
|
|
53
|
+
|
|
22
54
|
const inputClasses = classNames(
|
|
23
55
|
'ds-textarea',
|
|
24
56
|
'ds-input',
|
|
@@ -28,19 +60,10 @@ export const TextArea = (props: TextAreaProps) => {
|
|
|
28
60
|
className,
|
|
29
61
|
);
|
|
30
62
|
|
|
31
|
-
const autosizeTextArea = () => {
|
|
32
|
-
if (inputRef.current) {
|
|
33
|
-
inputRef.current.style.height = 'auto';
|
|
34
|
-
const verticalPadding = 4;
|
|
35
|
-
const newHeight = inputRef.current.scrollHeight;
|
|
36
|
-
inputRef.current.style.height = `${newHeight + verticalPadding}px`;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
63
|
const handleOnChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
41
64
|
onChange?.(e);
|
|
42
65
|
if (autoSize) {
|
|
43
|
-
autosizeTextArea();
|
|
66
|
+
autosizeTextArea(inputRef.current);
|
|
44
67
|
}
|
|
45
68
|
};
|
|
46
69
|
|
|
@@ -50,10 +73,13 @@ export const TextArea = (props: TextAreaProps) => {
|
|
|
50
73
|
className={inputClasses}
|
|
51
74
|
disabled={disabled}
|
|
52
75
|
onChange={handleOnChange}
|
|
76
|
+
value={value}
|
|
53
77
|
{...rest}
|
|
54
78
|
/>
|
|
55
79
|
);
|
|
56
|
-
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
TextArea.displayName = 'TextArea';
|
|
57
83
|
|
|
58
84
|
export namespace TextArea {
|
|
59
85
|
export type Props = TextAreaProps;
|