big-design icon indicating copy to clipboard operation
big-design copied to clipboard

feat(component): create base combobox functionality

Open davelinke opened this issue 5 months ago • 3 comments
trafficstars

What?

Created a ComboBox component to add diverse usage than our current select component

Why?

There are other functionalities such as being able to page the results in a selection or adding the ability to first search through a set to then provide the combobox options that shouldn't be added to the current select component.

Screenshots/Screen Recordings

combobox

Testing/Proof

dev.tsx code
import { ComboBox, Form, FormGroup, Box } from '@bigcommerce/big-design';
import React, { FunctionComponent, useState } from 'react';

const allCountries = [
  "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria",
  "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan",
  "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia",
  "Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China", "Colombia", "Comoros", "Congo (Congo-Brazzaville)", "Costa Rica",
  "Croatia", "Cuba", "Cyprus", "Czechia (Czech Republic)", "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador",
  "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini (fmr. " + "Swaziland)", "Ethiopia", "Fiji", "Finland", "France",
  "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau",
  "Guyana", "Haiti", "Holy See", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq",
  "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait",
  "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
  "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico",
  "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar (formerly Burma)", "Namibia", "Nauru",
  "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman",
  "Pakistan", "Palau", "Palestine State", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal",
  "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe",
  "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia",
  "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria",
  "Tajikistan", "Tanzania", "Thailand", "Timor-Leste", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan",
  "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States of America", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela",
  "Vietnam", "Yemen", "Zambia", "Zimbabwe"
];

const ComboBoxTest: FunctionComponent = () => {
  // First ComboBox (paged)
  const [value, setValue] = useState('mx');
  const handleChange = (val) => setValue(val);
  const [optionsLoading, setOptionsLoading] = useState(false);

  type Option = { value: string; content: string };

  const generateOptions = (start: number, num: number): Option[] => {
    return allCountries.slice(start, start + num).map((country) => ({
      value: country.toLowerCase().replace(/[^a-z]/g, '-'),
      content: country,
    }));
  };

  const [selectOptions, setSelectOptions] = useState<Option[]>(generateOptions(0, 9));

  const expandOptions = async () => {
    setOptionsLoading(true);
    // Simulate async option fetch
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setSelectOptions((prevOptions) => {
      const start = prevOptions.length;
      const newOptions = generateOptions(start, 9);
      return [...prevOptions, ...newOptions];
    });
    setOptionsLoading(false);
  };

  const hasMoreOptions = selectOptions.length < allCountries.length;

  // Second ComboBox (search after 3 letters)
  const [searchValue, setSearchValue] = useState('');
  const [searchOptions, setSearchOptions] = useState<Option[]>([]);
  const [searchLoading, setSearchLoading] = useState(false);

  const handleSearchInputChange = async (input: string) => {
    setSearchValue(input);

    if (input.length < 2) {
      setSearchOptions([]);
      return;
    }

    setSearchLoading(true);

    // Simulate async fetch
    await new Promise((resolve) => setTimeout(resolve, 500));

    const filtered = allCountries
      .filter((country) => country.toLowerCase().includes(input.toLowerCase()))
      .map((country) => ({
        value: country.toLowerCase().replace(/[^a-z]/g, '-'),
        content: country,
      }));

    setSearchOptions(filtered);
    setSearchLoading(false);
  };

  const handleSearchOptionChange = (val) => setSearchValue(val);

  return (
    <Box padding={'xxLarge'}>
      <Form>
        <FormGroup>
          <ComboBox
            filterable={true}
            label="Countries (Paged)"
            maxHeight={300}
            onOptionChange={handleChange}
            options={selectOptions}
            placeholder="Choose country"
            placement="bottom-start"
            required
            value={value}
            onScrollToBottom={hasMoreOptions ? expandOptions : undefined}
            optionsLoading={optionsLoading}
          />
        </FormGroup>
        <FormGroup>
          <ComboBox
            filterable={true}
            label="Countries (Search after 2 letters)"
            maxHeight={300}
            onOptionChange={handleSearchOptionChange}
            options={searchOptions}
            placeholder="Type at least 2 letters"
            placement="bottom-start"
            required
            value={searchValue}
            optionsLoading={searchLoading}
            onInputChange={handleSearchInputChange}
          />
        </FormGroup>
      </Form>
    </Box>
  );
};

export default ComboBoxTest;

davelinke avatar Jun 13 '25 20:06 davelinke