@transferwise/components 0.0.0-experimental-db313a8 → 0.0.0-experimental-d44d661

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 (82) hide show
  1. package/build/dateInput/DateInput.js +8 -33
  2. package/build/dateInput/DateInput.js.map +1 -1
  3. package/build/dateInput/DateInput.mjs +10 -35
  4. package/build/dateInput/DateInput.mjs.map +1 -1
  5. package/build/dateLookup/DateLookup.js +2 -4
  6. package/build/dateLookup/DateLookup.js.map +1 -1
  7. package/build/dateLookup/DateLookup.mjs +2 -4
  8. package/build/dateLookup/DateLookup.mjs.map +1 -1
  9. package/build/dateLookup/dateTrigger/DateTrigger.js +0 -2
  10. package/build/dateLookup/dateTrigger/DateTrigger.js.map +1 -1
  11. package/build/dateLookup/dateTrigger/DateTrigger.mjs +0 -2
  12. package/build/dateLookup/dateTrigger/DateTrigger.mjs.map +1 -1
  13. package/build/field/Field.js +2 -7
  14. package/build/field/Field.js.map +1 -1
  15. package/build/field/Field.mjs +8 -13
  16. package/build/field/Field.mjs.map +1 -1
  17. package/build/inputs/InputGroup.js +1 -1
  18. package/build/inputs/InputGroup.js.map +1 -1
  19. package/build/inputs/InputGroup.mjs +2 -2
  20. package/build/inputs/InputGroup.mjs.map +1 -1
  21. package/build/inputs/SelectInput.js +3 -9
  22. package/build/inputs/SelectInput.js.map +1 -1
  23. package/build/inputs/SelectInput.mjs +3 -9
  24. package/build/inputs/SelectInput.mjs.map +1 -1
  25. package/build/inputs/_BottomSheet.js +29 -1
  26. package/build/inputs/_BottomSheet.js.map +1 -1
  27. package/build/inputs/_BottomSheet.mjs +30 -2
  28. package/build/inputs/_BottomSheet.mjs.map +1 -1
  29. package/build/inputs/contexts.js +4 -8
  30. package/build/inputs/contexts.js.map +1 -1
  31. package/build/inputs/contexts.mjs +4 -7
  32. package/build/inputs/contexts.mjs.map +1 -1
  33. package/build/label/Label.js +8 -14
  34. package/build/label/Label.js.map +1 -1
  35. package/build/label/Label.mjs +8 -14
  36. package/build/label/Label.mjs.map +1 -1
  37. package/build/main.css +1 -0
  38. package/build/moneyInput/MoneyInput.js +5 -6
  39. package/build/moneyInput/MoneyInput.js.map +1 -1
  40. package/build/moneyInput/MoneyInput.mjs +5 -6
  41. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  42. package/build/phoneNumberInput/PhoneNumberInput.js +2 -24
  43. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  44. package/build/phoneNumberInput/PhoneNumberInput.mjs +4 -26
  45. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  46. package/build/styles/dimmer/Dimmer.css +1 -0
  47. package/build/styles/main.css +1 -0
  48. package/build/types/dateInput/DateInput.d.ts +2 -2
  49. package/build/types/dateInput/DateInput.d.ts.map +1 -1
  50. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  51. package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts +0 -1
  52. package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts.map +1 -1
  53. package/build/types/field/Field.d.ts.map +1 -1
  54. package/build/types/inputs/InputGroup.d.ts.map +1 -1
  55. package/build/types/inputs/SelectInput.d.ts +1 -3
  56. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  57. package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
  58. package/build/types/inputs/contexts.d.ts +1 -6
  59. package/build/types/inputs/contexts.d.ts.map +1 -1
  60. package/build/types/label/Label.d.ts +15 -5
  61. package/build/types/label/Label.d.ts.map +1 -1
  62. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  63. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/common/bottomSheet/BottomSheet.test.story.tsx +73 -0
  66. package/src/dateInput/DateInput.tsx +13 -43
  67. package/src/dateLookup/DateLookup.tsx +3 -5
  68. package/src/dateLookup/dateTrigger/DateTrigger.tsx +0 -3
  69. package/src/dimmer/Dimmer.css +1 -0
  70. package/src/dimmer/Dimmer.less +1 -0
  71. package/src/field/Field.tsx +5 -6
  72. package/src/inputs/InputGroup.tsx +4 -3
  73. package/src/inputs/SelectInput.spec.tsx +33 -0
  74. package/src/inputs/SelectInput.test.story.tsx +76 -0
  75. package/src/inputs/SelectInput.tsx +3 -13
  76. package/src/inputs/_BottomSheet.tsx +19 -6
  77. package/src/inputs/contexts.tsx +3 -12
  78. package/src/label/Label.tsx +20 -26
  79. package/src/main.css +1 -0
  80. package/src/moneyInput/MoneyInput.spec.tsx +1 -2
  81. package/src/moneyInput/MoneyInput.tsx +6 -7
  82. package/src/phoneNumberInput/PhoneNumberInput.tsx +10 -31
