@transferwise/components 45.13.0 → 45.14.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 (85) hide show
  1. package/build/index.esm.js +152 -91
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +156 -94
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +1 -1
  6. package/build/styles/inputs/InputGroup.css +1 -0
  7. package/build/styles/main.css +1 -1
  8. package/build/types/avatarWrapper/AvatarWrapper.d.ts +18 -28
  9. package/build/types/avatarWrapper/AvatarWrapper.d.ts.map +1 -1
  10. package/build/types/avatarWrapper/index.d.ts +1 -1
  11. package/build/types/avatarWrapper/index.d.ts.map +1 -1
  12. package/build/types/badge/Badge.d.ts +2 -2
  13. package/build/types/badge/Badge.d.ts.map +1 -1
  14. package/build/types/badge/index.d.ts +2 -1
  15. package/build/types/badge/index.d.ts.map +1 -1
  16. package/build/types/body/Body.d.ts +2 -2
  17. package/build/types/common/hooks/useEffectEvent.d.ts +2 -0
  18. package/build/types/common/hooks/useEffectEvent.d.ts.map +1 -0
  19. package/build/types/common/hooks/useResizeObserver.d.ts +3 -0
  20. package/build/types/common/hooks/useResizeObserver.d.ts.map +1 -0
  21. package/build/types/common/index.d.ts +1 -1
  22. package/build/types/common/panel/Panel.d.ts +1 -1
  23. package/build/types/common/propsValues/profileType.d.ts +2 -0
  24. package/build/types/common/propsValues/profileType.d.ts.map +1 -1
  25. package/build/types/common/propsValues/sentiment.d.ts +1 -0
  26. package/build/types/common/propsValues/sentiment.d.ts.map +1 -1
  27. package/build/types/common/responsivePanel/ResponsivePanel.d.ts +1 -1
  28. package/build/types/dimmer/Dimmer.d.ts +1 -1
  29. package/build/types/image/Image.d.ts +17 -21
  30. package/build/types/image/Image.d.ts.map +1 -1
  31. package/build/types/image/index.d.ts +1 -1
  32. package/build/types/image/index.d.ts.map +1 -1
  33. package/build/types/index.d.ts +2 -0
  34. package/build/types/index.d.ts.map +1 -1
  35. package/build/types/inlineAlert/InlineAlert.d.ts +12 -15
  36. package/build/types/inlineAlert/InlineAlert.d.ts.map +1 -1
  37. package/build/types/inlineAlert/index.d.ts +1 -1
  38. package/build/types/inlineAlert/index.d.ts.map +1 -1
  39. package/build/types/inputs/Input.d.ts +4 -8
  40. package/build/types/inputs/Input.d.ts.map +1 -1
  41. package/build/types/inputs/InputGroup.d.ts +21 -0
  42. package/build/types/inputs/InputGroup.d.ts.map +1 -0
  43. package/build/types/inputs/TextArea.d.ts +4 -7
  44. package/build/types/inputs/TextArea.d.ts.map +1 -1
  45. package/build/types/promoCard/PromoCard.d.ts.map +1 -1
  46. package/build/types/select/searchBox/SearchBox.d.ts +1 -1
  47. package/build/types/utilities/cssValueWithUnit.d.ts +2 -0
  48. package/build/types/utilities/cssValueWithUnit.d.ts.map +1 -0
  49. package/package.json +1 -1
  50. package/src/avatarWrapper/AvatarWrapper.spec.tsx +105 -0
  51. package/src/avatarWrapper/AvatarWrapper.story.tsx +1 -41
  52. package/src/avatarWrapper/{AvatarWrapper.js → AvatarWrapper.tsx} +25 -56
  53. package/src/avatarWrapper/__snapshots__/{AvatarWrapper.spec.js.snap → AvatarWrapper.spec.tsx.snap} +76 -76
  54. package/src/badge/Badge.tsx +2 -2
  55. package/src/badge/index.ts +2 -0
  56. package/src/common/hooks/useEffectEvent.ts +22 -0
  57. package/src/common/hooks/useResizeObserver.ts +22 -0
  58. package/src/common/index.js +1 -1
  59. package/src/common/propsValues/profileType.ts +3 -0
  60. package/src/common/propsValues/sentiment.ts +10 -0
  61. package/src/image/{Image.spec.js → Image.spec.tsx} +5 -5
  62. package/src/image/{Image.story.js → Image.story.tsx} +3 -3
  63. package/src/image/Image.tsx +65 -0
  64. package/src/index.ts +2 -0
  65. package/src/inlineAlert/{InlineAlert.story.js → InlineAlert.story.tsx} +3 -2
  66. package/src/inlineAlert/InlineAlert.tsx +59 -0
  67. package/src/inputs/Input.tsx +13 -7
  68. package/src/inputs/InputGroup.css +1 -0
  69. package/src/inputs/InputGroup.less +61 -0
  70. package/src/inputs/InputGroup.story.tsx +73 -0
  71. package/src/inputs/InputGroup.tsx +142 -0
  72. package/src/inputs/TextArea.tsx +7 -6
  73. package/src/main.css +1 -1
  74. package/src/main.less +1 -0
  75. package/src/promoCard/PromoCard.tsx +1 -6
  76. package/src/promoCard/__snapshots__/PromoCardGroup.spec.tsx.snap +2 -2
  77. package/src/utilities/cssValueWithUnit.ts +3 -0
  78. package/src/avatarWrapper/AvatarWrapper.spec.js +0 -81
  79. package/src/badge/index.js +0 -1
  80. package/src/image/Image.js +0 -78
  81. package/src/inlineAlert/InlineAlert.js +0 -62
  82. /package/src/avatarWrapper/{index.js → index.ts} +0 -0
  83. /package/src/image/{index.js → index.ts} +0 -0
  84. /package/src/inlineAlert/{InlineAlert.spec.js → InlineAlert.spec.tsx} +0 -0
  85. /package/src/inlineAlert/{index.js → index.ts} +0 -0
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Types
3
3
  */
