@transferwise/components 46.111.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 +13 -3
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +13 -3
- 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/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 +8 -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/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 +2 -2
- 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 +7 -0
- package/src/inputs/SelectInput.tsx +29 -4
- 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/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/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
|
}
|
|
@@ -28,6 +28,13 @@ const meta = {
|
|
|
28
28
|
onClose: fn() satisfies Mock,
|
|
29
29
|
onOpen: fn() satisfies Mock,
|
|
30
30
|
},
|
|
31
|
+
argTypes: {
|
|
32
|
+
parentId: {
|
|
33
|
+
table: {
|
|
34
|
+
category: 'WDS internal',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
31
38
|
parameters: { actions: { argTypesRegex: '' } },
|
|
32
39
|
} satisfies Meta<typeof SelectInput>;
|
|
33
40
|
export default meta;
|
|
@@ -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,6 +181,8 @@ 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;
|
|
181
188
|
onOpen?: () => void;
|
|
@@ -246,6 +253,7 @@ const noop = () => {};
|
|
|
246
253
|
|
|
247
254
|
export function SelectInput<T = string, M extends boolean = false>({
|
|
248
255
|
id: idProp,
|
|
256
|
+
parentId,
|
|
249
257
|
name,
|
|
250
258
|
multiple,
|
|
251
259
|
placeholder,
|
|
@@ -262,6 +270,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
262
270
|
size = 'md',
|
|
263
271
|
className,
|
|
264
272
|
UNSAFE_triggerButtonProps,
|
|
273
|
+
triggerRef: externalTriggerRef,
|
|
265
274
|
onFilterChange = noop,
|
|
266
275
|
onChange,
|
|
267
276
|
onOpen,
|
|
@@ -300,7 +309,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
300
309
|
}
|
|
301
310
|
});
|
|
302
311
|
|
|
303
|
-
const
|
|
312
|
+
const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
|
|
304
313
|
|
|
305
314
|
const screenSm = useScreenSize(Breakpoint.SMALL);
|
|
306
315
|
const OptionsOverlay = screenSm ? Popover : BottomSheet;
|
|
@@ -368,7 +377,12 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
368
377
|
value={{
|
|
369
378
|
ref: (node) => {
|
|
370
379
|
ref(node);
|
|
371
|
-
|
|
380
|
+
if (externalTriggerRef) {
|
|
381
|
+
// eslint-disable-next-line no-param-reassign
|
|
382
|
+
externalTriggerRef.current = node;
|
|
383
|
+
} else {
|
|
384
|
+
internalTriggerRef.current = node;
|
|
385
|
+
}
|
|
372
386
|
},
|
|
373
387
|
...inputAttributes,
|
|
374
388
|
...UNSAFE_triggerButtonProps,
|
|
@@ -411,7 +425,9 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
411
425
|
onClear != null
|
|
412
426
|
? () => {
|
|
413
427
|
onClear();
|
|
414
|
-
|
|
428
|
+
(externalTriggerRef?.current ?? internalTriggerRef.current)?.focus({
|
|
429
|
+
preventScroll: true,
|
|
430
|
+
});
|
|
415
431
|
}
|
|
416
432
|
: undefined,
|
|
417
433
|
disabled: uiDisabled,
|
|
@@ -432,6 +448,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
432
448
|
>
|
|
433
449
|
<SelectInputOptions
|
|
434
450
|
id={id ? `${id}Search` : undefined}
|
|
451
|
+
parentId={parentId}
|
|
435
452
|
items={items}
|
|
436
453
|
renderValue={renderValue}
|
|
437
454
|
renderFooter={renderFooter}
|
|
@@ -534,7 +551,13 @@ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContai
|
|
|
534
551
|
interface SelectInputOptionsProps<T = string>
|
|
535
552
|
extends Pick<
|
|
536
553
|
SelectInputProps<T>,
|
|
537
|
-
|
|
554
|
+
| 'items'
|
|
555
|
+
| 'renderValue'
|
|
556
|
+
| 'renderFooter'
|
|
557
|
+
| 'filterable'
|
|
558
|
+
| 'filterPlaceholder'
|
|
559
|
+
| 'id'
|
|
560
|
+
| 'parentId'
|
|
538
561
|
> {
|
|
539
562
|
searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
|
|
540
563
|
listboxRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
@@ -546,6 +569,7 @@ interface SelectInputOptionsProps<T = string>
|
|
|
546
569
|
|
|
547
570
|
function SelectInputOptions<T = string>({
|
|
548
571
|
id,
|
|
572
|
+
parentId,
|
|
549
573
|
items,
|
|
550
574
|
renderValue = String,
|
|
551
575
|
renderFooter,
|
|
@@ -696,6 +720,7 @@ function SelectInputOptions<T = string>({
|
|
|
696
720
|
items.some((item) => item.type === 'group') &&
|
|
697
721
|
'np-select-input-listbox-container--has-group',
|
|
698
722
|
)}
|
|
723
|
+
data-wds-parent={parentId ?? undefined}
|
|
699
724
|
>
|
|
700
725
|
{resultsEmpty ? (
|
|
701
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
|
}
|
package/src/label/Label.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import messages from './Label.messages';
|
|
|
3
3
|
import { useIntl } from 'react-intl';
|
|
4
4
|
import Body from '../body';
|
|
5
5
|
import { CommonProps } from '../common';
|
|
6
|
-
import { PropsWithChildren } from 'react';
|
|
6
|
+
import { forwardRef, PropsWithChildren } from 'react';
|
|
7
7
|
|
|
8
8
|
export type LabelProps = {
|
|
9
9
|
id?: string;
|
|
@@ -21,25 +21,29 @@ export type LabelProps = {
|
|
|
21
21
|
* <Field label={..} description={..} required={..}>..</Field>
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
const Label =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
className
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
|
25
|
+
({ className, children, htmlFor, id }: LabelProps, ref) => {
|
|
26
|
+
return (
|
|
27
|
+
<label
|
|
28
|
+
ref={ref}
|
|
29
|
+
id={id}
|
|
30
|
+
htmlFor={htmlFor}
|
|
31
|
+
className={clsx(
|
|
32
|
+
'np-label d-flex flex-column np-text-body-default-bold text-primary m-b-0',
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</label>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
Label.displayName = 'Label';
|
|
38
43
|
|
|
39
44
|
export type LabelOptionalProps = PropsWithChildren<CommonProps>;
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
Label.Optional = function Optional({ children, className }: LabelOptionalProps) {
|
|
46
|
+
const Optional = function Optional({ children, className }: LabelOptionalProps) {
|
|
43
47
|
const { formatMessage } = useIntl();
|
|
44
48
|
return (
|
|
45
49
|
<div>
|
|
@@ -53,8 +57,7 @@ Label.Optional = function Optional({ children, className }: LabelOptionalProps)
|
|
|
53
57
|
|
|
54
58
|
export type LabelDescriptionProps = PropsWithChildren<CommonProps> & { id?: string };
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
Label.Description = function Description({ id, children, className }: LabelDescriptionProps) {
|
|
60
|
+
const Description = function Description({ id, children, className }: LabelDescriptionProps) {
|
|
58
61
|
return children ? (
|
|
59
62
|
<Body id={id} className={clsx('text-secondary', className)}>
|
|
60
63
|
{children}
|
|
@@ -62,4 +65,7 @@ Label.Description = function Description({ id, children, className }: LabelDescr
|
|
|
62
65
|
) : null;
|
|
63
66
|
};
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
// eslint-disable-next-line functional/immutable-data
|
|
69
|
+
const LabelNamespace = Object.assign(Label, { Optional, Description });
|
|
70
|
+
|
|
71
|
+
export { LabelNamespace as Label };
|