@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
package/src/field/Field.tsx
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import { useId, useRef } from 'react';
|
|
2
|
+
import { useCallback, useId, useRef, useState } from 'react';
|
|
3
|
+
import { useIntl } from 'react-intl';
|
|
3
4
|
|
|
5
|
+
import Body from '../body';
|
|
4
6
|
import { Sentiment } from '../common';
|
|
7
|
+
import messages from './Field.messages';
|
|
5
8
|
import { InlinePrompt, type InlinePromptProps } from '../prompt';
|
|
6
9
|
import {
|
|
10
|
+
TextareaCharacterCountProvider,
|
|
11
|
+
type TextareaCharacterCountState,
|
|
7
12
|
FieldLabelContextProvider,
|
|
8
13
|
InputDescribedByProvider,
|
|
9
14
|
InputIdContextProvider,
|
|
@@ -54,6 +59,7 @@ export const Field = ({
|
|
|
54
59
|
children,
|
|
55
60
|
...props
|
|
56
61
|
}: FieldProps) => {
|
|
62
|
+
const { formatMessage } = useIntl();
|
|
57
63
|
const labelRef = useRef<HTMLLabelElement>(null);
|
|
58
64
|
const sentiment = props.error ? Sentiment.NEGATIVE : propType;
|
|
59
65
|
const message = propMessage || props.error;
|
|
@@ -66,6 +72,18 @@ export const Field = ({
|
|
|
66
72
|
|
|
67
73
|
const messageId = useId();
|
|
68
74
|
const descriptionId = useId();
|
|
75
|
+
const textareaCharCounterId = useId();
|
|
76
|
+
|
|
77
|
+
const [textareaCharacterCount, setTextareaCharacterCount] =
|
|
78
|
+
useState<TextareaCharacterCountState>(null);
|
|
79
|
+
const handleTextareaCharacterCount = useCallback(
|
|
80
|
+
(state: TextareaCharacterCountState) => setTextareaCharacterCount(state),
|
|
81
|
+
[],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const isNearCharLimit =
|
|
85
|
+
textareaCharacterCount != null &&
|
|
86
|
+
textareaCharacterCount.current >= textareaCharacterCount.max * 0.8;
|
|
69
87
|
|
|
70
88
|
/**
|
|
71
89
|
* form control can have multiple messages to describe it,
|
|
@@ -79,6 +97,9 @@ export const Field = ({
|
|
|
79
97
|
if (message) {
|
|
80
98
|
messageIds.push(messageId);
|
|
81
99
|
}
|
|
100
|
+
if (textareaCharacterCount) {
|
|
101
|
+
messageIds.push(textareaCharCounterId);
|
|
102
|
+
}
|
|
82
103
|
return messageIds.length > 0 ? messageIds.join(' ') : undefined;
|
|
83
104
|
}
|
|
84
105
|
|
|
@@ -87,43 +108,69 @@ export const Field = ({
|
|
|
87
108
|
<InputIdContextProvider value={inputId}>
|
|
88
109
|
<InputDescribedByProvider value={ariaDescribedbyByIds()}>
|
|
89
110
|
<InputInvalidProvider value={hasError}>
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
<TextareaCharacterCountProvider value={handleTextareaCharacterCount}>
|
|
112
|
+
<div
|
|
113
|
+
className={clsx(
|
|
114
|
+
'np-field form-group d-block',
|
|
115
|
+
{
|
|
116
|
+
'has-success': sentiment === Sentiment.POSITIVE,
|
|
117
|
+
'has-warning': sentiment === Sentiment.WARNING,
|
|
118
|
+
'has-error': hasError,
|
|
119
|
+
'has-info': sentiment === Sentiment.NEUTRAL,
|
|
120
|
+
},
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{label != null ? (
|
|
125
|
+
<>
|
|
126
|
+
<Label ref={labelRef} id={labelId} htmlFor={inputId}>
|
|
127
|
+
{required ? label : <Label.Optional>{label}</Label.Optional>}
|
|
128
|
+
</Label>
|
|
129
|
+
<Label.Description id={descriptionId}>{description}</Label.Description>
|
|
130
|
+
<div className="np-field-control">{children}</div>
|
|
131
|
+
</>
|
|
132
|
+
) : (
|
|
133
|
+
children
|
|
134
|
+
)}
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
{(message || textareaCharacterCount) && (
|
|
137
|
+
<div className="np-field-validation">
|
|
138
|
+
{message && (
|
|
139
|
+
<InlinePrompt
|
|
140
|
+
sentiment={sentiment}
|
|
141
|
+
id={messageId}
|
|
142
|
+
mediaLabel={messageIconLabel}
|
|
143
|
+
className="np-field__prompt"
|
|
144
|
+
loading={messageLoading}
|
|
145
|
+
width="full"
|
|
146
|
+
>
|
|
147
|
+
{message}
|
|
148
|
+
</InlinePrompt>
|
|
149
|
+
)}
|
|
150
|
+
{textareaCharacterCount && (
|
|
151
|
+
<Body
|
|
152
|
+
as="span"
|
|
153
|
+
id={textareaCharCounterId}
|
|
154
|
+
{...(isNearCharLimit
|
|
155
|
+
? {
|
|
156
|
+
role: 'status' as const,
|
|
157
|
+
'aria-live': 'polite' as const,
|
|
158
|
+
'aria-atomic': 'true' as const,
|
|
159
|
+
}
|
|
160
|
+
: {})}
|
|
161
|
+
aria-label={formatMessage(messages.characterCount, {
|
|
162
|
+
current: textareaCharacterCount.current,
|
|
163
|
+
max: textareaCharacterCount.max,
|
|
164
|
+
})}
|
|
165
|
+
className="np-field-textarea-char-counter"
|
|
166
|
+
>
|
|
167
|
+
{textareaCharacterCount.current}/{textareaCharacterCount.max}
|
|
168
|
+
</Body>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</TextareaCharacterCountProvider>
|
|
127
174
|
</InputInvalidProvider>
|
|
128
175
|
</InputDescribedByProvider>
|
|
129
176
|
</InputIdContextProvider>
|
package/src/header/Header.tsx
CHANGED
|
@@ -104,7 +104,7 @@ const Header: FunctionComponent<HeaderProps> = React.forwardRef(
|
|
|
104
104
|
useEffect(() => {
|
|
105
105
|
if (as === 'legend' && internalRef.current) {
|
|
106
106
|
const { parentElement } = internalRef.current;
|
|
107
|
-
if (
|
|
107
|
+
if (parentElement?.tagName.toLowerCase() !== 'fieldset') {
|
|
108
108
|
console.warn(
|
|
109
109
|
'Legends should be the first child in a fieldset, and this is not possible when including an action',
|
|
110
110
|
);
|
|
@@ -121,7 +121,7 @@ const Header: FunctionComponent<HeaderProps> = React.forwardRef(
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
return (
|
|
124
|
-
<div {...commonProps} {...props} ref={ref
|
|
124
|
+
<div {...commonProps} {...props} ref={ref}>
|
|
125
125
|
<Title as={as} type={levelTypography} className="np-header__title">
|
|
126
126
|
{title}
|
|
127
127
|
</Title>
|
package/src/i18n/en.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"neptune.Expander.expandAriaLabel": "Expand",
|
|
21
21
|
"neptune.ExpressiveMoneyInput.currency.search.placeholder": "Type a currency / country",
|
|
22
22
|
"neptune.ExpressiveMoneyInput.currency.select.currency": "Select currency",
|
|
23
|
+
"neptune.Field.characterCount": "{current} of {max} characters used",
|
|
23
24
|
"neptune.FlowNavigation.back": "back to previous step",
|
|
24
25
|
"neptune.Info.ariaLabel": "More information",
|
|
25
26
|
"neptune.Label.optional": "(Optional)",
|
|
@@ -65,10 +65,12 @@ export function SelectInputBottomSheet({
|
|
|
65
65
|
return (
|
|
66
66
|
<>
|
|
67
67
|
{open ? <PreventScroll /> : null}
|
|
68
|
+
{/* eslint-disable react-hooks/refs -- setReference is a callback ref from floating-ui, safe to pass during render */}
|
|
68
69
|
{renderTrigger?.({
|
|
69
70
|
ref: refs.setReference,
|
|
70
71
|
getInteractionProps: getReferenceProps,
|
|
71
72
|
})}
|
|
73
|
+
{/* eslint-enable react-hooks/refs */}
|
|
72
74
|
|
|
73
75
|
<FloatingPortal>
|
|
74
76
|
<ThemeProvider theme="personal" screenMode={theme === 'personal' ? screenMode : 'light'}>
|
|
@@ -94,6 +96,7 @@ export function SelectInputBottomSheet({
|
|
|
94
96
|
<Fragment
|
|
95
97
|
key={floatingKey} // Force inner state invalidation on open
|
|
96
98
|
>
|
|
99
|
+
{/* eslint-disable react-hooks/refs -- setFloating is a callback ref from floating-ui, safe to pass during render */}
|
|
97
100
|
<TransitionChild
|
|
98
101
|
ref={refs.setFloating}
|
|
99
102
|
as="div"
|
|
@@ -102,6 +105,7 @@ export function SelectInputBottomSheet({
|
|
|
102
105
|
leaveTo="np-bottom-sheet-v2-content--closed"
|
|
103
106
|
{...getFloatingProps()}
|
|
104
107
|
>
|
|
108
|
+
{/* eslint-enable react-hooks/refs */}
|
|
105
109
|
<div className="np-bottom-sheet-v2-header">
|
|
106
110
|
<CloseButton
|
|
107
111
|
size={Size.SMALL}
|
|
@@ -77,7 +77,7 @@ export function SelectInputOptions<T = string>({
|
|
|
77
77
|
const intl = useIntl();
|
|
78
78
|
const virtualiserHandlerRef = useRef<VirtualizerHandle>(null);
|
|
79
79
|
const controllerRef = filterable ? searchInputRef : listboxRef;
|
|
80
|
-
const
|
|
80
|
+
const initialRenderRef = useRef(true);
|
|
81
81
|
|
|
82
82
|
const needle = useMemo(() => {
|
|
83
83
|
if (filterable) {
|
|
@@ -166,28 +166,42 @@ export function SelectInputOptions<T = string>({
|
|
|
166
166
|
// Items shown once shall be kept mounted until the needle changes, otherwise
|
|
167
167
|
// the scroll position may jump around inadvertently. Pattern adopted from:
|
|
168
168
|
// https://inokawa.github.io/virtua/?path=/story/advanced-keep-offscreen-items--append-only
|
|
169
|
-
const [
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
const [virtualState, setVirtualState] = useState<{
|
|
170
|
+
needle: typeof needle;
|
|
171
|
+
length: number;
|
|
172
|
+
mountedIndexes: number[];
|
|
173
|
+
}>({
|
|
174
|
+
needle,
|
|
175
|
+
length: filteredItems.length,
|
|
176
|
+
mountedIndexes: [],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Note: virtualState.mountedIndexes is in deps but only read in the guarded branch.
|
|
180
|
+
// This means external updates to mountedIndexes will trigger this effect but hit the guard
|
|
181
|
+
// and bail out early. This is intentional and harmless - the guard ensures no unnecessary work.
|
|
172
182
|
useEffect(() => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (filteredItems.length > 0) {
|
|
185
|
-
setMountedIndexes((prevMountedIndexes) => {
|
|
186
|
-
// Create a new array with existing indexes plus the last item index
|
|
187
|
-
return [...new Set([...prevMountedIndexes, filteredItems.length - 1])]; // Sorting is redundant by nature here
|
|
183
|
+
if (virtualState.needle !== needle || virtualState.length !== filteredItems.length) {
|
|
184
|
+
const needleChanged = virtualState.needle !== needle;
|
|
185
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing virtual scroll state with filtered items
|
|
186
|
+
setVirtualState({
|
|
187
|
+
needle,
|
|
188
|
+
length: filteredItems.length,
|
|
189
|
+
mountedIndexes: needleChanged
|
|
190
|
+
? [] // Reset on needle change
|
|
191
|
+
: filteredItems.length > 0
|
|
192
|
+
? [...new Set([...virtualState.mountedIndexes, filteredItems.length - 1])] // Add last index
|
|
193
|
+
: virtualState.mountedIndexes,
|
|
188
194
|
});
|
|
189
195
|
}
|
|
190
|
-
}, [
|
|
196
|
+
}, [
|
|
197
|
+
needle,
|
|
198
|
+
filteredItems.length,
|
|
199
|
+
virtualState.needle,
|
|
200
|
+
virtualState.length,
|
|
201
|
+
virtualState.mountedIndexes,
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
const { mountedIndexes } = virtualState;
|
|
191
205
|
|
|
192
206
|
const listboxContainerRef = useRef<HTMLDivElement>(null);
|
|
193
207
|
useEffect(() => {
|
|
@@ -200,7 +214,7 @@ export function SelectInputOptions<T = string>({
|
|
|
200
214
|
}, []);
|
|
201
215
|
|
|
202
216
|
useEffect(() => {
|
|
203
|
-
|
|
217
|
+
initialRenderRef.current = false;
|
|
204
218
|
}, []);
|
|
205
219
|
|
|
206
220
|
const showStatus = resultsEmpty;
|
|
@@ -251,7 +265,7 @@ export function SelectInputOptions<T = string>({
|
|
|
251
265
|
className="np-select-input-options-container"
|
|
252
266
|
onAriaActiveDescendantChange={(value: React.AriaAttributes['aria-activedescendant']) => {
|
|
253
267
|
if (controllerRef.current != null) {
|
|
254
|
-
if (!
|
|
268
|
+
if (!initialRenderRef.current && value != null) {
|
|
255
269
|
controllerRef.current.setAttribute('aria-activedescendant', value);
|
|
256
270
|
} else {
|
|
257
271
|
controllerRef.current.removeAttribute('aria-activedescendant');
|
|
@@ -288,7 +302,7 @@ export function SelectInputOptions<T = string>({
|
|
|
288
302
|
const inputValue = event.currentTarget.value;
|
|
289
303
|
|
|
290
304
|
// Free up resources and ensure not to go out of bounds
|
|
291
|
-
|
|
305
|
+
setVirtualState((prev) => ({ ...prev, mountedIndexes: [] }));
|
|
292
306
|
onFilterChange(inputValue);
|
|
293
307
|
}}
|
|
294
308
|
onInput={(event) => {
|
|
@@ -358,7 +372,7 @@ export function SelectInputOptions<T = string>({
|
|
|
358
372
|
virtualiserHandlerRef.current.viewportSize,
|
|
359
373
|
);
|
|
360
374
|
|
|
361
|
-
|
|
375
|
+
setVirtualState((prev) => {
|
|
362
376
|
// Create an array of all indexes that should be visible
|
|
363
377
|
|
|
364
378
|
const visibleIndexes = [];
|
|
@@ -368,9 +382,11 @@ export function SelectInputOptions<T = string>({
|
|
|
368
382
|
}
|
|
369
383
|
|
|
370
384
|
// Combine with previous indexes and sort
|
|
371
|
-
|
|
372
|
-
(
|
|
373
|
-
);
|
|
385
|
+
const newMountedIndexes = [
|
|
386
|
+
...new Set([...prev.mountedIndexes, ...visibleIndexes]),
|
|
387
|
+
].sort((a, b) => a - b);
|
|
388
|
+
|
|
389
|
+
return { ...prev, mountedIndexes: newMountedIndexes };
|
|
374
390
|
});
|
|
375
391
|
}}
|
|
376
392
|
>
|
|
@@ -85,10 +85,12 @@ export function SelectInputPopover({
|
|
|
85
85
|
return (
|
|
86
86
|
<>
|
|
87
87
|
{open ? <PreventScroll /> : null}
|
|
88
|
+
{/* eslint-disable react-hooks/refs -- setReference is a callback ref from floating-ui, safe to pass during render */}
|
|
88
89
|
{renderTrigger({
|
|
89
90
|
ref: refs.setReference,
|
|
90
91
|
getInteractionProps: getReferenceProps,
|
|
91
92
|
})}
|
|
93
|
+
{/* eslint-enable react-hooks/refs */}
|
|
92
94
|
|
|
93
95
|
<FloatingPortal>
|
|
94
96
|
<ThemeProvider theme="personal" screenMode={theme === 'personal' ? screenMode : 'light'}>
|
|
@@ -104,6 +106,7 @@ export function SelectInputPopover({
|
|
|
104
106
|
>
|
|
105
107
|
<FocusScope>
|
|
106
108
|
<FloatingFocusManager context={context}>
|
|
109
|
+
{/* eslint-disable react-hooks/refs -- setFloating is a callback ref from floating-ui, safe to pass during render */}
|
|
107
110
|
<div
|
|
108
111
|
key={floatingKey} // Force inner state invalidation on open
|
|
109
112
|
ref={refs.setFloating}
|
|
@@ -114,6 +117,7 @@ export function SelectInputPopover({
|
|
|
114
117
|
style={floatingStyles}
|
|
115
118
|
{...getFloatingProps()}
|
|
116
119
|
>
|
|
120
|
+
{/* eslint-enable react-hooks/refs */}
|
|
117
121
|
<div
|
|
118
122
|
className={clsx('np-popover-v2', title && 'np-popover-v2--has-title', {
|
|
119
123
|
'np-popover-v2--padding-md': padding === 'md',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import mergeProps from 'merge-props';
|
|
2
|
-
import { useEffect, useRef, useState, useDeferredValue } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState, useDeferredValue } from 'react';
|
|
3
3
|
import { Listbox as ListboxBase } from '@headlessui/react';
|
|
4
4
|
import { useScreenSize } from '../../common/hooks/useScreenSize';
|
|
5
5
|
import { Breakpoint } from '../../common/propsValues/breakpoint';
|
|
@@ -60,6 +60,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
60
60
|
const initialized = useRef(false);
|
|
61
61
|
const handleClose = useEffectEvent(onClose ?? (() => {}));
|
|
62
62
|
const handleOpen = useEffectEvent(onOpen ?? (() => {}));
|
|
63
|
+
|
|
63
64
|
useEffect(() => {
|
|
64
65
|
if (initialized.current) {
|
|
65
66
|
if (open) {
|
|
@@ -70,29 +71,34 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
70
71
|
} else {
|
|
71
72
|
initialized.current = true;
|
|
72
73
|
}
|
|
73
|
-
}, [
|
|
74
|
+
}, [open]);
|
|
74
75
|
|
|
75
76
|
const [filterQuery, _setFilterQuery] = useState('');
|
|
76
77
|
const deferredFilterQuery = useDeferredValue(filterQuery);
|
|
77
|
-
const
|
|
78
|
-
_setFilterQuery(query);
|
|
79
|
-
if (query !== filterQuery) {
|
|
80
|
-
onFilterChange({
|
|
81
|
-
query,
|
|
82
|
-
queryNormalized: query ? searchableString(query) : null,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
|
|
78
|
+
const previousFilterQueryRef = useRef(filterQuery);
|
|
88
79
|
|
|
89
|
-
const
|
|
90
|
-
|
|
80
|
+
const setFilterQuery = useCallback(
|
|
81
|
+
(query: string) => {
|
|
82
|
+
_setFilterQuery(query);
|
|
83
|
+
if (query !== previousFilterQueryRef.current) {
|
|
84
|
+
onFilterChange({
|
|
85
|
+
query,
|
|
86
|
+
queryNormalized: query ? searchableString(query) : null,
|
|
87
|
+
});
|
|
88
|
+
previousFilterQueryRef.current = query;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[onFilterChange],
|
|
92
|
+
);
|
|
91
93
|
|
|
94
|
+
const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
|
|
92
95
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
93
96
|
const listboxRef = useRef<HTMLDivElement>(null);
|
|
94
97
|
const controllerRef = filterable ? searchInputRef : listboxRef;
|
|
95
98
|
|
|
99
|
+
const screenSm = useScreenSize(Breakpoint.SMALL);
|
|
100
|
+
const OptionsOverlay = screenSm ? SelectInputPopover : SelectInputBottomSheet;
|
|
101
|
+
|
|
96
102
|
/**
|
|
97
103
|
* Attempts to resolve the `listbox` label
|
|
98
104
|
* @see https://storybook.wise.design/?path=/docs/forms-selectinput-accessibility--docs#labelling
|
|
@@ -29,7 +29,7 @@ export function SelectInputTriggerButton<T extends SelectInputTriggerButtonEleme
|
|
|
29
29
|
ref={ref}
|
|
30
30
|
as={PolymorphicWithOverrides}
|
|
31
31
|
role="combobox"
|
|
32
|
-
__overrides={{ as, size, ...interactionProps }
|
|
32
|
+
__overrides={{ as, size, ...interactionProps }}
|
|
33
33
|
{...mergeProps({ onClick, onKeyDown }, restProps)}
|
|
34
34
|
/>
|
|
35
35
|
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
+
|
|
4
|
+
import { Field } from '../field/Field';
|
|
5
|
+
import { Sentiment } from '../common';
|
|
6
|
+
import { TextArea } from './TextArea';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TextArea must be the only input with `maxLength` inside a given Field.
|
|
10
|
+
* The character counter uses shared context β multiple TextAreas with maxLength
|
|
11
|
+
* in the same Field will race to set the counter state.
|
|
12
|
+
*/
|
|
13
|
+
const meta: Meta<typeof TextArea> = {
|
|
14
|
+
title: 'Forms/TextArea',
|
|
15
|
+
component: TextArea,
|
|
16
|
+
argTypes: {
|
|
17
|
+
value: {
|
|
18
|
+
control: 'text',
|
|
19
|
+
},
|
|
20
|
+
maxLength: {
|
|
21
|
+
control: 'number',
|
|
22
|
+
},
|
|
23
|
+
placeholder: {
|
|
24
|
+
control: 'text',
|
|
25
|
+
},
|
|
26
|
+
disabled: {
|
|
27
|
+
control: 'boolean',
|
|
28
|
+
},
|
|
29
|
+
rows: {
|
|
30
|
+
control: 'number',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof TextArea>;
|
|
37
|
+
|
|
38
|
+
/** Explore all props via the controls panel. */
|
|
39
|
+
export const Playground: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
value: '',
|
|
42
|
+
maxLength: 200,
|
|
43
|
+
placeholder: 'Type something...',
|
|
44
|
+
},
|
|
45
|
+
render: (args) => {
|
|
46
|
+
const [value, setValue] = useState(args.value ?? '');
|
|
47
|
+
useEffect(() => setValue(args.value ?? ''), [args.value]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Field label="Message" required={false}>
|
|
51
|
+
<TextArea {...args} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
52
|
+
</Field>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const Basic: Story = {
|
|
58
|
+
render: () => {
|
|
59
|
+
const [value, setValue] = useState('');
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Field label="Message" required={false}>
|
|
63
|
+
<TextArea maxLength={200} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
64
|
+
</Field>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
parameters: {
|
|
68
|
+
docs: {
|
|
69
|
+
source: {
|
|
70
|
+
code: `<Field label="Message" required={false}>
|
|
71
|
+
<TextArea
|
|
72
|
+
maxLength={200}
|
|
73
|
+
value={value}
|
|
74
|
+
onChange={({ target }) => setValue(target.value)}
|
|
75
|
+
/>
|
|
76
|
+
</Field>`,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const WithError: Story = {
|
|
83
|
+
render: () => {
|
|
84
|
+
const [value, setValue] = useState('');
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Field
|
|
88
|
+
label="Message"
|
|
89
|
+
required={false}
|
|
90
|
+
sentiment={Sentiment.NEGATIVE}
|
|
91
|
+
message="You have exceeded the character limit"
|
|
92
|
+
>
|
|
93
|
+
<TextArea maxLength={200} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
94
|
+
</Field>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { userEvent, within } from 'storybook/test';
|
|
3
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
4
|
+
|
|
5
|
+
import { Field } from '../field/Field';
|
|
6
|
+
import { TextArea } from './TextArea';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof TextArea> = {
|
|
9
|
+
title: 'Forms/TextArea/Tests',
|
|
10
|
+
component: TextArea,
|
|
11
|
+
tags: ['!autodocs', '!manifest'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof TextArea>;
|
|
16
|
+
|
|
17
|
+
export const AsciiCharacters: Story = {
|
|
18
|
+
name: 'counter at limit with ASCII input',
|
|
19
|
+
render: () => {
|
|
20
|
+
const [value, setValue] = useState('');
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Field label="Message" required={false}>
|
|
24
|
+
<TextArea maxLength={10} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
25
|
+
</Field>
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
play: async ({ canvasElement }) => {
|
|
29
|
+
const canvas = within(canvasElement);
|
|
30
|
+
await userEvent.type(canvas.getByRole('textbox'), '0123456789');
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const EmojiCharacters: Story = {
|
|
35
|
+
name: 'counter with emoji input',
|
|
36
|
+
render: () => {
|
|
37
|
+
const [value, setValue] = useState('');
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Field label="Message" required={false}>
|
|
41
|
+
<TextArea maxLength={10} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
42
|
+
</Field>
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
play: async ({ canvasElement }) => {
|
|
46
|
+
const canvas = within(canvasElement);
|
|
47
|
+
const textarea = canvas.getByRole('textbox');
|
|
48
|
+
await userEvent.click(textarea);
|
|
49
|
+
await userEvent.paste('π±ππ±π');
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const EmojiAtLimit: Story = {
|
|
54
|
+
name: 'truncates emoji paste exceeding limit',
|
|
55
|
+
render: () => {
|
|
56
|
+
const [value, setValue] = useState('');
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Field label="Message" required={false}>
|
|
60
|
+
<TextArea maxLength={10} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
61
|
+
</Field>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
play: async ({ canvasElement }) => {
|
|
65
|
+
const canvas = within(canvasElement);
|
|
66
|
+
const textarea = canvas.getByRole('textbox');
|
|
67
|
+
await userEvent.click(textarea);
|
|
68
|
+
await userEvent.paste('π±ππ±ππ±ππ±ππ±ππ±ππ±π');
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const CJKCharacters: Story = {
|
|
73
|
+
name: 'truncates CJK paste exceeding limit',
|
|
74
|
+
render: () => {
|
|
75
|
+
const [value, setValue] = useState('');
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Field label="Message" required={false}>
|
|
79
|
+
<TextArea maxLength={10} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
80
|
+
</Field>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
play: async ({ canvasElement }) => {
|
|
84
|
+
const canvas = within(canvasElement);
|
|
85
|
+
const textarea = canvas.getByRole('textbox');
|
|
86
|
+
await userEvent.click(textarea);
|
|
87
|
+
await userEvent.paste('επ£Ίεπ£Ίεπ£Ίεπ£Ίεπ£Ίεπ£Ί');
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const InitialValueExceedsLimit: Story = {
|
|
92
|
+
name: 'initial value exceeding maxLength is preserved',
|
|
93
|
+
render: () => {
|
|
94
|
+
const [value, setValue] = useState('Hello World!');
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Field label="Message" required={false}>
|
|
98
|
+
<TextArea maxLength={3} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
99
|
+
</Field>
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const EmojiTypingBlocked: Story = {
|
|
105
|
+
name: 'blocks emoji paste when already at limit',
|
|
106
|
+
render: () => {
|
|
107
|
+
const [value, setValue] = useState('');
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Field label="Message" required={false}>
|
|
111
|
+
<TextArea maxLength={3} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
112
|
+
</Field>
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
play: async ({ canvasElement }) => {
|
|
116
|
+
const canvas = within(canvasElement);
|
|
117
|
+
const textarea = canvas.getByRole('textbox');
|
|
118
|
+
await userEvent.click(textarea);
|
|
119
|
+
await userEvent.paste('π±π');
|
|
120
|
+
await userEvent.paste('πππ');
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const CJKTypingBlocked: Story = {
|
|
125
|
+
name: 'blocks CJK paste when already at limit',
|
|
126
|
+
render: () => {
|
|
127
|
+
const [value, setValue] = useState('');
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Field label="Message" required={false}>
|
|
131
|
+
<TextArea maxLength={3} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
132
|
+
</Field>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
play: async ({ canvasElement }) => {
|
|
136
|
+
const canvas = within(canvasElement);
|
|
137
|
+
const textarea = canvas.getByRole('textbox');
|
|
138
|
+
await userEvent.click(textarea);
|
|
139
|
+
await userEvent.paste('επ£Ί');
|
|
140
|
+
await userEvent.paste('επ£Ίεπ£Ίεπ£Ί');
|
|
141
|
+
},
|
|
142
|
+
};
|