@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.
Files changed (130) hide show
  1. package/build/common/panel/Panel.js +1 -0
  2. package/build/common/panel/Panel.js.map +1 -1
  3. package/build/common/panel/Panel.mjs +1 -0
  4. package/build/common/panel/Panel.mjs.map +1 -1
  5. package/build/common/responsivePanel/ResponsivePanel.js +6 -1
  6. package/build/common/responsivePanel/ResponsivePanel.js.map +1 -1
  7. package/build/common/responsivePanel/ResponsivePanel.mjs +6 -1
  8. package/build/common/responsivePanel/ResponsivePanel.mjs.map +1 -1
  9. package/build/dateInput/DateInput.js +46 -24
  10. package/build/dateInput/DateInput.js.map +1 -1
  11. package/build/dateInput/DateInput.mjs +48 -26
  12. package/build/dateInput/DateInput.mjs.map +1 -1
  13. package/build/dateLookup/DateLookup.js +5 -2
  14. package/build/dateLookup/DateLookup.js.map +1 -1
  15. package/build/dateLookup/DateLookup.mjs +5 -2
  16. package/build/dateLookup/DateLookup.mjs.map +1 -1
  17. package/build/dateLookup/dateTrigger/DateTrigger.js +2 -0
  18. package/build/dateLookup/dateTrigger/DateTrigger.js.map +1 -1
  19. package/build/dateLookup/dateTrigger/DateTrigger.mjs +2 -0
  20. package/build/dateLookup/dateTrigger/DateTrigger.mjs.map +1 -1
  21. package/build/field/Field.js +7 -2
  22. package/build/field/Field.js.map +1 -1
  23. package/build/field/Field.mjs +13 -8
  24. package/build/field/Field.mjs.map +1 -1
  25. package/build/inputs/InputGroup.js +1 -1
  26. package/build/inputs/InputGroup.js.map +1 -1
  27. package/build/inputs/InputGroup.mjs +2 -2
  28. package/build/inputs/InputGroup.mjs.map +1 -1
  29. package/build/inputs/SelectInput.js +13 -3
  30. package/build/inputs/SelectInput.js.map +1 -1
  31. package/build/inputs/SelectInput.mjs +13 -3
  32. package/build/inputs/SelectInput.mjs.map +1 -1
  33. package/build/inputs/contexts.js +8 -4
  34. package/build/inputs/contexts.js.map +1 -1
  35. package/build/inputs/contexts.mjs +7 -4
  36. package/build/inputs/contexts.mjs.map +1 -1
  37. package/build/label/Label.js +14 -8
  38. package/build/label/Label.js.map +1 -1
  39. package/build/label/Label.mjs +14 -8
  40. package/build/label/Label.mjs.map +1 -1
  41. package/build/listItem/Prompt/ListItemPrompt.js +1 -1
  42. package/build/listItem/Prompt/ListItemPrompt.js.map +1 -1
  43. package/build/listItem/Prompt/ListItemPrompt.mjs +1 -1
  44. package/build/listItem/Prompt/ListItemPrompt.mjs.map +1 -1
  45. package/build/main.css +163 -153
  46. package/build/moneyInput/MoneyInput.js +6 -5
  47. package/build/moneyInput/MoneyInput.js.map +1 -1
  48. package/build/moneyInput/MoneyInput.mjs +6 -5
  49. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  50. package/build/phoneNumberInput/PhoneNumberInput.js +25 -3
  51. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  52. package/build/phoneNumberInput/PhoneNumberInput.mjs +27 -5
  53. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  54. package/build/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.js +23 -23
  55. package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -0
  56. package/build/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.mjs +23 -23
  57. package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -0
  58. package/build/styles/inputs/Input.css +5 -0
  59. package/build/styles/inputs/TextArea.css +5 -0
  60. package/build/styles/listItem/ListItem.css +5 -153
  61. package/build/styles/listItem/Prompt/ListItemPrompt.css +0 -153
  62. package/build/styles/main.css +163 -153
  63. package/build/types/common/panel/Panel.d.ts +2 -0
  64. package/build/types/common/panel/Panel.d.ts.map +1 -1
  65. package/build/types/common/responsivePanel/ResponsivePanel.d.ts +1 -0
  66. package/build/types/common/responsivePanel/ResponsivePanel.d.ts.map +1 -1
  67. package/build/types/dateInput/DateInput.d.ts +2 -2
  68. package/build/types/dateInput/DateInput.d.ts.map +1 -1
  69. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  70. package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts +1 -0
  71. package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts.map +1 -1
  72. package/build/types/field/Field.d.ts.map +1 -1
  73. package/build/types/inputs/InputGroup.d.ts.map +1 -1
  74. package/build/types/inputs/SelectInput.d.ts +8 -1
  75. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  76. package/build/types/inputs/contexts.d.ts +6 -1
  77. package/build/types/inputs/contexts.d.ts.map +1 -1
  78. package/build/types/label/Label.d.ts +5 -15
  79. package/build/types/label/Label.d.ts.map +1 -1
  80. package/build/types/listItem/Prompt/ListItemPrompt.d.ts +1 -1
  81. package/build/types/listItem/Prompt/ListItemPrompt.d.ts.map +1 -1
  82. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  83. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  84. package/build/types/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.d.ts +1 -1
  85. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -0
  86. package/build/types/prompt/InlinePrompt/index.d.ts.map +1 -0
  87. package/build/types/prompt/index.d.ts +3 -0
  88. package/build/types/prompt/index.d.ts.map +1 -0
  89. package/package.json +2 -2
  90. package/src/DisabledComponents.story.tsx +156 -0
  91. package/src/common/panel/Panel.tsx +2 -0
  92. package/src/common/responsivePanel/ResponsivePanel.tsx +7 -1
  93. package/src/dateInput/DateInput.spec.tsx +45 -7
  94. package/src/dateInput/DateInput.story.tsx +2 -0
  95. package/src/dateInput/DateInput.tsx +65 -30
  96. package/src/dateLookup/DateLookup.spec.tsx +16 -0
  97. package/src/dateLookup/DateLookup.tsx +6 -3
  98. package/src/dateLookup/dateTrigger/DateTrigger.tsx +3 -0
  99. package/src/field/Field.tsx +6 -5
  100. package/src/inputs/Input.css +5 -0
  101. package/src/inputs/InputGroup.tsx +3 -4
  102. package/src/inputs/SelectInput.story.tsx +7 -0
  103. package/src/inputs/SelectInput.tsx +29 -4
  104. package/src/inputs/TextArea.css +5 -0
  105. package/src/inputs/_common.less +5 -0
  106. package/src/inputs/contexts.tsx +12 -3
  107. package/src/label/Label.tsx +26 -20
  108. package/src/listItem/ListItem.css +5 -153
  109. package/src/listItem/ListItem.less +5 -0
  110. package/src/listItem/Prompt/ListItemPrompt.css +0 -153
  111. package/src/listItem/Prompt/ListItemPrompt.less +0 -2
  112. package/src/listItem/Prompt/ListItemPrompt.tsx +1 -1
  113. package/src/main.css +163 -153
  114. package/src/main.less +1 -0
  115. package/src/moneyInput/MoneyInput.spec.tsx +16 -1
  116. package/src/moneyInput/MoneyInput.tsx +7 -6
  117. package/src/phoneNumberInput/PhoneNumberInput.spec.tsx +32 -0
  118. package/src/phoneNumberInput/PhoneNumberInput.tsx +32 -11
  119. package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.spec.tsx +2 -2
  120. package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.tsx +4 -4
  121. package/src/prompt/index.ts +6 -0
  122. package/build/listItem/Prompt/InlinePrompt/InlinePrompt.js.map +0 -1
  123. package/build/listItem/Prompt/InlinePrompt/InlinePrompt.mjs.map +0 -1
  124. package/build/types/listItem/Prompt/InlinePrompt/InlinePrompt.d.ts.map +0 -1
  125. package/build/types/listItem/Prompt/InlinePrompt/index.d.ts.map +0 -1
  126. /package/build/styles/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.css +0 -0
  127. /package/build/types/{listItem/Prompt → prompt}/InlinePrompt/index.d.ts +0 -0
  128. /package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.css +0 -0
  129. /package/src/{listItem/Prompt → prompt}/InlinePrompt/InlinePrompt.less +0 -0
  130. /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
