@transferwise/components 46.127.1 → 46.128.1

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 (81) hide show
  1. package/build/alert/Alert.js +3 -0
  2. package/build/alert/Alert.js.map +1 -1
  3. package/build/alert/Alert.mjs +3 -0
  4. package/build/alert/Alert.mjs.map +1 -1
  5. package/build/index.js +1 -0
  6. package/build/index.js.map +1 -1
  7. package/build/index.mjs +1 -1
  8. package/build/inputs/SelectInput.js +81 -12
  9. package/build/inputs/SelectInput.js.map +1 -1
  10. package/build/inputs/SelectInput.mjs +81 -13
  11. package/build/inputs/SelectInput.mjs.map +1 -1
  12. package/build/listItem/Button/ListItemButton.js +4 -3
  13. package/build/listItem/Button/ListItemButton.js.map +1 -1
  14. package/build/listItem/Button/ListItemButton.mjs +5 -4
  15. package/build/listItem/Button/ListItemButton.mjs.map +1 -1
  16. package/build/main.css +15 -7
  17. package/build/prompt/ActionPrompt/ActionPrompt.js +6 -4
  18. package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -1
  19. package/build/prompt/ActionPrompt/ActionPrompt.mjs +6 -4
  20. package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -1
  21. package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -1
  22. package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
  23. package/build/prompt/InlinePrompt/InlinePrompt.js +1 -1
  24. package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
  25. package/build/prompt/InlinePrompt/InlinePrompt.mjs +1 -1
  26. package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
  27. package/build/styles/main.css +15 -7
  28. package/build/styles/prompt/ActionPrompt/ActionPrompt.css +4 -0
  29. package/build/styles/prompt/InfoPrompt/InfoPrompt.css +7 -5
  30. package/build/styles/prompt/InlinePrompt/InlinePrompt.css +3 -2
  31. package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +1 -0
  32. package/build/types/alert/Alert.d.ts +15 -0
  33. package/build/types/alert/Alert.d.ts.map +1 -1
  34. package/build/types/index.d.ts +1 -1
  35. package/build/types/index.d.ts.map +1 -1
  36. package/build/types/inputs/SelectInput.d.ts +19 -0
  37. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  38. package/build/types/listItem/Button/ListItemButton.d.ts +7 -4
  39. package/build/types/listItem/Button/ListItemButton.d.ts.map +1 -1
  40. package/build/types/listItem/ListItem.d.ts +4 -4
  41. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +7 -0
  42. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -1
  43. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +4 -2
  44. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
  45. package/package.json +5 -5
  46. package/src/alert/Alert.story.tsx +4 -0
  47. package/src/alert/Alert.test.story.tsx +1 -1
  48. package/src/alert/Alert.tsx +16 -0
  49. package/src/iconButton/IconButton.story.tsx +173 -48
  50. package/src/iconButton/IconButton.test.story.tsx +194 -0
  51. package/src/index.ts +1 -0
  52. package/src/inputs/SelectInput.story.tsx +33 -20
  53. package/src/inputs/SelectInput.test.story.tsx +1285 -5
  54. package/src/inputs/SelectInput.tsx +93 -15
  55. package/src/listItem/Button/ListItemButton.tsx +30 -28
  56. package/src/listItem/_stories/ListItem.story.tsx +0 -1
  57. package/src/main.css +15 -7
  58. package/src/prompt/ActionPrompt/ActionPrompt.accessibility.docs.mdx +2 -18
  59. package/src/prompt/ActionPrompt/ActionPrompt.css +4 -0
  60. package/src/prompt/ActionPrompt/ActionPrompt.less +5 -1
  61. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +323 -108
  62. package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +86 -3
  63. package/src/prompt/ActionPrompt/ActionPrompt.tsx +17 -6
  64. package/src/prompt/InfoPrompt/InfoPrompt.accessibility.docs.mdx +79 -0
  65. package/src/prompt/InfoPrompt/InfoPrompt.css +7 -5
  66. package/src/prompt/InfoPrompt/InfoPrompt.less +8 -8
  67. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +112 -82
  68. package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +54 -1
  69. package/src/prompt/InfoPrompt/InfoPrompt.tsx +4 -2
  70. package/src/prompt/InlinePrompt/InlinePrompt.accessibility.docs.mdx +63 -0
  71. package/src/prompt/InlinePrompt/InlinePrompt.css +3 -2
  72. package/src/prompt/InlinePrompt/InlinePrompt.less +2 -2
  73. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +25 -30
  74. package/src/prompt/InlinePrompt/InlinePrompt.test.story.tsx +21 -0
  75. package/src/prompt/InlinePrompt/InlinePrompt.test.tsx +10 -3
  76. package/src/prompt/InlinePrompt/InlinePrompt.tsx +1 -1
  77. package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +1 -0
  78. package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +2 -1
  79. package/src/sentimentSurface/SentimentSurface.docs.mdx +1 -1
  80. package/src/sentimentSurface/SentimentSurface.story.tsx +1 -1
  81. package/src/sentimentSurface/SentimentSurface.test.story.tsx +1 -1