4
4
  export type { InputProps } from './inputs/Input';
5
+ export type { InputGroupProps } from './inputs/InputGroup';
5
6
  export type { TextAreaProps } from './inputs/TextArea';
6
7
  export type { UploadedFile, UploadError, UploadResponse } from './uploadInput/types';
7
8
  export type { ModalProps } from './modal';
@@ -52,6 +53,7 @@ export { default as Image } from './image';
52
53
  export { default as Info } from './info';
53
54
  export { default as InlineAlert } from './inlineAlert';
54
55
  export { Input } from './inputs/Input';
56
+ export { InputGroup } from './inputs/InputGroup';
55
57
  export { TextArea } from './inputs/TextArea';
56
58
  export { default as InputWithDisplayFormat } from './inputWithDisplayFormat';
57
59
  export { default as InstructionsList } from './instructionsList';
@@ -1,14 +1,15 @@
1
1
  import { select, text } from '@storybook/addon-knobs';
2
+ import { Meta } from '@storybook/react';
2
3
 
3
4
  import { Sentiment } from '../common';
4
5
  import { Input } from '../inputs/Input';
5
6
 
6
- import InlineAlert from './InlineAlert';
7
+ import InlineAlert, { InlineAlertProps } from './InlineAlert';
7
8
 
8
9
  export default {
9
10
  component: InlineAlert,
10
11
  title: 'Forms/InlineAlert',
11
- };
12
+ } as Meta<InlineAlertProps>;
12
13
 