- <div
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
- role="group" // Add role attribute to indicate container for interactive elements
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
- </div>
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 targetParent = target.closest('.tw-date');
370
- const relatedParent = relatedTarget && relatedTarget.closest('.tw-date');
371
- return targetParent !== relatedParent;
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 relatedTarget or the activeElement is not part of this DateInput component.
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 blurElementParent = target.closest('.tw-date');
380
- // Even though FocusEvent.relatedTarget is supported by IE
381
- // (https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/relatedTarget)
382
- // "IE11 sets document.activeElement to the next focused element before the blur event is called."
383
- // (https://stackoverflow.com/a/49325196/986241)
384
- // Therefore if the relatedTarget is null, we try the document.activeElement,
385
- // which may contain the HTML element that is gaining focus
386
- const focusElement =
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 { OverlayIdContext, OverlayIdProvider } from '../provider/overlay/OverlayIdProvider';
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
- {...inputAttributes}
323
- id={id}
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}
@@ -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
- FieldLabelIdContextProvider,
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
- <FieldLabelIdContextProvider value={labelId}>
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
- </FieldLabelIdContextProvider>
115
+ </FieldLabelContextProvider>
115
116
  );
116
117
  };
@@ -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
- FieldLabelIdContextProvider,
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
- <FieldLabelIdContextProvider value={undefined}>
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
- </FieldLabelIdContextProvider>
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 triggerRef = useRef<HTMLButtonElement | null>(null);
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
- triggerRef.current = node;
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
- triggerRef.current?.focus({ preventScroll: true });
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
- 'items' | 'renderValue' | 'renderFooter' | 'filterable' | 'filterPlaceholder' | 'id'
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">
@@ -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
  }
@@ -15,6 +15,11 @@
15
15
  transition-timing-function: ease-in-out;
16
16
  transition-duration: 300ms;
17
17
 
18
+ .disabled &,
19
+ :disabled & {
20
+ opacity: unset;
21
+ }
22
+
18
23
  &:focus-visible {
19
24
  outline: none;
20
25
  }
@@ -1,7 +1,12 @@
1
1
  import { createContext, useContext } from 'react';
2
2
 
3
- const FieldLabelIdContext = createContext<string | undefined>(undefined);
4
- export const FieldLabelIdContextProvider = FieldLabelIdContext.Provider;
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(FieldLabelIdContext);
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
  }
@@ -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 = ({ className, children, htmlFor, id }: LabelProps) => {
25
- return (
26
- <label
27
- id={id}
28
- htmlFor={htmlFor}
29
- className={clsx(
30
- 'np-label d-flex flex-column np-text-body-default-bold text-primary m-b-0',
31
- className,
32
- )}
33
- >
34
- {children}
35
- </label>
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
- // eslint-disable-next-line functional/immutable-data
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
- // eslint-disable-next-line functional/immutable-data
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
- export { Label };
68
+ // eslint-disable-next-line functional/immutable-data
69
+ const LabelNamespace = Object.assign(Label, { Optional, Description });
70
+
71
+ export { LabelNamespace as Label };