import type { Meta, StoryObj } from '@storybook/react-webpack5'; import { expect, fireEvent, fn, type Mock, screen, userEvent, waitFor, within, } from 'storybook/test'; import { Flag } from '@wise/art'; import { useEffect, useRef, useState } from 'react'; import { allModes } from '../../../../.storybook/modes'; import { withVariantConfig } from '../../../../.storybook/helpers'; import Body from '../../../body'; import { Field } from '../../../field/Field'; import { lorem5, lorem500 } from '../../../test-utils'; import { sortByRelevance } from '../SelectInput.utils'; import { SelectInput, type SelectInputItem, SelectInputOptionContent, type SelectInputProps, } from '..'; const meta = { title: 'Forms/SelectInput/Tests', component: SelectInput, args: { onFilterChange: fn() satisfies Mock, onChange: fn() satisfies Mock, onClose: fn() satisfies Mock, onOpen: fn() satisfies Mock, }, tags: ['!autodocs', '!manifest'], } satisfies Meta; export default meta; type Story = StoryObj>; interface TestMonth { id: number; name: string; } const testMonths: TestMonth[] = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ].map((name, index) => ({ id: index + 1, name })); interface TestCurrency { code: string; name: string; countries?: string[]; } const testPopularCurrencies: TestCurrency[] = [ { code: 'USD', name: 'United States Dollar', countries: ['Hong Kong', 'Saudi Arabia'] }, { code: 'EUR', name: 'Euro', countries: ['Spain', 'Germany', 'France', 'Austria', 'Estonia'] }, { code: 'GBP', name: 'British pound', countries: ['England', 'Scotland', 'Wales'] }, ]; const testAllCurrencies: TestCurrency[] = [ ...testPopularCurrencies, { code: 'AUD', name: 'Australian dollar' }, { code: 'CAD', name: 'Canadian dollar', countries: ['Canada'] }, { code: 'ÅLD', name: 'Ålandian peso', countries: ['Ålandia'] }, ].sort((a, b) => a.code.localeCompare(b.code)); function testCurrencyOption(currency: TestCurrency) { return { type: 'option', value: currency, filterMatchers: [currency.code, currency.name, ...(currency.countries ?? [])], } satisfies SelectInputItem; } /** * Visual regression story: all three sizes rendered across every theme. * - `sm` — opened, simple (months) list * - `md` — closed * - `lg` — opened, grouped currencies list */ export const Variants: Story = { render: function Render() { const smRef = useRef(null); const lgRef = useRef(null); const simpleItems: SelectInputItem[] = testMonths.map((month) => ({ type: 'option', value: month, })); const groupedItems: SelectInputItem[] = [ { type: 'group', label: 'Popular currencies', options: testPopularCurrencies.map(testCurrencyOption), }, { type: 'group', label: 'All currencies', options: testAllCurrencies.map(testCurrencyOption), }, ]; useEffect(() => { const timer = setTimeout(() => { smRef.current?.click(); setTimeout(() => { lgRef.current?.click(); }, 100); }, 200); return () => clearTimeout(timer); }, []); return (

Small (opened)

size="sm" placeholder="Month" items={simpleItems} triggerRef={smRef} renderValue={(month) => } />

Medium

size="md" placeholder="Month" items={simpleItems} renderValue={(month) => } />

Large (opened)

size="lg" placeholder="Currency" items={groupedItems} triggerRef={lgRef} renderValue={(currency) => ( } /> )} />
); }, ...withVariantConfig(['default', 'dark', 'bright-green', 'forest-green', 'rtl']), }; /** * Test story with a large list of countries where almost all have "United States dollar" * in their metadata. This reproduces the bug where searching "united" would scroll to * the bottom of the list instead of staying at the top. * https://transferwise.atlassian.net/browse/DS-8048 */ export const VirtualizedCountryListTest: Story<{ title: string; value: string }> = { args: { items: [ // Popular countries group - all have USD in metadata { type: 'group', label: 'Popular countries', options: [ { type: 'option', value: { title: 'United States', value: 'US' }, filterMatchers: ['United States', 'USA', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'United Kingdom', value: 'GB' }, filterMatchers: [ 'United Kingdom', 'UK', 'British pound', 'GBP', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Germany', value: 'DE' }, filterMatchers: [ 'Germany', 'Deutschland', 'Euro', 'EUR', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'France', value: 'FR' }, filterMatchers: ['France', 'Euro', 'EUR', 'United States dollar', 'USD'], }, ], }, // All countries group - massive list with USD in most { type: 'group', label: 'All countries', options: [ { type: 'option', value: { title: 'Afghanistan', value: 'AF' }, filterMatchers: ['Afghanistan', 'Afghan afghani', 'AFN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Albania', value: 'AL' }, filterMatchers: ['Albania', 'Albanian lek', 'ALL', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Algeria', value: 'DZ' }, filterMatchers: ['Algeria', 'Algerian dinar', 'DZD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Andorra', value: 'AD' }, filterMatchers: ['Andorra', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Angola', value: 'AO' }, filterMatchers: ['Angola', 'Angolan kwanza', 'AOA', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Argentina', value: 'AR' }, filterMatchers: ['Argentina', 'Argentine peso', 'ARS', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Armenia', value: 'AM' }, filterMatchers: ['Armenia', 'Armenian dram', 'AMD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Australia', value: 'AU' }, filterMatchers: [ 'Australia', 'Australian dollar', 'AUD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Austria', value: 'AT' }, filterMatchers: ['Austria', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Azerbaijan', value: 'AZ' }, filterMatchers: [ 'Azerbaijan', 'Azerbaijani manat', 'AZN', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Bahamas', value: 'BS' }, filterMatchers: ['Bahamas', 'Bahamian dollar', 'BSD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Bahrain', value: 'BH' }, filterMatchers: ['Bahrain', 'Bahraini dinar', 'BHD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Bangladesh', value: 'BD' }, filterMatchers: [ 'Bangladesh', 'Bangladeshi taka', 'BDT', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Barbados', value: 'BB' }, filterMatchers: ['Barbados', 'Barbadian dollar', 'BBD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Belarus', value: 'BY' }, filterMatchers: ['Belarus', 'Belarusian ruble', 'BYN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Belgium', value: 'BE' }, filterMatchers: ['Belgium', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Belize', value: 'BZ' }, filterMatchers: ['Belize', 'Belize dollar', 'BZD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Benin', value: 'BJ' }, filterMatchers: [ 'Benin', 'West African CFA franc', 'XOF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Bhutan', value: 'BT' }, filterMatchers: ['Bhutan', 'Bhutanese ngultrum', 'BTN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Bolivia', value: 'BO' }, filterMatchers: ['Bolivia', 'Bolivian boliviano', 'BOB', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Bosnia and Herzegovina', value: 'BA' }, filterMatchers: [ 'Bosnia', 'Herzegovina', 'Convertible mark', 'BAM', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Botswana', value: 'BW' }, filterMatchers: ['Botswana', 'Botswana pula', 'BWP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Brazil', value: 'BR' }, filterMatchers: ['Brazil', 'Brazilian real', 'BRL', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Brunei', value: 'BN' }, filterMatchers: ['Brunei', 'Brunei dollar', 'BND', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Bulgaria', value: 'BG' }, filterMatchers: ['Bulgaria', 'Bulgarian lev', 'BGN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Burkina Faso', value: 'BF' }, filterMatchers: [ 'Burkina Faso', 'West African CFA franc', 'XOF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Burundi', value: 'BI' }, filterMatchers: ['Burundi', 'Burundian franc', 'BIF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Cambodia', value: 'KH' }, filterMatchers: ['Cambodia', 'Cambodian riel', 'KHR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Cameroon', value: 'CM' }, filterMatchers: [ 'Cameroon', 'Central African CFA franc', 'XAF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Canada', value: 'CA' }, filterMatchers: ['Canada', 'Canadian dollar', 'CAD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Cape Verde', value: 'CV' }, filterMatchers: [ 'Cape Verde', 'Cape Verdean escudo', 'CVE', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Central African Republic', value: 'CF' }, filterMatchers: [ 'Central African Republic', 'Central African CFA franc', 'XAF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Chad', value: 'TD' }, filterMatchers: [ 'Chad', 'Central African CFA franc', 'XAF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Chile', value: 'CL' }, filterMatchers: ['Chile', 'Chilean peso', 'CLP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'China', value: 'CN' }, filterMatchers: ['China', 'Chinese yuan', 'CNY', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Colombia', value: 'CO' }, filterMatchers: ['Colombia', 'Colombian peso', 'COP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Comoros', value: 'KM' }, filterMatchers: ['Comoros', 'Comorian franc', 'KMF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Congo', value: 'CG' }, filterMatchers: [ 'Congo', 'Central African CFA franc', 'XAF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Costa Rica', value: 'CR' }, filterMatchers: [ 'Costa Rica', 'Costa Rican colón', 'CRC', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Croatia', value: 'HR' }, filterMatchers: ['Croatia', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Cuba', value: 'CU' }, filterMatchers: ['Cuba', 'Cuban peso', 'CUP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Cyprus', value: 'CY' }, filterMatchers: ['Cyprus', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Czech Republic', value: 'CZ' }, filterMatchers: [ 'Czech Republic', 'Czech koruna', 'CZK', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Denmark', value: 'DK' }, filterMatchers: ['Denmark', 'Danish krone', 'DKK', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Djibouti', value: 'DJ' }, filterMatchers: ['Djibouti', 'Djiboutian franc', 'DJF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Dominica', value: 'DM' }, filterMatchers: [ 'Dominica', 'East Caribbean dollar', 'XCD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Dominican Republic', value: 'DO' }, filterMatchers: [ 'Dominican Republic', 'Dominican peso', 'DOP', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Ecuador', value: 'EC' }, filterMatchers: ['Ecuador', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Egypt', value: 'EG' }, filterMatchers: ['Egypt', 'Egyptian pound', 'EGP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'El Salvador', value: 'SV' }, filterMatchers: ['El Salvador', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Equatorial Guinea', value: 'GQ' }, filterMatchers: [ 'Equatorial Guinea', 'Central African CFA franc', 'XAF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Eritrea', value: 'ER' }, filterMatchers: ['Eritrea', 'Eritrean nakfa', 'ERN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Estonia', value: 'EE' }, filterMatchers: ['Estonia', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Ethiopia', value: 'ET' }, filterMatchers: ['Ethiopia', 'Ethiopian birr', 'ETB', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Fiji', value: 'FJ' }, filterMatchers: ['Fiji', 'Fijian dollar', 'FJD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Finland', value: 'FI' }, filterMatchers: ['Finland', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Gabon', value: 'GA' }, filterMatchers: [ 'Gabon', 'Central African CFA franc', 'XAF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Gambia', value: 'GM' }, filterMatchers: ['Gambia', 'Gambian dalasi', 'GMD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Georgia', value: 'GE' }, filterMatchers: ['Georgia', 'Georgian lari', 'GEL', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Ghana', value: 'GH' }, filterMatchers: ['Ghana', 'Ghanaian cedi', 'GHS', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Greece', value: 'GR' }, filterMatchers: ['Greece', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Grenada', value: 'GD' }, filterMatchers: [ 'Grenada', 'East Caribbean dollar', 'XCD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Guatemala', value: 'GT' }, filterMatchers: [ 'Guatemala', 'Guatemalan quetzal', 'GTQ', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Guinea', value: 'GN' }, filterMatchers: ['Guinea', 'Guinean franc', 'GNF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Guinea-Bissau', value: 'GW' }, filterMatchers: [ 'Guinea-Bissau', 'West African CFA franc', 'XOF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Guyana', value: 'GY' }, filterMatchers: ['Guyana', 'Guyanese dollar', 'GYD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Haiti', value: 'HT' }, filterMatchers: ['Haiti', 'Haitian gourde', 'HTG', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Honduras', value: 'HN' }, filterMatchers: ['Honduras', 'Honduran lempira', 'HNL', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Hungary', value: 'HU' }, filterMatchers: ['Hungary', 'Hungarian forint', 'HUF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Iceland', value: 'IS' }, filterMatchers: ['Iceland', 'Icelandic króna', 'ISK', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'India', value: 'IN' }, filterMatchers: ['India', 'Indian rupee', 'INR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Indonesia', value: 'ID' }, filterMatchers: [ 'Indonesia', 'Indonesian rupiah', 'IDR', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Iran', value: 'IR' }, filterMatchers: ['Iran', 'Iranian rial', 'IRR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Iraq', value: 'IQ' }, filterMatchers: ['Iraq', 'Iraqi dinar', 'IQD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Ireland', value: 'IE' }, filterMatchers: ['Ireland', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Israel', value: 'IL' }, filterMatchers: ['Israel', 'Israeli new shekel', 'ILS', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Italy', value: 'IT' }, filterMatchers: ['Italy', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Jamaica', value: 'JM' }, filterMatchers: ['Jamaica', 'Jamaican dollar', 'JMD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Japan', value: 'JP' }, filterMatchers: ['Japan', 'Japanese yen', 'JPY', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Jordan', value: 'JO' }, filterMatchers: ['Jordan', 'Jordanian dinar', 'JOD', 'United States dollar', 'USD'], }, // This one country does NOT have USD - to verify filtering works { type: 'option', value: { title: 'Kazakhstan', value: 'KZ' }, filterMatchers: ['Kazakhstan', 'Kazakhstani tenge', 'KZT'], }, { type: 'option', value: { title: 'Kenya', value: 'KE' }, filterMatchers: ['Kenya', 'Kenyan shilling', 'KES', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Kiribati', value: 'KI' }, filterMatchers: ['Kiribati', 'Australian dollar', 'AUD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Kuwait', value: 'KW' }, filterMatchers: ['Kuwait', 'Kuwaiti dinar', 'KWD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Kyrgyzstan', value: 'KG' }, filterMatchers: ['Kyrgyzstan', 'Kyrgyzstani som', 'KGS', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Laos', value: 'LA' }, filterMatchers: ['Laos', 'Lao kip', 'LAK', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Latvia', value: 'LV' }, filterMatchers: ['Latvia', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Lebanon', value: 'LB' }, filterMatchers: ['Lebanon', 'Lebanese pound', 'LBP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Lesotho', value: 'LS' }, filterMatchers: ['Lesotho', 'Lesotho loti', 'LSL', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Liberia', value: 'LR' }, filterMatchers: ['Liberia', 'Liberian dollar', 'LRD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Libya', value: 'LY' }, filterMatchers: ['Libya', 'Libyan dinar', 'LYD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Liechtenstein', value: 'LI' }, filterMatchers: ['Liechtenstein', 'Swiss franc', 'CHF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Lithuania', value: 'LT' }, filterMatchers: ['Lithuania', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Luxembourg', value: 'LU' }, filterMatchers: ['Luxembourg', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Madagascar', value: 'MG' }, filterMatchers: ['Madagascar', 'Malagasy ariary', 'MGA', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Malawi', value: 'MW' }, filterMatchers: ['Malawi', 'Malawian kwacha', 'MWK', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Malaysia', value: 'MY' }, filterMatchers: ['Malaysia', 'Malaysian ringgit', 'MYR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Maldives', value: 'MV' }, filterMatchers: ['Maldives', 'Maldivian rufiyaa', 'MVR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Mali', value: 'ML' }, filterMatchers: [ 'Mali', 'West African CFA franc', 'XOF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Malta', value: 'MT' }, filterMatchers: ['Malta', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Marshall Islands', value: 'MH' }, filterMatchers: ['Marshall Islands', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Mauritania', value: 'MR' }, filterMatchers: [ 'Mauritania', 'Mauritanian ouguiya', 'MRU', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Mauritius', value: 'MU' }, filterMatchers: ['Mauritius', 'Mauritian rupee', 'MUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Mexico', value: 'MX' }, filterMatchers: ['Mexico', 'Mexican peso', 'MXN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Micronesia', value: 'FM' }, filterMatchers: ['Micronesia', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Moldova', value: 'MD' }, filterMatchers: ['Moldova', 'Moldovan leu', 'MDL', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Monaco', value: 'MC' }, filterMatchers: ['Monaco', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Mongolia', value: 'MN' }, filterMatchers: ['Mongolia', 'Mongolian tögrög', 'MNT', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Montenegro', value: 'ME' }, filterMatchers: ['Montenegro', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Morocco', value: 'MA' }, filterMatchers: ['Morocco', 'Moroccan dirham', 'MAD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Mozambique', value: 'MZ' }, filterMatchers: [ 'Mozambique', 'Mozambican metical', 'MZN', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Myanmar', value: 'MM' }, filterMatchers: ['Myanmar', 'Burmese kyat', 'MMK', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Namibia', value: 'NA' }, filterMatchers: ['Namibia', 'Namibian dollar', 'NAD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Nauru', value: 'NR' }, filterMatchers: ['Nauru', 'Australian dollar', 'AUD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Nepal', value: 'NP' }, filterMatchers: ['Nepal', 'Nepalese rupee', 'NPR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Netherlands', value: 'NL' }, filterMatchers: ['Netherlands', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'New Zealand', value: 'NZ' }, filterMatchers: [ 'New Zealand', 'New Zealand dollar', 'NZD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Nicaragua', value: 'NI' }, filterMatchers: [ 'Nicaragua', 'Nicaraguan córdoba', 'NIO', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Niger', value: 'NE' }, filterMatchers: [ 'Niger', 'West African CFA franc', 'XOF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Nigeria', value: 'NG' }, filterMatchers: ['Nigeria', 'Nigerian naira', 'NGN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'North Macedonia', value: 'MK' }, filterMatchers: [ 'North Macedonia', 'Macedonian denar', 'MKD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Norway', value: 'NO' }, filterMatchers: ['Norway', 'Norwegian krone', 'NOK', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Oman', value: 'OM' }, filterMatchers: ['Oman', 'Omani rial', 'OMR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Pakistan', value: 'PK' }, filterMatchers: ['Pakistan', 'Pakistani rupee', 'PKR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Palau', value: 'PW' }, filterMatchers: ['Palau', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Panama', value: 'PA' }, filterMatchers: ['Panama', 'Panamanian balboa', 'PAB', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Papua New Guinea', value: 'PG' }, filterMatchers: [ 'Papua New Guinea', 'Papua New Guinean kina', 'PGK', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Paraguay', value: 'PY' }, filterMatchers: [ 'Paraguay', 'Paraguayan guaraní', 'PYG', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Peru', value: 'PE' }, filterMatchers: ['Peru', 'Peruvian sol', 'PEN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Philippines', value: 'PH' }, filterMatchers: [ 'Philippines', 'Philippine peso', 'PHP', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Poland', value: 'PL' }, filterMatchers: ['Poland', 'Polish złoty', 'PLN', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Portugal', value: 'PT' }, filterMatchers: ['Portugal', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Qatar', value: 'QA' }, filterMatchers: ['Qatar', 'Qatari riyal', 'QAR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Romania', value: 'RO' }, filterMatchers: ['Romania', 'Romanian leu', 'RON', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Russia', value: 'RU' }, filterMatchers: ['Russia', 'Russian ruble', 'RUB', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Rwanda', value: 'RW' }, filterMatchers: ['Rwanda', 'Rwandan franc', 'RWF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Saint Lucia', value: 'LC' }, filterMatchers: [ 'Saint Lucia', 'East Caribbean dollar', 'XCD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Samoa', value: 'WS' }, filterMatchers: ['Samoa', 'Samoan tālā', 'WST', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'San Marino', value: 'SM' }, filterMatchers: ['San Marino', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Saudi Arabia', value: 'SA' }, filterMatchers: ['Saudi Arabia', 'Saudi riyal', 'SAR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Senegal', value: 'SN' }, filterMatchers: [ 'Senegal', 'West African CFA franc', 'XOF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Serbia', value: 'RS' }, filterMatchers: ['Serbia', 'Serbian dinar', 'RSD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Seychelles', value: 'SC' }, filterMatchers: [ 'Seychelles', 'Seychellois rupee', 'SCR', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Sierra Leone', value: 'SL' }, filterMatchers: [ 'Sierra Leone', 'Sierra Leonean leone', 'SLL', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Singapore', value: 'SG' }, filterMatchers: ['Singapore', 'Singapore dollar', 'SGD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Slovakia', value: 'SK' }, filterMatchers: ['Slovakia', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Slovenia', value: 'SI' }, filterMatchers: ['Slovenia', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Solomon Islands', value: 'SB' }, filterMatchers: [ 'Solomon Islands', 'Solomon Islands dollar', 'SBD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Somalia', value: 'SO' }, filterMatchers: ['Somalia', 'Somali shilling', 'SOS', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'South Africa', value: 'ZA' }, filterMatchers: [ 'South Africa', 'South African rand', 'ZAR', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'South Korea', value: 'KR' }, filterMatchers: ['South Korea', 'Korean won', 'KRW', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'South Sudan', value: 'SS' }, filterMatchers: [ 'South Sudan', 'South Sudanese pound', 'SSP', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Spain', value: 'ES' }, filterMatchers: ['Spain', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Sri Lanka', value: 'LK' }, filterMatchers: ['Sri Lanka', 'Sri Lankan rupee', 'LKR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Sudan', value: 'SD' }, filterMatchers: ['Sudan', 'Sudanese pound', 'SDG', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Suriname', value: 'SR' }, filterMatchers: ['Suriname', 'Surinamese dollar', 'SRD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Sweden', value: 'SE' }, filterMatchers: ['Sweden', 'Swedish krona', 'SEK', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Switzerland', value: 'CH' }, filterMatchers: ['Switzerland', 'Swiss franc', 'CHF', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Syria', value: 'SY' }, filterMatchers: ['Syria', 'Syrian pound', 'SYP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Taiwan', value: 'TW' }, filterMatchers: ['Taiwan', 'New Taiwan dollar', 'TWD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Tajikistan', value: 'TJ' }, filterMatchers: [ 'Tajikistan', 'Tajikistani somoni', 'TJS', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Tanzania', value: 'TZ' }, filterMatchers: [ 'Tanzania', 'Tanzanian shilling', 'TZS', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Thailand', value: 'TH' }, filterMatchers: ['Thailand', 'Thai baht', 'THB', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Timor-Leste', value: 'TL' }, filterMatchers: ['Timor-Leste', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Togo', value: 'TG' }, filterMatchers: [ 'Togo', 'West African CFA franc', 'XOF', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Tonga', value: 'TO' }, filterMatchers: ['Tonga', 'Tongan paʻanga', 'TOP', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Trinidad and Tobago', value: 'TT' }, filterMatchers: [ 'Trinidad and Tobago', 'Trinidad and Tobago dollar', 'TTD', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Tunisia', value: 'TN' }, filterMatchers: ['Tunisia', 'Tunisian dinar', 'TND', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Turkey', value: 'TR' }, filterMatchers: ['Turkey', 'Turkish lira', 'TRY', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Turkmenistan', value: 'TM' }, filterMatchers: [ 'Turkmenistan', 'Turkmenistan manat', 'TMT', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Tuvalu', value: 'TV' }, filterMatchers: ['Tuvalu', 'Australian dollar', 'AUD', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Uganda', value: 'UG' }, filterMatchers: ['Uganda', 'Ugandan shilling', 'UGX', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Ukraine', value: 'UA' }, filterMatchers: ['Ukraine', 'Ukrainian hryvnia', 'UAH', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'United Arab Emirates', value: 'AE' }, filterMatchers: [ 'United Arab Emirates', 'UAE', 'UAE dirham', 'AED', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Uruguay', value: 'UY' }, filterMatchers: ['Uruguay', 'Uruguayan peso', 'UYU', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Uzbekistan', value: 'UZ' }, filterMatchers: [ 'Uzbekistan', 'Uzbekistani soʻm', 'UZS', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Vanuatu', value: 'VU' }, filterMatchers: ['Vanuatu', 'Vanuatu vatu', 'VUV', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Vatican City', value: 'VA' }, filterMatchers: ['Vatican City', 'Euro', 'EUR', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Venezuela', value: 'VE' }, filterMatchers: [ 'Venezuela', 'Venezuelan bolívar', 'VES', 'United States dollar', 'USD', ], }, { type: 'option', value: { title: 'Vietnam', value: 'VN' }, filterMatchers: ['Vietnam', 'Vietnamese đồng', 'VND', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Yemen', value: 'YE' }, filterMatchers: ['Yemen', 'Yemeni rial', 'YER', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Zambia', value: 'ZM' }, filterMatchers: ['Zambia', 'Zambian kwacha', 'ZMW', 'United States dollar', 'USD'], }, { type: 'option', value: { title: 'Zimbabwe', value: 'ZW' }, filterMatchers: ['Zimbabwe', 'Zimbabwean dollar', 'ZWL', 'United States dollar', 'USD'], }, ], }, ], placeholder: 'Select a country', filterable: true, filterPlaceholder: 'Search countries...', sortFilteredOptions: sortByRelevance((value) => value.title), size: 'lg', renderValue: (value) => value.title, compareValues: (a, b) => a?.value === b?.value, } satisfies Story<{ title: string; value: string }>['args'], /** * Test steps: * 1. Open the dropdown * 2. Search for "united" * 3. Expected: List should stay at the top showing United States, United Kingdom, United Arab Emirates first * 4. Bug (if present): List would scroll to the bottom automatically * * Note: Kazakhstan is the only country without USD in metadata - searching for it should * show it's properly filtered out when searching "united". */ play: async ({ canvasElement }) => { const canvas = within(canvasElement); const combobox = canvas.getByRole('combobox'); await userEvent.click(combobox); const searchInput = screen.getByPlaceholderText('Search countries...'); await userEvent.type(searchInput, 'united'); }, }; const wait = async (duration = 500) => new Promise((resolve) => { setTimeout(resolve, duration); }); const currencyInteractionArgs = { items: [ { type: 'group', label: 'Popular currencies', options: testPopularCurrencies.map(testCurrencyOption), }, { type: 'group', label: 'All currencies', options: testAllCurrencies.map(testCurrencyOption), }, ], defaultValue: testPopularCurrencies[0], renderValue: (currency: TestCurrency) => ( ), renderFooter: ({ resultsEmpty, queryNormalized, }: { resultsEmpty: boolean; queryNormalized: string | null | undefined; }) => resultsEmpty && queryNormalized != null && /^[a-z]{3}$/u.test(queryNormalized) ? ( <> It is not possible to use {queryNormalized.toUpperCase()} yet.{' '} e.preventDefault()}> Email me when it is available. ) : ( <> Cannot find it?{' '} e.preventDefault()}> Request the currency you need, {' '} and we will notify you once it is available. ), filterable: true, filterPlaceholder: 'Type a currency / country', size: 'lg', } satisfies Story['args']; export const BasicInteraction: Story = { args: { placeholder: 'Month', items: testMonths.map((month) => ({ type: 'option', value: month })), renderValue: (month) => , }, render: function Render({ onChange, onClear, ...args }) { const [selectedMonth, setSelectedMonth] = useState(null); return ( { setSelectedMonth(month); onChange?.(month); }} onClear={() => { setSelectedMonth(null); onClear?.(); }} /> ); }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('renders placeholder', async () => { const triggerButton = canvas.getByRole('combobox'); await waitFor(async () => expect(triggerButton).toHaveTextContent('Month')); }); await step('selects option via mouse', async () => { const triggerButton = canvas.getByRole('combobox'); await userEvent.click(triggerButton); await userEvent.unhover(triggerButton); const option = within(screen.getByRole('listbox')).getByRole('option', { name: 'May' }); await userEvent.click(option); await waitFor(async () => expect(triggerButton).toHaveTextContent('May')); }); }, }; export const KeyboardInteraction: Story = { args: currencyInteractionArgs, play: async ({ step }) => { await step('Tab to the combobox', async () => { await userEvent.tab(); }); await step('Open the combobox', async () => { await userEvent.keyboard(' '); }); await step('Check if options are displayed', async () => { await waitFor(async () => expect(await screen.findAllByRole('option')).toHaveLength(9)); await waitFor(async () => expect(screen.getByText(/^Cannot find it\?/u)).toBeInTheDocument()); }); await step('Type "huf" in the combobox', async () => { // this is workaround for Chromatic: Firefox and Edge tests failing. // Unclear on a proper solution await wait(750); const input = await screen.findByRole('combobox'); await userEvent.type(input, 'huf'); }); await step('Check if no options are displayed', async () => { // this is workaround for Chromatic: Firefox and Edge tests failing. // Unclear on a proper solution await wait(750); await expect(await screen.findByText(/^No results found/u)).toBeInTheDocument(); await expect( await screen.findByText(/^It is not possible to use HUF yet\./u), ).toBeInTheDocument(); await waitFor( async () => { await expect(screen.queryAllByRole('option')).toHaveLength(0); }, { timeout: 1500 }, ); }); await step('Remove last two characters', async () => { await wait(500); const input = await screen.findByRole('combobox'); await userEvent.type(input, '{Backspace}{Backspace}'); }); await step('Check if two options are displayed', async () => { await waitFor(async () => expect(within(screen.getByRole('listbox')).queryAllByRole('option')).toHaveLength(2), ); }); await step('Type "e" in the combobox', async () => { const input = await screen.findByRole('combobox'); await userEvent.type(input, '{Backspace}e'); }); await step('Check if aria-activedescendant is set', async () => { const input = screen.getByRole('combobox'); await waitFor(async () => expect(input).toHaveAttribute('aria-activedescendant')); }); }, }; export const MultiSelectInteraction: Story = { args: { ...currencyInteractionArgs, multiple: true, placeholder: 'Choose currencies…', defaultValue: [testPopularCurrencies[0]], renderValue: (currency, withinTrigger) => withinTrigger ? ( currency.code ) : ( ), }, play: async ({ canvasElement, step, args }) => { const canvas = within(canvasElement); await step('Open the combobox', async () => { const triggerButton = canvas.getByRole('combobox'); await userEvent.click(triggerButton); await wait(500); await userEvent.unhover(triggerButton); await expect(args.onOpen).toHaveBeenCalledOnce(); }); await step('Select EUR option', async () => { const option = within(screen.getByRole('listbox')).getAllByRole('option', { name: /^EUR/u, })[0]; await wait(500); await userEvent.click(option); }); await step('Check if selected options are displayed in trigger', async () => { const triggerButton = document.querySelector('button[role="combobox"]'); await waitFor(async () => expect(triggerButton).toHaveTextContent('USD, EUR')); }); }, }; /** * Verifies that currencies with non-ASCII characters (e.g. ÅLD / Ålandian peso) * render correctly in the dropdown and can be found via filter. */ export const SpecialCharacterOptions: Story = { args: { ...currencyInteractionArgs, filterable: true, filterPlaceholder: 'Type a currency', }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Open the combobox', async () => { const triggerButton = canvas.getByRole('combobox'); await userEvent.click(triggerButton); await userEvent.unhover(triggerButton); }); await step('ÅLD option is visible in dropdown', async () => { await waitFor(async () => expect( within(screen.getByRole('listbox')).getByRole('option', { name: /ÅLD/u }), ).toBeInTheDocument(), ); }); await step('Filter for Ålandian peso', async () => { await wait(500); const input = await screen.findByRole('combobox'); await userEvent.type(input, 'åla'); }); await step('Only ÅLD option remains', async () => { await waitFor(async () => expect(within(screen.getByRole('listbox')).getAllByRole('option')).toHaveLength(1), ); }); }, }; export const WithClearInteraction: Story = { args: { ...currencyInteractionArgs, onClear: fn() satisfies Mock, }, play: async ({ step }) => { await step('Has clear button', async () => { const clearBtn = await screen.findByRole('button', { name: 'Clear' }); await expect(clearBtn).toBeInTheDocument(); }); }, }; /** * This test ensures that when the SelectInput is used within a scrollable page, * opening the dropdown does not cause any unwanted scrolling or layout shifts. * Expected preview should start with green bar at the top, with yellow section * not in the viewport. The issue is particularly prominent on iOS Safari. * * NB: This test is disabled in Chromatic as there's no obvious way to control * element of a snapshot. It's to be primarily used in manual testing * on an actual device or a simulator as it cannot be reproduced with mobile * emulation modes on desktop browsers. */ export const SmoothScrollReset: Story = { args: { items: Array.from({ length: 15 }).map((_, id) => ({ type: 'option', value: `option ${id + 1}`, })), placeholder: 'Select option', }, decorators: [ (Story) => ( <>
This block should not be in the viewport.
{lorem500}
), ], play: async ({ canvasElement }) => { document.documentElement.scrollTop = 400; await wait(); const canvas = within(canvasElement); // cannot use userEvent.click as it crashes on iOS Safari in the simulator await fireEvent.click(canvas.getByRole('combobox')); }, globals: { viewport: { value: allModes.largeMobile.viewport, isRotated: false }, }, parameters: { chromatic: { disableSnapshot: true, }, }, }; interface OptionWithDescription { id: string; title: string; description?: string; } const optionsWithDescription: OptionWithDescription[] = [ { id: '1', title: 'Option One', description: 'This is the first option' }, { id: '2', title: 'Option Two (no description)' }, { id: '3', title: 'Option Three', description: 'This is the third option' }, ]; /** * Test story to verify that SelectInput correctly displays an item with a description * when shown in the trigger button. The description should be visible in the trigger * after selection. */ export const TriggerWithDescription: Story = { args: { items: optionsWithDescription.map((option) => ({ type: 'option', value: option, })), defaultValue: optionsWithDescription[0], placeholder: 'Select an option', renderValue: (option, withinTrigger) => ( ), }, render: function Render({ onChange, ...args }) { const [selectedOption, setSelectedOption] = useState( optionsWithDescription[0], ); return ( { setSelectedOption(option); onChange?.(option); }} /> ); }, }; /** Basic months dropdown at 400% zoom for accessibility testing. */ export const Zoom400: Story = { render: function Render({ onChange, onClear, ...args }) { const triggerRef = useRef(null); const [selectedMonth, setSelectedMonth] = useState(null); useEffect(() => { const timer = setTimeout(() => { triggerRef.current?.click(); }, 200); return () => clearTimeout(timer); }, []); return ( { setSelectedMonth(month); onChange?.(month); }} onClear={() => { setSelectedMonth(null); onClear?.(); }} /> ); }, args: { placeholder: 'Month', items: testMonths.map((month) => ({ type: 'option', value: month, })), renderValue: (month) => , }, ...withVariantConfig(['400%']), };