@transferwise/components 0.0.0-experimental-8d46704 → 0.0.0-experimental-1067f73

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.
@@ -6,7 +6,6 @@ import {
6
6
  createContext,
7
7
  forwardRef,
8
8
  useContext,
9
- useDeferredValue,
10
9
  useEffect,
11
10
  useId,
12
11
  useMemo,
@@ -14,7 +13,6 @@ import {
14
13
  useState,
15
14
  } from 'react';
16
15
  import { useIntl } from 'react-intl';
17
- import { VList } from 'virtua';
18
16
 
19
17
  import { useEffectEvent } from '../common/hooks/useEffectEvent';
20
18
  import { useScreenSize } from '../common/hooks/useScreenSize';
@@ -31,8 +29,6 @@ import { InputGroup } from './InputGroup';
31
29
  import { SearchInput } from './SearchInput';
32
30
  import messages from './SelectInput.messages';
33
31
 
34
- const MAX_ITEMS_WITHOUT_VIRTUALIZATION = 50;
35
-
36
32
  function searchableString(value: string) {
37
33
  return value.trim().replace(/\s+/gu, ' ').normalize('NFKC').toLowerCase();
38
34
  }
@@ -44,7 +40,7 @@ function inferSearchableStrings(value: unknown) {
44
40
 
45
41
  if (typeof value === 'object' && value != null) {
46
42
  return Object.values(value)
47
- .filter((innerValue) => typeof innerValue === 'string')
43
+ .filter((innerValue): innerValue is string => typeof innerValue === 'string')
48
44
  .map((innerValue) => searchableString(innerValue));
49
45
  }
50
46
 
@@ -116,23 +112,20 @@ function dedupeSelectInputItems<T>(
116
112
  });
117
113
  }
118
114
 