@@ -1,5 +1,5 @@
1
1
  import { clsx } from 'clsx';
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { 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 { useFieldLabelRef, useInputAttributes } from '../inputs/contexts';
22
+ import { 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<HTMLFieldSetElement>;
35
- onBlur?: React.FocusEventHandler<HTMLFieldSetElement>;
34
+ onFocus?: React.FocusEventHandler<HTMLDivElement>;
35
+ onBlur?: React.FocusEventHandler<HTMLDivElement>;
36
36
  dayLabel?: string;
37
37
  dayAutoComplete?: string;
38
38
  monthLabel?: string;
@@ -75,10 +75,6 @@ const DateInput = ({
75
75
  selectProps = {},
76
76
  }: DateInputProps) => {
77
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);
82
78
  const id = idProp ?? inputAttributes.id;
83
79
  const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes['aria-labelledby'];
84
80
 
@@ -125,10 +121,6 @@ const DateInput = ({
125
121
  );
126
122
  const monthNames = getMonthNames(locale, monthFormat);
127
123
 
128
- const monthYearOnly = mode === DateMode.MONTH_YEAR;
129
- const monthBeforeDay = MDY.has(locale);
130
- const yearFirst = YMD.has(locale);
131
-
132
124
  dayLabel ||= formatMessage(messages.dayLabel);
133
125
  monthLabel ||= formatMessage(messages.monthLabel);
134
126
  yearLabel ||= formatMessage(messages.yearLabel);
@@ -138,29 +130,6 @@ const DateInput = ({
138
130
  year: placeholders?.year || formatMessage(messages.yearPlaceholder),
139
131
  };
140
132
 
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
-
164
133
  const getDateAsString = (date: Date) => {
165
134
  if (!isDateValid(date)) {
166
135
  return '';
@@ -183,7 +152,6 @@ const DateInput = ({
183
152
  <label className="d-flex flex-column">
184
153
  <Body type={Typography.BODY_DEFAULT}>{monthLabel}</Body>
185
154
  <SelectInput
186
- triggerRef={monthRef}
187
155
  id={`${id}:month`}
188
156
  parentId={DATE_INPUT_PARENT_ID}
189
157
  name="month"
@@ -277,6 +245,8 @@ const DateInput = ({
277
245
  }
278
246
  };
279
247
 
248
+ const monthYearOnly = mode === DateMode.MONTH_YEAR;
249
+
280
250
  const monthWidth = clsx({
281
251
  'col-sm-8 tw-date--month': monthYearOnly,
282
252
  'col-sm-5 tw-date--month': !monthYearOnly,
@@ -293,7 +263,6 @@ const DateInput = ({
293
263
  <Body type={Typography.BODY_DEFAULT}>{dayLabel}</Body>
294
264
  <div className={`input-group input-group-${size}`}>
295
265
  <Input
296
- ref={dayRef}
297
266
  id={`${id}:day`}
298
267
  type="text"
299
268
  inputMode="numeric"
@@ -321,7 +290,6 @@ const DateInput = ({
321
290
  <Body type={Typography.BODY_DEFAULT}>{yearLabel}</Body>
322
291
  <div className={`input-group input-group-${size}`}>
323
292
  <Input
324
- ref={yearRef}
325
293
  id={`${id}:year`}
326
294
  type="text"
327
295
  inputMode="numeric"
@@ -341,16 +309,18 @@ const DateInput = ({
341
309
  </div>
342
310
  );
343
311
  };
312
+ const monthBeforeDay = MDY.has(locale);
313
+ const yearFirst = YMD.has(locale);
344
314
 
345
315
  return (
346
- <fieldset
316
+ <div
317
+ {...inputAttributes}
347
318
  id={id}
319
+ data-wds-dateinput=""
348
320
  className="tw-date"
349
- aria-describedby={inputAttributes['aria-describedby']}
350
- aria-invalid={inputAttributes['aria-invalid']}
351
321
  aria-labelledby={ariaLabelledBy}
352
322
  aria-label={ariaLabel}
353
- data-wds-dateinput=""
323
+ role="group" // Add role attribute to indicate container for interactive elements
354
324
  onFocus={(event) =>
355
325
  shouldPropagateOnFocus(event) ? onFocus?.(event) : event.stopPropagation()
356
326
  }
@@ -394,7 +364,7 @@ const DateInput = ({
394
364
  );
395
365
  })()}
396
366
  </div>
397
- </fieldset>
367
+ </div>
398
368
  );
399
369
  };
400
370
 
@@ -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 { OverlayIdProvider } from '../provider/overlay/OverlayIdProvider';
16
+ import { OverlayIdContext, OverlayIdProvider } from '../provider/overlay/OverlayIdProvider';
17
17
  import DateTrigger from './dateTrigger';
18
18
  import DayCalendar from './dayCalendar';
19
19
  import { getStartOfDay } from './getStartOfDay';
@@ -319,15 +319,13 @@ class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateL
319
319
  return (
320
320
  <div
321
321
  ref={this.element}
322
- aria-labelledby={id}
323
- aria-invalid={inputAttributes?.['aria-invalid']}
324
- aria-describedby={inputAttributes?.['aria-describedby']}
322
+ {...inputAttributes}
323
+ id={id}
325
324
  className="input-group"
326
325
  onKeyDown={this.handleKeyDown}
327
326
  >
328
327
  <OverlayIdProvider open={open}>
329
328
  <DateTrigger
330
- id={id}
331
329
  ariaLabelledBy={ariaLabelledBy}
332
330
  selectedDate={selectedDate}
333
331
  size={size}
@@ -21,7 +21,6 @@ interface DateTriggerProps {
21
21
  disabled: boolean;
22
22
  onClick: () => void;
23
23
  onClear?: () => void;
24
- id?: string;
25
24
  }
26
25
 
27
26
  const DateTrigger: React.FC<DateTriggerProps> = ({
@@ -29,7 +28,6 @@ const DateTrigger: React.FC<DateTriggerProps> = ({
29
28
  size = Size.MEDIUM,
30
29
  placeholder,
31
30
  label,
32
- id,
33
31
  monthFormat,
34
32
  disabled,
35
33
  ariaLabelledBy,
@@ -44,7 +42,6 @@ const DateTrigger: React.FC<DateTriggerProps> = ({
44
42
  return (
45
43
  <>
46
44
  <button
47
- id={id}
48
45
  aria-haspopup="dialog"
49
46
  aria-expanded={overlayId != null}
50
47
  aria-controls={overlayId}
@@ -1,5 +1,6 @@
1
1
  .no-scroll {
2
2
  overflow: hidden;
3
+ scroll-behavior: unset !important;
3
4
  }
4
5
  .dimmer {
5
6
  position: fixed;
@@ -6,6 +6,7 @@
6
6
  // (see https://bugs.webkit.org/show_bug.cgi?id=153852 & https://bugs.webkit.org/show_bug.cgi?id=220908)
7
7
  .no-scroll {
8
8
  overflow: hidden;
9
+ scroll-behavior: unset !important;
9
10
  }
10
11
 
11
12
  .dimmer {
@@ -1,10 +1,10 @@
1
1
  import { clsx } from 'clsx';
2
- import { useId, useRef } from 'react';
2
+ import { useId } from 'react';
3
3
 
4
4
  import { Sentiment } from '../common';
5
5
  import InlineAlert from '../inlineAlert/InlineAlert';
6
6
  import {
7
- FieldLabelContextProvider,
7
+ FieldLabelIdContextProvider,
8
8
  InputDescribedByProvider,
9
9
  InputIdContextProvider,
10
10
  InputInvalidProvider,
@@ -46,7 +46,6 @@ export const Field = ({
46
46
  children,
47
47
  ...props
48
48
  }: FieldProps) => {
49
- const labelRef = useRef<HTMLLabelElement>(null);
50
49
  const sentiment = props.error ? Sentiment.NEGATIVE : propType;
51
50
  const message = propMessage || props.error;
52
51
  const hasError = sentiment === Sentiment.NEGATIVE;
@@ -75,7 +74,7 @@ export const Field = ({
75
74
  }
76
75
 
77
76
  return (
78
- <FieldLabelContextProvider value={{ id: labelId, ref: labelRef }}>
77
+ <FieldLabelIdContextProvider value={labelId}>
79
78
  <InputIdContextProvider value={inputId}>
80
79
  <InputDescribedByProvider value={ariaDescribedbyByIds()}>
81
80
  <InputInvalidProvider value={hasError}>
@@ -93,7 +92,7 @@ export const Field = ({
93
92
  >
94
93
  {label != null ? (
95
94
  <>
96
- <Label ref={labelRef} id={labelId} htmlFor={inputId}>
95
+ <Label id={labelId} htmlFor={inputId}>
97
96
  {required ? label : <Label.Optional>{label}</Label.Optional>}
98
97
  </Label>
99
98
  <Label.Description id={descriptionId}>{description}</Label.Description>
@@ -112,6 +111,6 @@ export const Field = ({
112
111
  </InputInvalidProvider>
113
112
  </InputDescribedByProvider>
114
113
  </InputIdContextProvider>
115
- </FieldLabelContextProvider>
114
+ </FieldLabelIdContextProvider>
116
115
  );
117
116
  };
@@ -4,10 +4,11 @@ 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
- FieldLabelContextProvider,
7
+ FieldLabelIdContextProvider,
8
8
  InputDescribedByProvider,
9
9
  InputIdContextProvider,
10
10
  InputInvalidProvider,
11
+ useInputAttributes,
11
12
  } from './contexts';
12
13
 
13
14
  type InputPaddingContextType = [
@@ -128,7 +129,7 @@ function InputAddon({
128
129
 
129
130
  return (
130
131
  /* Prevent nested controls from being labeled redundantly */
131
- <FieldLabelContextProvider value={undefined}>
132
+ <FieldLabelIdContextProvider value={undefined}>
132
133
  <InputIdContextProvider value={undefined}>
133
134
  <InputDescribedByProvider value={undefined}>
134
135
  <InputInvalidProvider value={undefined}>
@@ -152,6 +153,6 @@ function InputAddon({
152
153
  </InputInvalidProvider>
153
154
  </InputDescribedByProvider>
154
155
  </InputIdContextProvider>
155
- </FieldLabelContextProvider>
156
+ </FieldLabelIdContextProvider>
156
157
  );
157
158
  }
@@ -3,13 +3,19 @@ import { userEvent } from '@testing-library/user-event';
3
3
 
4
4
  import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
5
5
 
6
+ import { useScreenSize } from '../common/hooks/useScreenSize';
6
7
  import { SelectInput, type SelectInputOptionItem, type SelectInputProps } from './SelectInput';
7
8
  import { Field } from '../field/Field';
8
9
 
9
10
  mockMatchMedia();
10
11
  mockResizeObserver();
12
+ jest.mock('../common/hooks/useScreenSize');
11
13
 
12
14
  describe('SelectInput', () => {
15
+ beforeEach(() => {
16
+ (useScreenSize as jest.Mock).mockReturnValue(true);
17
+ });
18
+
13
19
  it('renders placeholder', () => {
14
20
  render(
15
21
  <SelectInput
@@ -309,4 +315,31 @@ describe('SelectInput', () => {
309
315
  expect(listBox).not.toHaveAttribute('aria-labelledby');
310
316
  });
311
317
  });
318
+
319
+ describe('smooth scroll reset', () => {
320
+ const options: SelectInputOptionItem[] = [
321
+ { type: 'option', value: 'Banana' },
322
+ { type: 'option', value: 'Orange' },
323
+ { type: 'option', value: 'Olive' },
324
+ ];
325
+
326
+ beforeEach(() => {
327
+ document.documentElement.style.scrollBehavior = 'smooth';
328
+ (useScreenSize as jest.Mock).mockReturnValue(false);
329
+ });
330
+
331
+ afterEach(() => {
332
+ document.documentElement.removeAttribute('style');
333
+ });
334
+
335
+ it('renders with custom scroll behavior', async () => {
336
+ const { container } = render(<SelectInput items={options} />);
337
+
338
+ await userEvent.click(screen.getByRole('combobox'));
339
+ expect(document.documentElement).toHaveStyle({ scrollBehavior: 'unset' });
340
+
341
+ await userEvent.click(document.documentElement);
342
+ expect(container).toHaveStyle({ scrollBehavior: '' });
343
+ });
344
+ });
312
345
  });
@@ -0,0 +1,76 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { fn, type Mock, userEvent, within } from 'storybook/test';
3
+
4
+ import { Field } from '../field/Field';
5
+ import { SelectInput, type SelectInputProps } from './SelectInput';
6
+ import { lorem5, lorem500 } from '../test-utils';
7
+ import Body from '../body';
8
+ import { allModes } from '../../.storybook/modes';
9
+
10
+ const meta = {
11
+ title: 'Forms/SelectInput/tests',
12
+ component: SelectInput,
13
+ args: {
14
+ onFilterChange: fn() satisfies Mock,
15
+ onChange: fn() satisfies Mock,
16
+ onClose: fn() satisfies Mock,
17
+ onOpen: fn() satisfies Mock,
18
+ },
19
+ tags: ['!autodocs'],
20
+ } satisfies Meta<typeof SelectInput>;
21
+ export default meta;
22
+
23
+ type Story<T, M extends boolean = false> = StoryObj<SelectInputProps<T, M>>;
24
+
25
+ /**
26
+ * This test ensures that when the SelectInput is used within a scrollable page,
27
+ * opening the dropdown does not cause any unwanted scrolling or layout shifts.
28
+ * Expected snapshot should start with green bar at the top, with yellow section
29
+ * not in the viewport
30
+ */
31
+ export const SmoothScrollReset: Story<string> = {
32
+ args: {
33
+ items: Array.from({ length: 15 }).map((_, id) => ({
34
+ type: 'option',
35
+ value: `option ${id + 1}`,
36
+ })),
37
+ placeholder: 'Select option',
38
+ },
39
+ decorators: [
40
+ (Story) => (
41
+ <>
42
+ <style>{`html { scroll-behavior: smooth; }`}</style>
43
+ <div style={{ maxWidth: 400 }}>
44
+ <div
45
+ className="d-flex align-items-center justify-content-center"
46
+ style={{ height: 400, backgroundColor: 'var(--color-bright-yellow)' }}
47
+ >
48
+ This block should not be visible on a snapshot
49
+ </div>
50
+ <div style={{ height: 10, backgroundColor: 'var(--color-bright-green)' }} />
51
+ <Field id="el1" label={lorem5}>
52
+ <Story />
53
+ </Field>
54
+ <Body as="p">{lorem500}</Body>
55
+ </div>
56
+ </>
57
+ ),
58
+ ],
59
+ play: async ({ canvasElement }) => {
60
+ window.scrollTo({ top: 400, behavior: 'instant' });
61
+ const canvas = within(canvasElement);
62
+ const triggerButton = canvas.getByRole('combobox');
63
+ await userEvent.click(triggerButton);
64
+ },
65
+ globals: {
66
+ viewport: { value: allModes.largeMobile.viewport, isRotated: false },
67
+ },
68
+ parameters: {
69
+ chromatic: {
70
+ delay: 1000,
71
+ modes: {
72
+ largeMobile: allModes.largeMobile,
73
+ }
74
+ },
75
+ },
76
+ };
@@ -181,8 +181,6 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
181
181
  UNSAFE_triggerButtonProps?: WithInputAttributesProps['inputAttributes'] & {
182
182
  'aria-label'?: string;
183
183
  };
184
- /** Ref to the select trigger button element. */
185
- triggerRef?: React.MutableRefObject<HTMLButtonElement | null>;
186
184
  onFilterChange?: (args: { query: string; queryNormalized: string | null }) => void;
187
185
  onChange?: (value: M extends true ? T[] : T) => void;
188
186
  onOpen?: () => void;
@@ -270,7 +268,6 @@ export function SelectInput<T = string, M extends boolean = false>({
270
268
  size = 'md',
271
269
  className,
272
270
  UNSAFE_triggerButtonProps,
273
- triggerRef: externalTriggerRef,
274
271
  onFilterChange = noop,
275
272
  onChange,
276
273
  onOpen,
@@ -309,7 +306,7 @@ export function SelectInput<T = string, M extends boolean = false>({
309
306
  }
310
307
  });
311
308
 
312
- const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
309
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
313
310
 
314
311
  const screenSm = useScreenSize(Breakpoint.SMALL);
315
312
  const OptionsOverlay = screenSm ? Popover : BottomSheet;
@@ -377,12 +374,7 @@ export function SelectInput<T = string, M extends boolean = false>({
377
374
  value={{
378
375
  ref: (node) => {
379
376
  ref(node);
380
- if (externalTriggerRef) {
381
- // eslint-disable-next-line no-param-reassign
382
- externalTriggerRef.current = node;
383
- } else {
384
- internalTriggerRef.current = node;
385
- }
377
+ triggerRef.current = node;
386
378
  },
387
379
  ...inputAttributes,
388
380
  ...UNSAFE_triggerButtonProps,
@@ -425,9 +417,7 @@ export function SelectInput<T = string, M extends boolean = false>({
425
417
  onClear != null
426
418
  ? () => {
427
419
  onClear();
428
- (externalTriggerRef?.current ?? internalTriggerRef.current)?.focus({
429
- preventScroll: true,
430
- });
420
+ triggerRef.current?.focus({ preventScroll: true });
431
421
  }
432
422
  : undefined,
433
423
  disabled: uiDisabled,
@@ -10,12 +10,11 @@ import { Transition } from '@headlessui/react';
10
10
  import { FocusScope } from '@react-aria/focus';
11
11
  import { ThemeProvider, useTheme } from '@wise/components-theming';
12
12
  import { clsx } from 'clsx';
13
- import { Fragment, useState } from 'react';
13
+ import { Fragment, useState, useEffect } from 'react';
14
14
 
15
- import { CloseButton } from '../common/closeButton';
15
+ import { CloseButton, Size } from '../common';
16
16
  import { useVirtualKeyboard } from '../common/hooks/useVirtualKeyboard';
17
17
  import { PreventScroll } from '../common/preventScroll/PreventScroll';
18
- import { Size } from '../common/propsValues/size';
19
18
 
20
19
  export interface BottomSheetProps {
21
20
  open: boolean;
@@ -33,6 +32,19 @@ export interface BottomSheetProps {
33
32
  onCloseEnd?: () => void;
34
33
  }
35
34
 
35
+ /**
36
+ * App pages set scroll-behavior to 'smooth' which causes mobile Safari to
37
+ * slow-scroll and glitch. This function temporarily disables that behaviour
38
+ * while the BottomSheet is open.
39
+ */
40
+ const freezeScroll = (shouldFreeze = true) => {
41
+ if (shouldFreeze) {
42
+ document.documentElement.style.scrollBehavior = 'unset';
43
+ } else {
44
+ document.documentElement.style.removeProperty('scroll-behavior');
45
+ }
46
+ };
47
+
36
48
  export function BottomSheet({
37
49
  open,
38
50
  renderTrigger,
@@ -44,7 +56,6 @@ export function BottomSheet({
44
56
  onCloseEnd,
45
57
  }: BottomSheetProps) {
46
58
  useVirtualKeyboard();
47
-
48
59
  const { refs, context } = useFloating<Element>({
49
60
  open,
50
61
  onOpenChange: (value) => {
@@ -54,12 +65,14 @@ export function BottomSheet({
54
65
  },
55
66
  });
56
67
 
68
+ useEffect(() => {
69
+ freezeScroll(open);
70
+ }, [open]);
71
+
57
72
  const dismiss = useDismiss(context);
58
73
  const role = useRole(context);
59
74
  const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, role]);
60
-
61
75
  const [floatingKey, setFloatingKey] = useState(0);
62
-
63
76
  const { theme, screenMode } = useTheme();
64
77
 
65
78
  return (
@@ -1,12 +1,7 @@
1
1
  import { createContext, useContext } from 'react';
2
2
 
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;
3
+ const FieldLabelIdContext = createContext<string | undefined>(undefined);
4
+ export const FieldLabelIdContextProvider = FieldLabelIdContext.Provider;
10
5
 
11
6
  const InputIdContext = createContext<string | undefined>(undefined);
12
7
  export const InputIdContextProvider = InputIdContext.Provider;
@@ -23,7 +18,7 @@ interface UseInputAttributesArgs {
23
18
  }
24
19
 
25
20
  export function useInputAttributes({ nonLabelable }: UseInputAttributesArgs = {}) {
26
- const labelId = useContext(FieldLabelContext)?.id;
21
+ const labelId = useContext(FieldLabelIdContext);
27
22
  return {
28
23
  id: useContext(InputIdContext),
29
24
  'aria-labelledby': nonLabelable ? labelId : undefined,
@@ -32,10 +27,6 @@ export function useInputAttributes({ nonLabelable }: UseInputAttributesArgs = {}
32
27
  } satisfies React.HTMLAttributes<HTMLElement>;
33
28
  }
34
29
 
35
- export function useFieldLabelRef() {
36
- return useContext(FieldLabelContext)?.ref;
37
- }
38
-
39
30
  export interface WithInputAttributesProps {
40
31
  inputAttributes: ReturnType<typeof useInputAttributes>;
41
32
  }
@@ -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 { forwardRef, PropsWithChildren } from 'react';
6
+ import { PropsWithChildren } from 'react';
7
7
 
8
8
  export type LabelProps = {
9
9
  id?: string;
@@ -21,29 +21,25 @@ export type LabelProps = {
21
21
  * <Field label={..} description={..} required={..}>..</Field>
22
22
  * ```
23
23
  */
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';
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
+ };
43
38
 
44
39
  export type LabelOptionalProps = PropsWithChildren<CommonProps>;
45
40
 
46
- const Optional = function Optional({ children, className }: LabelOptionalProps) {
41
+ // eslint-disable-next-line functional/immutable-data
42
+ Label.Optional = function Optional({ children, className }: LabelOptionalProps) {
47
43
  const { formatMessage } = useIntl();
48
44
  return (
49
45
  <div>
@@ -57,7 +53,8 @@ const Optional = function Optional({ children, className }: LabelOptionalProps)
57
53
 
58
54
  export type LabelDescriptionProps = PropsWithChildren<CommonProps> & { id?: string };
59
55
 
60
- const Description = function Description({ id, children, className }: LabelDescriptionProps) {
56
+ // eslint-disable-next-line functional/immutable-data
57
+ Label.Description = function Description({ id, children, className }: LabelDescriptionProps) {
61
58
  return children ? (
62
59
  <Body id={id} className={clsx('text-secondary', className)}>
63
60
  {children}
@@ -65,7 +62,4 @@ const Description = function Description({ id, children, className }: LabelDescr
65
62
  ) : null;
66
63
  };
67
64
 
68
- // eslint-disable-next-line functional/immutable-data
69
- const LabelNamespace = Object.assign(Label, { Optional, Description });
70
-
71
- export { LabelNamespace as Label };
65
+ export { Label };
package/src/main.css CHANGED
@@ -2081,6 +2081,7 @@ button.np-option {
2081
2081
  }
2082
2082
  .no-scroll {
2083
2083
  overflow: hidden;
2084
+ scroll-behavior: unset !important;
2084
2085
  }
2085
2086
  .dimmer {
2086
2087
  position: fixed;
@@ -406,7 +406,7 @@ describe('Money Input', () => {
406
406
  </>,
407
407
  );
408
408
 
409
- expect(screen.getAllByLabelText('Prioritized label')[0]).toHaveClass('np-form-control');
409
+ expect(screen.getAllByLabelText('Prioritized label')[0]).toHaveClass('input-group');
410
410
  });
411
411
 
412
412
  it('supports `Field` for labeling', () => {
@@ -415,7 +415,6 @@ describe('Money Input', () => {
415
415
  <MoneyInput {...initialProps} />
416
416
  </Field>,
417
417
  );
418
-
419
418
  expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Recipient gets/);
420
419
  });
421
420