@@ -197,6 +197,52 @@ function sortSelectInputItems<T>(
197
197
  return flattenedOption.sort((a, b) => compareFn(a, b, searchQuery));
198
198
  }
199
199
 
200
+ /**
201
+ * A prebuilt sort function for `sortFilteredOptions` that sorts options by relevance to the search query.
202
+ * Prioritizes: exact matches > starts with > contains > alphabetical.
203
+ *
204
+ * @param getLabel - Function to extract the label string from the option value. Defaults to using `title` property.
205
+ *
206
+ * @example
207
+ * ```tsx
208
+ * <SelectInput
209
+ * filterable
210
+ * sortFilteredOptions={sortByRelevance((value) => value.name)}
211
+ * // ...
212
+ * />
213
+ * ```
214
+ */
215
+ export function sortByRelevance<T>(
216
+ getLabel: (value: T) => string = (value) => (value as { title: string }).title,
217
+ ): (a: SelectInputOptionItem<T>, b: SelectInputOptionItem<T>, searchQuery: string) => number {
218
+ return (a, b, searchQuery) => {
219
+ const normalizedQuery = searchQuery.toLowerCase();
220
+ const labelA = getLabel(a.value).toLowerCase();
221
+ const labelB = getLabel(b.value).toLowerCase();
222
+
223
+ // Prioritize exact matches
224
+ const aExactMatch = labelA === normalizedQuery;
225
+ const bExactMatch = labelB === normalizedQuery;
226
+ if (aExactMatch && !bExactMatch) return -1;
227
+ if (!aExactMatch && bExactMatch) return 1;
228
+
229
+ // Then prioritize options where label starts with the search query
230
+ const aStartsWith = labelA.startsWith(normalizedQuery);
231
+ const bStartsWith = labelB.startsWith(normalizedQuery);
232
+ if (aStartsWith && !bStartsWith) return -1;
233
+ if (!aStartsWith && bStartsWith) return 1;
234
+
235
+ // Then prioritize options where label contains the search query
236
+ const aContains = labelA.includes(normalizedQuery);
237
+ const bContains = labelB.includes(normalizedQuery);
238
+ if (aContains && !bContains) return -1;
239
+ if (!aContains && bContains) return 1;
240
+
241
+ // Finally sort alphabetically
242
+ return labelA.localeCompare(labelB);
243
+ };
244
+ }
245
+
200
246
  export interface SelectInputProps<T = string, M extends boolean = false> {
201
247
  id?: string;
202
248
  /**
@@ -558,6 +604,8 @@ export function SelectInput<T = string, M extends boolean = false>({
558
604
  );
559
605
  }
560
606
 
607
+ SelectInput.sortByRelevance = sortByRelevance;
608
+
561
609
  const SelectInputTriggerButtonPropsContext = createContext<{
562
610
  ref?: React.ForwardedRef<HTMLButtonElement | null>;
563
611
  id?: string;
@@ -741,15 +789,35 @@ function SelectInputOptions<T = string>({
741
789
  return items;
742
790
  }
743
791
 
744
- const filtered = filterSelectInputItems(dedupeSelectInputItems(items, compareValues), (item) =>
745
- selectInputOptionItemIncludesNeedle(item, needle),
746
- );
792
+ const dedupedItems = dedupeSelectInputItems(items, compareValues);
747
793
 
748
794
  if (sortFilteredOptions) {
795
+ // When sorting, filter out non-matching items completely to avoid ghost items
796
+ const filtered = dedupedItems.map((item) => {
797
+ if (item.type === 'option') {
798
+ return selectInputOptionItemIncludesNeedle(item, needle)
799
+ ? item
800
+ : { ...item, value: undefined };
801
+ }
802
+ if (item.type === 'group') {
803
+ return {
804
+ ...item,
805
+ options: item.options.map((option) =>
806
+ selectInputOptionItemIncludesNeedle(option, needle)
807
+ ? option
808
+ : { ...option, value: undefined },
809
+ ),
810
+ };
811
+ }
812
+ return item;
813
+ });
814
+
749
815
  return sortSelectInputItems(filtered, sortFilteredOptions, filterQuery);
750
816
  }
751
817
 
752
- return filtered;
818
+ return filterSelectInputItems(dedupedItems, (item) =>
819
+ selectInputOptionItemIncludesNeedle(item, needle),
820
+ );
753
821
  // eslint-disable-next-line react-hooks/exhaustive-deps
754
822
  }, [needle, items, compareValues]);
755
823
  const resultsEmpty = needle != null && filteredItems.length === 0;
@@ -760,17 +828,28 @@ function SelectInputOptions<T = string>({
760
828
  // the scroll position may jump around inadvertently. Pattern adopted from:
761
829
  // https://inokawa.github.io/virtua/?path=/story/advanced-keep-offscreen-items--append-only
762
830
  const [mountedIndexes, setMountedIndexes] = useState<number[]>([]);
831
+ const prevNeedleRef = useRef(needle);
832
+
763
833
  useEffect(() => {
764
- // Ensure the 'End' key works as intended by keeping the last item mounted
765
- setMountedIndexes((prevMountedIndexes) => {
766
- const indexes = new Set(prevMountedIndexes);
767
- indexes.add(filteredItems.length - 1);
768
- return [...indexes]; // Sorting is redundant by nature here
769
- });
770
- }, [
771
- needle, // Needed as `filteredItems.length` may be equal between two updates
772
- filteredItems.length,
773
- ]);
834
+ const needleChanged = prevNeedleRef.current !== needle;
835
+ prevNeedleRef.current = needle;
836
+
837
+ if (needleChanged) {
838
+ // Reset mounted indexes when search changes to avoid stale scroll positions
839
+ setMountedIndexes([]);
840
+ return;
841
+ }
842
+
843
+ // Ensure the 'End' key works as intended by keeping the last item mounted.
844
+ // Skipped on needle change to prevent auto-scrolling on search.
845
+ if (filteredItems.length > 0) {
846
+ setMountedIndexes((prevMountedIndexes) => {
847
+ const indexes = new Set(prevMountedIndexes);
848
+ indexes.add(filteredItems.length - 1);
849
+ return [...indexes]; // Sorting is redundant by nature here
850
+ });
851
+ }
852
+ }, [needle, filteredItems.length]);
774
853
 
775
854
  const listboxContainerRef = useRef<HTMLDivElement>(null);
776
855
  useEffect(() => {
@@ -927,7 +1006,6 @@ function SelectInputOptions<T = string>({
927
1006
  ) : (
928
1007
  <Virtualizer
929
1008
  ref={virtualiserHandlerRef}
930
- key={needle}
931
1009
  data={filteredItems}
932
1010
  keepMounted={mountedIndexes}
933
1011
  scrollRef={listboxRef} // `VList` doesn't expose this
@@ -1,4 +1,4 @@
1
- import { useContext } from 'react';
1
+ import { useContext, forwardRef } from 'react';
2
2
  import { clsx } from 'clsx';
3
3
  import ButtonComp, { type ButtonAddonIcon, type NewButtonProps } from '../../button';
4
4
  import { useListItemControl } from '../useListItemControl';
@@ -22,35 +22,37 @@ export type ListItemButtonProps = Omit<
22
22
  * <br />
23
23
  * Please refer to the [Design documentation](https://wise.design/components/list-item---button) for details.
24
24
  */
25
- export const Button = ({
26
- priority = 'secondary-neutral',
27
- partiallyInteractive,
28
- ...props
29
- }: ListItemButtonProps) => {
30
- const { baseItemProps } = useListItemControl('button', { partiallyInteractive, ...props });
31
- const { ids, describedByIds } = useContext(ListItemContext);
25
+ export const Button = forwardRef<HTMLButtonElement, ListItemButtonProps>(
26
+ (
27
+ { priority = 'secondary-neutral', partiallyInteractive, ...props }: ListItemButtonProps,
28
+ ref,
29
+ ) => {
30
+ const { baseItemProps } = useListItemControl('button', { partiallyInteractive, ...props });
31
+ const { ids, describedByIds } = useContext(ListItemContext);
32
32
 
33
- const commonProps = {
34
- ...props,
35
- className: clsx(
36
- 'wds-list-item-control',
37
- !partiallyInteractive && props.href && 'wds-list-item-control_pseudo-element',
38
- ),
39
- id: ids.control,
40
- priority,
41
- v2: true,
42
- size: 'sm',
43
- disabled: baseItemProps.disabled,
44
- };
33
+ const commonProps = {
34
+ ...props,
35
+ className: clsx(
36
+ 'wds-list-item-control',
37
+ !partiallyInteractive && props.href && 'wds-list-item-control_pseudo-element',
38
+ ),
39
+ id: ids.control,
40
+ priority,
41
+ v2: true,
42
+ size: 'sm',
43
+ disabled: baseItemProps.disabled,
44
+ };
45
45
 
46
- const buttonContentId = props.href || partiallyInteractive ? '' : `${ids.control}_content`;
46
+ const buttonContentId = props.href || partiallyInteractive ? '' : `${ids.control}_content`;
47
47
 
48
- return (
49
- <ButtonComp
50
- aria-describedby={`${buttonContentId} ${describedByIds}`}
51
- {...(commonProps as NewButtonProps)}
52
- />
53
- );
54
- };
48
+ return (
49
+ <ButtonComp
50
+ ref={ref}
51
+ aria-describedby={`${buttonContentId} ${describedByIds}`}
52
+ {...(commonProps as NewButtonProps)}
53
+ />
54
+ );
55
+ },
56
+ );
55
57
 
56
58
  Button.displayName = 'ListItem.Button';
@@ -252,7 +252,6 @@ const getPropsForPreview = (args: PreviewStoryArgs): [ListItemProps, Partial<Lis
252
252
  };
253
253
 
254
254
  export const Playground: StoryObj<PreviewStoryArgs> = {
255
- tags: ['!autodocs'],
256
255
  render: (args: PreviewStoryArgs) => {
257
256
  const [props, previewProps] = getPropsForPreview(args);
258
257
 
package/src/main.css CHANGED
@@ -5346,6 +5346,7 @@ html:not([dir="rtl"]) .np-navigation-option {
5346
5346
  padding: var(--Prompt-padding, var(--padding-x-small));
5347
5347
  text-align: left;
5348
5348
  word-break: break-word;
5349
+ width: 100%;
5349
5350
  }
5350
5351
  .wds-prompt__content-wrapper {
5351
5352
  display: grid;
@@ -5405,8 +5406,9 @@ html:not([dir="rtl"]) .np-navigation-option {
5405
5406
  .wds-inline-prompt:has(button):active {
5406
5407
  background-color: var(--color-sentiment-background-surface-active);
5407
5408
  }
5408
- .wds-inline-prompt--full-width {
5409
- width: 100%;
5409
+ .wds-inline-prompt--auto-width {
5410
+ width: auto;
5411
+ width: initial;
5410
5412
  }
5411
5413
  .wds-inline-prompt--muted {
5412
5414
  opacity: 0.93;
@@ -5442,13 +5444,16 @@ html:not([dir="rtl"]) .np-navigation-option {
5442
5444
  stroke: currentColor;
5443
5445
  }
5444
5446
  .wds-info-prompt {
5447
+ --Prompt-border-radius: var(--radius-medium);
5445
5448
  --Prompt-gap: var(--size-8);
5446
- --Prompt-padding: 12px;
5449
+ --Prompt-padding: var(--size-12);
5447
5450
  }
5448
5451
  .wds-info-prompt__content {
5449
5452
  display: flex;
5450
5453
  flex-direction: column;
5451
5454
  justify-content: center;
5455
+ max-width: calc(48px * 10);
5456
+ max-width: calc(var(--size-48) * 10);
5452
5457
  }
5453
5458
  .wds-info-prompt__content:has(.wds-info-prompt__title) {
5454
5459
  justify-content: flex-start;
@@ -5457,17 +5462,16 @@ html:not([dir="rtl"]) .np-navigation-option {
5457
5462
  .wds-info-prompt__title,
5458
5463
  .wds-info-prompt__description {
5459
5464
  display: block;
5460
- color: var(--color-sentiment-primary);
5461
5465
  }
5462
5466
  .wds-info-prompt__action {
5467
+ align-self: flex-start;
5463
5468
  margin-top: var(--Prompt-gap);
5464
5469
  }
5465
- .wds-info-prompt__media {
5466
- display: flex;
5467
- }
5468
5470
  .wds-info-prompt__media svg {
5469
5471
  width: 24px;
5472
+ width: var(--size-24);
5470
5473
  height: 24px;
5474
+ height: var(--size-24);
5471
5475
  }
5472
5476
  .wds-info-prompt .wds-prompt__media-wrapper {
5473
5477
  padding: 0;
@@ -7484,3 +7488,7 @@ html:not([dir="rtl"]) .np-navigation-option {
7484
7488
  min-width: fit-content;
7485
7489
  }
7486
7490
  }
7491
+ .wds-action-prompt__content {
7492
+ max-width: calc(48px * 10);
7493
+ max-width: calc(var(--size-48) * 10);
7494
+ }
@@ -1,6 +1,6 @@
1
1
  import { Meta, Canvas, Source } from '@storybook/addon-docs/blocks';
2
2
 
3
- <Meta title="Prompts/ActionPrompt/Accessibility" />
3
+ <Meta title="Prompts/ActionPrompt/Accessibility" tags={['new']} />
4
4
 
5
5
  # Accessibility
6
6
 
@@ -31,23 +31,7 @@ If you want to provide a custom label for screen readers, you can use the `aria-
31
31
 
32
32
  ## Media
33
33
 
34
- You can use the `aria-hidden` attribute on media assets to hide them from screen readers if they're not providing any additional information. This is useful when the media is purely decorative or when its content is already conveyed through other means (e.g., text).
35
-
36
- <Source
37
- dark
38
- code={`
39
- <ActionPrompt
40
- media={{
41
- 'aria-hidden': true,
42
- avatar: { asset: <People /> },
43
- }}
44
- title="Sync contacts for a faster experience"
45
- ...
46
- />
47
- `}
48
- />
49
-
50
- You can also use `aria-label` on media assets to provide a custom label for screen readers.
34
+ You can use `aria-label` on media assets to provide a custom label for screen readers.
51
35
 
52
36
  <Source
53
37
  dark
@@ -19,3 +19,7 @@
19
19
  min-width: fit-content;
20
20
  }
21
21
  }
22
+ .wds-action-prompt__content {
23
+ max-width: calc(48px * 10);
24
+ max-width: calc(var(--size-48) * 10);
25
+ }
@@ -20,4 +20,8 @@
20
20
  }
21
21
  }
22
22
  }
23
- }
23
+
24
+ &__content {
25
+ max-width: calc(var(--size-48) * 10);
26
+ }
27
+ }