@transferwise/components 46.141.0 → 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/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/expressiveMoneyInput/amountInput/AmountInput.js +1 -1
- package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -1
- package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +1 -1
- package/build/expressiveMoneyInput/amountInput/AmountInput.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/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/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/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/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/common/circle/Circle.d.ts +1 -1
- package/build/types/common/circle/Circle.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/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/test-utils/index.d.ts +2 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +2 -2
- 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/common/circle/Circle.tsx +5 -1
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.test.story.tsx +43 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.less +2 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +1 -1
- 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/i18n/en.json +1 -0
- 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/styles/less/core/_typography.less +28 -6
- package/src/styles/less/neptune.css +15 -1
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +1 -0
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/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)",
|
|
@@ -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
|
+
};
|
package/src/inputs/TextArea.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { Merge } from '../utils';
|
|
5
5
|
import { inputClassNameBase } from './_common';
|
|
6
|
-
import { useInputAttributes } from './contexts';
|
|
6
|
+
import { useTextareaCharacterCount, useInputAttributes } from './contexts';
|
|
7
7
|
|
|
8
8
|
export interface TextAreaProps extends Merge<
|
|
9
9
|
React.ComponentPropsWithRef<'textarea'>,
|
|
@@ -13,15 +13,20 @@ export interface TextAreaProps extends Merge<
|
|
|
13
13
|
> {}
|
|
14
14
|
|
|
15
15
|
export const TextArea = forwardRef(function TextArea(
|
|
16
|
-
{ className, ...restProps }: TextAreaProps,
|
|
16
|
+
{ className, maxLength, ...restProps }: TextAreaProps,
|
|
17
17
|
reference: React.ForwardedRef<HTMLTextAreaElement | null>,
|
|
18
18
|
) {
|
|
19
19
|
const inputAttributes = useInputAttributes();
|
|
20
|
+
const value = restProps.value ?? restProps.defaultValue ?? '';
|
|
21
|
+
const currentLength = typeof value === 'string' ? value.length : String(value).length;
|
|
22
|
+
|
|
23
|
+
useTextareaCharacterCount(currentLength, maxLength);
|
|
20
24
|
|
|
21
25
|
return (
|
|
22
26
|
<textarea
|
|
23
27
|
ref={reference}
|
|
24
28
|
className={clsx(className, inputClassNameBase(), 'np-text-area')}
|
|
29
|
+
maxLength={maxLength}
|
|
25
30
|
{...inputAttributes}
|
|
26
31
|
{...restProps}
|
|
27
32
|
/>
|
package/src/inputs/contexts.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react';
|
|
1
|
+
import { createContext, useContext, useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
type FieldLabelContextType = {
|
|
4
4
|
id?: string;
|
|
@@ -36,6 +36,23 @@ export function useFieldLabelRef() {
|
|
|
36
36
|
return useContext(FieldLabelContext)?.ref;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export type TextareaCharacterCountState = { current: number; max: number } | null;
|
|
40
|
+
|
|
41
|
+
const TextareaCharacterCountContext = createContext<
|
|
42
|
+
((state: TextareaCharacterCountState) => void) | undefined
|
|
43
|
+
>(undefined);
|
|
44
|
+
export const TextareaCharacterCountProvider = TextareaCharacterCountContext.Provider;
|
|
45
|
+
|
|
46
|
+
export function useTextareaCharacterCount(current: number, max: number | undefined) {
|
|
47
|
+
const setCharacterCount = useContext(TextareaCharacterCountContext);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (setCharacterCount && max != null) {
|
|
50
|
+
setCharacterCount({ current, max });
|
|
51
|
+
return () => setCharacterCount(null);
|
|
52
|
+
}
|
|
53
|
+
}, [setCharacterCount, current, max]);
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
export interface WithInputAttributesProps {
|
|
40
57
|
inputAttributes: ReturnType<typeof useInputAttributes>;
|
|
41
58
|
}
|
package/src/main.css
CHANGED
|
@@ -3238,7 +3238,16 @@ a,
|
|
|
3238
3238
|
.np-text-display-extra-large,
|
|
3239
3239
|
.np-text-display-large,
|
|
3240
3240
|
.np-text-display-medium,
|
|
3241
|
-
.np-text-display-small
|
|
3241
|
+
.np-text-display-small,
|
|
3242
|
+
.display-1--forced,
|
|
3243
|
+
.display-2--forced,
|
|
3244
|
+
.display-3--forced,
|
|
3245
|
+
.display-4--forced,
|
|
3246
|
+
.display-5--forced,
|
|
3247
|
+
.np-text-display-extra-large--forced,
|
|
3248
|
+
.np-text-display-large--forced,
|
|
3249
|
+
.np-text-display-medium--forced,
|
|
3250
|
+
.np-text-display-small--forced {
|
|
3242
3251
|
font-family: 'Wise Sans', 'Inter', sans-serif;
|
|
3243
3252
|
font-family: var(--font-family-display);
|
|
3244
3253
|
font-synthesis: none;
|
|
@@ -3285,9 +3294,14 @@ a,
|
|
|
3285
3294
|
* of Japanese ones for the logged out ones (exposed by the Editorial DS). Unfortunately,
|
|
3286
3295
|
* font files are browser-cached and we carried over to launchpad, where it causes issues
|
|
3287
3296
|
* for unsupported locales, especially those that share glyphs, like Japanese and Chinese.
|
|
3297
|
+
* There are exceptions for small UI parts where Wise Sans is fine or expected — e.g. the
|
|
3298
|
+
* numeric input of ExpressiveMoneyInput.
|
|
3299
|
+
* Add `--forced` BEM modifier to the original class name to guarantee it.
|
|
3288
3300
|
*/
|
|
3289
3301
|
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
3290
3302
|
font-family: var(--font-family-regular);
|
|
3303
|
+
line-height: 1.2;
|
|
3304
|
+
line-height: var(--line-height-title);
|
|
3291
3305
|
}
|
|
3292
3306
|
|
|
3293
3307
|
/* DEPRECATED(.np-text-display-extra-large): use .np-text-display-large instead */
|
|
@@ -26633,10 +26647,10 @@ a[data-toggle="tooltip"] {
|
|
|
26633
26647
|
}
|
|
26634
26648
|
|
|
26635
26649
|
.np-dot-mask {
|
|
26636
|
-
-webkit-mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
|
|
26637
|
-
mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
|
|
26638
|
-
-webkit-mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
|
|
26639
|
-
mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black 0);
|
|
26650
|
+
-webkit-mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
|
|
26651
|
+
mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-dot-size) / 2)) left calc(100% - (var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
|
|
26652
|
+
-webkit-mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
|
|
26653
|
+
mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-dot-size) / 2)) left calc(100% - calc(var(--np-dot-size) / 2)), transparent 0, transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)), black calc(var(--np-dot-size) / 2 + var(--np-dot-offset) + 0.5px));
|
|
26640
26654
|
}
|
|
26641
26655
|
|
|
26642
26656
|
.np-dot-badge {
|
|
@@ -29935,18 +29949,36 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
29935
29949
|
stroke-dasharray: var(--wds-list-item-spotlight-strokeDashSize) var(--wds-list-item-spotlight-strokeDashSize);
|
|
29936
29950
|
}
|
|
29937
29951
|
|
|
29938
|
-
.np-field-control
|
|
29939
|
-
.np-field__prompt {
|
|
29952
|
+
.np-field-control {
|
|
29940
29953
|
margin-top: 4px;
|
|
29941
29954
|
margin-top: var(--size-4);
|
|
29942
29955
|
}
|
|
29943
29956
|
|
|
29957
|
+
.np-field-validation {
|
|
29958
|
+
display: flex;
|
|
29959
|
+
align-items: flex-start;
|
|
29960
|
+
margin-top: 4px;
|
|
29961
|
+
margin-top: var(--size-4);
|
|
29962
|
+
gap: 8px;
|
|
29963
|
+
gap: var(--size-8);
|
|
29964
|
+
}
|
|
29965
|
+
|
|
29966
|
+
.np-field-textarea-char-counter {
|
|
29967
|
+
min-width: 55px;
|
|
29968
|
+
text-align: right;
|
|
29969
|
+
margin-left: auto;
|
|
29970
|
+
padding: 4px 0;
|
|
29971
|
+
padding: var(--size-4) 0;
|
|
29972
|
+
color: #768e9c;
|
|
29973
|
+
color: var(--color-content-tertiary);
|
|
29974
|
+
}
|
|
29975
|
+
|
|
29944
29976
|
.np-field .form-group--typeahead[class],
|
|
29945
29977
|
.np-field .np-checkbox-label[class] {
|
|
29946
29978
|
margin-bottom: 0;
|
|
29947
29979
|
}
|
|
29948
29980
|
|
|
29949
|
-
.np-field:has(.wds-radio-group) .np-
|
|
29981
|
+
.np-field:has(.wds-radio-group) .np-field-validation {
|
|
29950
29982
|
margin-top: 12px;
|
|
29951
29983
|
margin-top: var(--size-12);
|
|
29952
29984
|
}
|
|
@@ -31052,6 +31084,7 @@ button.np-link {
|
|
|
31052
31084
|
flex-grow: 1;
|
|
31053
31085
|
text-align: right;
|
|
31054
31086
|
background-color: transparent;
|
|
31087
|
+
line-height: inherit;
|
|
31055
31088
|
}
|
|
31056
31089
|
|
|
31057
31090
|
.wds-amount-input-input:focus-visible {
|
|
@@ -31062,6 +31095,7 @@ button.np-link {
|
|
|
31062
31095
|
flex-grow: 0;
|
|
31063
31096
|
display: flex;
|
|
31064
31097
|
align-items: center;
|
|
31098
|
+
line-height: inherit;
|
|
31065
31099
|
}
|
|
31066
31100
|
|
|
31067
31101
|
.wds-currency-selector:disabled {
|
|
@@ -8,7 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
/* DEPRECATED: use .np-text-*-title instead */
|
|
10
10
|
/* stylelint-disable-next-line selector-list-comma-newline-after */
|
|
11
|
-
.h1,
|
|
11
|
+
.h1,
|
|
12
|
+
.h2,
|
|
13
|
+
.h3,
|
|
14
|
+
.h4,
|
|
15
|
+
.h5,
|
|
16
|
+
.h6,
|
|
17
|
+
.title-1,
|
|
18
|
+
.title-2,
|
|
19
|
+
.title-3,
|
|
20
|
+
.title-4,
|
|
21
|
+
.title-5,
|
|
12
22
|
h1,
|
|
13
23
|
h2,
|
|
14
24
|
h3,
|
|
@@ -114,8 +124,12 @@ h6,
|
|
|
114
124
|
|
|
115
125
|
/* DEPRECATED: use .np-text-body-default instead */
|
|
116
126
|
/* stylelint-disable-next-line selector-list-comma-newline-after */
|
|
117
|
-
.body-2,
|
|
118
|
-
body,
|
|
127
|
+
.body-2,
|
|
128
|
+
.body-3,
|
|
129
|
+
.small,
|
|
130
|
+
.tiny,
|
|
131
|
+
body,
|
|
132
|
+
small,
|
|
119
133
|
.np-text-body-default {
|
|
120
134
|
font-size: var(--font-size-14);
|
|
121
135
|
line-height: 155%;
|
|
@@ -139,7 +153,8 @@ body, small,
|
|
|
139
153
|
|
|
140
154
|
/* DEPRECATED: use .np-text-body-large instead */
|
|
141
155
|
/* stylelint-disable-next-line selector-list-comma-newline-after */
|
|
142
|
-
.body-1,
|
|
156
|
+
.body-1,
|
|
157
|
+
.value,
|
|
143
158
|
.np-text-body-large {
|
|
144
159
|
font-weight: var(--font-weight-regular);
|
|
145
160
|
font-size: var(--font-size-16);
|
|
@@ -204,8 +219,11 @@ a,
|
|
|
204
219
|
.np-text-display-large,
|
|
205
220
|
.np-text-display-medium,
|
|
206
221
|
.np-text-display-small {
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
&,
|
|
223
|
+
&--forced {
|
|
224
|
+
font-family: var(--font-family-display);
|
|
225
|
+
font-synthesis: none;
|
|
226
|
+
}
|
|
209
227
|
|
|
210
228
|
:lang(ja) &,
|
|
211
229
|
:lang(th) &,
|
|
@@ -216,8 +234,12 @@ a,
|
|
|
216
234
|
* of Japanese ones for the logged out ones (exposed by the Editorial DS). Unfortunately,
|
|
217
235
|
* font files are browser-cached and we carried over to launchpad, where it causes issues
|
|
218
236
|
* for unsupported locales, especially those that share glyphs, like Japanese and Chinese.
|
|
237
|
+
* There are exceptions for small UI parts where Wise Sans is fine or expected — e.g. the
|
|
238
|
+
* numeric input of ExpressiveMoneyInput.
|
|
239
|
+
* Add `--forced` BEM modifier to the original class name to guarantee it.
|
|
219
240
|
*/
|
|
220
241
|
font-family: var(--font-family-regular);
|
|
242
|
+
line-height: var(--line-height-title);
|
|
221
243
|
}
|
|
222
244
|
}
|
|
223
245
|
|
|
@@ -3238,7 +3238,16 @@ a,
|
|
|
3238
3238
|
.np-text-display-extra-large,
|
|
3239
3239
|
.np-text-display-large,
|
|
3240
3240
|
.np-text-display-medium,
|
|
3241
|
-
.np-text-display-small
|
|
3241
|
+
.np-text-display-small,
|
|
3242
|
+
.display-1--forced,
|
|
3243
|
+
.display-2--forced,
|
|
3244
|
+
.display-3--forced,
|
|
3245
|
+
.display-4--forced,
|
|
3246
|
+
.display-5--forced,
|
|
3247
|
+
.np-text-display-extra-large--forced,
|
|
3248
|
+
.np-text-display-large--forced,
|
|
3249
|
+
.np-text-display-medium--forced,
|
|
3250
|
+
.np-text-display-small--forced {
|
|
3242
3251
|
font-family: 'Wise Sans', 'Inter', sans-serif;
|
|
3243
3252
|
font-family: var(--font-family-display);
|
|
3244
3253
|
font-synthesis: none;
|
|
@@ -3285,9 +3294,14 @@ a,
|
|
|
3285
3294
|
* of Japanese ones for the logged out ones (exposed by the Editorial DS). Unfortunately,
|
|
3286
3295
|
* font files are browser-cached and we carried over to launchpad, where it causes issues
|
|
3287
3296
|
* for unsupported locales, especially those that share glyphs, like Japanese and Chinese.
|
|
3297
|
+
* There are exceptions for small UI parts where Wise Sans is fine or expected — e.g. the
|
|
3298
|
+
* numeric input of ExpressiveMoneyInput.
|
|
3299
|
+
* Add `--forced` BEM modifier to the original class name to guarantee it.
|
|
3288
3300
|
*/
|
|
3289
3301
|
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
3290
3302
|
font-family: var(--font-family-regular);
|
|
3303
|
+
line-height: 1.2;
|
|
3304
|
+
line-height: var(--line-height-title);
|
|
3291
3305
|
}
|
|
3292
3306
|
|
|
3293
3307
|
/* DEPRECATED(.np-text-display-extra-large): use .np-text-display-large instead */
|