119
- function selectInputOptionItemIncludesNeedle<T>(item: SelectInputOptionItem<T>, needle: string) {
115
+ function filterSelectInputOptionItem<T>(item: SelectInputOptionItem<T>, needle: string) {
120
116
  return inferSearchableStrings(item.filterMatchers ?? item.value).some((haystack) =>
121
117
  haystack.includes(needle),
122
118
  );
123
119
  }
124
120
 
125
- function filterSelectInputItems<T>(
126
- items: readonly SelectInputItem<T>[],
127
- predicate: (item: SelectInputOptionItem<T>) => boolean,
128
- ) {
121
+ function filterSelectInputItems<T>(items: readonly SelectInputItem<T>[], needle: string) {
129
122
  return items.filter((item) => {
130
123
  switch (item.type) {
131
124
  case 'option': {
132
- return predicate(item);
125
+ return filterSelectInputOptionItem(item, needle);
133
126
  }
134
127
  case 'group': {
135
- return item.options.some((option) => predicate(option));
128
+ return item.options.some((option) => filterSelectInputOptionItem(option, needle));
136
129
  }
137
130
  default:
138
131
  }
@@ -278,15 +271,12 @@ export function SelectInput<T = string, M extends boolean = false>({
278
271
  }, [handleClose, open]);
279
272
 
280
273
  const [filterQuery, _setFilterQuery] = useState('');
281
- const deferredFilterQuery = useDeferredValue(filterQuery);
282
274
  const setFilterQuery = useEffectEvent((query: string) => {
283
275
  _setFilterQuery(query);
284
- if (query !== filterQuery) {
285
- onFilterChange({
286
- query,
287
- queryNormalized: query ? searchableString(query) : null,
288
- });
289
- }
276
+ onFilterChange({
277
+ query,
278
+ queryNormalized: query ? searchableString(query) : null,
279
+ });
290
280
  });
291
281
 
292
282
  const triggerRef = useRef<HTMLButtonElement | null>(null);
@@ -304,7 +294,9 @@ export function SelectInput<T = string, M extends boolean = false>({
304
294
  multiple={multiple}
305
295
  defaultValue={defaultValue}
306
296
  value={controlledValue}
307
- by={compareValues}
297
+ // TODO: Remove assertion when upgrading TypeScript to v5
298
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
299
+ by={compareValues as any}
308
300
  disabled={disabled}
309
301
  onChange={
310
302
  ((value) => {
@@ -357,8 +349,8 @@ export function SelectInput<T = string, M extends boolean = false>({
357
349
  content: !placeholderShown ? (
358
350
  <SelectInputOptionContentWithinTriggerContext.Provider value>
359
351
  {multiple && Array.isArray(value)
360
- ? (value as readonly NonNullable<T>[])
361
- .map((option) => renderValue(option, true))
352
+ ? value
353
+ .map((option: NonNullable<T>) => renderValue(option, true))
362
354
  .filter((node) => node != null)
363
355
  .join(', ')
364
356
  : renderValue(value as NonNullable<T>, true)}
@@ -387,7 +379,9 @@ export function SelectInput<T = string, M extends boolean = false>({
387
379
  setOpen(false);
388
380
  }}
389
381
  onCloseEnd={() => {
390
- setFilterQuery('');
382
+ if (filterQuery !== '') {
383
+ setFilterQuery('');
384
+ }
391
385
  }}
392
386
  >
393
387
  <SelectInputOptions
@@ -398,8 +392,7 @@ export function SelectInput<T = string, M extends boolean = false>({
398
392
  filterPlaceholder={filterPlaceholder}
399
393
  searchInputRef={searchInputRef}
400
394
  listboxRef={listboxRef}
401
- value={value}
402
- filterQuery={deferredFilterQuery}
395
+ filterQuery={filterQuery}
403
396
  onFilterChange={setFilterQuery}
404
397
  />
405
398
  </OptionsOverlay>
@@ -488,7 +481,6 @@ interface SelectInputOptionsProps<T = string>
488
481
  > {
489
482
  searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
490
483
  listboxRef: React.MutableRefObject<HTMLDivElement | null>;
491
- value: readonly T[] | T;
492
484
  filterQuery: string;
493
485
  onFilterChange: (query: string) => void;
494
486
  }
@@ -501,7 +493,6 @@ function SelectInputOptions<T = string>({
501
493
  filterPlaceholder,
502
494
  searchInputRef,
503
495
  listboxRef,
504
- value,
505
496
  filterQuery,
506
497
  onFilterChange,
507
498
  }: SelectInputOptionsProps<T>) {
@@ -515,12 +506,7 @@ function SelectInputOptions<T = string>({
515
506
  }
516
507
  return undefined;
517
508
  }, [filterQuery, filterable]);
518
-
519
- const filteredItems =
520
- needle != null
521
- ? filterSelectInputItems(items, (item) => selectInputOptionItemIncludesNeedle(item, needle))
522
- : items;
523
- const resultsEmpty = needle != null && filteredItems.length === 0;
509
+ const resultsEmpty = needle != null && filterSelectInputItems(items, needle).length === 0;
524
510
 
525
511
  const listboxContainerRef = useRef<HTMLDivElement>(null);
526
512
  useEffect(() => {
@@ -536,30 +522,6 @@ function SelectInputOptions<T = string>({
536
522
  const statusId = useId();
537
523
  const listboxId = useId();
538
524
 
539
- const virtualized = filteredItems.length > MAX_ITEMS_WITHOUT_VIRTUALIZATION;
540
-
541
- const values = useMemo(
542
- () => new Set(Array.isArray(value) ? (value as readonly T[]) : [value as T]),
543
- [value],
544
- );
545
-
546
- const listboxProps = {
547
- id: listboxId,
548
- role: 'listbox',
549
- 'aria-orientation': 'vertical',
550
- tabIndex: 0,
551
- className: 'np-select-input-listbox',
552
- children: (needle != null ? dedupeSelectInputItems(items) : items).map((item, index) => (
553
- <SelectInputItemView
554
- // eslint-disable-next-line react/no-array-index-key
555
- key={index}
556
- item={item}
557
- renderValue={renderValue}
558
- needle={needle}
559
- />
560
- )),
561
- } as const;
562
-
563
525
  return (
564
526
  <ListboxBase.Options
565
527
  as={SelectInputOptionsContainer}
@@ -587,7 +549,7 @@ function SelectInputOptions<T = string>({
587
549
  ref={searchInputRef}
588
550
  shape="rectangle"
589
551
  placeholder={filterPlaceholder}
590
- defaultValue={filterQuery}
552
+ value={filterQuery}
591
553
  aria-controls={listboxId}
592
554
  aria-describedby={showStatus ? statusId : undefined}
593
555
  onKeyDown={(event) => {
@@ -609,9 +571,7 @@ function SelectInputOptions<T = string>({
609
571
  tabIndex={-1}
610
572
  className={classNames(
611
573
  'np-select-input-listbox-container',
612
- virtualized && 'np-select-input-listbox-container--virtualized',
613
- needle == null &&
614
- items.some((item) => item.type === 'group') &&
574
+ items.some((item) => item.type === 'group') &&
615
575
  'np-select-input-listbox-container--has-group',
616
576
  )}
617
577
  >
@@ -622,23 +582,24 @@ function SelectInputOptions<T = string>({
622
582
  </div>
623
583
  ) : null}
624
584
 
625
- {!virtualized ? (
626
- <div ref={listboxRef} {...listboxProps} />
627
- ) : (
628
- <VList
629
- {...listboxProps}
630
- key={needle}
631
- keepMounted={(() => {
632
- let index = 0;
633
- return filterSelectInputItems(filteredItems, (item) => values.has(item.value)).map(
634
- (item) => {
635
- index = filteredItems.indexOf(item, index + 1);
636
- return index;
637
- },
638
- );
639
- })()}
640
- />
641
- )}
585
+ <div
586
+ ref={listboxRef}
587
+ id={listboxId}
588
+ role="listbox"
589
+ aria-orientation="vertical"
590
+ tabIndex={0}
591
+ className="np-select-input-listbox"
592
+ >
593
+ {(needle != null ? dedupeSelectInputItems(items) : items).map((item, index) => (
594
+ <SelectInputItemView
595
+ // eslint-disable-next-line react/no-array-index-key
596
+ key={index}
597
+ item={item}
598
+ renderValue={renderValue}
599
+ needle={needle}
600
+ />
601
+ ))}
602
+ </div>
642
603
 
643
604
  {renderFooter != null ? (
644
605
  <footer className="np-select-input-footer">
@@ -678,10 +639,7 @@ function SelectInputItemView<T = string>({
678
639
  }: SelectInputItemViewProps<T>) {
679
640
  switch (item.type) {
680
641
  case 'option': {
681
- if (
682
- item.value != null &&
683
- (needle == null || selectInputOptionItemIncludesNeedle(item, needle))
684
- ) {
642
+ if (item.value != null && (needle == null || filterSelectInputOptionItem(item, needle))) {
685
643
  return (
686
644
  <SelectInputOption value={item.value} disabled={item.disabled}>
687
645
  {renderValue(item.value, false)}
package/src/main.css CHANGED
@@ -2655,9 +2655,6 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2655
2655
  height: auto;
2656
2656
  }
2657
2657
  }
2658
- .np-select-input-listbox-container--virtualized {
2659
- height: 100vh;
2660
- }
2661
2658
  .np-select-input-listbox-container--has-group {
2662
2659
  scroll-padding-top: 32px;
2663
2660
  scroll-padding-top: var(--size-32);
@@ -79,7 +79,7 @@
79
79
 
80
80
  .progress-bar {
81
81
  .float(left);
82
-
82
+
83
83
  -webkit-backface-visibility: hidden;
84
84
  height: 100%;
85
85
  background-color: var(--color-interactive-primary);
@@ -87,7 +87,7 @@
87
87
  border-bottom-left-radius: 1px;
88
88
  transition: width 0.6s ease-in-out;
89
89
  will-change: width;
90
-
90
+
91
91
  &::after {
92
92
  .float(right);
93
93
 
@@ -0,0 +1,49 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+ import Stepper from './Stepper';
4
+
5
+ const STEPS = [
6
+ {
7
+ label: 'One',
8
+ onClick: fn(),
9
+ },
10
+ {
11
+ label: 'Two',
12
+ hoverLabel: (
13
+ <>
14
+ <div>
15
+ <strong>Diana Jaramillo</strong>
16
+ </div>
17
+ dianajarm123@gmail.com
18
+ </>
19
+ ),
20
+ onClick: fn(),
21
+ },
22
+ { label: 'Recipient', onClick: fn() },
23
+ { label: 'Smellification', onClick: fn() },
24
+ { label: 'Battle', onClick: fn() },
25
+ ];
26
+
27
+ export default {
28
+ component: Stepper,
29
+ title: 'Navigation/Stepper',
30
+ argTypes: {
31
+ activeStep: {
32
+ control: 'radio',
33
+ options: [...Array(STEPS.length).keys()],
34
+ },
35
+ },
36
+ tags: ['autodocs'],
37
+ } satisfies Meta<typeof Stepper>;
38
+
39
+ type Story = StoryObj<typeof Stepper>;
40
+
41
+ export const Basic: Story = {
42
+ render: ({ activeStep, steps }) => {
43
+ return <Stepper activeStep={activeStep} steps={steps} />;
44
+ },
45
+ args: {
46
+ activeStep: 2,
47
+ steps: STEPS,
48
+ },
49
+ };
@@ -23,7 +23,10 @@ export interface StepperProps {
23
23
  className?: string;
24
24
  }
25
25
 
26
- /* eslint-disable react/no-array-index-key */
26
+ /**
27
+ * This component is considered user-unfriendly and inaccessible on its own and will likely be made internal in the future. Please use `FlowNavigation` instead.
28
+ * @see https://storybook.wise.design/?path=/story/navigation-flownavigation--variants
29
+ */
27
30
  const Stepper = ({ steps, activeStep = 0, className }: StepperProps) => {
28
31
  const { isRTL } = useDirection();
29
32
 
@@ -50,6 +53,7 @@ const Stepper = ({ steps, activeStep = 0, className }: StepperProps) => {
50
53
  ) : (
51
54
  <span className="tw-stepper__step-label">{step.label}</span>
52
55
  );
56
+
53
57
  return (
54
58
  <li
55
59
  key={index}
@@ -83,10 +87,10 @@ const Stepper = ({ steps, activeStep = 0, className }: StepperProps) => {
83
87
  <div className="progress">
84
88
  <div className="progress-bar" style={{ width: `${percentageCompleted * 100}%` }} />
85
89
  </div>
86
- <ul className="tw-stepper-steps p-t-1 m-b-0">{steps.map(renderStep)}</ul>
90
+
91
+ <ol className="tw-stepper-steps p-t-1 m-b-0">{steps.map(renderStep)}</ol>
87
92
  </div>
88
93
  );
89
94
  };
90
- /* eslint-enable react/no-array-index-key */
91
95
 
92
96
  export default Stepper;
@@ -1,40 +0,0 @@
1
- import { action } from '@storybook/addon-actions';
2
- import { select } from '@storybook/addon-knobs';
3
-
4
- import Stepper from './Stepper';
5
-
6
- export default {
7
- component: Stepper,
8
- title: 'Navigation/Stepper',
9
- };
10
-
11
- export const Basic = () => {
12
- const activeStep = select('activeStep', [0, 1, 2, 3, 4], 0);
13
- return (
14
- <Stepper
15
- activeStep={activeStep}
16
- steps={[
17
- {
18
- label: 'One',
19
- onClick() {
20
- action('You clicked on step 1, which triggered this function, which alerted you.');
21
- },
22
- },
23
- {
24
- label: 'Two',
25
- hoverLabel: (
26
- <>
27
- <div>
28
- <strong>Diana Jaramillo</strong>
29
- </div>
30
- dianajarm123@gmail.com
31
- </>
32
- ),
33
- },
34
- { label: 'Recipient' },
35
- { label: 'Smellification' },
36
- { label: 'Battle' },
37
- ]}
38
- />
39
- );
40
- };