@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.
Files changed (80) hide show
  1. package/build/index.js +931 -523
  2. package/build/index.js.map +1 -1
  3. package/build/index.mjs +928 -523
  4. package/build/index.mjs.map +1 -1
  5. package/build/main.css +135 -0
  6. package/build/styles/carousel/Carousel.css +135 -0
  7. package/build/styles/main.css +135 -0
  8. package/build/types/carousel/Carousel.d.ts +26 -0
  9. package/build/types/carousel/Carousel.d.ts.map +1 -0
  10. package/build/types/carousel/index.d.ts +3 -0
  11. package/build/types/carousel/index.d.ts.map +1 -0
  12. package/build/types/common/card/Card.d.ts +2 -2
  13. package/build/types/common/card/Card.d.ts.map +1 -1
  14. package/build/types/dateInput/DateInput.d.ts +5 -4
  15. package/build/types/dateInput/DateInput.d.ts.map +1 -1
  16. package/build/types/dateLookup/DateLookup.d.ts +11 -4
  17. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  18. package/build/types/field/Field.d.ts +12 -0
  19. package/build/types/field/Field.d.ts.map +1 -0
  20. package/build/types/index.d.ts +6 -0
  21. package/build/types/index.d.ts.map +1 -1
  22. package/build/types/inputs/Input.d.ts.map +1 -1
  23. package/build/types/inputs/SelectInput.d.ts +1 -1
  24. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  25. package/build/types/inputs/TextArea.d.ts.map +1 -1
  26. package/build/types/inputs/_common.d.ts +2 -2
  27. package/build/types/inputs/_common.d.ts.map +1 -1
  28. package/build/types/inputs/contexts.d.ts +24 -0
  29. package/build/types/inputs/contexts.d.ts.map +1 -0
  30. package/build/types/label/Label.d.ts +9 -0
  31. package/build/types/label/Label.d.ts.map +1 -0
  32. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
  33. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  34. package/build/types/promoCard/PromoCard.d.ts +16 -5
  35. package/build/types/promoCard/PromoCard.d.ts.map +1 -1
  36. package/build/types/radioGroup/RadioGroup.d.ts.map +1 -1
  37. package/build/types/switch/Switch.d.ts +6 -3
  38. package/build/types/switch/Switch.d.ts.map +1 -1
  39. package/package.json +3 -3
  40. package/src/carousel/Carousel.css +135 -0
  41. package/src/carousel/Carousel.less +133 -0
  42. package/src/carousel/Carousel.spec.tsx +221 -0
  43. package/src/carousel/Carousel.story.tsx +63 -0
  44. package/src/carousel/Carousel.tsx +345 -0
  45. package/src/carousel/index.ts +3 -0
  46. package/src/common/card/Card.tsx +51 -43
  47. package/src/dateInput/DateInput.rtl.spec.tsx +17 -0
  48. package/src/dateInput/DateInput.tsx +28 -22
  49. package/src/dateLookup/DateLookup.keyboardEvents.spec.js +2 -2
  50. package/src/dateLookup/DateLookup.rtl.spec.tsx +21 -0
  51. package/src/dateLookup/DateLookup.state.spec.js +5 -5
  52. package/src/dateLookup/DateLookup.tests.story.tsx +4 -11
  53. package/src/dateLookup/DateLookup.tsx +24 -9
  54. package/src/dateLookup/DateLookup.view.spec.js +11 -11
  55. package/src/field/Field.spec.tsx +95 -0
  56. package/src/field/Field.story.tsx +59 -0
  57. package/src/field/Field.tsx +70 -0
  58. package/src/index.ts +6 -0
  59. package/src/inputs/Input.tsx +5 -3
  60. package/src/inputs/SelectInput.spec.tsx +10 -0
  61. package/src/inputs/SelectInput.tsx +9 -4
  62. package/src/inputs/TextArea.tsx +6 -3
  63. package/src/inputs/_ButtonInput.tsx +2 -2
  64. package/src/inputs/_common.ts +2 -2
  65. package/src/inputs/contexts.tsx +45 -0
  66. package/src/label/Label.spec.tsx +26 -0
  67. package/src/label/Label.story.tsx +37 -0
  68. package/src/label/Label.tsx +20 -0
  69. package/src/main.css +135 -0
  70. package/src/main.less +1 -0
  71. package/src/phoneNumberInput/PhoneNumberInput.story.tsx +16 -22
  72. package/src/phoneNumberInput/PhoneNumberInput.tsx +14 -2
  73. package/src/promoCard/PromoCard.story.tsx +2 -2
  74. package/src/promoCard/PromoCard.tsx +30 -9
  75. package/src/radioGroup/RadioGroup.rtl.spec.tsx +14 -0
  76. package/src/radioGroup/RadioGroup.story.tsx +26 -0
  77. package/src/radioGroup/RadioGroup.tsx +4 -1
  78. package/src/switch/Switch.spec.tsx +10 -0
  79. package/src/switch/Switch.tsx +22 -13
  80. 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';
@@ -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, formControlClassNameBase({ size }), 'np-input', {
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
  {
@@ -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 { formControlClassNameBase } from './_common';
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, formControlClassNameBase(), 'np-text-area')}
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 { formControlClassNameBase } from './_common';
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, formControlClassNameBase({ size }), 'np-button-input')}
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}
@@ -2,11 +2,11 @@ import classNames from 'classnames';
2
2
 
3
3
  import { SizeLarge, SizeMedium, SizeSmall } from '../common';
4
4
 
5
- export type FormControlPropsBase = {
5
+ export type InputPropsBase = {
6
6
  size?: 'auto' | SizeSmall | SizeMedium | SizeLarge;
7
7
  };
8
8
 
9
- export function formControlClassNameBase({ size = 'auto' }: FormControlPropsBase = {}) {
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
@@ -5,6 +5,7 @@
5
5
  @import "./badge/Badge.less";
6
6
  @import "./button/Button.less";
7
7
  @import "./card/Card.less";
8
+ @import "./carousel/Carousel.less";
8
9
  @import "./checkboxButton/CheckboxButton.less";
9
10
  @import "./chips/Chip.less";
10
11
  @import "./circularButton/CircularButton.less";
@@ -1,29 +1,23 @@
1
- import { select, boolean, object } from '@storybook/addon-knobs';
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
2
 
3
3
  import PhoneNumberInput from './PhoneNumberInput';
4
4
 
5
- export default {
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 const Basic = () => {
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
- return (
19
- <PhoneNumberInput
20
- disabled={disabled}
21
- required={required}
22
- size={size}
23
- selectProps={selectProps}
24
- searchPlaceholder="searchPlaceholder"
25
- placeholder="placeholder"
26
- onChange={console.log}
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?: React.FocusEventHandler<HTMLInputElement>;
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,