@transferwise/components 46.111.1 → 46.112.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 (52) hide show
  1. package/build/avatarLayout/AvatarLayout.js.map +1 -1
  2. package/build/avatarLayout/AvatarLayout.mjs.map +1 -1
  3. package/build/avatarView/AvatarView.js +27 -29
  4. package/build/avatarView/AvatarView.js.map +1 -1
  5. package/build/avatarView/AvatarView.mjs +27 -29
  6. package/build/avatarView/AvatarView.mjs.map +1 -1
  7. package/build/avatarView/{NotificationDot.js → Dot.js} +14 -12
  8. package/build/avatarView/Dot.js.map +1 -0
  9. package/build/avatarView/{NotificationDot.mjs → Dot.mjs} +14 -12
  10. package/build/avatarView/Dot.mjs.map +1 -0
  11. package/build/badge/BadgeAssets.js.map +1 -1
  12. package/build/badge/BadgeAssets.mjs.map +1 -1
  13. package/build/inputs/SelectInput.js +41 -2
  14. package/build/inputs/SelectInput.js.map +1 -1
  15. package/build/inputs/SelectInput.mjs +41 -2
  16. package/build/inputs/SelectInput.mjs.map +1 -1
  17. package/build/main.css +17 -11
  18. package/build/styles/avatarView/AvatarView.css +17 -11
  19. package/build/styles/avatarView/Dot.css +26 -0
  20. package/build/styles/main.css +17 -11
  21. package/build/types/avatarLayout/AvatarLayout.d.ts +1 -1
  22. package/build/types/avatarLayout/AvatarLayout.d.ts.map +1 -1
  23. package/build/types/avatarView/AvatarView.d.ts +1 -2
  24. package/build/types/avatarView/AvatarView.d.ts.map +1 -1
  25. package/build/types/avatarView/Dot.d.ts +8 -0
  26. package/build/types/avatarView/Dot.d.ts.map +1 -0
  27. package/build/types/badge/BadgeAssets.d.ts +1 -1
  28. package/build/types/badge/BadgeAssets.d.ts.map +1 -1
  29. package/build/types/inputs/SelectInput.d.ts +20 -1
  30. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/src/avatarLayout/AvatarLayout.tsx +1 -1
  33. package/src/avatarView/AvatarView.css +17 -11
  34. package/src/avatarView/AvatarView.less +1 -1
  35. package/src/avatarView/AvatarView.story.tsx +92 -36
  36. package/src/avatarView/AvatarView.tsx +35 -30
  37. package/src/avatarView/Dot.css +26 -0
  38. package/src/avatarView/Dot.less +31 -0
  39. package/src/avatarView/Dot.tsx +42 -0
  40. package/src/badge/BadgeAssets.tsx +1 -1
  41. package/src/inputs/SelectInput.story.tsx +94 -0
  42. package/src/inputs/SelectInput.tsx +84 -1
  43. package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +89 -25
  44. package/src/main.css +17 -11
  45. package/build/avatarView/NotificationDot.js.map +0 -1
  46. package/build/avatarView/NotificationDot.mjs.map +0 -1
  47. package/build/styles/avatarView/NotificationDot.css +0 -20
  48. package/build/types/avatarView/NotificationDot.d.ts +0 -8
  49. package/build/types/avatarView/NotificationDot.d.ts.map +0 -1
  50. package/src/avatarView/NotificationDot.css +0 -20
  51. package/src/avatarView/NotificationDot.less +0 -24
  52. package/src/avatarView/NotificationDot.tsx +0 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "46.111.1",
3
+ "version": "46.112.0",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -84,8 +84,8 @@
84
84
  "storybook-addon-tag-badges": "^2.0.2",
85
85
  "storybook-addon-test-codegen": "^2.0.1",
86
86
  "@transferwise/less-config": "3.1.2",
87
- "@wise/components-theming": "1.7.0",
88
87
  "@transferwise/neptune-css": "14.25.0",
88
+ "@wise/components-theming": "1.7.0",
89
89
  "@wise/wds-configs": "0.0.0"
90
90
  },
