@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.
- package/build/dateInput/DateInput.js +8 -33
- package/build/dateInput/DateInput.js.map +1 -1
- package/build/dateInput/DateInput.mjs +10 -35
- package/build/dateInput/DateInput.mjs.map +1 -1
- package/build/dateLookup/DateLookup.js +2 -4
- package/build/dateLookup/DateLookup.js.map +1 -1
- package/build/dateLookup/DateLookup.mjs +2 -4
- package/build/dateLookup/DateLookup.mjs.map +1 -1
- package/build/dateLookup/dateTrigger/DateTrigger.js +0 -2
- package/build/dateLookup/dateTrigger/DateTrigger.js.map +1 -1
- package/build/dateLookup/dateTrigger/DateTrigger.mjs +0 -2
- package/build/dateLookup/dateTrigger/DateTrigger.mjs.map +1 -1
- package/build/field/Field.js +2 -7
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.mjs +8 -13
- 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 +3 -9
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +3 -9
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/inputs/_BottomSheet.js +29 -1
- package/build/inputs/_BottomSheet.js.map +1 -1
- package/build/inputs/_BottomSheet.mjs +30 -2
- package/build/inputs/_BottomSheet.mjs.map +1 -1
- package/build/inputs/contexts.js +4 -8
- package/build/inputs/contexts.js.map +1 -1
- package/build/inputs/contexts.mjs +4 -7
- package/build/inputs/contexts.mjs.map +1 -1
- package/build/label/Label.js +8 -14
- package/build/label/Label.js.map +1 -1
- package/build/label/Label.mjs +8 -14
- package/build/label/Label.mjs.map +1 -1
- package/build/main.css +1 -0
- package/build/moneyInput/MoneyInput.js +5 -6
- package/build/moneyInput/MoneyInput.js.map +1 -1
- package/build/moneyInput/MoneyInput.mjs +5 -6
- package/build/moneyInput/MoneyInput.mjs.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.js +2 -24
- package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.mjs +4 -26
- package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
- package/build/styles/dimmer/Dimmer.css +1 -0
- package/build/styles/main.css +1 -0
- 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 +0 -1
- 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 +1 -3
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
- package/build/types/inputs/contexts.d.ts +1 -6
- package/build/types/inputs/contexts.d.ts.map +1 -1
- package/build/types/label/Label.d.ts +15 -5
- package/build/types/label/Label.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/package.json +1 -1
- package/src/common/bottomSheet/BottomSheet.test.story.tsx +73 -0
- package/src/dateInput/DateInput.tsx +13 -43
- package/src/dateLookup/DateLookup.tsx +3 -5
- package/src/dateLookup/dateTrigger/DateTrigger.tsx +0 -3
- package/src/dimmer/Dimmer.css +1 -0
- package/src/dimmer/Dimmer.less +1 -0
- package/src/field/Field.tsx +5 -6
- package/src/inputs/InputGroup.tsx +4 -3
- package/src/inputs/SelectInput.spec.tsx +33 -0
- package/src/inputs/SelectInput.test.story.tsx +76 -0
- package/src/inputs/SelectInput.tsx +3 -13
- package/src/inputs/_BottomSheet.tsx +19 -6
- package/src/inputs/contexts.tsx +3 -12
- package/src/label/Label.tsx +20 -26
- package/src/main.css +1 -0
- package/src/moneyInput/MoneyInput.spec.tsx +1 -2
- package/src/moneyInput/MoneyInput.tsx +6 -7
- package/src/phoneNumberInput/PhoneNumberInput.tsx +10 -31
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import {
|
|
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 {
|
|
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<
|
|
35
|
-
onBlur?: React.FocusEventHandler<
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
323
|
-
|
|
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}
|
package/src/dimmer/Dimmer.css
CHANGED
package/src/dimmer/Dimmer.less
CHANGED
package/src/field/Field.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import { useId
|
|
2
|
+
import { useId } from 'react';
|
|
3
3
|
|
|
4
4
|
import { Sentiment } from '../common';
|
|
5
5
|
import InlineAlert from '../inlineAlert/InlineAlert';
|
|
6
6
|
import {
|
|
7
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
package/src/inputs/contexts.tsx
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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(
|
|
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
|
}
|
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 {
|
|
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 =
|
|
25
|
-
(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
className
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
const LabelNamespace = Object.assign(Label, { Optional, Description });
|
|
70
|
-
|
|
71
|
-
export { LabelNamespace as Label };
|
|
65
|
+
export { Label };
|
package/src/main.css
CHANGED
|
@@ -406,7 +406,7 @@ describe('Money Input', () => {
|
|
|
406
406
|
</>,
|
|
407
407
|
);
|
|
408
408
|
|
|
409
|
-
expect(screen.getAllByLabelText('Prioritized label')[0]).toHaveClass('
|
|
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
|
|