@transferwise/components 46.140.1 → 46.142.0
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/avatarLayout/AvatarLayout.js +15 -1
- package/build/avatarLayout/AvatarLayout.js.map +1 -1
- package/build/avatarLayout/AvatarLayout.mjs +15 -1
- package/build/avatarLayout/AvatarLayout.mjs.map +1 -1
- package/build/avatarView/AvatarView.js +6 -2
- package/build/avatarView/AvatarView.js.map +1 -1
- package/build/avatarView/AvatarView.mjs +6 -2
- package/build/avatarView/AvatarView.mjs.map +1 -1
- package/build/avatarView/Dot.js +8 -0
- package/build/avatarView/Dot.js.map +1 -1
- package/build/avatarView/Dot.mjs +8 -0
- package/build/avatarView/Dot.mjs.map +1 -1
- package/build/avatarWrapper/AvatarWrapper.js +3 -4
- package/build/avatarWrapper/AvatarWrapper.js.map +1 -1
- package/build/avatarWrapper/AvatarWrapper.mjs +4 -5
- package/build/avatarWrapper/AvatarWrapper.mjs.map +1 -1
- package/build/button/LegacyButton.js.map +1 -1
- package/build/button/LegacyButton.mjs.map +1 -1
- package/build/common/circle/Circle.js +6 -2
- package/build/common/circle/Circle.js.map +1 -1
- package/build/common/circle/Circle.mjs +6 -2
- package/build/common/circle/Circle.mjs.map +1 -1
- package/build/common/hooks/useHasIntersected/useHasIntersected.js +6 -4
- package/build/common/hooks/useHasIntersected/useHasIntersected.js.map +1 -1
- package/build/common/hooks/useHasIntersected/useHasIntersected.mjs +6 -4
- package/build/common/hooks/useHasIntersected/useHasIntersected.mjs.map +1 -1
- package/build/common/liveRegion/LiveRegion.js +4 -1
- package/build/common/liveRegion/LiveRegion.js.map +1 -1
- package/build/common/liveRegion/LiveRegion.mjs +4 -1
- package/build/common/liveRegion/LiveRegion.mjs.map +1 -1
- package/build/dateInput/DateInput.js +10 -10
- package/build/dateInput/DateInput.js.map +1 -1
- package/build/dateInput/DateInput.mjs +10 -10
- package/build/dateInput/DateInput.mjs.map +1 -1
- package/build/dateLookup/monthCalendar/table/MonthCalendarTable.js +1 -1
- package/build/dateLookup/monthCalendar/table/MonthCalendarTable.js.map +1 -1
- package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs +1 -1
- package/build/dateLookup/monthCalendar/table/MonthCalendarTable.mjs.map +1 -1
- package/build/dateLookup/yearCalendar/table/YearCalendarTable.js +1 -1
- package/build/dateLookup/yearCalendar/table/YearCalendarTable.js.map +1 -1
- package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs +1 -1
- package/build/dateLookup/yearCalendar/table/YearCalendarTable.mjs.map +1 -1
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.js.map +1 -1
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs.map +1 -1
- package/build/expressiveMoneyInput/amountInput/AmountInput.js +18 -12
- package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -1
- package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +19 -13
- package/build/expressiveMoneyInput/amountInput/AmountInput.mjs.map +1 -1
- package/build/expressiveMoneyInput/hooks/useInputStyle.js +8 -6
- package/build/expressiveMoneyInput/hooks/useInputStyle.js.map +1 -1
- package/build/expressiveMoneyInput/hooks/useInputStyle.mjs +9 -7
- package/build/expressiveMoneyInput/hooks/useInputStyle.mjs.map +1 -1
- package/build/field/Field.js +63 -32
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.messages.js +14 -0
- package/build/field/Field.messages.js.map +1 -0
- package/build/field/Field.messages.mjs +10 -0
- package/build/field/Field.messages.mjs.map +1 -0
- package/build/field/Field.mjs +65 -34
- package/build/field/Field.mjs.map +1 -1
- package/build/header/Header.js +1 -1
- package/build/header/Header.js.map +1 -1
- package/build/header/Header.mjs +1 -1
- package/build/header/Header.mjs.map +1 -1
- package/build/i18n/en.json +1 -0
- package/build/i18n/en.json.js +1 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +1 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.js.map +1 -1
- package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.mjs.map +1 -1
- package/build/inputs/SelectInput/Options/SelectInputOptions.js +34 -22
- package/build/inputs/SelectInput/Options/SelectInputOptions.js.map +1 -1
- package/build/inputs/SelectInput/Options/SelectInputOptions.mjs +35 -23
- package/build/inputs/SelectInput/Options/SelectInputOptions.mjs.map +1 -1
- package/build/inputs/SelectInput/Popover/SelectInputPopover.js.map +1 -1
- package/build/inputs/SelectInput/Popover/SelectInputPopover.mjs.map +1 -1
- package/build/inputs/SelectInput/SelectInput.js +8 -6
- package/build/inputs/SelectInput/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput/SelectInput.mjs +9 -7
- package/build/inputs/SelectInput/SelectInput.mjs.map +1 -1
- package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
- package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
- package/build/inputs/TextArea.js +5 -0
- package/build/inputs/TextArea.js.map +1 -1
- package/build/inputs/TextArea.mjs +6 -1
- package/build/inputs/TextArea.mjs.map +1 -1
- package/build/inputs/contexts.js +16 -0
- package/build/inputs/contexts.js.map +1 -1
- package/build/inputs/contexts.mjs +16 -2
- package/build/inputs/contexts.mjs.map +1 -1
- package/build/main.css +42 -8
- package/build/nudge/Nudge.js +31 -15
- package/build/nudge/Nudge.js.map +1 -1
- package/build/nudge/Nudge.mjs +32 -16
- package/build/nudge/Nudge.mjs.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.js +9 -12
- package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.mjs +9 -12
- package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
- package/build/promoCard/PromoCardGroup.js +34 -16
- package/build/promoCard/PromoCardGroup.js.map +1 -1
- package/build/promoCard/PromoCardGroup.mjs +35 -17
- package/build/promoCard/PromoCardGroup.mjs.map +1 -1
- package/build/segmentedControl/SegmentedControl.js +6 -1
- package/build/segmentedControl/SegmentedControl.js.map +1 -1
- package/build/segmentedControl/SegmentedControl.mjs +7 -2
- package/build/segmentedControl/SegmentedControl.mjs.map +1 -1
- package/build/styles/avatarView/AvatarView.css +4 -4
- package/build/styles/avatarView/Dot.css +4 -4
- package/build/styles/css/neptune.css +15 -1
- package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
- package/build/styles/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
- package/build/styles/field/Field.css +19 -3
- package/build/styles/main.css +42 -8
- package/build/styles/styles/less/neptune.css +15 -1
- package/build/tabs/Tabs.js +1 -1
- package/build/tabs/Tabs.js.map +1 -1
- package/build/tabs/Tabs.mjs +1 -1
- package/build/tabs/Tabs.mjs.map +1 -1
- package/build/tooltip/Tooltip.js +6 -3
- package/build/tooltip/Tooltip.js.map +1 -1
- package/build/tooltip/Tooltip.mjs +6 -3
- package/build/tooltip/Tooltip.mjs.map +1 -1
- package/build/types/avatarView/AvatarView.d.ts +1 -1
- package/build/types/avatarView/AvatarView.d.ts.map +1 -1
- package/build/types/avatarView/Dot.d.ts.map +1 -1
- package/build/types/avatarWrapper/AvatarWrapper.d.ts.map +1 -1
- package/build/types/common/circle/Circle.d.ts +1 -1
- package/build/types/common/circle/Circle.d.ts.map +1 -1
- package/build/types/common/hooks/useHasIntersected/useHasIntersected.d.ts.map +1 -1
- package/build/types/common/liveRegion/LiveRegion.d.ts.map +1 -1
- package/build/types/dateLookup/monthCalendar/table/MonthCalendarTable.d.ts.map +1 -1
- package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts.map +1 -1
- package/build/types/expressiveMoneyInput/amountInput/AmountInput.d.ts.map +1 -1
- package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts +2 -2
- package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts.map +1 -1
- package/build/types/expressiveMoneyInput/hooks/useSelectionRange.d.ts.map +1 -1
- package/build/types/field/Field.d.ts.map +1 -1
- package/build/types/field/Field.messages.d.ts +8 -0
- package/build/types/field/Field.messages.d.ts.map +1 -0
- package/build/types/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.d.ts.map +1 -1
- package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts.map +1 -1
- package/build/types/inputs/SelectInput/Popover/SelectInputPopover.d.ts.map +1 -1
- package/build/types/inputs/SelectInput/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/TextArea.d.ts.map +1 -1
- package/build/types/inputs/contexts.d.ts +6 -0
- package/build/types/inputs/contexts.d.ts.map +1 -1
- package/build/types/nudge/Nudge.d.ts.map +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/promoCard/PromoCardGroup.d.ts.map +1 -1
- package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +2 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/build/types/tooltip/Tooltip.d.ts.map +1 -1
- package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
- package/build/uploadInput/UploadInput.js +29 -25
- package/build/uploadInput/UploadInput.js.map +1 -1
- package/build/uploadInput/UploadInput.mjs +29 -25
- package/build/uploadInput/UploadInput.mjs.map +1 -1
- package/package.json +3 -3
- package/src/avatarLayout/AvatarLayout.story.tsx +1 -1
- package/src/avatarLayout/AvatarLayout.tsx +4 -0
- package/src/avatarView/AvatarView.css +4 -4
- package/src/avatarView/AvatarView.story.tsx +17 -13
- package/src/avatarView/AvatarView.tsx +5 -1
- package/src/avatarView/Dot.css +4 -4
- package/src/avatarView/Dot.less +6 -6
- package/src/avatarView/Dot.tsx +2 -0
- package/src/avatarWrapper/AvatarWrapper.test.tsx +33 -3
- package/src/avatarWrapper/AvatarWrapper.tsx +5 -6
- package/src/button/LegacyButton.tsx +1 -1
- package/src/button/_stories/Button.test.story.tsx +3 -3
- package/src/common/circle/Circle.tsx +5 -1
- package/src/common/hooks/useContainerSize.test.tsx +1 -1
- package/src/common/hooks/useHasIntersected/useHasIntersected.ts +12 -4
- package/src/common/liveRegion/LiveRegion.tsx +5 -2
- package/src/dateInput/DateInput.tsx +10 -10
- package/src/dateLookup/monthCalendar/table/MonthCalendarTable.tsx +1 -5
- package/src/dateLookup/yearCalendar/table/YearCalendarTable.tsx +1 -1
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.test.story.tsx +43 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +1 -1
- package/src/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.less +2 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +23 -16
- package/src/expressiveMoneyInput/hooks/useInputStyle.ts +20 -8
- package/src/expressiveMoneyInput/hooks/useSelectionRange.ts +2 -0
- package/src/field/Field.css +19 -3
- package/src/field/Field.less +17 -3
- package/src/field/Field.messages.ts +8 -0
- package/src/field/Field.story.tsx +5 -1
- package/src/field/Field.test.tsx +90 -0
- package/src/field/Field.tsx +84 -37
- package/src/header/Header.tsx +2 -2
- package/src/i18n/en.json +1 -0
- package/src/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.tsx +4 -0
- package/src/inputs/SelectInput/Options/SelectInputOptions.tsx +43 -27
- package/src/inputs/SelectInput/Popover/SelectInputPopover.tsx +4 -0
- package/src/inputs/SelectInput/SelectInput.tsx +21 -15
- package/src/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +1 -1
- package/src/inputs/TextArea.story.tsx +97 -0
- package/src/inputs/TextArea.test.story.tsx +142 -0
- package/src/inputs/TextArea.tsx +7 -2
- package/src/inputs/contexts.tsx +18 -1
- package/src/main.css +42 -8
- package/src/nudge/Nudge.tsx +29 -20
- package/src/phoneNumberInput/PhoneNumberInput.test.tsx +16 -0
- package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -13
- package/src/promoCard/PromoCard.story.tsx +3 -3
- package/src/promoCard/PromoCardGroup.tsx +39 -21
- package/src/segmentedControl/SegmentedControl.test.tsx +25 -0
- package/src/segmentedControl/SegmentedControl.tsx +7 -1
- package/src/select/Select.story.tsx +1 -1
- package/src/styles/less/core/_typography.less +28 -6
- package/src/styles/less/neptune.css +15 -1
- package/src/tabs/Tabs.tsx +1 -1
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +1 -0
- package/src/tooltip/Tooltip.tsx +3 -0
- package/src/uploadInput/UploadInput.test.tsx +19 -0
- package/src/uploadInput/UploadInput.tsx +28 -24
|
@@ -134,10 +134,10 @@ const DateInput = ({
|
|
|
134
134
|
const monthBeforeDay = MDY.has(locale);
|
|
135
135
|
const yearFirst = YMD.has(locale);
|
|
136
136
|
|
|
137
|
-
dayLabel
|
|
138
|
-
monthLabel
|
|
139
|
-
yearLabel
|
|
140
|
-
|
|
137
|
+
const resolvedDayLabel = dayLabel || formatMessage(messages.dayLabel);
|
|
138
|
+
const resolvedMonthLabel = monthLabel || formatMessage(messages.monthLabel);
|
|
139
|
+
const resolvedYearLabel = yearLabel || formatMessage(messages.yearLabel);
|
|
140
|
+
const resolvedPlaceholders = {
|
|
141
141
|
day: placeholders?.day || formatMessage(messages.dayPlaceholder),
|
|
142
142
|
month: placeholders?.month || formatMessage(messages.monthLabel),
|
|
143
143
|
year: placeholders?.year || formatMessage(messages.yearPlaceholder),
|
|
@@ -186,14 +186,14 @@ const DateInput = ({
|
|
|
186
186
|
const getSelectElement = () => {
|
|
187
187
|
return (
|
|
188
188
|
<label className="d-flex flex-column">
|
|
189
|
-
<Body type={Typography.BODY_DEFAULT}>{
|
|
189
|
+
<Body type={Typography.BODY_DEFAULT}>{resolvedMonthLabel}</Body>
|
|
190
190
|
<SelectInput
|
|
191
191
|
triggerRef={monthRef}
|
|
192
192
|
id={`${id}:month`}
|
|
193
193
|
parentId={DATE_INPUT_PARENT_ID}
|
|
194
194
|
name="month"
|
|
195
195
|
disabled={disabled}
|
|
196
|
-
placeholder={
|
|
196
|
+
placeholder={resolvedPlaceholders.month}
|
|
197
197
|
items={Array.from({ length: 12 }, (_, index) => ({ type: 'option', value: index }))}
|
|
198
198
|
size={size}
|
|
199
199
|
value={month}
|
|
@@ -295,7 +295,7 @@ const DateInput = ({
|
|
|
295
295
|
return (
|
|
296
296
|
<div className="col-sm-3 tw-date--day">
|
|
297
297
|
<label>
|
|
298
|
-
<Body type={Typography.BODY_DEFAULT}>{
|
|
298
|
+
<Body type={Typography.BODY_DEFAULT}>{resolvedDayLabel}</Body>
|
|
299
299
|
<div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
|
|
300
300
|
<Input
|
|
301
301
|
ref={dayRef}
|
|
@@ -306,7 +306,7 @@ const DateInput = ({
|
|
|
306
306
|
name="day"
|
|
307
307
|
autoComplete={dayAutoComplete}
|
|
308
308
|
value={displayDay || ''}
|
|
309
|
-
placeholder={
|
|
309
|
+
placeholder={resolvedPlaceholders.day}
|
|
310
310
|
disabled={disabled}
|
|
311
311
|
min={1}
|
|
312
312
|
max={31}
|
|
@@ -323,7 +323,7 @@ const DateInput = ({
|
|
|
323
323
|
return (
|
|
324
324
|
<div className="col-sm-4 tw-date--year">
|
|
325
325
|
<label>
|
|
326
|
-
<Body type={Typography.BODY_DEFAULT}>{
|
|
326
|
+
<Body type={Typography.BODY_DEFAULT}>{resolvedYearLabel}</Body>
|
|
327
327
|
<div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
|
|
328
328
|
<Input
|
|
329
329
|
ref={yearRef}
|
|
@@ -333,7 +333,7 @@ const DateInput = ({
|
|
|
333
333
|
pattern="[0-9]*"
|
|
334
334
|
name="year"
|
|
335
335
|
autoComplete={yearAutoComplete}
|
|
336
|
-
placeholder={
|
|
336
|
+
placeholder={resolvedPlaceholders.year}
|
|
337
337
|
value={displayYear || ''}
|
|
338
338
|
disabled={disabled}
|
|
339
339
|
min={0}
|
|
@@ -43,11 +43,7 @@ const MonthCalendarTable = ({
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
const isActive = (month: number) => {
|
|
46
|
-
return !!(
|
|
47
|
-
selectedDate &&
|
|
48
|
-
month === selectedDate.getMonth() &&
|
|
49
|
-
viewYear === selectedDate.getFullYear()
|
|
50
|
-
);
|
|
46
|
+
return !!(month === selectedDate?.getMonth() && viewYear === selectedDate?.getFullYear());
|
|
51
47
|
};
|
|
52
48
|
|
|
53
49
|
const isThisMonth = (month: number) => {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
flex-grow: 1;
|
|
22
22
|
text-align: right;
|
|
23
23
|
background-color: transparent;
|
|
24
|
+
line-height: inherit;
|
|
24
25
|
}
|
|
25
26
|
.wds-amount-input-input:focus-visible {
|
|
26
27
|
outline: none;
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
flex-grow: 0;
|
|
30
31
|
display: flex;
|
|
31
32
|
align-items: center;
|
|
33
|
+
line-height: inherit;
|
|
32
34
|
}
|
|
33
35
|
.wds-currency-selector:disabled {
|
|
34
36
|
opacity: 1 !important;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
2
3
|
import ExpressiveMoneyInput, { Props as ExpressiveMoneyInputProps } from './ExpressiveMoneyInput';
|
|
3
4
|
|
|
4
5
|
const meta: Meta<typeof ExpressiveMoneyInput> = {
|
|
@@ -21,3 +22,45 @@ export const WithAutofocus: Story = {
|
|
|
21
22
|
autoFocus: true,
|
|
22
23
|
},
|
|
23
24
|
};
|
|
25
|
+
|
|
26
|
+
const locales = [
|
|
27
|
+
{ lang: 'en', label: 'English', currency: 'GBP' },
|
|
28
|
+
{ lang: 'cs', label: 'Czech', currency: 'CZK' },
|
|
29
|
+
{ lang: 'de', label: 'German', currency: 'EUR' },
|
|
30
|
+
{ lang: 'es', label: 'Spanish', currency: 'EUR' },
|
|
31
|
+
{ lang: 'fr', label: 'French', currency: 'EUR' },
|
|
32
|
+
{ lang: 'hu', label: 'Hungarian', currency: 'HUF', supportsDecimals: false },
|
|
33
|
+
{ lang: 'id', label: 'Indonesian', currency: 'IDR', supportsDecimals: false },
|
|
34
|
+
{ lang: 'it', label: 'Italian', currency: 'EUR' },
|
|
35
|
+
{ lang: 'ja', label: 'Japanese', currency: 'JPY', supportsDecimals: false },
|
|
36
|
+
{ lang: 'nl', label: 'Dutch', currency: 'EUR' },
|
|
37
|
+
{ lang: 'pl', label: 'Polish', currency: 'PLN' },
|
|
38
|
+
{ lang: 'pt', label: 'Portuguese', currency: 'EUR' },
|
|
39
|
+
{ lang: 'ro', label: 'Romanian', currency: 'RON' },
|
|
40
|
+
{ lang: 'ru', label: 'Russian', currency: 'RUB' },
|
|
41
|
+
{ lang: 'th', label: 'Thai', currency: 'THB' },
|
|
42
|
+
{ lang: 'tr', label: 'Turkish', currency: 'TRY' },
|
|
43
|
+
{ lang: 'uk', label: 'Ukrainian', currency: 'UAH' },
|
|
44
|
+
{ lang: 'zh-CN', label: 'Simplified Chinese', currency: 'CNY' },
|
|
45
|
+
{ lang: 'zh-HK', label: 'Traditional Chinese', currency: 'HKD' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Verifies that the .wds-text-display--forced class correctly overrides locale-specific
|
|
50
|
+
* font restrictions (ja, zh-HK, zh-CN, th) so amounts render in Wise Sans across all
|
|
51
|
+
* supported locales. Each row represents a different language context.
|
|
52
|
+
*/
|
|
53
|
+
export const LocaleFontOverride = () => (
|
|
54
|
+
<>
|
|
55
|
+
{locales.map(({ lang, label, currency, supportsDecimals = true }) => (
|
|
56
|
+
<div key={lang} lang={lang} className="m-b-4">
|
|
57
|
+
<ExpressiveMoneyInput
|
|
58
|
+
label={`${label} locale (${lang})`}
|
|
59
|
+
currency={currency}
|
|
60
|
+
amount={supportsDecimals ? 1234.56 : 123456}
|
|
61
|
+
onAmountChange={fn()}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
flex-grow: 1;
|
|
22
22
|
text-align: right;
|
|
23
23
|
background-color: transparent;
|
|
24
|
+
line-height: inherit;
|
|
24
25
|
}
|
|
25
26
|
.wds-amount-input-input:focus-visible {
|
|
26
27
|
outline: none;
|
|
@@ -29,4 +30,5 @@
|
|
|
29
30
|
flex-grow: 0;
|
|
30
31
|
display: flex;
|
|
31
32
|
align-items: center;
|
|
33
|
+
line-height: inherit;
|
|
32
34
|
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
flex-grow: 1;
|
|
30
30
|
text-align: right;
|
|
31
31
|
background-color: transparent;
|
|
32
|
+
line-height: inherit;
|
|
32
33
|
|
|
33
34
|
&:focus-visible {
|
|
34
35
|
outline: none;
|
|
@@ -39,5 +40,6 @@
|
|
|
39
40
|
flex-grow: 0;
|
|
40
41
|
display: flex;
|
|
41
42
|
align-items: center;
|
|
43
|
+
line-height: inherit;
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -42,7 +42,7 @@ export const AmountInput = ({
|
|
|
42
42
|
const intl = useIntl();
|
|
43
43
|
const { focus, setFocus, visualFocus, setVisualFocus } = useFocus();
|
|
44
44
|
|
|
45
|
-
const [value, setValue] = useState<string>(
|
|
45
|
+
const [value, setValue] = useState<string>(() =>
|
|
46
46
|
amount
|
|
47
47
|
? getFormattedString({
|
|
48
48
|
value: amount,
|
|
@@ -51,6 +51,7 @@ export const AmountInput = ({
|
|
|
51
51
|
})
|
|
52
52
|
: '',
|
|
53
53
|
);
|
|
54
|
+
const prevAmountRef = useRef<string | number | null | undefined>(amount);
|
|
54
55
|
const numericValue = useMemo(() => {
|
|
55
56
|
return getUnformattedNumber({
|
|
56
57
|
value,
|
|
@@ -85,23 +86,28 @@ export const AmountInput = ({
|
|
|
85
86
|
const decimalMode = decimalSeparator && value.includes(decimalSeparator);
|
|
86
87
|
|
|
87
88
|
useEffect(() => {
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
if (prevAmountRef.current !== amount) {
|
|
90
|
+
prevAmountRef.current = amount;
|
|
91
|
+
|
|
92
|
+
// Only update the displayed value if not focused (preserve user input when focused)
|
|
93
|
+
if (!focus) {
|
|
94
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing external prop to internal state when unfocused
|
|
95
|
+
setValue(
|
|
96
|
+
amount
|
|
97
|
+
? getFormattedString({
|
|
98
|
+
value: amount,
|
|
99
|
+
currency,
|
|
100
|
+
locale: intl.locale,
|
|
101
|
+
})
|
|
102
|
+
: '',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
98
105
|
}
|
|
99
|
-
|
|
100
|
-
}, [amount]);
|
|
106
|
+
}, [amount, focus, currency, intl.locale]);
|
|
101
107
|
|
|
102
108
|
useEffect(() => {
|
|
103
109
|
onFocusChange?.(visualFocus);
|
|
104
|
-
}, [visualFocus]);
|
|
110
|
+
}, [onFocusChange, visualFocus]);
|
|
105
111
|
|
|
106
112
|
const shouldReformatAfterUserInput = (newValue: string) => {
|
|
107
113
|
// don't reformat if formatting would wipe out user's input
|
|
@@ -246,6 +252,7 @@ export const AmountInput = ({
|
|
|
246
252
|
const addonContent = useMemo((): string | null | undefined => {
|
|
247
253
|
// because we're using a separate "addon" element for the placeholder decimals, there is a possibility that the input itself will become scrollable
|
|
248
254
|
// and the decimals will appear on top of the input. Safest thing to do is to just hide the addon if there is not enough room
|
|
255
|
+
// eslint-disable-next-line react-hooks/refs -- Reading layout dimensions for overflow detection
|
|
249
256
|
if (isInputPossiblyOverflowing({ ref, value })) {
|
|
250
257
|
return null;
|
|
251
258
|
}
|
|
@@ -284,14 +291,14 @@ export const AmountInput = ({
|
|
|
284
291
|
// whenever decimals are shown, we need to account for the full decimal part for the font size calculation
|
|
285
292
|
value: addonContent ? valueWithFullDecimals : value,
|
|
286
293
|
focus: visualFocus,
|
|
287
|
-
inputElement: ref
|
|
294
|
+
inputElement: ref,
|
|
288
295
|
loading,
|
|
289
296
|
});
|
|
290
297
|
|
|
291
298
|
return (
|
|
292
299
|
<div className="wds-amount-input-container">
|
|
293
300
|
<div
|
|
294
|
-
className={clsx('wds-amount-input-input-container', 'np-text-display-large')}
|
|
301
|
+
className={clsx('wds-amount-input-input-container', 'np-text-display-large--forced')}
|
|
295
302
|
style={style}
|
|
296
303
|
>
|
|
297
304
|
<input
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type RefObject,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import { Props as ExpressiveMoneyInputProps } from '../ExpressiveMoneyInput';
|
|
3
10
|
|
|
4
11
|
type InputStyleObject = {
|
|
5
12
|
value: string;
|
|
6
13
|
focus: boolean;
|
|
7
|
-
inputElement: HTMLInputElement | null;
|
|
14
|
+
inputElement: RefObject<HTMLInputElement> | null;
|
|
8
15
|
} & Pick<ExpressiveMoneyInputProps, 'loading'>;
|
|
9
16
|
|
|
10
17
|
export const useInputStyle = ({ value, focus, inputElement, loading }: InputStyleObject) => {
|
|
11
18
|
const initialRender = !useTimeout(300);
|
|
12
|
-
const inputWidth = useFirstDefinedValue(inputElement?.clientWidth, [
|
|
19
|
+
const inputWidth = useFirstDefinedValue(inputElement?.current?.clientWidth, [
|
|
20
|
+
inputElement?.current,
|
|
21
|
+
value,
|
|
22
|
+
]);
|
|
13
23
|
|
|
14
|
-
const getStyle = (): CSSProperties => {
|
|
24
|
+
const getStyle = useCallback((): CSSProperties => {
|
|
15
25
|
const fontSize = getFontSize(value, focus, inputWidth);
|
|
16
26
|
|
|
17
27
|
return {
|
|
@@ -23,13 +33,14 @@ export const useInputStyle = ({ value, focus, inputElement, loading }: InputStyl
|
|
|
23
33
|
transition: initialRender ? 'none' : undefined,
|
|
24
34
|
color: loading ? 'var(--color-interactive-secondary)' : undefined,
|
|
25
35
|
};
|
|
26
|
-
};
|
|
36
|
+
}, [value, focus, inputWidth, loading, initialRender]);
|
|
27
37
|
|
|
28
|
-
const [style, setStyle] = useState(getStyle());
|
|
38
|
+
const [style, setStyle] = useState<CSSProperties>(() => getStyle());
|
|
29
39
|
|
|
30
40
|
useLayoutEffect(() => {
|
|
41
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- Computing style based on layout measurements
|
|
31
42
|
setStyle(getStyle());
|
|
32
|
-
}, [value, focus, loading, inputWidth]);
|
|
43
|
+
}, [value, focus, loading, inputWidth, getStyle]);
|
|
33
44
|
|
|
34
45
|
return style;
|
|
35
46
|
};
|
|
@@ -60,10 +71,11 @@ const useFirstDefinedValue = (newValue: number | undefined, dependencies: unknow
|
|
|
60
71
|
|
|
61
72
|
useLayoutEffect(() => {
|
|
62
73
|
if (typeof newValue !== 'undefined' && typeof value === 'undefined') {
|
|
74
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- Lazy initialization from prop
|
|
63
75
|
setValue(newValue);
|
|
64
76
|
}
|
|
65
77
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
-
}, [...dependencies, value]);
|
|
78
|
+
}, [...dependencies, value, newValue]);
|
|
67
79
|
|
|
68
80
|
return value;
|
|
69
81
|
};
|
|
@@ -15,9 +15,11 @@ export const useSelectionRange = () => {
|
|
|
15
15
|
selection.current = undefined;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
/* eslint-disable react-hooks/refs */
|
|
18
19
|
return {
|
|
19
20
|
selection: selection.current,
|
|
20
21
|
handleSelect,
|
|
21
22
|
handleSelectionBlur,
|
|
22
23
|
};
|
|
24
|
+
/* eslint-enable react-hooks/refs */
|
|
23
25
|
};
|
package/src/field/Field.css
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
.np-field-control
|
|
2
|
-
.np-field__prompt {
|
|
1
|
+
.np-field-control {
|
|
3
2
|
margin-top: 4px;
|
|
4
3
|
margin-top: var(--size-4);
|
|
5
4
|
}
|
|
5
|
+
.np-field-validation {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: flex-start;
|
|
8
|
+
margin-top: 4px;
|
|
9
|
+
margin-top: var(--size-4);
|
|
10
|
+
gap: 8px;
|
|
11
|
+
gap: var(--size-8);
|
|
12
|
+
}
|
|
13
|
+
.np-field-textarea-char-counter {
|
|
14
|
+
min-width: 55px;
|
|
15
|
+
text-align: right;
|
|
16
|
+
margin-left: auto;
|
|
17
|
+
padding: 4px 0;
|
|
18
|
+
padding: var(--size-4) 0;
|
|
19
|
+
color: #768e9c;
|
|
20
|
+
color: var(--color-content-tertiary);
|
|
21
|
+
}
|
|
6
22
|
.np-field .form-group--typeahead[class],
|
|
7
23
|
.np-field .np-checkbox-label[class] {
|
|
8
24
|
margin-bottom: 0;
|
|
9
25
|
}
|
|
10
|
-
.np-field:has(.wds-radio-group) .np-
|
|
26
|
+
.np-field:has(.wds-radio-group) .np-field-validation {
|
|
11
27
|
margin-top: 12px;
|
|
12
28
|
margin-top: var(--size-12);
|
|
13
29
|
}
|
package/src/field/Field.less
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
.np-field {
|
|
2
|
-
&-control
|
|
3
|
-
&__prompt {
|
|
2
|
+
&-control {
|
|
4
3
|
margin-top: var(--size-4);
|
|
5
4
|
}
|
|
6
5
|
|
|
6
|
+
&-validation {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: flex-start;
|
|
9
|
+
margin-top: var(--size-4);
|
|
10
|
+
gap: var(--size-8);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&-textarea-char-counter {
|
|
14
|
+
min-width: 55px;
|
|
15
|
+
text-align: right;
|
|
16
|
+
margin-left: auto;
|
|
17
|
+
padding: var(--size-4) 0;
|
|
18
|
+
color: var(--color-content-tertiary);
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
// @FIXME space between individual fields should be 24px, while some older inputs
|
|
8
22
|
// inject extraneous space.
|
|
9
23
|
.form-group--typeahead[class],
|
|
10
24
|
.np-checkbox-label[class] {
|
|
11
25
|
margin-bottom: 0;
|
|
12
26
|
}
|
|
13
|
-
&:has(.wds-radio-group)
|
|
27
|
+
&:has(.wds-radio-group) &-validation {
|
|
14
28
|
margin-top: var(--size-12);
|
|
15
29
|
}
|
|
16
30
|
}
|
|
@@ -84,7 +84,11 @@ export const Basic = (args: FieldProps) => {
|
|
|
84
84
|
messageIconLabel={args.messageIconLabel}
|
|
85
85
|
messageLoading={args.messageLoading}
|
|
86
86
|
>
|
|
87
|
-
<TextArea
|
|
87
|
+
<TextArea
|
|
88
|
+
maxLength={200}
|
|
89
|
+
value={value}
|
|
90
|
+
onChange={({ target }) => setValue(target.value)}
|
|
91
|
+
/>
|
|
88
92
|
</Field>
|
|
89
93
|
|
|
90
94
|
<Field
|
package/src/field/Field.test.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Info from '../info/Info';
|
|
2
2
|
import { Input } from '../inputs/Input';
|
|
3
|
+
import { TextArea } from '../inputs/TextArea';
|
|
3
4
|
import { mockMatchMedia, render, screen, userEvent } from '../test-utils';
|
|
4
5
|
|
|
5
6
|
import { Field } from './Field';
|
|
@@ -163,4 +164,93 @@ describe('Field', () => {
|
|
|
163
164
|
expect(screen.getByTestId('InlinePrompt_ProcessIndicator')).toBeInTheDocument();
|
|
164
165
|
expect(screen.getByText('Processing your request')).toBeInTheDocument();
|
|
165
166
|
});
|
|
167
|
+
|
|
168
|
+
describe('TextArea character count', () => {
|
|
169
|
+
it('renders counter when TextArea has maxLength', () => {
|
|
170
|
+
render(
|
|
171
|
+
<Field label="Message">
|
|
172
|
+
<TextArea maxLength={200} value="hello" onChange={() => {}} />
|
|
173
|
+
</Field>,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(screen.getByText('5/200')).toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does not render counter when TextArea has no maxLength', () => {
|
|
180
|
+
render(
|
|
181
|
+
<Field label="Message">
|
|
182
|
+
<TextArea value="hello" onChange={() => {}} />
|
|
183
|
+
</Field>,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(screen.queryByText(/\/\d+/)).not.toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('includes counter id in aria-describedby of the textarea', () => {
|
|
190
|
+
render(
|
|
191
|
+
<Field label="Message">
|
|
192
|
+
<TextArea maxLength={200} value="hello" onChange={() => {}} />
|
|
193
|
+
</Field>,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const textarea = screen.getByRole('textbox');
|
|
197
|
+
const counter = screen.getByText('5/200');
|
|
198
|
+
expect(textarea).toHaveAttribute('aria-describedby', expect.stringContaining(counter.id));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not have role=status below 80% threshold', () => {
|
|
202
|
+
render(
|
|
203
|
+
<Field label="Message">
|
|
204
|
+
<TextArea maxLength={200} value="short" onChange={() => {}} />
|
|
205
|
+
</Field>,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const counter = screen.getByText('5/200');
|
|
209
|
+
expect(counter).not.toHaveAttribute('role');
|
|
210
|
+
expect(counter).not.toHaveAttribute('aria-live');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('has role=status and aria-live=polite at 80% threshold', () => {
|
|
214
|
+
const value = 'x'.repeat(160);
|
|
215
|
+
render(
|
|
216
|
+
<Field label="Message">
|
|
217
|
+
<TextArea maxLength={200} value={value} onChange={() => {}} />
|
|
218
|
+
</Field>,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const counter = screen.getByText('160/200');
|
|
222
|
+
expect(counter).toHaveAttribute('role', 'status');
|
|
223
|
+
expect(counter).toHaveAttribute('aria-live', 'polite');
|
|
224
|
+
expect(counter).toHaveAttribute('aria-atomic', 'true');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('updates counter when text changes', () => {
|
|
228
|
+
const { rerender } = render(
|
|
229
|
+
<Field label="Message">
|
|
230
|
+
<TextArea maxLength={200} value="hi" onChange={() => {}} />
|
|
231
|
+
</Field>,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(screen.getByText('2/200')).toBeInTheDocument();
|
|
235
|
+
|
|
236
|
+
rerender(
|
|
237
|
+
<Field label="Message">
|
|
238
|
+
<TextArea maxLength={200} value="hello world" onChange={() => {}} />
|
|
239
|
+
</Field>,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(screen.getByText('11/200')).toBeInTheDocument();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('provides accessible aria-label on the counter', () => {
|
|
246
|
+
render(
|
|
247
|
+
<Field label="Message">
|
|
248
|
+
<TextArea maxLength={200} value="hello" onChange={() => {}} />
|
|
249
|
+
</Field>,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const counter = screen.getByText('5/200');
|
|
253
|
+
expect(counter).toHaveAttribute('aria-label', '5 of 200 characters used');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
166
256
|
});
|