@transferwise/components 46.122.0 → 46.123.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.
- package/build/inputs/SelectInput.js +55 -6
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +55 -6
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/main.css +97 -77
- package/build/prompt/InlinePrompt/InlinePrompt.js +8 -10
- package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.mjs +9 -11
- package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.js +45 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs +43 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -0
- package/build/styles/main.css +97 -77
- package/build/styles/prompt/InlinePrompt/InlinePrompt.css +2 -23
- package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +41 -0
- package/build/types/inputs/SelectInput.d.ts +2 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +1 -1
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts +30 -0
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -0
- package/build/types/prompt/PrimitivePrompt/index.d.ts +3 -0
- package/build/types/prompt/PrimitivePrompt/index.d.ts.map +1 -0
- package/package.json +8 -8
- package/src/alert/Alert.tests.story.tsx +1 -0
- package/src/button/_stories/Button.brightGreen.tests.story.tsx +1 -1
- package/src/button/_stories/Button.dark.tests.story.tsx +1 -1
- package/src/button/_stories/Button.default.tests.story.tsx +1 -1
- package/src/button/_stories/Button.forestGreen.tests.story.tsx +1 -1
- package/src/button/_stories/Button.tests.story.tsx +1 -1
- package/src/circularButton/CircularButton.tests.story.tsx +1 -0
- package/src/dateInput/DateInput.tests.story.tsx +1 -0
- package/src/dateLookup/DateLookup.tests.story.tsx +1 -0
- package/src/header/Header.tests.story.tsx +1 -1
- package/src/inputs/SelectInput.spec.tsx +177 -0
- package/src/inputs/SelectInput.story.tsx +68 -0
- package/src/inputs/SelectInput.tsx +95 -10
- package/src/legacylistItem/LegacyListItem.tests.story.tsx +1 -1
- package/src/listItem/ListItem.spec.tsx +6 -8
- package/src/listItem/_stories/ListItem.focus.test.story.tsx +1 -1
- package/src/listItem/_stories/ListItem.layout.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.brightGreen.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.dark.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.forestGreen.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.medium.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.neutral.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.personal.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.rtl.test.story.tsx +1 -1
- package/src/listItem/_stories/variants/ListItem.small.test.story.tsx +1 -1
- package/src/main.css +97 -77
- package/src/main.less +2 -1
- package/src/prompt/InlinePrompt/InlinePrompt.css +2 -23
- package/src/prompt/InlinePrompt/InlinePrompt.less +3 -18
- package/src/prompt/InlinePrompt/InlinePrompt.spec.tsx +6 -6
- package/src/prompt/InlinePrompt/InlinePrompt.tsx +6 -7
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +41 -0
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +37 -0
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +70 -0
- package/src/prompt/PrimitivePrompt/index.ts +2 -0
- package/src/radioGroup/RadioGroup.test.story.tsx +1 -1
- package/src/sentimentSurface/SentimentSurface.tests.story.tsx +1 -1
- package/src/snackbar/Snackbar.tests.story.tsx +1 -1
- package/src/stepper/Stepper.tests.story.tsx +1 -0
- package/src/summary/Summary.tests.story.tsx +1 -0
- package/src/title/Title.test.story.tsx +1 -0
- package/src/upload/Upload.tests.story.tsx +1 -0
- package/src/uploadInput/UploadInput.tests.story.tsx +1 -0
- package/src/withId/withId.story.tsx +1 -1
|
@@ -263,6 +263,183 @@ describe('SelectInput', () => {
|
|
|
263
263
|
expect(screen.getByLabelText(/Currency/)).toHaveAttribute('aria-haspopup');
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
it('deduplicates search results across groups using compareValues as key', async () => {
|
|
267
|
+
interface Currency {
|
|
268
|
+
code: string;
|
|
269
|
+
name: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const usdInGroup1: Currency = { code: 'USD', name: 'US Dollar' };
|
|
273
|
+
const usdInGroup2: Currency = { code: 'USD', name: 'US Dollar' };
|
|
274
|
+
const eur: Currency = { code: 'EUR', name: 'Euro' };
|
|
275
|
+
const gbp: Currency = { code: 'GBP', name: 'British Pound' };
|
|
276
|
+
|
|
277
|
+
render(
|
|
278
|
+
<SelectInput<Currency>
|
|
279
|
+
items={[
|
|
280
|
+
{
|
|
281
|
+
type: 'group',
|
|
282
|
+
label: 'Popular',
|
|
283
|
+
options: [
|
|
284
|
+
{ type: 'option', value: usdInGroup1 },
|
|
285
|
+
{ type: 'option', value: eur },
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: 'group',
|
|
290
|
+
label: 'All currencies',
|
|
291
|
+
options: [
|
|
292
|
+
{ type: 'option', value: usdInGroup2 },
|
|
293
|
+
{ type: 'option', value: gbp },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
]}
|
|
297
|
+
compareValues="code"
|
|
298
|
+
renderValue={(currency) => currency.name}
|
|
299
|
+
filterable
|
|
300
|
+
/>,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const trigger = screen.getByRole('combobox');
|
|
304
|
+
await userEvent.click(trigger);
|
|
305
|
+
|
|
306
|
+
const listbox = screen.getByRole('listbox');
|
|
307
|
+
|
|
308
|
+
// Before filtering, should show all 4 options (no deduplication yet)
|
|
309
|
+
let options = within(listbox).getAllByRole('option');
|
|
310
|
+
expect(options).toHaveLength(4);
|
|
311
|
+
|
|
312
|
+
const usdOptions = within(listbox).getAllByText('US Dollar');
|
|
313
|
+
expect(usdOptions).toHaveLength(2);
|
|
314
|
+
|
|
315
|
+
// Start filtering - type a search query to trigger deduplication
|
|
316
|
+
const searchInput = screen.getByRole('combobox', { expanded: true });
|
|
317
|
+
await userEvent.type(searchInput, 'u');
|
|
318
|
+
|
|
319
|
+
// After filtering with 'u', should show 3 unique options (USD deduplicated, EUR, GBP)
|
|
320
|
+
options = within(listbox).getAllByRole('option');
|
|
321
|
+
expect(options).toHaveLength(3);
|
|
322
|
+
expect(within(listbox).getByText('Euro')).toBeInTheDocument();
|
|
323
|
+
expect(within(listbox).getByText('British Pound')).toBeInTheDocument();
|
|
324
|
+
|
|
325
|
+
// Filter more specifically for 'dollar'
|
|
326
|
+
await userEvent.clear(searchInput);
|
|
327
|
+
await userEvent.type(searchInput, 'dollar');
|
|
328
|
+
|
|
329
|
+
const filteredOptions = within(listbox).getAllByRole('option');
|
|
330
|
+
// Should only show 1 USD option, not 2
|
|
331
|
+
expect(filteredOptions).toHaveLength(1);
|
|
332
|
+
expect(within(listbox).getByText('US Dollar')).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('deduplicates search results across groups using compareValues as function', async () => {
|
|
336
|
+
interface Item {
|
|
337
|
+
id: number;
|
|
338
|
+
label: string;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const item1Group1: Item = { id: 1, label: 'Item One' };
|
|
342
|
+
const item2Group1: Item = { id: 2, label: 'Item Two' };
|
|
343
|
+
|
|
344
|
+
const item1Group2: Item = { id: 1, label: 'Item One' };
|
|
345
|
+
|
|
346
|
+
render(
|
|
347
|
+
<SelectInput<Item>
|
|
348
|
+
items={[
|
|
349
|
+
{
|
|
350
|
+
type: 'group',
|
|
351
|
+
label: 'Group A',
|
|
352
|
+
options: [
|
|
353
|
+
{ type: 'option', value: item1Group1 },
|
|
354
|
+
{ type: 'option', value: item2Group1 },
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
type: 'group',
|
|
359
|
+
label: 'Group B',
|
|
360
|
+
options: [{ type: 'option', value: item1Group2 }],
|
|
361
|
+
},
|
|
362
|
+
]}
|
|
363
|
+
compareValues={(a, b) => a?.id === b?.id}
|
|
364
|
+
renderValue={(item) => item.label}
|
|
365
|
+
filterable
|
|
366
|
+
/>,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const trigger = screen.getByRole('combobox');
|
|
370
|
+
await userEvent.click(trigger);
|
|
371
|
+
|
|
372
|
+
const listbox = screen.getByRole('listbox');
|
|
373
|
+
|
|
374
|
+
// Before filtering, should show all 3 options (no deduplication yet)
|
|
375
|
+
let options = within(listbox).getAllByRole('option');
|
|
376
|
+
expect(options).toHaveLength(3);
|
|
377
|
+
|
|
378
|
+
// Start filtering - type a search query to trigger deduplication
|
|
379
|
+
const searchInput = screen.getByRole('combobox', { expanded: true });
|
|
380
|
+
await userEvent.type(searchInput, 'item');
|
|
381
|
+
|
|
382
|
+
// After filtering, should show 2 unique options (item with id:1 deduplicated, item with id:2)
|
|
383
|
+
options = within(listbox).getAllByRole('option');
|
|
384
|
+
expect(options).toHaveLength(2);
|
|
385
|
+
expect(within(listbox).getByText('Item One')).toBeInTheDocument();
|
|
386
|
+
expect(within(listbox).getByText('Item Two')).toBeInTheDocument();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('sorts filtered options using sortFilteredOptions prop', async () => {
|
|
390
|
+
interface Country {
|
|
391
|
+
code: string;
|
|
392
|
+
name: string;
|
|
393
|
+
keywords: string[];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const countries: Country[] = [
|
|
397
|
+
{ code: 'AD', name: 'Andorra', keywords: ['united states dollar'] },
|
|
398
|
+
{ code: 'DE', name: 'Germany', keywords: ['EUR'] },
|
|
399
|
+
{ code: 'US', name: 'United States', keywords: ['United States dollar', 'USD'] },
|
|
400
|
+
{ code: 'ZM', name: 'Zambia', keywords: ['USD', 'united states dollar'] },
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
render(
|
|
404
|
+
<SelectInput<Country>
|
|
405
|
+
items={countries.map((country) => ({
|
|
406
|
+
type: 'option',
|
|
407
|
+
value: country,
|
|
408
|
+
filterMatchers: country.keywords,
|
|
409
|
+
}))}
|
|
410
|
+
renderValue={(country) => country.name}
|
|
411
|
+
filterable
|
|
412
|
+
sortFilteredOptions={(a, b, searchQuery) => {
|
|
413
|
+
const query = searchQuery.toLowerCase();
|
|
414
|
+
const nameA = a.value.name.toLowerCase();
|
|
415
|
+
const nameB = b.value.name.toLowerCase();
|
|
416
|
+
|
|
417
|
+
const aMatch = nameA.includes(query);
|
|
418
|
+
const bMatch = nameB.includes(query);
|
|
419
|
+
|
|
420
|
+
if (aMatch && !bMatch) return -1;
|
|
421
|
+
if (!aMatch && bMatch) return 1;
|
|
422
|
+
|
|
423
|
+
return nameA.localeCompare(nameB);
|
|
424
|
+
}}
|
|
425
|
+
/>,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const trigger = screen.getByRole('combobox');
|
|
429
|
+
await userEvent.click(trigger);
|
|
430
|
+
|
|
431
|
+
const searchInput = screen.getByRole('combobox', { expanded: true });
|
|
432
|
+
await userEvent.type(searchInput, 'united');
|
|
433
|
+
|
|
434
|
+
const listbox = screen.getByRole('listbox');
|
|
435
|
+
const options = within(listbox).getAllByRole('option');
|
|
436
|
+
|
|
437
|
+
expect(options).toHaveLength(3);
|
|
438
|
+
expect(options[0]).toHaveTextContent('United States');
|
|
439
|
+
expect(options[1]).toHaveTextContent('Andorra');
|
|
440
|
+
expect(options[2]).toHaveTextContent('Zambia');
|
|
441
|
+
});
|
|
442
|
+
|
|
266
443
|
describe('listbox label', () => {
|
|
267
444
|
const fieldLabel = 'Fruits';
|
|
268
445
|
const triggerLabel = 'Select fruit';
|
|
@@ -616,3 +616,71 @@ export const WithAutocomplete: Story<string> = {
|
|
|
616
616
|
);
|
|
617
617
|
},
|
|
618
618
|
};
|
|
619
|
+
|
|
620
|
+
interface CountryWithCurrency extends Country {
|
|
621
|
+
keywords: string[];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const countriesWithCurrency: CountryWithCurrency[] = [
|
|
625
|
+
{ code: 'AD', name: 'Andorra', keywords: ['united states dollar'] },
|
|
626
|
+
{ code: 'DE', name: 'Germany', keywords: ['EUR'] },
|
|
627
|
+
{ code: 'US', name: 'United States', keywords: ['USD'] },
|
|
628
|
+
{ code: 'ZM', name: 'Zambia', keywords: ['USD', 'united states dollar'] },
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
function countryWithCurrencyOption(country: CountryWithCurrency) {
|
|
632
|
+
return {
|
|
633
|
+
type: 'option',
|
|
634
|
+
value: country,
|
|
635
|
+
filterMatchers: [country.name, country.code, ...country.keywords],
|
|
636
|
+
} satisfies SelectInputItem<CountryWithCurrency>;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export const WithCustomSearchResultSorting: Story<CountryWithCurrency> = {
|
|
640
|
+
args: {
|
|
641
|
+
items: countriesWithCurrency.map(countryWithCurrencyOption),
|
|
642
|
+
compareValues: 'code',
|
|
643
|
+
renderValue: (country) => (
|
|
644
|
+
<SelectInputOptionContent
|
|
645
|
+
title={country.name}
|
|
646
|
+
icon={<Flag code={country.code} intrinsicSize={24} />}
|
|
647
|
+
/>
|
|
648
|
+
),
|
|
649
|
+
value: countriesWithCurrency[0],
|
|
650
|
+
filterable: true,
|
|
651
|
+
filterPlaceholder: 'Type a currency / country',
|
|
652
|
+
sortFilteredOptions: (a, b, searchQuery) => {
|
|
653
|
+
const normalizedQuery = searchQuery.toLowerCase();
|
|
654
|
+
const nameA = a.value.name.toLowerCase();
|
|
655
|
+
const nameB = b.value.name.toLowerCase();
|
|
656
|
+
|
|
657
|
+
// Prioritize countries where name contains the search query
|
|
658
|
+
const aNameMatch = nameA.includes(normalizedQuery);
|
|
659
|
+
const bNameMatch = nameB.includes(normalizedQuery);
|
|
660
|
+
|
|
661
|
+
if (aNameMatch && !bNameMatch) return -1;
|
|
662
|
+
if (!aNameMatch && bNameMatch) return 1;
|
|
663
|
+
|
|
664
|
+
// Then sort alphabetically
|
|
665
|
+
return nameA.localeCompare(nameB);
|
|
666
|
+
},
|
|
667
|
+
size: 'lg',
|
|
668
|
+
} satisfies Story<CountryWithCurrency>['args'],
|
|
669
|
+
decorators: [
|
|
670
|
+
(Story) => (
|
|
671
|
+
<div>
|
|
672
|
+
<p className="m-b-3 np-text-body-default" style={{ maxWidth: '600px' }}>
|
|
673
|
+
This example demonstrates custom sorting with the <code>sortFilteredOptions</code> prop.
|
|
674
|
+
<br />
|
|
675
|
+
<br />
|
|
676
|
+
Try searching for "united" - Without the custom sorter, it would just be the
|
|
677
|
+
provided options order.
|
|
678
|
+
<br />
|
|
679
|
+
With a customer sorter function in this story, the countries with "United" in
|
|
680
|
+
their name appear first, followed by countries that only match via keywords.
|
|
681
|
+
</p>
|
|
682
|
+
<Story />
|
|
683
|
+
</div>
|
|
684
|
+
),
|
|
685
|
+
],
|
|
686
|
+
};
|
|
@@ -93,8 +93,13 @@ export type SelectInputItem<T = string> =
|
|
|
93
93
|
function dedupeSelectInputOptionItem<T>(
|
|
94
94
|
item: SelectInputOptionItem<T>,
|
|
95
95
|
existingValues: Set<T>,
|
|
96
|
+
compareValues?: (a: T, b: T) => boolean,
|
|
96
97
|
): SelectInputOptionItem<T | undefined> {
|
|
97
|
-
|
|
98
|
+
const isDuplicate = compareValues
|
|
99
|
+
? Array.from(existingValues).some((existingValue) => compareValues(item.value, existingValue))
|
|
100
|
+
: existingValues.has(item.value);
|
|
101
|
+
|
|
102
|
+
if (!isDuplicate) {
|
|
98
103
|
existingValues.add(item.value);
|
|
99
104
|
return item;
|
|
100
105
|
}
|
|
@@ -108,18 +113,20 @@ function dedupeSelectInputOptionItem<T>(
|
|
|
108
113
|
*/
|
|
109
114
|
function dedupeSelectInputItems<T>(
|
|
110
115
|
items: readonly SelectInputItem<T>[],
|
|
116
|
+
compareValues?: (a: T, b: T) => boolean,
|
|
111
117
|
): SelectInputItem<T | undefined>[] {
|
|
112
118
|
const existingValues = new Set<T>();
|
|
119
|
+
|
|
113
120
|
return items.map((item) => {
|
|
114
121
|
switch (item.type) {
|
|
115
122
|
case 'option': {
|
|
116
|
-
return dedupeSelectInputOptionItem(item, existingValues);
|
|
123
|
+
return dedupeSelectInputOptionItem(item, existingValues, compareValues);
|
|
117
124
|
}
|
|
118
125
|
case 'group': {
|
|
119
126
|
return {
|
|
120
127
|
...item,
|
|
121
128
|
options: item.options.map((option) =>
|
|
122
|
-
dedupeSelectInputOptionItem(option, existingValues),
|
|
129
|
+
dedupeSelectInputOptionItem(option, existingValues, compareValues),
|
|
123
130
|
),
|
|
124
131
|
};
|
|
125
132
|
}
|
|
@@ -153,6 +160,38 @@ function filterSelectInputItems<T>(
|
|
|
153
160
|
});
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Flattens and sorts filtered options using the provided comparator.
|
|
165
|
+
* Extracts all options from groups, filters out undefined values (deduplicated items),
|
|
166
|
+
* sorts them, and returns as a flat list of option items.
|
|
167
|
+
*/
|
|
168
|
+
function sortSelectInputItems<T>(
|
|
169
|
+
items: readonly SelectInputItem<T | undefined>[],
|
|
170
|
+
compareFn: (
|
|
171
|
+
a: SelectInputOptionItem<NonNullable<T>>,
|
|
172
|
+
b: SelectInputOptionItem<NonNullable<T>>,
|
|
173
|
+
searchQuery: string,
|
|
174
|
+
) => number,
|
|
175
|
+
searchQuery: string,
|
|
176
|
+
): SelectInputItem<NonNullable<T>>[] {
|
|
177
|
+
const flattenedOption = items.flatMap((item) => {
|
|
178
|
+
if (item.type === 'option') {
|
|
179
|
+
return item.value !== undefined ? [item as SelectInputOptionItem<NonNullable<T>>] : [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (item.type === 'group') {
|
|
183
|
+
return item.options.filter(
|
|
184
|
+
(option): option is SelectInputOptionItem<NonNullable<T>> => option.value !== undefined,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return [];
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// eslint-disable-next-line functional/immutable-data
|
|
192
|
+
return flattenedOption.sort((a, b) => compareFn(a, b, searchQuery));
|
|
193
|
+
}
|
|
194
|
+
|
|
156
195
|
export interface SelectInputProps<T = string, M extends boolean = false> {
|
|
157
196
|
id?: string;
|
|
158
197
|
/**
|
|
@@ -203,6 +242,11 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
|
|
|
203
242
|
}) => React.ReactNode;
|
|
204
243
|
filterable?: boolean;
|
|
205
244
|
filterPlaceholder?: string;
|
|
245
|
+
sortFilteredOptions?: (
|
|
246
|
+
a: SelectInputOptionItem<NonNullable<T>>,
|
|
247
|
+
b: SelectInputOptionItem<NonNullable<T>>,
|
|
248
|
+
searchQuery: string,
|
|
249
|
+
) => number;
|
|
206
250
|
disabled?: boolean;
|
|
207
251
|
size?: 'sm' | 'md' | 'lg';
|
|
208
252
|
className?: string;
|
|
@@ -297,6 +341,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
297
341
|
renderTrigger = defaultRenderTrigger,
|
|
298
342
|
filterable,
|
|
299
343
|
filterPlaceholder,
|
|
344
|
+
sortFilteredOptions,
|
|
300
345
|
disabled,
|
|
301
346
|
size = 'md',
|
|
302
347
|
className,
|
|
@@ -481,10 +526,12 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
481
526
|
id={id ? `${id}Search` : undefined}
|
|
482
527
|
parentId={parentId}
|
|
483
528
|
items={items}
|
|
529
|
+
compareValues={compareValues}
|
|
484
530
|
renderValue={renderValue}
|
|
485
531
|
renderFooter={renderFooter}
|
|
486
532
|
filterable={filterable}
|
|
487
533
|
filterPlaceholder={filterPlaceholder}
|
|
534
|
+
sortFilteredOptions={sortFilteredOptions}
|
|
488
535
|
searchInputRef={searchInputRef}
|
|
489
536
|
listboxRef={listboxRef}
|
|
490
537
|
filterQuery={deferredFilterQuery}
|
|
@@ -589,7 +636,15 @@ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContai
|
|
|
589
636
|
|
|
590
637
|
interface SelectInputOptionsProps<T = string> extends Pick<
|
|
591
638
|
SelectInputProps<T>,
|
|
592
|
-
|
|
639
|
+
| 'items'
|
|
640
|
+
| 'renderValue'
|
|
641
|
+
| 'renderFooter'
|
|
642
|
+
| 'filterable'
|
|
643
|
+
| 'filterPlaceholder'
|
|
644
|
+
| 'id'
|
|
645
|
+
| 'parentId'
|
|
646
|
+
| 'compareValues'
|
|
647
|
+
| 'sortFilteredOptions'
|
|
593
648
|
> {
|
|
594
649
|
searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
|
|
595
650
|
listboxRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
@@ -606,10 +661,12 @@ function SelectInputOptions<T = string>({
|
|
|
606
661
|
id,
|
|
607
662
|
parentId,
|
|
608
663
|
items,
|
|
664
|
+
compareValues: compareValuesProp,
|
|
609
665
|
renderValue = String,
|
|
610
666
|
renderFooter,
|
|
611
667
|
filterable = false,
|
|
612
668
|
filterPlaceholder,
|
|
669
|
+
sortFilteredOptions,
|
|
613
670
|
searchInputRef,
|
|
614
671
|
listboxRef,
|
|
615
672
|
filterQuery,
|
|
@@ -651,12 +708,40 @@ function SelectInputOptions<T = string>({
|
|
|
651
708
|
}
|
|
652
709
|
}, [controllerRef, needle]);
|
|
653
710
|
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
711
|
+
const compareValues = useMemo(() => {
|
|
712
|
+
if (!compareValuesProp) {
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (typeof compareValuesProp === 'function') {
|
|
717
|
+
return (a: NonNullable<T>, b: NonNullable<T>) => compareValuesProp(a, b);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const key = compareValuesProp;
|
|
721
|
+
return (a: NonNullable<T>, b: NonNullable<T>) => {
|
|
722
|
+
if (typeof a === 'object' && a != null && typeof b === 'object' && b != null) {
|
|
723
|
+
return (a as Record<string, unknown>)[key] === (b as Record<string, unknown>)[key];
|
|
724
|
+
}
|
|
725
|
+
return a === b;
|
|
726
|
+
};
|
|
727
|
+
}, [compareValuesProp]);
|
|
728
|
+
|
|
729
|
+
const filteredItems: readonly SelectInputItem<NonNullable<T> | undefined>[] = useMemo(() => {
|
|
730
|
+
if (needle == null) {
|
|
731
|
+
return items;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const filtered = filterSelectInputItems(dedupeSelectInputItems(items, compareValues), (item) =>
|
|
735
|
+
selectInputOptionItemIncludesNeedle(item, needle),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
if (sortFilteredOptions) {
|
|
739
|
+
return sortSelectInputItems(filtered, sortFilteredOptions, filterQuery);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return filtered;
|
|
743
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
744
|
+
}, [needle, items, compareValues]);
|
|
660
745
|
const resultsEmpty = needle != null && filteredItems.length === 0;
|
|
661
746
|
|
|
662
747
|
const virtualized = filteredItems.length > MAX_ITEMS_WITHOUT_VIRTUALIZATION;
|
|
@@ -1526,10 +1526,9 @@ describe('ListItem', () => {
|
|
|
1526
1526
|
);
|
|
1527
1527
|
expect(screen.queryByText(prompt)).not.toBeInTheDocument();
|
|
1528
1528
|
expect(screen.getByText(disabledPromptMessage)).toBeInTheDocument();
|
|
1529
|
-
expect(
|
|
1530
|
-
'
|
|
1531
|
-
|
|
1532
|
-
);
|
|
1529
|
+
expect(
|
|
1530
|
+
screen.getByTestId('InlinePrompt_Muted').parentNode?.parentNode?.parentNode,
|
|
1531
|
+
).toHaveAttribute('id', expect.stringMatching(/_prompt$/));
|
|
1533
1532
|
});
|
|
1534
1533
|
|
|
1535
1534
|
it('should render muted prompt if disabled and disabledPromptMessage are set', () => {
|
|
@@ -1537,10 +1536,9 @@ describe('ListItem', () => {
|
|
|
1537
1536
|
<ListItem title="Test Title" disabled disabledPromptMessage={disabledPromptMessage} />,
|
|
1538
1537
|
);
|
|
1539
1538
|
expect(screen.getByText(disabledPromptMessage)).toBeInTheDocument();
|
|
1540
|
-
expect(
|
|
1541
|
-
'
|
|
1542
|
-
|
|
1543
|
-
);
|
|
1539
|
+
expect(
|
|
1540
|
+
screen.getByTestId('InlinePrompt_Muted').parentNode?.parentNode?.parentNode,
|
|
1541
|
+
).toHaveAttribute('id', expect.stringMatching(/_prompt$/));
|
|
1544
1542
|
});
|
|
1545
1543
|
});
|
|
1546
1544
|
});
|
|
@@ -21,7 +21,7 @@ const waitForListItem = async (canvas: ReturnType<typeof within>, timeout = 3000
|
|
|
21
21
|
export default {
|
|
22
22
|
component: ListItem,
|
|
23
23
|
title: 'Content/ListItem/tests/focus',
|
|
24
|
-
tags: ['!autodocs'],
|
|
24
|
+
tags: ['!autodocs', '!manifest'],
|
|
25
25
|
parameters: {
|
|
26
26
|
controls: { disable: true },
|
|
27
27
|
actions: { disable: true },
|
|
@@ -6,7 +6,7 @@ import { allModes } from '../../../../.storybook/modes';
|
|
|
6
6
|
export default {
|
|
7
7
|
component: ListItem,
|
|
8
8
|
title: 'Content/ListItem/tests/variants/theme: bright-green',
|
|
9
|
-
tags: ['!autodocs'],
|
|
9
|
+
tags: ['!autodocs', '!manifest'],
|
|
10
10
|
parameters: {
|
|
11
11
|
controls: { disable: true },
|
|
12
12
|
actions: { disable: true },
|
|
@@ -6,7 +6,7 @@ import { allModes } from '../../../../.storybook/modes';
|
|
|
6
6
|
export default {
|
|
7
7
|
component: ListItem,
|
|
8
8
|
title: 'Content/ListItem/tests/variants/theme: dark',
|
|
9
|
-
tags: ['!autodocs'],
|
|
9
|
+
tags: ['!autodocs', '!manifest'],
|
|
10
10
|
parameters: {
|
|
11
11
|
controls: { disable: true },
|
|
12
12
|
actions: { disable: true },
|
|
@@ -6,7 +6,7 @@ import { allModes } from '../../../../.storybook/modes';
|
|
|
6
6
|
export default {
|
|
7
7
|
component: ListItem,
|
|
8
8
|
title: 'Content/ListItem/tests/variants/theme: forest-green',
|
|
9
|
-
tags: ['!autodocs'],
|
|
9
|
+
tags: ['!autodocs', '!manifest'],
|
|
10
10
|
parameters: {
|
|
11
11
|
controls: { disable: true },
|
|
12
12
|
actions: { disable: true },
|
|
@@ -6,7 +6,7 @@ import { withSizedContainer } from '../helpers';
|
|
|
6
6
|
export default {
|
|
7
7
|
component: ListItem,
|
|
8
8
|
title: 'Content/ListItem/tests/variants/cq: medium',
|
|
9
|
-
tags: ['!autodocs'],
|
|
9
|
+
tags: ['!autodocs', '!manifest'],
|
|
10
10
|
parameters: {
|
|
11
11
|
controls: { disable: true },
|
|
12
12
|
actions: { disable: true },
|
|
@@ -11,7 +11,7 @@ import { VariantStory } from './helpers';
|
|
|
11
11
|
export default {
|
|
12
12
|
component: ListItem,
|
|
13
13
|
title: 'Content/ListItem/tests/variants/theme: personal',
|
|
14
|
-
tags: ['!autodocs'],
|
|
14
|
+
tags: ['!autodocs', '!manifest'],
|
|
15
15
|
parameters: {
|
|
16
16
|
controls: { disable: true },
|
|
17
17
|
actions: { disable: true },
|
|
@@ -6,7 +6,7 @@ import { allModes } from '../../../../.storybook/modes';
|
|
|
6
6
|
export default {
|
|
7
7
|
component: ListItem,
|
|
8
8
|
title: 'Content/ListItem/tests/variants/rtl',
|
|
9
|
-
tags: ['!autodocs'],
|
|
9
|
+
tags: ['!autodocs', '!manifest'],
|
|
10
10
|
parameters: {
|
|
11
11
|
controls: { disable: true },
|
|
12
12
|
actions: { disable: true },
|
|
@@ -6,7 +6,7 @@ import { withSizedContainer } from '../helpers';
|
|
|
6
6
|
export default {
|
|
7
7
|
component: ListItem,
|
|
8
8
|
title: 'Content/ListItem/tests/variants/cq: small',
|
|
9
|
-
tags: ['!autodocs'],
|
|
9
|
+
tags: ['!autodocs', '!manifest'],
|
|
10
10
|
parameters: {
|
|
11
11
|
controls: { disable: true },
|
|
12
12
|
actions: { disable: true },
|