91
91
  "peerDependencies": {
@@ -4,7 +4,7 @@ import { useDirection } from '../common/hooks';
4
4
 
5
5
  type SingleAvatarType = { asset?: AvatarViewProps['children'] } & Omit<
6
6
  AvatarViewProps,
7
- 'notification' | 'selected' | 'size' | 'badge' | 'children' | 'interactive'
7
+ 'selected' | 'size' | 'badge' | 'children' | 'interactive'
8
8
  >;
9
9
 
10
10
  export type Props = {
@@ -1,23 +1,29 @@
1
- .np-notification-dot {
2
- --np-notification-dot-size: 14px;
1
+ .np-dot {
2
+ --np-dot-size: 14px;
3
3
  position: relative;
4
4
  display: inline-block;
5
5
  }
6
- .np-notification-dot-mask {
7
- -webkit-mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-notification-dot-size) / 2)) left calc(100% - (var(--np-notification-dot-size) / 2)), transparent 0, transparent calc(var(--np-notification-dot-size) / 2 + var(--np-notification-dot-offset)), black 0);
8
- mask-image: radial-gradient(circle at bottom calc(100% - (var(--np-notification-dot-size) / 2)) left calc(100% - (var(--np-notification-dot-size) / 2)), transparent 0, transparent calc(var(--np-notification-dot-size) / 2 + var(--np-notification-dot-offset)), black 0);
9
- -webkit-mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-notification-dot-size) / 2)) left calc(100% - calc(var(--np-notification-dot-size) / 2)), transparent 0, transparent calc(var(--np-notification-dot-size) / 2 + var(--np-notification-dot-offset)), black 0);
10
- mask-image: radial-gradient(circle at bottom calc(100% - calc(var(--np-notification-dot-size) / 2)) left calc(100% - calc(var(--np-notification-dot-size) / 2)), transparent 0, transparent calc(var(--np-notification-dot-size) / 2 + var(--np-notification-dot-offset)), black 0);
6
+ .np-dot-mask {
7
+ -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);
8
+ 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);
9
+ -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);
10
+ 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);
11
11
  }
12
- .np-notification-dot-badge {
12
+ .np-dot-badge {
13
13
  position: absolute;
14
- width: var(--np-notification-dot-size);
15
- height: var(--np-notification-dot-size);
16
- background-color: var(--color-sentiment-negative);
14
+ width: var(--np-dot-size);
15
+ height: var(--np-dot-size);
17
16
  border-radius: 9999px;
18
17
  border-radius: var(--radius-full);
19
18
  right: 0;
20
19
  }
20
+ .np-dot-badge-notification {
21
+ background-color: var(--color-sentiment-negative);
22
+ }
23
+ .np-dot-badge-online {
24
+ background-color: #00a2dd;
25
+ background-color: var(--color-interactive-accent);
26
+ }
21
27
  .np-avatar-view .np-avatar-view-content {
22
28
  color: #37517e;
23
29
  color: var(--color-content-primary);
@@ -1,4 +1,4 @@
1
- @import './NotificationDot.less';
1
+ @import './Dot.less';
2
2
 
3
3
  .np-avatar-view {
4
4
  .np-avatar-view-content {
@@ -67,42 +67,6 @@ export const Selected: Story = {
67
67
  },
68
68
  };
69
69
 
70
- export const Notification: Story = {
71
- render: () => (
72
- <div
73
- style={{
74
- gap: '1em',
75
- display: 'grid',
76
- justifyContent: 'space-between',
77
- gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
78
- }}
79
- >
80
- {sizes.map((size) => (
81
- <Body type="body-large-bold">{size}</Body>
82
- ))}
83
- {sizes.map((size) => (
84
- <AvatarView key={size} size={size} notification>
85
- <Freeze />
86
- </AvatarView>
87
- ))}
88
- {sizes.map((size) => (
89
- <AvatarView key={size} size={size} notification interactive>
90
- <Freeze />
91
- </AvatarView>
92
- ))}
93
- {sizes.map((size) => (
94
- <AvatarView
95
- key={size}
96
- size={size}
97
- imgSrc="../avatar-rectangle-fox.webp"
98
- notification
99
- interactive
100
- />
101
- ))}
102
- </div>
103
- ),
104
- };
105
-
106
70
  export const Badge: Story = {
107
71
  render: () => {
108
72
  const currencies = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'CNY'];
@@ -219,6 +183,7 @@ export const Badge: Story = {
219
183
  badge={{ icon: <Convert /> }}
220
184
  />
221
185
  ))}
186
+
222
187
  {sizes.map((size) => (
223
188
  <AvatarView
224
189
  key={size}
@@ -241,11 +206,102 @@ export const Badge: Story = {
241
206
  }}
242
207
  />
243
208
  ))}
209
+
210
+ {sizes.map((size) => (
211
+ <AvatarView
212
+ key={size}
213
+ size={size}
214
+ imgSrc="../avatar-square-dude.webp"
215
+ badge={{ type: 'notification' }}
216
+ />
217
+ ))}
218
+
219
+ {sizes.map((size) => (
220
+ <AvatarView
221
+ key={size}
222
+ size={size}
223
+ imgSrc="../avatar-square-dude.webp"
224
+ badge={{ type: 'online' }}
225
+ />
226
+ ))}
244
227
  </div>
245
228
  );
