@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.
Files changed (93) hide show
  1. package/build/avatarLayout/AvatarLayout.js +15 -1
  2. package/build/avatarLayout/AvatarLayout.js.map +1 -1
  3. package/build/avatarLayout/AvatarLayout.mjs +15 -1
  4. package/build/avatarLayout/AvatarLayout.mjs.map +1 -1
  5. package/build/avatarView/AvatarView.js +6 -2
  6. package/build/avatarView/AvatarView.js.map +1 -1
  7. package/build/avatarView/AvatarView.mjs +6 -2
  8. package/build/avatarView/AvatarView.mjs.map +1 -1
  9. package/build/avatarView/Dot.js +8 -0
  10. package/build/avatarView/Dot.js.map +1 -1
  11. package/build/avatarView/Dot.mjs +8 -0
  12. package/build/avatarView/Dot.mjs.map +1 -1
  13. package/build/common/circle/Circle.js +6 -2
  14. package/build/common/circle/Circle.js.map +1 -1
  15. package/build/common/circle/Circle.mjs +6 -2
  16. package/build/common/circle/Circle.mjs.map +1 -1
  17. package/build/expressiveMoneyInput/amountInput/AmountInput.js +1 -1
  18. package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -1
  19. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +1 -1
  20. package/build/expressiveMoneyInput/amountInput/AmountInput.mjs.map +1 -1
  21. package/build/field/Field.js +63 -32
  22. package/build/field/Field.js.map +1 -1
  23. package/build/field/Field.messages.js +14 -0
  24. package/build/field/Field.messages.js.map +1 -0
  25. package/build/field/Field.messages.mjs +10 -0
  26. package/build/field/Field.messages.mjs.map +1 -0
  27. package/build/field/Field.mjs +65 -34
  28. package/build/field/Field.mjs.map +1 -1
  29. package/build/i18n/en.json +1 -0
  30. package/build/i18n/en.json.js +1 -0
  31. package/build/i18n/en.json.js.map +1 -1
  32. package/build/i18n/en.json.mjs +1 -0
  33. package/build/i18n/en.json.mjs.map +1 -1
  34. package/build/inputs/TextArea.js +5 -0
  35. package/build/inputs/TextArea.js.map +1 -1
  36. package/build/inputs/TextArea.mjs +6 -1
  37. package/build/inputs/TextArea.mjs.map +1 -1
  38. package/build/inputs/contexts.js +16 -0
  39. package/build/inputs/contexts.js.map +1 -1
  40. package/build/inputs/contexts.mjs +16 -2
  41. package/build/inputs/contexts.mjs.map +1 -1
  42. package/build/main.css +42 -8
  43. package/build/styles/avatarView/AvatarView.css +4 -4
  44. package/build/styles/avatarView/Dot.css +4 -4
  45. package/build/styles/css/neptune.css +15 -1
  46. package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
  47. package/build/styles/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
  48. package/build/styles/field/Field.css +19 -3
  49. package/build/styles/main.css +42 -8
  50. package/build/styles/styles/less/neptune.css +15 -1
  51. package/build/types/avatarView/AvatarView.d.ts +1 -1
  52. package/build/types/avatarView/AvatarView.d.ts.map +1 -1
  53. package/build/types/avatarView/Dot.d.ts.map +1 -1
  54. package/build/types/common/circle/Circle.d.ts +1 -1
  55. package/build/types/common/circle/Circle.d.ts.map +1 -1
  56. package/build/types/field/Field.d.ts.map +1 -1
  57. package/build/types/field/Field.messages.d.ts +8 -0
  58. package/build/types/field/Field.messages.d.ts.map +1 -0
  59. package/build/types/inputs/TextArea.d.ts.map +1 -1
  60. package/build/types/inputs/contexts.d.ts +6 -0
  61. package/build/types/inputs/contexts.d.ts.map +1 -1
  62. package/build/types/test-utils/index.d.ts +2 -0
  63. package/build/types/test-utils/index.d.ts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/avatarLayout/AvatarLayout.story.tsx +1 -1
  66. package/src/avatarLayout/AvatarLayout.tsx +4 -0
  67. package/src/avatarView/AvatarView.css +4 -4
  68. package/src/avatarView/AvatarView.story.tsx +17 -13
  69. package/src/avatarView/AvatarView.tsx +5 -1
  70. package/src/avatarView/Dot.css +4 -4
  71. package/src/avatarView/Dot.less +6 -6
  72. package/src/avatarView/Dot.tsx +2 -0
  73. package/src/common/circle/Circle.tsx +5 -1
  74. package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +2 -0
  75. package/src/expressiveMoneyInput/ExpressiveMoneyInput.test.story.tsx +43 -0
  76. package/src/expressiveMoneyInput/amountInput/AmountInput.css +2 -0
  77. package/src/expressiveMoneyInput/amountInput/AmountInput.less +2 -0
  78. package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +1 -1
  79. package/src/field/Field.css +19 -3
  80. package/src/field/Field.less +17 -3
  81. package/src/field/Field.messages.ts +8 -0
  82. package/src/field/Field.story.tsx +5 -1
  83. package/src/field/Field.test.tsx +90 -0
  84. package/src/field/Field.tsx +84 -37
  85. package/src/i18n/en.json +1 -0
  86. package/src/inputs/TextArea.story.tsx +97 -0
  87. package/src/inputs/TextArea.test.story.tsx +142 -0
  88. package/src/inputs/TextArea.tsx +7 -2
  89. package/src/inputs/contexts.tsx +18 -1
  90. package/src/main.css +42 -8
  91. package/src/styles/less/core/_typography.less +28 -6
  92. package/src/styles/less/neptune.css +15 -1
  93. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +1 -0
@@ -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
- <div
91
- className={clsx(
92
- 'np-field form-group d-block',
93
- {
94
- 'has-success': sentiment === Sentiment.POSITIVE,
95
- 'has-warning': sentiment === Sentiment.WARNING,
96
- 'has-error': hasError,
97
- 'has-info': sentiment === Sentiment.NEUTRAL,
98
- },
99
- className,
100
- )}
101
- >
102
- {label != null ? (
103
- <>
104
- <Label ref={labelRef} id={labelId} htmlFor={inputId}>
105
- {required ? label : <Label.Optional>{label}</Label.Optional>}
106
- </Label>
107
- <Label.Description id={descriptionId}>{description}</Label.Description>
108
- <div className="np-field-control">{children}</div>
109
- </>
110
- ) : (
111
- children
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
- {message && (
115
- <InlinePrompt
116
- sentiment={sentiment}
117
- id={messageId}
118
- mediaLabel={messageIconLabel}
119
- className="np-field__prompt"
120
- loading={messageLoading}
121
- width="full"
122
- >
123
- {message}
124
- </InlinePrompt>
125
- )}
126
- </div>
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
+ };
@@ -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
  />
@@ -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-field__prompt {
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, .h2, .h3, .h4, .h5, .h6, .title-1, .title-2, .title-3, .title-4, .title-5,
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, .body-3, .small, .tiny,
118
- body, small,
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, .value,
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
- font-family: var(--font-family-display);
208
- font-synthesis: none;
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 */
@@ -21,6 +21,7 @@ export const Basic: Story = {
21
21
  <TextareaWithDisplayFormat
22
22
  id={id}
23
23
  value="0000"
24
+ maxLength={20}
24
25
  displayPattern="**** - **** - ****"
25
26
  onChange={console.log}
26
27
  />