@transferwise/components 46.30.2 → 46.32.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/index.js +931 -523
- package/build/index.js.map +1 -1
- package/build/index.mjs +928 -523
- package/build/index.mjs.map +1 -1
- package/build/main.css +135 -0
- package/build/styles/carousel/Carousel.css +135 -0
- package/build/styles/main.css +135 -0
- package/build/types/carousel/Carousel.d.ts +26 -0
- package/build/types/carousel/Carousel.d.ts.map +1 -0
- package/build/types/carousel/index.d.ts +3 -0
- package/build/types/carousel/index.d.ts.map +1 -0
- package/build/types/common/card/Card.d.ts +2 -2
- package/build/types/common/card/Card.d.ts.map +1 -1
- package/build/types/dateInput/DateInput.d.ts +5 -4
- package/build/types/dateInput/DateInput.d.ts.map +1 -1
- package/build/types/dateLookup/DateLookup.d.ts +11 -4
- package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/field/Field.d.ts +12 -0
- package/build/types/field/Field.d.ts.map +1 -0
- package/build/types/index.d.ts +6 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputs/Input.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/TextArea.d.ts.map +1 -1
- package/build/types/inputs/_common.d.ts +2 -2
- package/build/types/inputs/_common.d.ts.map +1 -1
- package/build/types/inputs/contexts.d.ts +24 -0
- package/build/types/inputs/contexts.d.ts.map +1 -0
- package/build/types/label/Label.d.ts +9 -0
- package/build/types/label/Label.d.ts.map +1 -0
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/promoCard/PromoCard.d.ts +16 -5
- package/build/types/promoCard/PromoCard.d.ts.map +1 -1
- package/build/types/radioGroup/RadioGroup.d.ts.map +1 -1
- package/build/types/switch/Switch.d.ts +6 -3
- package/build/types/switch/Switch.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/carousel/Carousel.css +135 -0
- package/src/carousel/Carousel.less +133 -0
- package/src/carousel/Carousel.spec.tsx +221 -0
- package/src/carousel/Carousel.story.tsx +63 -0
- package/src/carousel/Carousel.tsx +345 -0
- package/src/carousel/index.ts +3 -0
- package/src/common/card/Card.tsx +51 -43
- package/src/dateInput/DateInput.rtl.spec.tsx +17 -0
- package/src/dateInput/DateInput.tsx +28 -22
- package/src/dateLookup/DateLookup.keyboardEvents.spec.js +2 -2
- package/src/dateLookup/DateLookup.rtl.spec.tsx +21 -0
- package/src/dateLookup/DateLookup.state.spec.js +5 -5
- package/src/dateLookup/DateLookup.tests.story.tsx +4 -11
- package/src/dateLookup/DateLookup.tsx +24 -9
- package/src/dateLookup/DateLookup.view.spec.js +11 -11
- package/src/field/Field.spec.tsx +95 -0
- package/src/field/Field.story.tsx +59 -0
- package/src/field/Field.tsx +70 -0
- package/src/index.ts +6 -0
- package/src/inputs/Input.tsx +5 -3
- package/src/inputs/SelectInput.spec.tsx +10 -0
- package/src/inputs/SelectInput.tsx +9 -4
- package/src/inputs/TextArea.tsx +6 -3
- package/src/inputs/_ButtonInput.tsx +2 -2
- package/src/inputs/_common.ts +2 -2
- package/src/inputs/contexts.tsx +45 -0
- package/src/label/Label.spec.tsx +26 -0
- package/src/label/Label.story.tsx +37 -0
- package/src/label/Label.tsx +20 -0
- package/src/main.css +135 -0
- package/src/main.less +1 -0
- package/src/phoneNumberInput/PhoneNumberInput.story.tsx +16 -22
- package/src/phoneNumberInput/PhoneNumberInput.tsx +14 -2
- package/src/promoCard/PromoCard.story.tsx +2 -2
- package/src/promoCard/PromoCard.tsx +30 -9
- package/src/radioGroup/RadioGroup.rtl.spec.tsx +14 -0
- package/src/radioGroup/RadioGroup.story.tsx +26 -0
- package/src/radioGroup/RadioGroup.tsx +4 -1
- package/src/switch/Switch.spec.tsx +10 -0
- package/src/switch/Switch.tsx +22 -13
- package/src/utilities/logActionRequired.js +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useId } from '@radix-ui/react-id';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
|
|
4
|
+
import { Sentiment } from '../common';
|
|
5
|
+
import InlineAlert from '../inlineAlert/InlineAlert';
|
|
6
|
+
import {
|
|
7
|
+
FieldLabelIdContextProvider,
|
|
8
|
+
InputDescribedByProvider,
|
|
9
|
+
InputIdContextProvider,
|
|
10
|
+
InputInvalidProvider,
|
|
11
|
+
} from '../inputs/contexts';
|
|
12
|
+
import { Label } from '../label/Label';
|
|
13
|
+
|
|
14
|
+
export type FieldProps = {
|
|
15
|
+
/** `null` disables auto-generating the `id` attribute, falling back to nesting-based label association over setting `htmlFor` explicitly. */
|
|
16
|
+
id?: string | null;
|
|
17
|
+
label: React.ReactNode;
|
|
18
|
+
hint?: React.ReactNode;
|
|
19
|
+
error?: React.ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Field = ({ id, label, hint, error, className, children }: FieldProps) => {
|
|
25
|
+
const hasError = Boolean(error);
|
|
26
|
+
const hasHint = Boolean(hint) && !hasError;
|
|
27
|
+
|
|
28
|
+
const labelId = useId();
|
|
29
|
+
|
|
30
|
+
const fallbackInputId = useId(); // TODO: Use `React.useId()` when react>=18
|
|
31
|
+
const inputId = id !== null ? id ?? fallbackInputId : undefined;
|
|
32
|
+
|
|
33
|
+
const descriptionId = useId(); // TODO: Use `React.useId()` when react>=18
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<FieldLabelIdContextProvider value={labelId}>
|
|
37
|
+
<InputIdContextProvider value={inputId}>
|
|
38
|
+
<InputDescribedByProvider value={hasError || hasHint ? descriptionId : undefined}>
|
|
39
|
+
<InputInvalidProvider value={hasError}>
|
|
40
|
+
<div
|
|
41
|
+
className={classNames(
|
|
42
|
+
'form-group d-block',
|
|
43
|
+
{
|
|
44
|
+
'has-error': hasError,
|
|
45
|
+
'has-info': hasHint,
|
|
46
|
+
},
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<Label id={labelId} htmlFor={inputId}>
|
|
51
|
+
{label}
|
|
52
|
+
{children}
|
|
53
|
+
</Label>
|
|
54
|
+
{hasHint && (
|
|
55
|
+
<InlineAlert type={Sentiment.NEUTRAL} id={descriptionId}>
|
|
56
|
+
{hint}
|
|
57
|
+
</InlineAlert>
|
|
58
|
+
)}
|
|
59
|
+
{hasError && (
|
|
60
|
+
<InlineAlert type={Sentiment.NEGATIVE} id={descriptionId}>
|
|
61
|
+
{error}
|
|
62
|
+
</InlineAlert>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</InputInvalidProvider>
|
|
66
|
+
</InputDescribedByProvider>
|
|
67
|
+
</InputIdContextProvider>
|
|
68
|
+
</FieldLabelIdContextProvider>
|
|
69
|
+
);
|
|
70
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { ActionOptionProps } from './actionOption';
|
|
|
6
6
|
export type { AlertAction, AlertProps, AlertType } from './alert';
|
|
7
7
|
export type { AvatarProps } from './avatar';
|
|
8
8
|
export type { BadgeProps } from './badge';
|
|
9
|
+
export type { CarouselProps } from './carousel';
|
|
9
10
|
export type { CircularButtonProps } from './circularButton';
|
|
10
11
|
export type {
|
|
11
12
|
BodyTypes,
|
|
@@ -21,6 +22,7 @@ export type { DateLookupProps } from './dateLookup';
|
|
|
21
22
|
export type { DecisionProps } from './decision/Decision';
|
|
22
23
|
export type { DimmerProps } from './dimmer';
|
|
23
24
|
export type { EmphasisProps } from './emphasis';
|
|
25
|
+
export type { FieldProps } from './field/Field';
|
|
24
26
|
export type { InfoProps } from './info';
|
|
25
27
|
export type { InputWithDisplayFormatProps } from './inputWithDisplayFormat';
|
|
26
28
|
export type { InputProps } from './inputs/Input';
|
|
@@ -37,6 +39,7 @@ export type {
|
|
|
37
39
|
} from './inputs/SelectInput';
|
|
38
40
|
export type { TextAreaProps } from './inputs/TextArea';
|
|
39
41
|
export type { InstructionsListProps } from './instructionsList';
|
|
42
|
+
export type { LabelProps } from './label/Label';
|
|
40
43
|
export type { LoaderProps } from './loader';
|
|
41
44
|
export type { MarkdownProps } from './markdown';
|
|
42
45
|
export type { ModalProps } from './modal';
|
|
@@ -82,6 +85,7 @@ export { default as AvatarWrapper } from './avatarWrapper';
|
|
|
82
85
|
export { default as Badge } from './badge';
|
|
83
86
|
export { default as Body } from './body';
|
|
84
87
|
export { default as Button } from './button';
|
|
88
|
+
export { default as Carousel } from './carousel';
|
|
85
89
|
export { default as Card } from './card';
|
|
86
90
|
export { default as Checkbox } from './checkbox';
|
|
87
91
|
export { default as CheckboxButton } from './checkboxButton';
|
|
@@ -103,6 +107,7 @@ export { default as Drawer } from './drawer';
|
|
|
103
107
|
export { default as DropFade } from './dropFade';
|
|
104
108
|
export { default as Emphasis } from './emphasis';
|
|
105
109
|
export { default as FlowNavigation } from './flowNavigation/FlowNavigation';
|
|
110
|
+
export { Field } from './field/Field';
|
|
106
111
|
export { default as Header } from './header';
|
|
107
112
|
export { default as Image } from './image';
|
|
108
113
|
export { default as Info } from './info';
|
|
@@ -118,6 +123,7 @@ export {
|
|
|
118
123
|
} from './inputs/SelectInput';
|
|
119
124
|
export { TextArea } from './inputs/TextArea';
|
|
120
125
|
export { default as InstructionsList } from './instructionsList';
|
|
126
|
+
export { Label } from './label/Label';
|
|
121
127
|
export { default as Link } from './link';
|
|
122
128
|
export { default as ListItem } from './listItem';
|
|
123
129
|
export { default as Loader } from './loader';
|
package/src/inputs/Input.tsx
CHANGED
|
@@ -3,9 +3,9 @@ import { forwardRef } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { SizeLarge, SizeMedium, SizeSmall } from '../common';
|
|
5
5
|
import { Merge } from '../utils';
|
|
6
|
-
|
|
6
|
+
import { inputClassNameBase } from './_common';
|
|
7
|
+
import { useInputAttributes } from './contexts';
|
|
7
8
|
import { useInputPaddings } from './InputGroup';
|
|
8
|
-
import { formControlClassNameBase } from './_common';
|
|
9
9
|
|
|
10
10
|
export interface InputProps
|
|
11
11
|
extends Merge<
|
|
@@ -21,17 +21,19 @@ export const Input = forwardRef(function Input(
|
|
|
21
21
|
{ size = 'auto', shape = 'rectangle', className, ...restProps }: InputProps,
|
|
22
22
|
reference: React.ForwardedRef<HTMLInputElement>,
|
|
23
23
|
) {
|
|
24
|
+
const inputAttributes = useInputAttributes();
|
|
24
25
|
const inputPaddings = useInputPaddings();
|
|
25
26
|
|
|
26
27
|
return (
|
|
27
28
|
<input
|
|
28
29
|
ref={reference}
|
|
29
|
-
className={classNames(className,
|
|
30
|
+
className={classNames(className, inputClassNameBase({ size }), 'np-input', {
|
|
30
31
|
'np-input--shape-rectangle': shape === 'rectangle',
|
|
31
32
|
'np-input--shape-pill': shape === 'pill',
|
|
32
33
|
})}
|
|
33
34
|
// eslint-disable-next-line react/forbid-dom-props
|
|
34
35
|
style={inputPaddings}
|
|
36
|
+
{...inputAttributes}
|
|
35
37
|
{...restProps}
|
|
36
38
|
/>
|
|
37
39
|
);
|
|
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
4
4
|
import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
|
|
5
5
|
|
|
6
6
|
import { SelectInput } from './SelectInput';
|
|
7
|
+
import { Field } from '../field/Field';
|
|
7
8
|
|
|
8
9
|
mockMatchMedia();
|
|
9
10
|
mockResizeObserver();
|
|
@@ -227,4 +228,13 @@ describe('SelectInput', () => {
|
|
|
227
228
|
const trigger = screen.getAllByRole('button')[0];
|
|
228
229
|
expect(trigger).toHaveAttribute('id', 'custom');
|
|
229
230
|
});
|
|
231
|
+
|
|
232
|
+
it('supports `Field` for labeling', () => {
|
|
233
|
+
render(
|
|
234
|
+
<Field label="Currency">
|
|
235
|
+
<SelectInput items={[{ type: 'option', value: 'USD' }]} value="USD" />
|
|
236
|
+
</Field>,
|
|
237
|
+
);
|
|
238
|
+
expect(screen.getByLabelText('Currency')).toHaveTextContent('USD');
|
|
239
|
+
});
|
|
230
240
|
});
|
|
@@ -13,12 +13,13 @@ import { Breakpoint } from '../common/propsValues/breakpoint';
|
|
|
13
13
|
import dateTriggerMessages from '../dateLookup/dateTrigger/DateTrigger.messages';
|
|
14
14
|
import { Merge } from '../utils';
|
|
15
15
|
|
|
16
|
-
import { InputGroup } from './InputGroup';
|
|
17
|
-
import { SearchInput } from './SearchInput';
|
|
18
|
-
import messages from './SelectInput.messages';
|
|
19
16
|
import { BottomSheet } from './_BottomSheet';
|
|
20
17
|
import { ButtonInput } from './_ButtonInput';
|
|
21
18
|
import { Popover } from './_Popover';
|
|
19
|
+
import { useInputAttributes } from './contexts';
|
|
20
|
+
import { InputGroup } from './InputGroup';
|
|
21
|
+
import { SearchInput } from './SearchInput';
|
|
22
|
+
import messages from './SelectInput.messages';
|
|
22
23
|
|
|
23
24
|
function searchableString(value: string) {
|
|
24
25
|
return value.trim().replace(/\s+/gu, ' ').normalize('NFKC').toLowerCase();
|
|
@@ -221,7 +222,7 @@ function SelectInputClearButton({ className, onClick }: SelectInputClearButtonPr
|
|
|
221
222
|
const noop = () => {};
|
|
222
223
|
|
|
223
224
|
export function SelectInput<T = string, M extends boolean = false>({
|
|
224
|
-
id,
|
|
225
|
+
id: idProp,
|
|
225
226
|
name,
|
|
226
227
|
multiple,
|
|
227
228
|
placeholder,
|
|
@@ -242,6 +243,9 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
242
243
|
onClose,
|
|
243
244
|
onClear,
|
|
244
245
|
}: SelectInputProps<T, M>) {
|
|
246
|
+
const inputAttributes = useInputAttributes();
|
|
247
|
+
const id = idProp ?? inputAttributes.id;
|
|
248
|
+
|
|
245
249
|
const [open, setOpen] = useState(false);
|
|
246
250
|
|
|
247
251
|
const initialized = useRef(false);
|
|
@@ -308,6 +312,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
308
312
|
ref(node);
|
|
309
313
|
triggerRef.current = node;
|
|
310
314
|
},
|
|
315
|
+
...inputAttributes,
|
|
311
316
|
id,
|
|
312
317
|
...mergeProps(
|
|
313
318
|
{
|
package/src/inputs/TextArea.tsx
CHANGED
|
@@ -2,8 +2,8 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { Merge } from '../utils';
|
|
5
|
-
|
|
6
|
-
import {
|
|
5
|
+
import { inputClassNameBase } from './_common';
|
|
6
|
+
import { useInputAttributes } from './contexts';
|
|
7
7
|
|
|
8
8
|
export interface TextAreaProps
|
|
9
9
|
extends Merge<
|
|
@@ -17,10 +17,13 @@ export const TextArea = forwardRef(function TextArea(
|
|
|
17
17
|
{ className, ...restProps }: TextAreaProps,
|
|
18
18
|
reference: React.ForwardedRef<HTMLTextAreaElement>,
|
|
19
19
|
) {
|
|
20
|
+
const inputAttributes = useInputAttributes();
|
|
21
|
+
|
|
20
22
|
return (
|
|
21
23
|
<textarea
|
|
22
24
|
ref={reference}
|
|
23
|
-
className={classNames(className,
|
|
25
|
+
className={classNames(className, inputClassNameBase(), 'np-text-area')}
|
|
26
|
+
{...inputAttributes}
|
|
24
27
|
{...restProps}
|
|
25
28
|
/>
|
|
26
29
|
);
|
|
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { useInputPaddings } from './InputGroup';
|
|
5
|
-
import {
|
|
5
|
+
import { inputClassNameBase } from './_common';
|
|
6
6
|
|
|
7
7
|
export interface ButtonInputProps extends React.ComponentPropsWithRef<'button'> {
|
|
8
8
|
size?: 'sm' | 'md' | 'lg';
|
|
@@ -18,7 +18,7 @@ export const ButtonInput = forwardRef(function ButtonInput(
|
|
|
18
18
|
<button
|
|
19
19
|
ref={ref}
|
|
20
20
|
type="button"
|
|
21
|
-
className={classNames(className,
|
|
21
|
+
className={classNames(className, inputClassNameBase({ size }), 'np-button-input')}
|
|
22
22
|
// eslint-disable-next-line react/forbid-dom-props
|
|
23
23
|
style={{ ...inputPaddings, ...style }}
|
|
24
24
|
{...restProps}
|
package/src/inputs/_common.ts
CHANGED
|
@@ -2,11 +2,11 @@ import classNames from 'classnames';
|
|
|
2
2
|
|
|
3
3
|
import { SizeLarge, SizeMedium, SizeSmall } from '../common';
|
|
4
4
|
|
|
5
|
-
export type
|
|
5
|
+
export type InputPropsBase = {
|
|
6
6
|
size?: 'auto' | SizeSmall | SizeMedium | SizeLarge;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export function
|
|
9
|
+
export function inputClassNameBase({ size = 'auto' }: InputPropsBase = {}) {
|
|
10
10
|
return classNames(
|
|
11
11
|
'form-control', // TODO: Deprecate
|
|
12
12
|
'np-form-control',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
const FieldLabelIdContext = createContext<string | undefined>(undefined);
|
|
4
|
+
export const FieldLabelIdContextProvider = FieldLabelIdContext.Provider;
|
|
5
|
+
|
|
6
|
+
const InputIdContext = createContext<string | undefined>(undefined);
|
|
7
|
+
export const InputIdContextProvider = InputIdContext.Provider;
|
|
8
|
+
|
|
9
|
+
const InputDescribedByContext = createContext<string | undefined>(undefined);
|
|
10
|
+
export const InputDescribedByProvider = InputDescribedByContext.Provider;
|
|
11
|
+
|
|
12
|
+
const InputInvalidContext = createContext<boolean | undefined>(undefined);
|
|
13
|
+
export const InputInvalidProvider = InputInvalidContext.Provider;
|
|
14
|
+
|
|
15
|
+
interface UseInputAttributesArgs {
|
|
16
|
+
/** Set this to `true` if the underlying element is not directly [labelable as per the HTML specification](https://html.spec.whatwg.org/multipage/forms.html#category-label). */
|
|
17
|
+
nonLabelable?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useInputAttributes({ nonLabelable }: UseInputAttributesArgs = {}) {
|
|
21
|
+
const labelId = useContext(FieldLabelIdContext);
|
|
22
|
+
return {
|
|
23
|
+
id: useContext(InputIdContext),
|
|
24
|
+
'aria-labelledby': nonLabelable ? labelId : undefined,
|
|
25
|
+
'aria-describedby': useContext(InputDescribedByContext),
|
|
26
|
+
'aria-invalid': useContext(InputInvalidContext),
|
|
27
|
+
} satisfies React.HTMLAttributes<HTMLElement>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WithInputAttributesProps {
|
|
31
|
+
inputAttributes: ReturnType<typeof useInputAttributes>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function withInputAttributes<T extends Partial<WithInputAttributesProps>>(
|
|
35
|
+
Component: React.ComponentType<T>,
|
|
36
|
+
args?: UseInputAttributesArgs,
|
|
37
|
+
) {
|
|
38
|
+
function ComponentWithInputAttributes(props: Omit<T, keyof WithInputAttributesProps>) {
|
|
39
|
+
return <Component inputAttributes={useInputAttributes(args)} {...(props as T)} />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ComponentWithInputAttributes.displayName = `withInputAttributes(${Component.displayName || Component.name || 'Component'})`;
|
|
43
|
+
|
|
44
|
+
return ComponentWithInputAttributes;
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Input } from '../inputs/Input';
|
|
2
|
+
import { render, screen } from '../test-utils';
|
|
3
|
+
import { Label } from './Label';
|
|
4
|
+
|
|
5
|
+
describe('Label', () => {
|
|
6
|
+
it('renders string labels', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Label>
|
|
9
|
+
Phone number
|
|
10
|
+
<Input readOnly />
|
|
11
|
+
</Label>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(screen.getByLabelText('Phone number')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
it('renders node type labels', () => {
|
|
17
|
+
render(
|
|
18
|
+
<Label>
|
|
19
|
+
<span>Phone number</span>
|
|
20
|
+
<Input readOnly />
|
|
21
|
+
</Label>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(screen.getByLabelText('Phone number')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import Info from '../info/Info';
|
|
4
|
+
import { Input } from '../inputs/Input';
|
|
5
|
+
import { Label } from './Label';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
component: Label,
|
|
9
|
+
title: 'Label',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Basic = () => {
|
|
13
|
+
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
14
|
+
return (
|
|
15
|
+
<Label>
|
|
16
|
+
Phone number
|
|
17
|
+
<Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
|
|
18
|
+
</Label>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const WithInfo = () => {
|
|
23
|
+
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
24
|
+
return (
|
|
25
|
+
<Label>
|
|
26
|
+
<span className="d-flex">
|
|
27
|
+
Phone number{' '}
|
|
28
|
+
<Info
|
|
29
|
+
content="This is some help in popover"
|
|
30
|
+
aria-label="The aria label"
|
|
31
|
+
className="m-l-1"
|
|
32
|
+
/>
|
|
33
|
+
</span>
|
|
34
|
+
<Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
|
|
35
|
+
</Label>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
|
|
3
|
+
export type LabelProps = {
|
|
4
|
+
id?: string;
|
|
5
|
+
htmlFor?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const Label = ({ id, htmlFor, className, children }: LabelProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<label
|
|
13
|
+
id={id}
|
|
14
|
+
htmlFor={htmlFor}
|
|
15
|
+
className={classNames('control-label d-flex flex-column gap-y-1 m-b-0', className)}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</label>
|
|
19
|
+
);
|
|
20
|
+
};
|
package/src/main.css
CHANGED
|
@@ -643,6 +643,141 @@ div.critical-comms .critical-comms-body {
|
|
|
643
643
|
border-radius: 16px 16px 0 0;
|
|
644
644
|
border-radius: var(--radius-medium) var(--radius-medium) 0 0;
|
|
645
645
|
}
|
|
646
|
+
.carousel-wrapper {
|
|
647
|
+
overflow: hidden;
|
|
648
|
+
}
|
|
649
|
+
.carousel {
|
|
650
|
+
display: flex;
|
|
651
|
+
align-items: center;
|
|
652
|
+
overflow-x: scroll;
|
|
653
|
+
overflow-y: hidden;
|
|
654
|
+
scroll-snap-type: x mandatory;
|
|
655
|
+
scroll-behavior: smooth;
|
|
656
|
+
gap: 16px;
|
|
657
|
+
gap: var(--size-16);
|
|
658
|
+
padding: 8px;
|
|
659
|
+
padding: var(--size-8);
|
|
660
|
+
margin: 8px;
|
|
661
|
+
margin: var(--size-8);
|
|
662
|
+
}
|
|
663
|
+
@media (max-width: 767px) {
|
|
664
|
+
.carousel {
|
|
665
|
+
gap: 8px;
|
|
666
|
+
gap: var(--size-8);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
.carousel__header {
|
|
670
|
+
display: flex;
|
|
671
|
+
align-items: center;
|
|
672
|
+
overflow: hidden;
|
|
673
|
+
min-height: 32px;
|
|
674
|
+
min-height: var(--size-32);
|
|
675
|
+
padding-bottom: 16px;
|
|
676
|
+
padding-bottom: var(--size-16);
|
|
677
|
+
}
|
|
678
|
+
.carousel__card,
|
|
679
|
+
.carousel__card:hover,
|
|
680
|
+
.carousel__card:focus,
|
|
681
|
+
.carousel__card:focus-within {
|
|
682
|
+
-webkit-text-decoration: none;
|
|
683
|
+
text-decoration: none;
|
|
684
|
+
transition: none !important;
|
|
685
|
+
box-shadow: none !important;
|
|
686
|
+
}
|
|
687
|
+
.carousel__card {
|
|
688
|
+
display: block;
|
|
689
|
+
position: relative;
|
|
690
|
+
text-align: left;
|
|
691
|
+
border: none;
|
|
692
|
+
overflow: hidden;
|
|
693
|
+
background: rgba(134,167,189,0.10196);
|
|
694
|
+
background: var(--color-background-neutral);
|
|
695
|
+
border-radius: 32px;
|
|
696
|
+
border-radius: var(--size-32);
|
|
697
|
+
scroll-snap-align: center;
|
|
698
|
+
-webkit-scroll-snap-align: center;
|
|
699
|
+
transition: all 0.4s !important;
|
|
700
|
+
}
|
|
701
|
+
@media (min-width: 1200px) {
|
|
702
|
+
.carousel__card {
|
|
703
|
+
min-width: 280px;
|
|
704
|
+
width: 280px;
|
|
705
|
+
height: 280px;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
@media (max-width: 1199px) {
|
|
709
|
+
.carousel__card {
|
|
710
|
+
min-width: 242px;
|
|
711
|
+
width: 242px;
|
|
712
|
+
height: 242px;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
@media (max-width: 767px) {
|
|
716
|
+
.carousel__card {
|
|
717
|
+
min-width: 336px;
|
|
718
|
+
width: 336px;
|
|
719
|
+
height: 336px;
|
|
720
|
+
scroll-snap-stop: always;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
.carousel__card:focus,
|
|
724
|
+
.carousel__card:has(:focus-visible) {
|
|
725
|
+
outline: var(--ring-outline-color) solid var(--ring-outline-width) !important;
|
|
726
|
+
outline-offset: var(--ring-outline-offset) !important;
|
|
727
|
+
}
|
|
728
|
+
.carousel__card:hover {
|
|
729
|
+
background-color: var(--color-background-neutral-hover);
|
|
730
|
+
}
|
|
731
|
+
.carousel__card:focus {
|
|
732
|
+
background-color: var(--color-background-neutral-hover);
|
|
733
|
+
}
|
|
734
|
+
.carousel__card-content {
|
|
735
|
+
height: 100%;
|
|
736
|
+
font-weight: normal;
|
|
737
|
+
padding: 24px;
|
|
738
|
+
padding: var(--size-24);
|
|
739
|
+
}
|
|
740
|
+
.carousel__scroll-button {
|
|
741
|
+
width: 32px;
|
|
742
|
+
width: var(--size-32);
|
|
743
|
+
height: 32px;
|
|
744
|
+
height: var(--size-32);
|
|
745
|
+
align-items: center;
|
|
746
|
+
justify-content: center;
|
|
747
|
+
}
|
|
748
|
+
.carousel__indicators {
|
|
749
|
+
display: flex;
|
|
750
|
+
justify-content: center;
|
|
751
|
+
padding-top: 8px;
|
|
752
|
+
padding-top: var(--size-8);
|
|
753
|
+
gap: 8px;
|
|
754
|
+
gap: var(--size-8);
|
|
755
|
+
}
|
|
756
|
+
.carousel__indicator {
|
|
757
|
+
width: 12px;
|
|
758
|
+
width: var(--size-12);
|
|
759
|
+
height: 12px;
|
|
760
|
+
height: var(--size-12);
|
|
761
|
+
border-radius: 8px;
|
|
762
|
+
border-radius: var(--size-8);
|
|
763
|
+
background: #c9cbce;
|
|
764
|
+
background: var(--color-interactive-secondary);
|
|
765
|
+
border: none;
|
|
766
|
+
-webkit-appearance: none;
|
|
767
|
+
-moz-appearance: none;
|
|
768
|
+
appearance: none;
|
|
769
|
+
transition: all 0.1s;
|
|
770
|
+
}
|
|
771
|
+
.carousel__indicator:hover {
|
|
772
|
+
width: 16px;
|
|
773
|
+
width: var(--size-16);
|
|
774
|
+
}
|
|
775
|
+
.carousel__indicator--selected,
|
|
776
|
+
.carousel__indicator--selected:hover {
|
|
777
|
+
background: var(--color-interactive-primary);
|
|
778
|
+
width: 24px;
|
|
779
|
+
width: var(--size-24);
|
|
780
|
+
}
|
|
646
781
|
.np-checkbox-button input[type="checkbox"] {
|
|
647
782
|
position: absolute;
|
|
648
783
|
width: 24px;
|
package/src/main.less
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
|
|
3
3
|
import PhoneNumberInput from './PhoneNumberInput';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const meta = {
|
|
6
6
|
component: PhoneNumberInput,
|
|
7
7
|
title: 'Forms/PhoneNumberInput',
|
|
8
|
-
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
} satisfies Meta<typeof PhoneNumberInput>;
|
|
9
10
|
|
|
10
|
-
export
|
|
11
|
-
const disabled = boolean('disabled', false);
|
|
12
|
-
const required = boolean('required', false);
|
|
13
|
-
const size = select('size', ['sm', 'md', 'lg'], 'md');
|
|
14
|
-
const selectProps = object('selectProps', {
|
|
15
|
-
className: 'custom-class',
|
|
16
|
-
});
|
|
11
|
+
export default meta;
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
export const Basic = {
|
|
16
|
+
args: {
|
|
17
|
+
searchPlaceholder: 'searchPlaceholder',
|
|
18
|
+
placeholder: 'placeholder',
|
|
19
|
+
selectProps: {
|
|
20
|
+
className: 'custom-class',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} satisfies Story;
|
|
@@ -28,7 +28,7 @@ export interface PhoneNumberInputProps {
|
|
|
28
28
|
initialValue?: string;
|
|
29
29
|
onChange: (value: string | null, prefix: string) => void;
|
|
30
30
|
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
|
31
|
-
onBlur?:
|
|
31
|
+
onBlur?: () => void;
|
|
32
32
|
countryCode?: string;
|
|
33
33
|
searchPlaceholder?: string;
|
|
34
34
|
size?: SizeSmall | SizeMedium | SizeLarge;
|
|
@@ -73,6 +73,13 @@ const PhoneNumberInput = ({
|
|
|
73
73
|
});
|
|
74
74
|
const [broadcastedValue, setBroadcastedValue] = useState<PhoneNumber | null>(null);
|
|
75
75
|
|
|
76
|
+
const [suffixDirty, setSuffixDirty] = useState(false);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (internalValue.suffix) {
|
|
79
|
+
setSuffixDirty(true);
|
|
80
|
+
}
|
|
81
|
+
}, [internalValue.suffix]);
|
|
82
|
+
|
|
76
83
|
const countriesByPrefix = useMemo(
|
|
77
84
|
() =>
|
|
78
85
|
groupCountriesByPrefix(
|
|
@@ -168,6 +175,11 @@ const PhoneNumberInput = ({
|
|
|
168
175
|
const country = prefix != null ? findCountryByPrefix(prefix) : null;
|
|
169
176
|
setInternalValue((prev) => ({ ...prev, prefix, format: country?.phoneFormat }));
|
|
170
177
|
}}
|
|
178
|
+
onClose={() => {
|
|
179
|
+
if (suffixDirty) {
|
|
180
|
+
onBlur?.();
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
171
183
|
{...selectProps}
|
|
172
184
|
/>
|
|
173
185
|
</div>
|
|
@@ -186,7 +198,7 @@ const PhoneNumberInput = ({
|
|
|
186
198
|
onChange={onSuffixChange}
|
|
187
199
|
onPaste={onPaste}
|
|
188
200
|
onFocus={onFocus}
|
|
189
|
-
onBlur={onBlur}
|
|
201
|
+
onBlur={() => onBlur?.()}
|
|
190
202
|
/>
|
|
191
203
|
</div>
|
|
192
204
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Meta, StoryObj } from '@storybook/react';
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import { StarFill } from '@transferwise/icons';
|
|
3
3
|
|
|
4
|
-
import PromoCard, { PromoCardCheckedProps, PromoCardLinkProps } from './PromoCard';
|
|
4
|
+
import PromoCard, { type PromoCardCheckedProps, type PromoCardLinkProps } from './PromoCard';
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof PromoCard> = {
|
|
7
7
|
component: PromoCard,
|