13
14
  export const Basic = () => {
14
15
  const type = select(
@@ -0,0 +1,59 @@
1
+ import { AlertCircle as AlertCircleIcon } from '@transferwise/icons';
2
+ import { useTheme } from '@wise/components-theming';
3
+ import classNames from 'classnames';
4
+ import { ReactNode } from 'react';
5
+
6
+ import withArrow, { AlertArrowPosition } from '../alert/withArrow';
7
+ import { Sentiment } from '../common';
8
+ import { SentimentString } from '../common/propsValues/sentiment';
9
+
10
+ export interface InlineAlertProps {
11
+ id?: string;
12
+ type?: Sentiment | SentimentString;
13
+ className?: string;
14
+ children: ReactNode;
15
+ }
16
+
17
+ const typeClassMap: Record<Sentiment.ERROR | Sentiment.NEGATIVE, string> = {
18
+ [Sentiment.ERROR]: 'danger',
19
+ [Sentiment.NEGATIVE]: 'danger',
20
+ };
21
+
22
+ const InlineAlert = ({ id, type = Sentiment.NEUTRAL, className, children }: InlineAlertProps) => {
23
+ const { isModern } = useTheme();
24
+
25
+ const typeClass = `alert-${typeClassMap[type as Sentiment.ERROR | Sentiment.NEGATIVE] || type}`;
26
+
27
+ if (isModern) {
28
+ return (
29
+ <div role="alert" id={id} className={classNames('alert alert-detach', typeClass, className)}>
30
+ {(type === 'error' || type === 'negative') && <AlertCircleIcon />}
31
+ <div>{children}</div>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ const getAlertContents = ({
37
+ children,
38
+ className,
39
+ }: {
40
+ children: ReactNode;
41
+ className?: string;
42
+ }) => {
43
+ return (
44
+ <div
45
+ role="alert"
46
+ id={id}
47
+ className={classNames('alert alert-detach p-x-2 p-y-1', typeClass, className)}
48
+ >
49
+ {children}
50
+ </div>
51
+ );
52
+ };
53
+
54
+ const AlertWithArrow = withArrow(getAlertContents, AlertArrowPosition.TOP_LEFT);
55
+
56
+ return <AlertWithArrow {...{ id, type, className, children }} />;
57
+ };
58
+
59
+ export default InlineAlert;
@@ -4,24 +4,30 @@ import { forwardRef } from 'react';
4
4
  import { SizeLarge, SizeMedium, SizeSmall } from '../common';
5
5
  import { Merge } from '../utils';
6
6
 
7
+ import { useInputPaddings } from './InputGroup';
7
8
  import { formControlClassNameBase } from './_common';
8
9
 
9
- export type InputProps = Merge<
10
- React.ComponentPropsWithRef<'input'>,
11
- {
12
- size?: 'auto' | SizeSmall | SizeMedium | SizeLarge;
13
- 'aria-invalid'?: boolean;
14
- }
15
- >;
10
+ export interface InputProps
11
+ extends Merge<
12
+ React.ComponentPropsWithRef<'input'>,
13
+ {
14
+ size?: 'auto' | SizeSmall | SizeMedium | SizeLarge;
15
+ 'aria-invalid'?: boolean;
16
+ }
17
+ > {}
16
18
 
17
19
  export const Input = forwardRef(function Input(
18
20
  { size = 'auto', className, ...restProps }: InputProps,
19
21
  reference: React.ForwardedRef<HTMLInputElement>,
20
22
  ) {
23
+ const inputPaddings = useInputPaddings();
24
+
21
25
  return (
22
26
  <input
23
27
  ref={reference}
24
28
  className={classNames(className, formControlClassNameBase({ size }))}
29
+ // eslint-disable-next-line react/forbid-dom-props
30
+ style={inputPaddings}
25
31
  {...restProps}
26
32
  />
27
33
  );
@@ -0,0 +1 @@
1
+ .np-input-group{border-radius:50%;border-radius:var(--radius-full);display:inline-grid;grid-auto-columns:minmax(0,1fr)}.np-input-group>*{grid-column-start:1;grid-row-start:1}.np-input-addon{align-items:center;color:#c9cbce;color:var(--color-interactive-secondary);display:inline-flex;pointer-events:none;transition-duration:.15s;transition-property:color,opacity;transition-timing-function:ease-out;z-index:10}.np-input-group:has(>:is(input,button,select):focus-visible) .np-input-addon{color:var(--color-interactive-primary)}.np-input-group:has(>:is(input,button,select):hover) .np-input-addon{color:#b5b7ba;color:var(--color-interactive-secondary-hover)}.np-input-addon--placement-start{justify-self:start}.np-input-addon--placement-end{justify-self:end}.np-input-addon--interactive>*{pointer-events:auto}.np-input-addon--padding-sm{padding-left:8px;padding-left:var(--size-8);padding-right:8px;padding-right:var(--size-8)}.np-input-addon--padding-md{padding-left:16px;padding-left:var(--size-16);padding-right:16px;padding-right:var(--size-16)}.np-input-addon--padding-md.np-input-addon--placement-start{padding-inline-end:8px;padding-inline-end:var(--size-8)}.np-input-addon--padding-md.np-input-addon--placement-end{padding-inline-start:8px;padding-inline-start:var(--size-8)}
@@ -0,0 +1,61 @@
1
+ .np-input-group {
2
+ display: inline-grid;
3
+ grid-auto-columns: minmax(0, 1fr);
4
+
5
+ > * {
6
+ grid-column-start: 1;
7
+ grid-row-start: 1;
8
+ }
9
+
10
+ /* Prevent unwanted `group-hover/input` triggers */
11
+ border-radius: var(--radius-full);
12
+ }
13
+
14
+ .np-input-addon {
15
+ pointer-events: none;
16
+ z-index: 10;
17
+ display: inline-flex;
18
+ align-items: center;
19
+ color: var(--color-interactive-secondary);
20
+ transition-property: color, opacity;
21
+ transition-timing-function: ease-out;
22
+ transition-duration: 150ms;
23
+
24
+ .np-input-group:has(>:is(input,button,select):focus-visible) & {
25
+ color: var(--color-interactive-primary);
26
+ }
27
+
28
+ .np-input-group:has(>:is(input,button,select):hover) & {
29
+ color: var(--color-interactive-secondary-hover);
30
+ }
31
+
32
+ &--placement-start {
33
+ justify-self: start;
34
+ }
35
+
36
+ &--placement-end {
37
+ justify-self: end;
38
+ }
39
+
40
+ &--interactive > * {
41
+ pointer-events: auto;
42
+ }
43
+
44
+ &--padding-sm {
45
+ padding-left: var(--size-8);
46
+ padding-right: var(--size-8);
47
+ }
48
+
49
+ &--padding-md {
50
+ padding-left: var(--size-16);
51
+ padding-right: var(--size-16);
52
+
53
+ &.np-input-addon--placement-start {
54
+ padding-inline-end: var(--size-8);
55
+ }
56
+
57
+ &.np-input-addon--placement-end {
58
+ padding-inline-start: var(--size-8);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,73 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Search } from '@transferwise/icons';
3
+ import { useRef, useState } from 'react';
4
+
5
+ import ActionButton from '../actionButton';
6
+
7
+ import { Input } from './Input';
8
+ import { InputGroup } from './InputGroup';
9
+
10
+ export default {
11
+ component: InputGroup,
12
+ title: 'Forms/InputGroup',
13
+ } satisfies Meta<typeof InputGroup>;
14
+
15
+ type Story = StoryObj<typeof InputGroup>;
16
+
17
+ export const WithPrefix: Story = {
18
+ render: (args) => <InputGroupWithPrefix {...args} />,
19
+ args: {
20
+ disabled: false,
21
+ },
22
+ };
23
+
24
+ function InputGroupWithPrefix(args: NonNullable<Story['args']>) {
25
+ return (
26
+ <InputGroup
27
+ {...args}
28
+ addonStart={{
29
+ content: <Search size={24} />,
30
+ initialContentWidth: 24,
31
+ }}
32
+ >
33
+ <Input placeholder="Search by name…" />
34
+ </InputGroup>
35
+ );
36
+ }
37
+
38
+ export const WithSuffix: Story = {
39
+ render: (args) => <InputGroupWithSuffix {...args} />,
40
+ args: {
41
+ disabled: false,
42
+ },
43
+ };
44
+
45
+ function InputGroupWithSuffix(args: NonNullable<Story['args']>) {
46
+ const ref = useRef<HTMLInputElement>(null);
47
+ const [value, setValue] = useState('Text value');
48
+
49
+ return (
50
+ <InputGroup
51
+ {...args}
52
+ addonEnd={{
53
+ content: (
54
+ <ActionButton
55
+ onClick={async () => {
56
+ await navigator.clipboard.writeText(value);
57
+ if (ref.current != null) {
58
+ ref.current.focus({ preventScroll: true });
59
+ ref.current.select();
60
+ }
61
+ }}
62
+ >
63
+ Copy
64
+ </ActionButton>
65
+ ),
66
+ interactive: true,
67
+ padding: 'sm',
68
+ }}
69
+ >
70
+ <Input ref={ref} value={value} onChange={(event) => setValue(event.currentTarget.value)} />
71
+ </InputGroup>
72
+ );
73
+ }
@@ -0,0 +1,142 @@
1
+ import classNames from 'classnames';
2
+ import { createContext, useContext, useMemo, useRef, useState } from 'react';
3
+
4
+ import { useResizeObserver } from '../common/hooks/useResizeObserver';
5
+ import { cssValueWithUnit } from '../utilities/cssValueWithUnit';
6
+
7
+ type InputPaddingContextType = [
8
+ number | string | undefined,
9
+ React.Dispatch<React.SetStateAction<number | string | undefined>>,
10
+ ];
11
+
12
+ const InputPaddingStartContext = createContext<InputPaddingContextType>([undefined, () => {}]);
13
+
14
+ const InputPaddingEndContext = createContext<InputPaddingContextType>([undefined, () => {}]);
15
+
16
+ export function useInputPaddings() {
17
+ const [paddingStart] = useContext(InputPaddingStartContext);
18
+ const [paddingEnd] = useContext(InputPaddingEndContext);
19
+
20
+ return {
21
+ paddingInlineStart: paddingStart,
22
+ paddingInlineEnd: paddingEnd,
23
+ } satisfies React.CSSProperties;
24
+ }
25
+
26
+ interface InputGroupAddon {
27
+ content: React.ReactNode;
28
+ initialContentWidth?: number | string;
29
+ interactive?: boolean;
30
+ padding?: 'none' | 'sm' | 'md';
31
+ }
32
+
33
+ export interface InputGroupProps {
34
+ addonStart?: InputGroupAddon;
35
+ addonEnd?: InputGroupAddon;
36
+ disabled?: boolean;
37
+ className?: string;
38
+ children?: React.ReactNode;
39
+ }
40
+
41
+ function inputPaddingInitialState({
42
+ initialContentWidth,
43
+ padding = inputAddonDefaultPadding,
44
+ }: Pick<
45
+ InputGroupAddon,
46
+ 'initialContentWidth' | 'padding'
47
+ > = {}): () => InputPaddingContextType[0] {
48
+ return () =>
49
+ initialContentWidth != null
50
+ ? `calc(${cssValueWithUnit(initialContentWidth)} + ${cssValueWithUnit(
51
+ inputAddonContentWidthAddendByPadding[padding],
52
+ )})`
53
+ : undefined;
54
+ }
55
+
56
+ export function InputGroup({
57
+ addonStart,
58
+ addonEnd,
59
+ disabled,
60
+ className,
61
+ children,
62
+ }: InputGroupProps) {
63
+ const [paddingStart, setPaddingStart] = useState(inputPaddingInitialState(addonStart));
64
+ const [paddingEnd, setPaddingEnd] = useState(inputPaddingInitialState(addonEnd));
65
+
66
+ return (
67
+ <InputPaddingStartContext.Provider
68
+ value={useMemo(() => [paddingStart, setPaddingStart], [paddingStart])}
69
+ >
70
+ <InputPaddingEndContext.Provider
71
+ value={useMemo(() => [paddingEnd, setPaddingEnd], [paddingEnd])}
72
+ >
73
+ <fieldset disabled={disabled} className={classNames(className, 'np-input-group')}>
74
+ {addonStart != null ? <InputAddon placement="start" {...addonStart} /> : null}
75
+ {children}
76
+ {addonEnd != null ? <InputAddon placement="end" {...addonEnd} /> : null}
77
+ </fieldset>
78
+ </InputPaddingEndContext.Provider>
79
+ </InputPaddingStartContext.Provider>
80
+ );
81
+ }
82
+
83
+ interface InputAddonProps extends Omit<InputGroupAddon, 'initialContentWidth'> {
84
+ placement: 'start' | 'end';
85
+ }
86
+
87
+ const inputAddonContentWidthAddendByPadding = {
88
+ none: 0,
89
+ sm: '1rem',
90
+ md: '1.5rem',
91
+ } satisfies {
92
+ [key in NonNullable<InputAddonProps['padding']>]: InputPaddingContextType[0];
93
+ };
94
+
95
+ const inputAddonDefaultPadding = 'md' satisfies InputAddonProps['padding'];
96
+
97
+ function InputAddon({
98
+ placement,
99
+ content,
100
+ interactive,
101
+ padding = inputAddonDefaultPadding,
102
+ }: InputAddonProps) {
103
+ const [, setInputPadding] = useContext(
104
+ placement === 'start' ? InputPaddingStartContext : InputPaddingEndContext,
105
+ );
106
+
107
+ const ref = useRef<HTMLSpanElement>(null);
108
+ useResizeObserver(ref, (entry) => {
109
+ // TODO: Remove fallback once most browsers support `borderBoxSize`
110
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
111
+ if (entry.borderBoxSize != null) {
112
+ setInputPadding(entry.borderBoxSize[0].inlineSize);
113
+ } else {
114
+ const targetStyle = getComputedStyle(entry.target);
115
+ setInputPadding(
116
+ entry.contentRect.width +
117
+ Number.parseFloat(targetStyle.paddingInlineStart) +
118
+ Number.parseFloat(targetStyle.paddingInlineEnd),
119
+ );
120
+ }
121
+ });
122
+
123
+ return (
124
+ <span
125
+ ref={ref}
126
+ className={classNames(
127
+ 'np-input-addon',
128
+ {
129
+ 'np-input-addon--placement-start': placement === 'start',
130
+ 'np-input-addon--placement-end': placement === 'end',
131
+ },
132
+ interactive && 'np-input-addon--interactive',
133
+ {
134
+ 'np-input-addon--padding-sm': padding === 'sm',
135
+ 'np-input-addon--padding-md': padding === 'md',
136
+ },
137
+ )}
138
+ >
139
+ {content}
140
+ </span>
141
+ );
142
+ }
@@ -5,12 +5,13 @@ import { Merge } from '../utils';
5
5
 
6
6
  import { formControlClassNameBase } from './_common';
7
7
 
8
- export type TextAreaProps = Merge<
9
- React.ComponentPropsWithRef<'textarea'>,
10
- {
11
- 'aria-invalid'?: boolean;
12
- }
13
- >;
8
+ export interface TextAreaProps
9
+ extends Merge<
10
+ React.ComponentPropsWithRef<'textarea'>,
11
+ {
12
+ 'aria-invalid'?: boolean;
13
+ }
14
+ > {}
14
15
 
15
16
  export const TextArea = forwardRef(function TextArea(
16
17
  { className, ...restProps }: TextAreaProps,