@transferwise/components 46.110.0 → 46.111.1
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/build/common/panel/Panel.js +1 -0
- package/build/common/panel/Panel.js.map +1 -1
- package/build/common/panel/Panel.mjs +1 -0
- package/build/common/panel/Panel.mjs.map +1 -1
- package/build/common/responsivePanel/ResponsivePanel.js +6 -1
- package/build/common/responsivePanel/ResponsivePanel.js.map +1 -1
- package/build/common/responsivePanel/ResponsivePanel.mjs +6 -1
- package/build/common/responsivePanel/ResponsivePanel.mjs.map +1 -1
- package/build/dateInput/DateInput.js +46 -24
- package/build/dateInput/DateInput.js.map +1 -1
- package/build/dateInput/DateInput.mjs +48 -26
- package/build/dateInput/DateInput.mjs.map +1 -1
- package/build/dateLookup/DateLookup.js +5 -2
- package/build/dateLookup/DateLookup.js.map +1 -1
- package/build/dateLookup/DateLookup.mjs +5 -2
- package/build/dateLookup/DateLookup.mjs.map +1 -1
- package/build/dateLookup/dateTrigger/DateTrigger.js +2 -0
- package/build/dateLookup/dateTrigger/DateTrigger.js.map +1 -1
- package/build/dateLookup/dateTrigger/DateTrigger.mjs +2 -0
- package/build/dateLookup/dateTrigger/DateTrigger.mjs.map +1 -1
- package/build/field/Field.js +7 -2
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.mjs +13 -8
- package/build/field/Field.mjs.map +1 -1
- package/build/inputs/InputGroup.js +1 -1
- package/build/inputs/InputGroup.js.map +1 -1
- package/build/inputs/InputGroup.mjs +2 -2
- package/build/inputs/InputGroup.mjs.map +1 -1
- package/build/inputs/SelectInput.js +19 -5
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +19 -5
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/inputs/contexts.js +8 -4
- package/build/inputs/contexts.js.map +1 -1
- package/build/inputs/contexts.mjs +7 -4
- package/build/inputs/contexts.mjs.map +1 -1
- package/build/label/Label.js +14 -8
- package/build/label/Label.js.map +1 -1
- package/build/label/Label.mjs +14 -8
- package/build/label/Label.mjs.map +1 -1
- package/build/listItem/ListItem.js.map +1 -1
- package/build/listItem/ListItem.mjs.map +1 -1
- package/build/listItem/Prompt/ListItemPrompt.js +1 -1
- package/build/listItem/Prompt/ListItemPrompt.js.map +1 -1
- package/build/listItem/Prompt/ListItemPrompt.mjs +1 -1
- package/build/listItem/Prompt/ListItemPrompt.mjs.map +1 -1
- package/build/main.css +163 -153
- package/build/moneyInput/MoneyInput.js +6 -5
- package/build/moneyInput/MoneyInput.js.map +1 -1
- package/build/moneyInput/MoneyInput.mjs +6 -5
- package/build/moneyInput/MoneyInput.mjs.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.js +25 -3
- package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.mjs +27 -5
- package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
- package/build/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.js +23 -23
- package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -0
- package/build/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.mjs +23 -23
- package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -0
- package/build/styles/inputs/Input.css +5 -0
- package/build/styles/inputs/TextArea.css +5 -0
- package/build/styles/listItem/ListItem.css +5 -153
- package/build/styles/listItem/Prompt/ListItemPrompt.css +0 -153
- package/build/styles/main.css +163 -153
- package/build/types/common/panel/Panel.d.ts +2 -0
- package/build/types/common/panel/Panel.d.ts.map +1 -1
- package/build/types/common/responsivePanel/ResponsivePanel.d.ts +1 -0
- package/build/types/common/responsivePanel/ResponsivePanel.d.ts.map +1 -1
- package/build/types/dateInput/DateInput.d.ts +2 -2
- package/build/types/dateInput/DateInput.d.ts.map +1 -1
- package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts +1 -0
- package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts.map +1 -1
- package/build/types/field/Field.d.ts.map +1 -1
- package/build/types/inputs/InputGroup.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +9 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/contexts.d.ts +6 -1
- package/build/types/inputs/contexts.d.ts.map +1 -1
- package/build/types/label/Label.d.ts +5 -15
- package/build/types/label/Label.d.ts.map +1 -1
- package/build/types/listItem/ListItem.d.ts.map +1 -1
- package/build/types/listItem/Prompt/ListItemPrompt.d.ts +1 -1
- package/build/types/listItem/Prompt/ListItemPrompt.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.d.ts +1 -1
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -0
- package/build/types/prompt/InlinePrompt/index.d.ts.map +1 -0
- package/build/types/prompt/index.d.ts +3 -0
- package/build/types/prompt/index.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/DisabledComponents.story.tsx +156 -0
- package/src/common/panel/Panel.tsx +2 -0
- package/src/common/responsivePanel/ResponsivePanel.tsx +7 -1
- package/src/dateInput/DateInput.spec.tsx +45 -7
- package/src/dateInput/DateInput.story.tsx +2 -0
- package/src/dateInput/DateInput.tsx +65 -30
- package/src/dateLookup/DateLookup.spec.tsx +16 -0
- package/src/dateLookup/DateLookup.tsx +6 -3
- package/src/dateLookup/dateTrigger/DateTrigger.tsx +3 -0
- package/src/field/Field.tsx +6 -5
- package/src/inputs/Input.css +5 -0
- package/src/inputs/InputGroup.tsx +3 -4
- package/src/inputs/SelectInput.story.tsx +30 -10
- package/src/inputs/SelectInput.tsx +36 -6
- package/src/inputs/TextArea.css +5 -0
- package/src/inputs/_common.less +5 -0
- package/src/inputs/contexts.tsx +12 -3
- package/src/label/Label.tsx +26 -20
- package/src/listItem/ListItem.css +5 -153
- package/src/listItem/ListItem.less +5 -0
- package/src/listItem/ListItem.tsx +2 -1
- package/src/listItem/Prompt/ListItemPrompt.css +0 -153
- package/src/listItem/Prompt/ListItemPrompt.less +0 -2
- package/src/listItem/Prompt/ListItemPrompt.tsx +1 -1
- package/src/main.css +163 -153
- package/src/main.less +1 -0
- package/src/moneyInput/MoneyInput.spec.tsx +16 -1
- package/src/moneyInput/MoneyInput.tsx +7 -6
- package/src/neptune-css/NeptuneCSS.story.tsx +142 -0
- package/src/phoneNumberInput/PhoneNumberInput.spec.tsx +32 -0
- package/src/phoneNumberInput/PhoneNumberInput.tsx +32 -11
- package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.spec.tsx +2 -2
- package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.tsx +4 -4
- package/src/prompt/index.ts +6 -0
- package/build/listItem/Prompt/InlinePrompt/InlinePrompt.js.map +0 -1
- package/build/listItem/Prompt/InlinePrompt/InlinePrompt.mjs.map +0 -1
- package/build/types/listItem/Prompt/InlinePrompt/InlinePrompt.d.ts.map +0 -1
- package/build/types/listItem/Prompt/InlinePrompt/index.d.ts.map +0 -1
- /package/build/styles/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.css +0 -0
- /package/build/types/{listItem/Prompt → prompt}/InlinePrompt/index.d.ts +0 -0
- /package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.css +0 -0
- /package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.less +0 -0
- /package/src/{listItem/Prompt → prompt}/InlinePrompt/index.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useIntl } from 'react-intl';
|
|
4
4
|
|
|
5
5
|
import Body from '../body';
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
Typography,
|
|
20
20
|
} from '../common';
|
|
21
21
|
import { MDY, YMD, getMonthNames, isDateValid, isMonthAndYearFormat } from '../common/dateUtils';
|
|
22
|
-
import { useInputAttributes } from '../inputs/contexts';
|
|
22
|
+
import { useFieldLabelRef, useInputAttributes } from '../inputs/contexts';
|
|
23
23
|
import messages from './DateInput.messages';
|
|
24
24
|
import { convertToLocalMidnight } from './utils';
|
|
25
25
|
|
|
@@ -31,8 +31,8 @@ export interface DateInputProps {
|
|
|
31
31
|
size?: SizeSmall | SizeMedium | SizeLarge;
|
|
32
32
|
value?: Date | string;
|
|
33
33
|
onChange: (value: string | null) => void;
|
|
34
|
-
onFocus?: React.FocusEventHandler<HTMLDivElement>;
|
|
35
|
-
onBlur?: React.FocusEventHandler<HTMLDivElement>;
|
|
34
|
+
onFocus?: React.FocusEventHandler<HTMLDivElement | HTMLFieldSetElement>;
|
|
35
|
+
onBlur?: React.FocusEventHandler<HTMLDivElement | HTMLFieldSetElement>;
|
|
36
36
|
dayLabel?: string;
|
|
37
37
|
dayAutoComplete?: string;
|
|
38
38
|
monthLabel?: string;
|
|
@@ -49,6 +49,11 @@ export interface DateInputProps {
|
|
|
49
49
|
selectProps?: Partial<SelectInputProps<number | null>>;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* To be passed to SelectInput's parentId prop for correct blur handling.
|
|
54
|
+
*/
|
|
55
|
+
const DATE_INPUT_PARENT_ID = 'dateInput';
|
|
56
|
+
|
|
52
57
|
const DateInput = ({
|
|
53
58
|
'aria-labelledby': ariaLabelledByProp,
|
|
54
59
|
'aria-label': ariaLabel,
|
|
@@ -70,6 +75,10 @@ const DateInput = ({
|
|
|
70
75
|
selectProps = {},
|
|
71
76
|
}: DateInputProps) => {
|
|
72
77
|
const inputAttributes = useInputAttributes({ nonLabelable: true });
|
|
78
|
+
const fieldLabelRef = useFieldLabelRef();
|
|
79
|
+
const dayRef = useRef<HTMLInputElement>(null);
|
|
80
|
+
const monthRef = useRef<HTMLButtonElement>(null);
|
|
81
|
+
const yearRef = useRef<HTMLInputElement>(null);
|
|
73
82
|
const id = idProp ?? inputAttributes.id;
|
|
74
83
|
const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes['aria-labelledby'];
|
|
75
84
|
|
|
@@ -116,6 +125,10 @@ const DateInput = ({
|
|
|
116
125
|
);
|
|
117
126
|
const monthNames = getMonthNames(locale, monthFormat);
|
|
118
127
|
|
|
128
|
+
const monthYearOnly = mode === DateMode.MONTH_YEAR;
|
|
129
|
+
const monthBeforeDay = MDY.has(locale);
|
|
130
|
+
const yearFirst = YMD.has(locale);
|
|
131
|
+
|
|
119
132
|
dayLabel ||= formatMessage(messages.dayLabel);
|
|
120
133
|
monthLabel ||= formatMessage(messages.monthLabel);
|
|
121
134
|
yearLabel ||= formatMessage(messages.yearLabel);
|
|
@@ -125,6 +138,29 @@ const DateInput = ({
|
|
|
125
138
|
year: placeholders?.year || formatMessage(messages.yearPlaceholder),
|
|
126
139
|
};
|
|
127
140
|
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const labelRef = fieldLabelRef?.current;
|
|
143
|
+
|
|
144
|
+
if (labelRef) {
|
|
145
|
+
const handleLabelClick = () => {
|
|
146
|
+
// Not the best way to do this, but we're forced to recreate the native Label-click behavior
|
|
147
|
+
if (monthYearOnly || monthBeforeDay) {
|
|
148
|
+
monthRef.current?.click();
|
|
149
|
+
} else if (yearFirst) {
|
|
150
|
+
yearRef.current?.focus();
|
|
151
|
+
} else {
|
|
152
|
+
dayRef.current?.focus();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
labelRef.addEventListener('click', handleLabelClick);
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
labelRef?.removeEventListener('click', handleLabelClick);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}, [fieldLabelRef, id, monthBeforeDay, monthYearOnly, yearFirst]);
|
|
163
|
+
|
|
128
164
|
const getDateAsString = (date: Date) => {
|
|
129
165
|
if (!isDateValid(date)) {
|
|
130
166
|
return '';
|
|
@@ -147,7 +183,9 @@ const DateInput = ({
|
|
|
147
183
|
<label className="d-flex flex-column">
|
|
148
184
|
<Body type={Typography.BODY_DEFAULT}>{monthLabel}</Body>
|
|
149
185
|
<SelectInput
|
|
186
|
+
triggerRef={monthRef}
|
|
150
187
|
id={`${id}:month`}
|
|
188
|
+
parentId={DATE_INPUT_PARENT_ID}
|
|
151
189
|
name="month"
|
|
152
190
|
disabled={disabled}
|
|
153
191
|
placeholder={placeholders?.month}
|
|
@@ -239,8 +277,6 @@ const DateInput = ({
|
|
|
239
277
|
}
|
|
240
278
|
};
|
|
241
279
|
|
|
242
|
-
const monthYearOnly = mode === DateMode.MONTH_YEAR;
|
|
243
|
-
|
|
244
280
|
const monthWidth = clsx({
|
|
245
281
|
'col-sm-8 tw-date--month': monthYearOnly,
|
|
246
282
|
'col-sm-5 tw-date--month': !monthYearOnly,
|
|
@@ -255,8 +291,9 @@ const DateInput = ({
|
|
|
255
291
|
<div className="col-sm-3 tw-date--day">
|
|
256
292
|
<label>
|
|
257
293
|
<Body type={Typography.BODY_DEFAULT}>{dayLabel}</Body>
|
|
258
|
-
<div className={`input-group input-group-${size}`}>
|
|
294
|
+
<div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
|
|
259
295
|
<Input
|
|
296
|
+
ref={dayRef}
|
|
260
297
|
id={`${id}:day`}
|
|
261
298
|
type="text"
|
|
262
299
|
inputMode="numeric"
|
|
@@ -282,8 +319,9 @@ const DateInput = ({
|
|
|
282
319
|
<div className="col-sm-4 tw-date--year">
|
|
283
320
|
<label>
|
|
284
321
|
<Body type={Typography.BODY_DEFAULT}>{yearLabel}</Body>
|
|
285
|
-
<div className={`input-group input-group-${size}`}>
|
|
322
|
+
<div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
|
|
286
323
|
<Input
|
|
324
|
+
ref={yearRef}
|
|
287
325
|
id={`${id}:year`}
|
|
288
326
|
type="text"
|
|
289
327
|
inputMode="numeric"
|
|
@@ -303,17 +341,16 @@ const DateInput = ({
|
|
|
303
341
|
</div>
|
|
304
342
|
);
|
|
305
343
|
};
|
|
306
|
-
const monthBeforeDay = MDY.has(locale);
|
|
307
|
-
const yearFirst = YMD.has(locale);
|
|
308
344
|
|
|
309
345
|
return (
|
|
310
|
-
<
|
|
311
|
-
className="tw-date"
|
|
312
|
-
{...inputAttributes}
|
|
346
|
+
<fieldset
|
|
313
347
|
id={id}
|
|
348
|
+
className="tw-date"
|
|
349
|
+
aria-describedby={inputAttributes['aria-describedby']}
|
|
350
|
+
aria-invalid={inputAttributes['aria-invalid']}
|
|
314
351
|
aria-labelledby={ariaLabelledBy}
|
|
315
352
|
aria-label={ariaLabel}
|
|
316
|
-
|
|
353
|
+
data-wds-dateinput=""
|
|
317
354
|
onFocus={(event) =>
|
|
318
355
|
shouldPropagateOnFocus(event) ? onFocus?.(event) : event.stopPropagation()
|
|
319
356
|
}
|
|
@@ -357,7 +394,7 @@ const DateInput = ({
|
|
|
357
394
|
);
|
|
358
395
|
})()}
|
|
359
396
|
</div>
|
|
360
|
-
</
|
|
397
|
+
</fieldset>
|
|
361
398
|
);
|
|
362
399
|
};
|
|
363
400
|
|
|
@@ -366,27 +403,25 @@ function shouldPropagateOnFocus({
|
|
|
366
403
|
target,
|
|
367
404
|
relatedTarget,
|
|
368
405
|
}: Pick<React.FocusEvent, 'target' | 'relatedTarget'>) {
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
return
|
|
406
|
+
const blurredElementParent = target.closest('[data-wds-dateinput]');
|
|
407
|
+
const focusedElementParent = relatedTarget?.closest('[data-wds-dateinput]');
|
|
408
|
+
return blurredElementParent !== focusedElementParent;
|
|
372
409
|
}
|
|
373
410
|
|
|
374
|
-
// Should only propagate if the
|
|
411
|
+
// Should only propagate if the focus-gaining element is not part
|
|
412
|
+
// of this DateInput component or the (dropdown) of the month select.
|
|
375
413
|
function shouldPropagateOnBlur({
|
|
376
414
|
target,
|
|
377
415
|
relatedTarget,
|
|
378
416
|
}: Pick<React.FocusEvent, 'target' | 'relatedTarget'>) {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
relatedTarget || (document.activeElement !== target ? document.activeElement : null);
|
|
388
|
-
const focusElementParent = focusElement && focusElement.closest('.tw-date');
|
|
389
|
-
return blurElementParent !== focusElementParent;
|
|
417
|
+
const blurredElementParent = target.closest('[data-wds-dateinput]');
|
|
418
|
+
const focusedElementParent = relatedTarget?.closest('[data-wds-dateinput]');
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
blurredElementParent !== focusedElementParent &&
|
|
422
|
+
!target?.closest(`[data-wds-parent="${DATE_INPUT_PARENT_ID}"]`) &&
|
|
423
|
+
!relatedTarget?.closest(`[data-wds-parent="${DATE_INPUT_PARENT_ID}"]`)
|
|
424
|
+
);
|
|
390
425
|
}
|
|
391
426
|
|
|
392
427
|
export default DateInput;
|
|
@@ -42,6 +42,22 @@ describe('DateLookup', () => {
|
|
|
42
42
|
expect(button).toHaveAttribute('aria-haspopup');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it('focuses trigger and opens panel when `Field` label is clicked', async () => {
|
|
46
|
+
const label = 'Date of birth';
|
|
47
|
+
|
|
48
|
+
render(
|
|
49
|
+
<Field label={label}>
|
|
50
|
+
<DateLookup value={initialValue} onChange={() => {}} />
|
|
51
|
+
</Field>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const button = screen.getByRole('button', { name: /^Date of birth/ });
|
|
55
|
+
await user.click(screen.getByLabelText(label));
|
|
56
|
+
|
|
57
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
58
|
+
expect(screen.getByRole('button', { name: /next/iu })).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
45
61
|
it.each([' ', '{Enter}', '{ArrowDown}', '{ArrowUp}', '{ArrowRight}', '{ArrowLeft}'] as const)(
|
|
46
62
|
"opens with '%s' and closes with '{Escape}'",
|
|
47
63
|
async (text: string) => {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { isWithinRange, moveToWithinRange, returnDateView } from '../common/dateUtils';
|
|
14
14
|
import ResponsivePanel from '../common/responsivePanel';
|
|
15
15
|
import { WithInputAttributesProps, withInputAttributes } from '../inputs/contexts';
|
|
16
|
-
import {
|
|
16
|
+
import { OverlayIdProvider } from '../provider/overlay/OverlayIdProvider';
|
|
17
17
|
import DateTrigger from './dateTrigger';
|
|
18
18
|
import DayCalendar from './dayCalendar';
|
|
19
19
|
import { getStartOfDay } from './getStartOfDay';
|
|
@@ -319,13 +319,15 @@ class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateL
|
|
|
319
319
|
return (
|
|
320
320
|
<div
|
|
321
321
|
ref={this.element}
|
|
322
|
-
{
|
|
323
|
-
|
|
322
|
+
aria-labelledby={id}
|
|
323
|
+
aria-invalid={inputAttributes?.['aria-invalid']}
|
|
324
|
+
aria-describedby={inputAttributes?.['aria-describedby']}
|
|
324
325
|
className="input-group"
|
|
325
326
|
onKeyDown={this.handleKeyDown}
|
|
326
327
|
>
|
|
327
328
|
<OverlayIdProvider open={open}>
|
|
328
329
|
<DateTrigger
|
|
330
|
+
id={id}
|
|
329
331
|
ariaLabelledBy={ariaLabelledBy}
|
|
330
332
|
selectedDate={selectedDate}
|
|
331
333
|
size={size}
|
|
@@ -341,6 +343,7 @@ class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateL
|
|
|
341
343
|
open={open}
|
|
342
344
|
className="tw-date-lookup-menu"
|
|
343
345
|
position={Position.BOTTOM}
|
|
346
|
+
considerHeight
|
|
344
347
|
onClose={this.discard}
|
|
345
348
|
>
|
|
346
349
|
{this.getCalendar()}
|
|
@@ -21,6 +21,7 @@ interface DateTriggerProps {
|
|
|
21
21
|
disabled: boolean;
|
|
22
22
|
onClick: () => void;
|
|
23
23
|
onClear?: () => void;
|
|
24
|
+
id?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const DateTrigger: React.FC<DateTriggerProps> = ({
|
|
@@ -28,6 +29,7 @@ const DateTrigger: React.FC<DateTriggerProps> = ({
|
|
|
28
29
|
size = Size.MEDIUM,
|
|
29
30
|
placeholder,
|
|
30
31
|
label,
|
|
32
|
+
id,
|
|
31
33
|
monthFormat,
|
|
32
34
|
disabled,
|
|
33
35
|
ariaLabelledBy,
|
|
@@ -42,6 +44,7 @@ const DateTrigger: React.FC<DateTriggerProps> = ({
|
|
|
42
44
|
return (
|
|
43
45
|
<>
|
|
44
46
|
<button
|
|
47
|
+
id={id}
|
|
45
48
|
aria-haspopup="dialog"
|
|
46
49
|
aria-expanded={overlayId != null}
|
|
47
50
|
aria-controls={overlayId}
|
package/src/field/Field.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import { useId } from 'react';
|
|
2
|
+
import { useId, useRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { Sentiment } from '../common';
|
|
5
5
|
import InlineAlert from '../inlineAlert/InlineAlert';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
FieldLabelContextProvider,
|
|
8
8
|
InputDescribedByProvider,
|
|
9
9
|
InputIdContextProvider,
|
|
10
10
|
InputInvalidProvider,
|
|
@@ -46,6 +46,7 @@ export const Field = ({
|
|
|
46
46
|
children,
|
|
47
47
|
...props
|
|
48
48
|
}: FieldProps) => {
|
|
49
|
+
const labelRef = useRef<HTMLLabelElement>(null);
|
|
49
50
|
const sentiment = props.error ? Sentiment.NEGATIVE : propType;
|
|
50
51
|
const message = propMessage || props.error;
|
|
51
52
|
const hasError = sentiment === Sentiment.NEGATIVE;
|
|
@@ -74,7 +75,7 @@ export const Field = ({
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
return (
|
|
77
|
-
<
|
|
78
|
+
<FieldLabelContextProvider value={{ id: labelId, ref: labelRef }}>
|
|
78
79
|
<InputIdContextProvider value={inputId}>
|
|
79
80
|
<InputDescribedByProvider value={ariaDescribedbyByIds()}>
|
|
80
81
|
<InputInvalidProvider value={hasError}>
|
|
@@ -92,7 +93,7 @@ export const Field = ({
|
|
|
92
93
|
>
|
|
93
94
|
{label != null ? (
|
|
94
95
|
<>
|
|
95
|
-
<Label id={labelId} htmlFor={inputId}>
|
|
96
|
+
<Label ref={labelRef} id={labelId} htmlFor={inputId}>
|
|
96
97
|
{required ? label : <Label.Optional>{label}</Label.Optional>}
|
|
97
98
|
</Label>
|
|
98
99
|
<Label.Description id={descriptionId}>{description}</Label.Description>
|
|
@@ -111,6 +112,6 @@ export const Field = ({
|
|
|
111
112
|
</InputInvalidProvider>
|
|
112
113
|
</InputDescribedByProvider>
|
|
113
114
|
</InputIdContextProvider>
|
|
114
|
-
</
|
|
115
|
+
</FieldLabelContextProvider>
|
|
115
116
|
);
|
|
116
117
|
};
|
package/src/inputs/Input.css
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
transition-duration: 300ms;
|
|
19
19
|
/* TODO: Remove these overrides once `.form-control` isn’t used anymore */
|
|
20
20
|
}
|
|
21
|
+
.disabled .np-form-control,
|
|
22
|
+
:disabled .np-form-control {
|
|
23
|
+
opacity: 1;
|
|
24
|
+
opacity: initial;
|
|
25
|
+
}
|
|
21
26
|
.np-form-control:focus-visible {
|
|
22
27
|
outline: none;
|
|
23
28
|
}
|
|
@@ -4,11 +4,10 @@ import { createContext, useContext, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
import { useResizeObserver } from '../common/hooks/useResizeObserver';
|
|
5
5
|
import { cssValueWithUnit } from '../utilities/cssValueWithUnit';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
FieldLabelContextProvider,
|
|
8
8
|
InputDescribedByProvider,
|
|
9
9
|
InputIdContextProvider,
|
|
10
10
|
InputInvalidProvider,
|
|
11
|
-
useInputAttributes,
|
|
12
11
|
} from './contexts';
|
|
13
12
|
|
|
14
13
|
type InputPaddingContextType = [
|
|
@@ -129,7 +128,7 @@ function InputAddon({
|
|
|
129
128
|
|
|
130
129
|
return (
|
|
131
130
|
/* Prevent nested controls from being labeled redundantly */
|
|
132
|
-
<
|
|
131
|
+
<FieldLabelContextProvider value={undefined}>
|
|
133
132
|
<InputIdContextProvider value={undefined}>
|
|
134
133
|
<InputDescribedByProvider value={undefined}>
|
|
135
134
|
<InputInvalidProvider value={undefined}>
|
|
@@ -153,6 +152,6 @@ function InputAddon({
|
|
|
153
152
|
</InputInvalidProvider>
|
|
154
153
|
</InputDescribedByProvider>
|
|
155
154
|
</InputIdContextProvider>
|
|
156
|
-
</
|
|
155
|
+
</FieldLabelContextProvider>
|
|
157
156
|
);
|
|
158
157
|
}
|
|
@@ -22,7 +22,19 @@ import {
|
|
|
22
22
|
const meta = {
|
|
23
23
|
title: 'Forms/SelectInput',
|
|
24
24
|
component: SelectInput,
|
|
25
|
-
|
|
25
|
+
args: {
|
|
26
|
+
onFilterChange: fn() satisfies Mock,
|
|
27
|
+
onChange: fn() satisfies Mock,
|
|
28
|
+
onClose: fn() satisfies Mock,
|
|
29
|
+
onOpen: fn() satisfies Mock,
|
|
30
|
+
},
|
|
31
|
+
argTypes: {
|
|
32
|
+
parentId: {
|
|
33
|
+
table: {
|
|
34
|
+
category: 'WDS internal',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
26
38
|
parameters: { actions: { argTypesRegex: '' } },
|
|
27
39
|
} satisfies Meta<typeof SelectInput>;
|
|
28
40
|
export default meta;
|
|
@@ -47,10 +59,6 @@ export const Months: Story<Month | null> = {
|
|
|
47
59
|
value: month,
|
|
48
60
|
})),
|
|
49
61
|
renderValue: (month) => <SelectInputOptionContent title={month.name} />,
|
|
50
|
-
onFilterChange: fn() satisfies Mock,
|
|
51
|
-
onChange: fn() satisfies Mock,
|
|
52
|
-
onClose: fn() satisfies Mock,
|
|
53
|
-
onClear: fn() satisfies Mock,
|
|
54
62
|
},
|
|
55
63
|
render: function Render({ onChange, onClear, ...args }) {
|
|
56
64
|
const [selectedMonth, setSelectedMonth] = useState<Month | null>(null);
|
|
@@ -183,7 +191,6 @@ const CurrenciesArgs = {
|
|
|
183
191
|
filterable: true,
|
|
184
192
|
filterPlaceholder: 'Type a currency / country',
|
|
185
193
|
size: 'lg',
|
|
186
|
-
onChange: fn() satisfies Mock,
|
|
187
194
|
} satisfies Story<Currency>['args'];
|
|
188
195
|
|
|
189
196
|
export const Currencies: Story<Currency> = {
|
|
@@ -265,7 +272,7 @@ export const MultipleCurrencies: Story<Currency, true> = {
|
|
|
265
272
|
/>
|
|
266
273
|
),
|
|
267
274
|
},
|
|
268
|
-
play: async ({ canvasElement, step }) => {
|
|
275
|
+
play: async ({ canvasElement, step, args }) => {
|
|
269
276
|
const canvas = within(canvasElement);
|
|
270
277
|
|
|
271
278
|
await step('Open the combobox', async () => {
|
|
@@ -273,6 +280,7 @@ export const MultipleCurrencies: Story<Currency, true> = {
|
|
|
273
280
|
await userEvent.click(triggerButton);
|
|
274
281
|
await wait(500);
|
|
275
282
|
await userEvent.unhover(triggerButton);
|
|
283
|
+
await expect(args.onOpen).toHaveBeenCalledOnce();
|
|
276
284
|
});
|
|
277
285
|
|
|
278
286
|
await step('Select EUR option', async () => {
|
|
@@ -349,6 +357,19 @@ export const WithSelectAll: Story<Currency, true> = {
|
|
|
349
357
|
},
|
|
350
358
|
};
|
|
351
359
|
|
|
360
|
+
export const WithClear: Story<Currency> = {
|
|
361
|
+
args: {
|
|
362
|
+
...CurrenciesArgs,
|
|
363
|
+
onClear: fn() satisfies Mock,
|
|
364
|
+
},
|
|
365
|
+
play: async ({ step }) => {
|
|
366
|
+
await step('Has clear button', async () => {
|
|
367
|
+
const clearBtn = await screen.findByRole('button', { name: 'Clear' });
|
|
368
|
+
await expect(clearBtn).toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
352
373
|
export const CustomTrigger: Story<Month> = {
|
|
353
374
|
args: {
|
|
354
375
|
placeholder: 'Month',
|
|
@@ -367,14 +388,14 @@ export const CustomTrigger: Story<Month> = {
|
|
|
367
388
|
<ChevronDown size={16} />
|
|
368
389
|
</SelectInputTriggerButton>
|
|
369
390
|
),
|
|
370
|
-
onChange: fn() satisfies Mock,
|
|
371
391
|
},
|
|
372
|
-
play: async ({ canvasElement, step }) => {
|
|
392
|
+
play: async ({ canvasElement, step, args }) => {
|
|
373
393
|
const canvas = within(canvasElement);
|
|
374
394
|
|
|
375
395
|
await step('Open the combobox', async () => {
|
|
376
396
|
const triggerButton = canvas.getByRole('combobox');
|
|
377
397
|
await userEvent.click(triggerButton);
|
|
398
|
+
await expect(args.onOpen).toHaveBeenCalledOnce();
|
|
378
399
|
});
|
|
379
400
|
},
|
|
380
401
|
};
|
|
@@ -414,7 +435,6 @@ export const Advanced: Story<Month> = {
|
|
|
414
435
|
),
|
|
415
436
|
filterable: true,
|
|
416
437
|
filterPlaceholder: 'Type a month’s name',
|
|
417
|
-
onChange: fn() satisfies Mock,
|
|
418
438
|
},
|
|
419
439
|
play: async ({ canvasElement, step }) => {
|
|
420
440
|
const canvas = within(canvasElement);
|
|
@@ -146,6 +146,11 @@ function filterSelectInputItems<T>(
|
|
|
146
146
|
|
|
147
147
|
export interface SelectInputProps<T = string, M extends boolean = false> {
|
|
148
148
|
id?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Sets the `data-wds-parent` attribute on the listbox container, which is needed for complex components like DateInput to correctly manage event handling.
|
|
151
|
+
* @internal
|
|
152
|
+
*/
|
|
153
|
+
parentId?: string;
|
|
149
154
|
name?: string;
|
|
150
155
|
multiple?: M;
|
|
151
156
|
placeholder?: string;
|
|
@@ -176,8 +181,11 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
|
|
|
176
181
|
UNSAFE_triggerButtonProps?: WithInputAttributesProps['inputAttributes'] & {
|
|
177
182
|
'aria-label'?: string;
|
|
178
183
|
};
|
|
184
|
+
/** Ref to the select trigger button element. */
|
|
185
|
+
triggerRef?: React.MutableRefObject<HTMLButtonElement | null>;
|
|
179
186
|
onFilterChange?: (args: { query: string; queryNormalized: string | null }) => void;
|
|
180
187
|
onChange?: (value: M extends true ? T[] : T) => void;
|
|
188
|
+
onOpen?: () => void;
|
|
181
189
|
onClose?: () => void;
|
|
182
190
|
onClear?: () => void;
|
|
183
191
|
}
|
|
@@ -245,6 +253,7 @@ const noop = () => {};
|
|
|
245
253
|
|
|
246
254
|
export function SelectInput<T = string, M extends boolean = false>({
|
|
247
255
|
id: idProp,
|
|
256
|
+
parentId,
|
|
248
257
|
name,
|
|
249
258
|
multiple,
|
|
250
259
|
placeholder,
|
|
@@ -261,8 +270,10 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
261
270
|
size = 'md',
|
|
262
271
|
className,
|
|
263
272
|
UNSAFE_triggerButtonProps,
|
|
273
|
+
triggerRef: externalTriggerRef,
|
|
264
274
|
onFilterChange = noop,
|
|
265
275
|
onChange,
|
|
276
|
+
onOpen,
|
|
266
277
|
onClose,
|
|
267
278
|
onClear,
|
|
268
279
|
}: SelectInputProps<T, M>) {
|
|
@@ -273,15 +284,18 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
273
284
|
|
|
274
285
|
const initialized = useRef(false);
|
|
275
286
|
const handleClose = useEffectEvent(onClose ?? (() => {}));
|
|
287
|
+
const handleOpen = useEffectEvent(onOpen ?? (() => {}));
|
|
276
288
|
useEffect(() => {
|
|
277
289
|
if (initialized.current) {
|
|
278
|
-
if (
|
|
290
|
+
if (open) {
|
|
291
|
+
handleOpen?.();
|
|
292
|
+
} else {
|
|
279
293
|
handleClose?.();
|
|
280
294
|
}
|
|
281
295
|
} else {
|
|
282
296
|
initialized.current = true;
|
|
283
297
|
}
|
|
284
|
-
}, [handleClose, open]);
|
|
298
|
+
}, [handleClose, handleOpen, open]);
|
|
285
299
|
|
|
286
300
|
const [filterQuery, _setFilterQuery] = useState('');
|
|
287
301
|
const deferredFilterQuery = useDeferredValue(filterQuery);
|
|
@@ -295,7 +309,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
295
309
|
}
|
|
296
310
|
});
|
|
297
311
|
|
|
298
|
-
const
|
|
312
|
+
const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
|
|
299
313
|
|
|
300
314
|
const screenSm = useScreenSize(Breakpoint.SMALL);
|
|
301
315
|
const OptionsOverlay = screenSm ? Popover : BottomSheet;
|
|
@@ -363,7 +377,12 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
363
377
|
value={{
|
|
364
378
|
ref: (node) => {
|
|
365
379
|
ref(node);
|
|
366
|
-
|
|
380
|
+
if (externalTriggerRef) {
|
|
381
|
+
// eslint-disable-next-line no-param-reassign
|
|
382
|
+
externalTriggerRef.current = node;
|
|
383
|
+
} else {
|
|
384
|
+
internalTriggerRef.current = node;
|
|
385
|
+
}
|
|
367
386
|
},
|
|
368
387
|
...inputAttributes,
|
|
369
388
|
...UNSAFE_triggerButtonProps,
|
|
@@ -406,7 +425,9 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
406
425
|
onClear != null
|
|
407
426
|
? () => {
|
|
408
427
|
onClear();
|
|
409
|
-
|
|
428
|
+
(externalTriggerRef?.current ?? internalTriggerRef.current)?.focus({
|
|
429
|
+
preventScroll: true,
|
|
430
|
+
});
|
|
410
431
|
}
|
|
411
432
|
: undefined,
|
|
412
433
|
disabled: uiDisabled,
|
|
@@ -427,6 +448,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
427
448
|
>
|
|
428
449
|
<SelectInputOptions
|
|
429
450
|
id={id ? `${id}Search` : undefined}
|
|
451
|
+
parentId={parentId}
|
|
430
452
|
items={items}
|
|
431
453
|
renderValue={renderValue}
|
|
432
454
|
renderFooter={renderFooter}
|
|
@@ -529,7 +551,13 @@ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContai
|
|
|
529
551
|
interface SelectInputOptionsProps<T = string>
|
|
530
552
|
extends Pick<
|
|
531
553
|
SelectInputProps<T>,
|
|
532
|
-
|
|
554
|
+
| 'items'
|
|
555
|
+
| 'renderValue'
|
|
556
|
+
| 'renderFooter'
|
|
557
|
+
| 'filterable'
|
|
558
|
+
| 'filterPlaceholder'
|
|
559
|
+
| 'id'
|
|
560
|
+
| 'parentId'
|
|
533
561
|
> {
|
|
534
562
|
searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
|
|
535
563
|
listboxRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
@@ -541,6 +569,7 @@ interface SelectInputOptionsProps<T = string>
|
|
|
541
569
|
|
|
542
570
|
function SelectInputOptions<T = string>({
|
|
543
571
|
id,
|
|
572
|
+
parentId,
|
|
544
573
|
items,
|
|
545
574
|
renderValue = String,
|
|
546
575
|
renderFooter,
|
|
@@ -691,6 +720,7 @@ function SelectInputOptions<T = string>({
|
|
|
691
720
|
items.some((item) => item.type === 'group') &&
|
|
692
721
|
'np-select-input-listbox-container--has-group',
|
|
693
722
|
)}
|
|
723
|
+
data-wds-parent={parentId ?? undefined}
|
|
694
724
|
>
|
|
695
725
|
{resultsEmpty ? (
|
|
696
726
|
<div id={statusId} className="np-select-input-options-status">
|
package/src/inputs/TextArea.css
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
transition-duration: 300ms;
|
|
19
19
|
/* TODO: Remove these overrides once `.form-control` isn’t used anymore */
|
|
20
20
|
}
|
|
21
|
+
.disabled .np-form-control,
|
|
22
|
+
:disabled .np-form-control {
|
|
23
|
+
opacity: 1;
|
|
24
|
+
opacity: initial;
|
|
25
|
+
}
|
|
21
26
|
.np-form-control:focus-visible {
|
|
22
27
|
outline: none;
|
|
23
28
|
}
|
package/src/inputs/_common.less
CHANGED
package/src/inputs/contexts.tsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
type FieldLabelContextType = {
|
|
4
|
+
id?: string;
|
|
5
|
+
ref?: React.RefObject<HTMLLabelElement>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const FieldLabelContext = createContext<FieldLabelContextType | undefined>(undefined);
|
|
9
|
+
export const FieldLabelContextProvider = FieldLabelContext.Provider;
|
|
5
10
|
|
|
6
11
|
const InputIdContext = createContext<string | undefined>(undefined);
|
|
7
12
|
export const InputIdContextProvider = InputIdContext.Provider;
|
|
@@ -18,7 +23,7 @@ interface UseInputAttributesArgs {
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export function useInputAttributes({ nonLabelable }: UseInputAttributesArgs = {}) {
|
|
21
|
-
const labelId = useContext(
|
|
26
|
+
const labelId = useContext(FieldLabelContext)?.id;
|
|
22
27
|
return {
|
|
23
28
|
id: useContext(InputIdContext),
|
|
24
29
|
'aria-labelledby': nonLabelable ? labelId : undefined,
|
|
@@ -27,6 +32,10 @@ export function useInputAttributes({ nonLabelable }: UseInputAttributesArgs = {}
|
|
|
27
32
|
} satisfies React.HTMLAttributes<HTMLElement>;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
export function useFieldLabelRef() {
|
|
36
|
+
return useContext(FieldLabelContext)?.ref;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
export interface WithInputAttributesProps {
|
|
31
40
|
inputAttributes: ReturnType<typeof useInputAttributes>;
|
|
32
41
|
}
|