246
229
  },
247
230
  };
248
231
 
232
+ export const Notification: Story = {
233
+ render: () => (
234
+ <div
235
+ style={{
236
+ gap: '1em',
237
+ display: 'grid',
238
+ justifyContent: 'space-between',
239
+ gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
240
+ }}
241
+ >
242
+ {sizes.map((size) => (
243
+ <Body type="body-large-bold">{size}</Body>
244
+ ))}
245
+ {sizes.map((size) => (
246
+ <AvatarView key={size} size={size} badge={{ type: 'notification' }}>
247
+ <Freeze />
248
+ </AvatarView>
249
+ ))}
250
+ {sizes.map((size) => (
251
+ <AvatarView key={size} size={size} badge={{ type: 'notification' }} interactive>
252
+ <Freeze />
253
+ </AvatarView>
254
+ ))}
255
+ {sizes.map((size) => (
256
+ <AvatarView
257
+ key={size}
258
+ size={size}
259
+ imgSrc="../avatar-rectangle-fox.webp"
260
+ badge={{ type: 'notification' }}
261
+ interactive
262
+ />
263
+ ))}
264
+ </div>
265
+ ),
266
+ };
267
+
268
+ export const Online: Story = {
269
+ tags: ['new'],
270
+ render: () => (
271
+ <div
272
+ style={{
273
+ gap: '1em',
274
+ display: 'grid',
275
+ justifyContent: 'space-between',
276
+ gridTemplate: `auto auto / repeat(${sizes.length}, min-content)`,
277
+ }}
278
+ >
279
+ {sizes.map((size) => (
280
+ <Body type="body-large-bold">{size}</Body>
281
+ ))}
282
+ {sizes.map((size) => (
283
+ <AvatarView key={size} size={size} badge={{ type: 'online' }}>
284
+ <Freeze />
285
+ </AvatarView>
286
+ ))}
287
+ {sizes.map((size) => (
288
+ <AvatarView key={size} size={size} badge={{ type: 'online' }} interactive>
289
+ <Freeze />
290
+ </AvatarView>
291
+ ))}
292
+ {sizes.map((size) => (
293
+ <AvatarView
294
+ key={size}
295
+ size={size}
296
+ imgSrc="../avatar-rectangle-fox.webp"
297
+ badge={{ type: 'online' }}
298
+ interactive
299
+ />
300
+ ))}
301
+ </div>
302
+ ),
303
+ };
304
+
249
305
  export const Images: Story = {
250
306
  render: () => {
251
307
  return (
@@ -1,5 +1,5 @@
1
1
  import Badge, { BadgeAssets, BadgeProps, BadgeAssetsProps } from '../badge';
2
- import NotificationDot from './NotificationDot';
2
+ import Dot, { DotProps } from './Dot';
3
3
  import Circle from '../common/circle';
4
4
  import Image from '../image';
5
5
  import { HTMLAttributes, PropsWithChildren, useState } from 'react';
@@ -26,7 +26,6 @@ export type Props = {
26
26
  profileName?: string | null;
27
27
  profileType?: ProfileTypeBusiness | ProfileTypePersonal;
28
28
  size?: 16 | 24 | 32 | 40 | 48 | 56 | 72;
29
- notification?: boolean;
30
29
  badge?: AvatarViewBadgeProps;
31
30
  interactive?: boolean;
32
31
  selected?: boolean;
@@ -40,7 +39,6 @@ function AvatarView({
40
39
  children = undefined,
41
40
  size = 48,
42
41
  selected,
43
- notification,
44
42
  badge,
45
43
  interactive = false,
46
44
  className,
@@ -62,7 +60,7 @@ function AvatarView({
62
60
  )}
63
61
  {...restProps}
64
62
  >
65
- <Badges avatar={{ size, notification, selected }} {...badge}>
63
+ <Badges avatar={{ size, selected }} {...badge}>
66
64
  <Circle
67
65
  size={size}
68
66
  fixedSize
@@ -115,7 +113,7 @@ const MAP_BADGE_POSITION = {
115
113
 
116
114
  type BadgesProps = AvatarViewBadgeProps &
117
115
  PropsWithChildren<{
118
- avatar: Pick<Props, 'selected' | 'size' | 'notification'>;
116
+ avatar: Pick<Props, 'selected' | 'size'>;
119
117
  }>;
120
118
 
121
119
  /**
@@ -128,36 +126,43 @@ function Badges({
128
126
  asset: customBadge,
129
127
  ...badgeAssets
130
128
  }: BadgesProps) {
131
- const { size = 48, selected, notification } = avatar;
129
+ const { size = 48, selected } = avatar;
132
130
  const anyBadge = Object.values({ customBadge, ...badgeAssets }).filter(Boolean).length > 0;
133
- if ((anyBadge || selected) && size > 16) {
134
- const badgeSize: BadgeAssetsProps['size'] = MAP_BADGE_ASSET_SIZE[size];
131
+
132
+ if ((!anyBadge && !selected) || size <= 16) {
133
+ return children;
134
+ }
135
+
136
+ if (badgeAssets.type === 'notification' || badgeAssets.type === 'online') {
135
137
  return (
136
- <Badge
137
- aria-label={ariaLabel}
138
- size={badgeSize}
139
- badge={
140
- customBadge ? (
141
- <Circle fixedSize size={badgeSize}>
142
- {customBadge}
143
- </Circle>
144
- ) : (
145
- <BadgeAssets {...(selected ? { status: 'positive' } : badgeAssets)} size={badgeSize} />
146
- )
147
- }
148
- style={{
149
- // @ts-expect-error CSS custom props allowed
150
- '--badge-content-position': `${MAP_BADGE_POSITION[size] ?? 0}px`,
151
- }}
152
- >
138
+ <Dot avatarSize={size} variant={badgeAssets.type === 'online' ? 'online' : 'notification'}>
153
139
  {children}
154
- </Badge>
140
+ </Dot>
155
141
  );
156
142
  }
157
- if (notification) {
158
- return <NotificationDot avatarSize={size}>{children}</NotificationDot>;
159
- }
160
- return children;
143
+
144
+ const badgeSize: BadgeAssetsProps['size'] = MAP_BADGE_ASSET_SIZE[size];
145
+ return (
146
+ <Badge
147
+ aria-label={ariaLabel}
148
+ size={badgeSize}
149
+ badge={
150
+ customBadge ? (
151
+ <Circle fixedSize size={badgeSize}>
152
+ {customBadge}
153
+ </Circle>
154
+ ) : (
155
+ <BadgeAssets {...(selected ? { status: 'positive' } : badgeAssets)} size={badgeSize} />
156
+ )
157
+ }
158
+ style={{
159
+ // @ts-expect-error CSS custom props allowed
160
+ '--badge-content-position': `${MAP_BADGE_POSITION[size] ?? 0}px`,
161
+ }}
162
+ >
163
+ {children}
164
+ </Badge>
165
+ );
161
166
  }
162
167
 
163
168
  function AvatarViewContent({
@@ -0,0 +1,26 @@
1
+ .np-dot {
2
+ --np-dot-size: 14px;
3
+ position: relative;
4
+ display: inline-block;
5
+ }
6
+ .np-dot-mask {
7
+ -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);
8
+ 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);
9
+ -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);
10
+ 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);
11
+ }
12
+ .np-dot-badge {
13
+ position: absolute;
14
+ width: var(--np-dot-size);
15
+ height: var(--np-dot-size);
16
+ border-radius: 9999px;
17
+ border-radius: var(--radius-full);
18
+ right: 0;
19
+ }
20
+ .np-dot-badge-notification {
21
+ background-color: var(--color-sentiment-negative);
22
+ }
23
+ .np-dot-badge-online {
24
+ background-color: #00a2dd;
25
+ background-color: var(--color-interactive-accent);
26
+ }
@@ -0,0 +1,31 @@
1
+ .np-dot {
2
+ --np-dot-size: 14px;
3
+ position: relative;
4
+ display: inline-block;
5
+
6
+ &-mask {
7
+ mask-image: radial-gradient(
8
+ circle at bottom calc(100% - calc(var(--np-dot-size) / 2))
9
+ left calc(100% - calc(var(--np-dot-size) / 2)),
10
+ transparent 0,
11
+ transparent calc(var(--np-dot-size) / 2 + var(--np-dot-offset)),
12
+ black 0
13
+ );
14
+ }
15
+
16
+ &-badge {
17
+ position: absolute;
18
+ width: var(--np-dot-size);
19
+ height: var(--np-dot-size);
20
+ border-radius: var(--radius-full);
21
+ right: 0;
22
+
23
+ &-notification {
24
+ background-color: var(--color-sentiment-negative);
25
+ }
26
+
27
+ &-online {
28
+ background-color: var(--color-interactive-accent);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,42 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import { Props as AvatarViewProps } from './AvatarView';
3
+ import { clsx } from 'clsx';
4
+
5
+ export type DotProps = Pick<HTMLAttributes<HTMLDivElement>, 'children'> & {
6
+ avatarSize?: AvatarViewProps['size'];
7
+ variant?: 'notification' | 'online';
8
+ };
9
+
10
+ /**
11
+ * Depending on avatar size, dot size and offset are different
12
+ */
13
+ const MAP_STYLE_CONFIG = {
14
+ 16: { size: 6, offset: 1 },
15
+ 24: { size: 8, offset: 2 },
16
+ 32: { size: 10, offset: 2 },
17
+ 40: { size: 10, offset: 2 },
18
+ 48: { size: 14, offset: 2 },
19
+ 56: { size: 16, offset: 3 },
20
+ 72: { size: 20, offset: 3 },
21
+ };
22
+
23
+ export default function Dot({ children, avatarSize = 48, variant = 'notification' }: DotProps) {
24
+ return (
25
+ <div
26
+ className="np-dot"
27
+ style={{
28
+ // @ts-expect-error CSS custom props allowed
29
+ '--np-dot-size': `${MAP_STYLE_CONFIG[avatarSize].size}px`,
30
+ '--np-dot-offset': `${MAP_STYLE_CONFIG[avatarSize].offset}px`,
31
+ }}
32
+ >
33
+ <div
34
+ className={clsx('np-dot-badge', {
35
+ 'np-dot-badge-notification': variant === 'notification',
36
+ 'np-dot-badge-online': variant === 'online',
37
+ })}
38
+ />
39
+ <div className="np-dot-mask">{children}</div>
40
+ </div>
41
+ );
42
+ }
@@ -9,7 +9,7 @@ export type Props = {
9
9
  flagCode?: string;
10
10
  imgSrc?: string;
11
11
  icon?: React.ReactNode;
12
- type?: 'action' | 'reference';
12
+ type?: 'action' | 'reference' | 'notification' | 'online';
13
13
  size?: 16 | 24;
14
14
  };
15
15
 
@@ -516,3 +516,97 @@ export const WithinModal: Story<Currency> = {
516
516
  },
517
517
  ],
518
518
  };
519
+
520
+ interface Country {
521
+ code: string;
522
+ name: string;
523
+ }
524
+
525
+ const countries: Country[] = [
526
+ { code: 'US', name: 'United States' },
527
+ { code: 'GB', name: 'United Kingdom' },
528
+ { code: 'CA', name: 'Canada' },
529
+ { code: 'AU', name: 'Australia' },
530
+ { code: 'DE', name: 'Germany' },
531
+ { code: 'FR', name: 'France' },
532
+ { code: 'JP', name: 'Japan' },
533
+ { code: 'BR', name: 'Brazil' },
534
+ { code: 'IN', name: 'India' },
535
+ { code: 'CN', name: 'China' },
536
+ { code: 'IT', name: 'Italy' },
537
+ { code: 'ES', name: 'Spain' },
538
+ { code: 'NL', name: 'Netherlands' },
539
+ { code: 'CH', name: 'Switzerland' },
540
+ { code: 'SE', name: 'Sweden' },
541
+ ];
542
+
543
+ function countryOption(country: Country) {
544
+ return {
545
+ type: 'option',
546
+ value: country.code,
547
+ filterMatchers: [country.code, country.name],
548
+ } satisfies SelectInputItem;
549
+ }
550
+
551
+ export const WithAutocomplete: Story<string> = {
552
+ args: {
553
+ name: 'country',
554
+ autocomplete: 'country-name',
555
+ placeholder: 'Select your country',
556
+ items: countries.map(countryOption),
557
+ renderValue: (countryCode, withinTrigger) => {
558
+ const country = countries.find((c) => c.code === countryCode);
559
+ return (
560
+ <SelectInputOptionContent
561
+ title={withinTrigger ? countryCode : country?.name || countryCode}
562
+ note={withinTrigger ? undefined : countryCode}
563
+ icon={<Flag code={countryCode} intrinsicSize={24} />}
564
+ />
565
+ );
566
+ },
567
+ filterable: true,
568
+ filterPlaceholder: 'Type a country name',
569
+ size: 'lg',
570
+ },
571
+ render: function Render({ onChange, onClear, ...args }) {
572
+ const [selectedCountry, setSelectedCountry] = useState<string | undefined>(undefined);
573
+
574
+ return (
575
+ <div>
576
+ <form
577
+ method="post"
578
+ onSubmit={(e) => {
579
+ e.preventDefault();
580
+ console.log(
581
+ `Form submitted with country: ${selectedCountry}. This saves data for browser autocomplete!`,
582
+ );
583
+ }}
584
+ >
585
+ <div>
586
+ <label htmlFor="country-select" className="block text-sm font-medium mb-2">
587
+ Country Selection with Autocomplete:
588
+ </label>
589
+ <SelectInput
590
+ {...args}
591
+ id="country-select"
592
+ value={selectedCountry}
593
+ onChange={(country) => {
594
+ setSelectedCountry(country);
595
+ onChange?.(country);
596
+ console.log('Country selected via SelectInput:', country);
597
+ }}
598
+ onClear={() => {
599
+ setSelectedCountry(undefined);
600
+ onClear?.();
601
+ }}
602
+ />
603
+ </div>
604
+
605
+ <Button type="submit" v2 className="m-t-2" data-testid="submit-btn">
606
+ Submit Form
607
+ </Button>
608
+ </form>
609
+ </div>
610
+ );
611
+ },
612
+ };
@@ -155,6 +155,25 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
155
155
  multiple?: M;
156
156
  placeholder?: string;
157
157
  items: readonly SelectInputItem<NonNullable<T>>[];
158
+ /**
159
+ * Enables browser autocomplete integration through the search input.
160
+ * Accepts standard HTML autocomplete values (e.g., "country-name", "address-level1").
161
+ *
162
+ * Requires `filterable={true}` to enable the search input.
163
+ *
164
+ * @example
165
+ * <SelectInput
166
+ * name="country"
167
+ * autocomplete="country-name"
168
+ * filterable={true}
169
+ * items={[{
170
+ * type: 'option',
171
+ * value: 'GB',
172
+ * filterMatchers: ['United Kingdom', 'UK']
173
+ * }]}
174
+ * />
175
+ */
176
+ autocomplete?: string;
158
177
  defaultValue?: M extends true ? readonly T[] : T;
159
178
  value?: M extends true ? readonly T[] : T;
160
179
  compareValues?:
@@ -257,6 +276,7 @@ export function SelectInput<T = string, M extends boolean = false>({
257
276
  name,
258
277
  multiple,
259
278
  placeholder,
279
+ autocomplete,
260
280
  items,
261
281
  defaultValue,
262
282
  value: controlledValue,
@@ -457,7 +477,15 @@ export function SelectInput<T = string, M extends boolean = false>({
457
477
  searchInputRef={searchInputRef}
458
478
  listboxRef={listboxRef}
459
479
  filterQuery={deferredFilterQuery}
480
+ autocomplete={autocomplete}
481
+ name={name}
460
482
  onFilterChange={setFilterQuery}
483
+ onAutocompleteSelect={(matchedValue) => {
484
+ onChange?.(matchedValue as M extends true ? T[] : T);
485
+ if (!multiple) {
486
+ setOpen(false);
487
+ }
488
+ }}
461
489
  {...getListBoxLabelProps()}
462
490
  />
463
491
  </OptionsOverlay>
@@ -565,6 +593,9 @@ interface SelectInputOptionsProps<T = string>
565
593
  onFilterChange: (query: string) => void;
566
594
  listBoxLabel?: string;
567
595
  listBoxLabelledBy?: string;
596
+ autocomplete?: string;
597
+ name?: string;
598
+ onAutocompleteSelect?: (value: T) => void;
568
599
  }
569
600
 
570
601
  function SelectInputOptions<T = string>({
@@ -581,6 +612,9 @@ function SelectInputOptions<T = string>({
581
612
  onFilterChange,
582
613
  listBoxLabel,
583
614
  listBoxLabelledBy,
615
+ autocomplete,
616
+ name,
617
+ onAutocompleteSelect,
584
618
  }: SelectInputOptionsProps<T>) {
585
619
  const intl = useIntl();
586
620
  const virtualiserHandlerRef = useRef<VirtualizerHandle>(null);
@@ -664,6 +698,35 @@ function SelectInputOptions<T = string>({
664
698
  );
665
699
  };
666
700
 
701
+ const findMatchingItem = (autocompleteValue: string): T | null => {
702
+ const flatOptions = items
703
+ .flatMap((item) =>
704
+ item.type === 'group' ? item.options : item.type === 'option' ? [item] : [],
705
+ )
706
+ .filter(
707
+ (item): item is SelectInputOptionItem<NonNullable<T>> =>
708
+ item.type === 'option' && item.value != null,
709
+ );
710
+
711
+ const exactMatch = flatOptions.find(
712
+ (option) =>
713
+ String(option.value) === autocompleteValue ||
714
+ option.filterMatchers?.some((matcher) => matcher === autocompleteValue),
715
+ );
716
+
717
+ if (exactMatch) {
718
+ return exactMatch.value;
719
+ }
720
+
721
+ const fuzzyMatch = flatOptions.find((option) =>
722
+ option.filterMatchers?.some((matcher) =>
723
+ matcher.toLowerCase().includes(autocompleteValue.toLowerCase()),
724
+ ),
725
+ );
726
+
727
+ return fuzzyMatch ? fuzzyMatch.value : null;
728
+ };
729
+
667
730
  return (
668
731
  <ListboxBase.Options
669
732
  as={SelectInputOptionsContainer}
@@ -684,6 +747,8 @@ function SelectInputOptions<T = string>({
684
747
  <SearchInput
685
748
  ref={searchInputRef}
686
749
  id={id}
750
+ name={name}
751
+ autoComplete={autocomplete}
687
752
  role="combobox"
688
753
  shape="rectangle"
689
754
  placeholder={filterPlaceholder}
@@ -703,8 +768,26 @@ function SelectInputOptions<T = string>({
703
768
  onChange={(event) => {
704
769
  // Free up resources and ensure not to go out of bounds when the
705
770
  // resulting item count is less than before
771
+ const inputValue = event.currentTarget.value;
772
+
773
+ // Free up resources and ensure not to go out of bounds
706
774
  setMountedIndexes([]);
707
- onFilterChange(event.currentTarget.value);
775
+ onFilterChange(inputValue);
776
+ }}
777
+ onInput={(event) => {
778
+ const inputValue = event.currentTarget.value;
779
+ const inputElement = event.currentTarget;
780
+
781
+ if (autocomplete && onAutocompleteSelect && inputValue) {
782
+ setTimeout(() => {
783
+ if (inputElement.value === inputValue && inputValue.length > 2) {
784
+ const matchedValue = findMatchingItem(inputValue);
785
+ if (matchedValue !== null) {
786
+ onAutocompleteSelect(matchedValue);
787
+ }
788
+ }
789
+ }, 50);
790
+ }
708
791
  }}
709
792
  />
710
793
  </div>