diff --git a/.changeset/stale-mails-do.md b/.changeset/stale-mails-do.md new file mode 100644 index 000000000..897507767 --- /dev/null +++ b/.changeset/stale-mails-do.md @@ -0,0 +1,369 @@ +--- +'@qwik-ui/headless': minor +--- + +# Combobox v2, New Dropdown Component, and Progress bar reaches beta! + +0.5 continues our move towards a 1.0 release. It includes a few breaking changes to the Combobox in order to make sure that the components have a clear API. + +Below is a migration guide of API's for the Combobox. + +## Combobox + +The combobox has been refactored from the ground up, including new features, components, and QOL updates. + +### Anatomy changes + +The new Combobox anatomy is as follows: + +```tsx +import { component$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck } from '@qwikest/icons/lucide'; + +export default component$(() => { + return ( + + label + + + + trigger + + + + + item label + + + + + + + ); +}); +``` + +### Anatomy Changes + +1. **Combobox.Option** has been renamed to **Combobox.Item**: + + - The item is no longer restricted to a string value; any UI can be placed inside the item. + - Use the `Combobox.ItemLabel` component to display the item's label, which becomes the item's value if no `value` prop is passed to the `Combobox.Item`. (required) + +2. **Combobox.Listbox** has been deprecated. + +3. **Combobox.ItemLabel** has been added: + + - Move the string value that was once inside `Combobox.Option` into `Combobox.ItemLabel`. (required) + +4. **Combobox.ItemIndicator** has been added: + + - This component is used to render UI based on the selected state of the item. (optional) + +5. **Combobox.Description** has been added: + + - The text rendered inside the description component is displayed to screen readers as an accessible description of the combobox. (optional) + +6. **Combobox.ErrorMessage** has been added: + + - When this component is rendered, the Combobox will be marked as invalid. (optional) + +7. **Combobox.HiddenNativeSelect** has been added: + + - A native select element allows the submission of forms with the combobox. This component is visually hidden and hidden from screen readers. (optional) + +8. **Combobox.Group** has been added: + + - Used to visually group related items together. (optional) + +9. **Combobox.GroupLabel** has been added: + + - Provides an accessible name for the group. (optional) + +10. **Combobox.Empty** has been added: + - Displays a message when there are no items to display. + - Previously, an empty popup was displayed when the combobox was empty. The new default behavior is to close the popup unless the `Combobox.Empty` component is rendered. (optional) + +### API Changes + +#### Rendering Items (required) + +The `optionRenderer$` prop on the `Combobox.Listbox` component has been deprecated. + +Instead: + +1. pass a `` as a child of the `` component. +2. pass a `Combobox.ItemLabel` as a child of the `` component. + +It should look something like this: + +```tsx + + + item label + {/* other content */} + + +``` + +You are now in full control of how the item is rendered. Because you control the rendering of the item, there is no need for the previous API's including the data's key values. + +> `optionDisabledKey`, `optionValueKey`, and `optionLabelKey` props have been removed. + +There is also no need to pass an `index` prop to the `Combobox.Item` component. It is handled automatically. + +#### Pass in distinct values + +The `value` prop has been added to the `Combobox.Item` component to allow for passing in a distinct value for the combobox. + +For example, identifying a user by their ID, rather than the display username. + +#### Add your own filter + +Filters are an important part of the combobox. It was a design decision in this refactor to make filtering data as easy as possible to integrate with other tools and libraries. + +The `filter$` prop has been replaced. Instead, items are by default filtered by the `includes` function. + +To opt-out of the default filter, add the `filter={false}` prop to the `Combobox.Root` component, which will disable the default filter. + +```tsx +import { component$, useSignal, useStyles$, useTask$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; +import { matchSorter } from 'match-sorter'; + +export default component$(() => { + useStyles$(styles); + + const inputValue = useSignal(''); + const filteredItems = useSignal([]); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + useTask$(({ track }) => { + track(() => inputValue.value); + + filteredItems.value = matchSorter(fruits, inputValue.value); + }); + + return ( + + Fruits + + + + + + + + {filteredItems.value.map((fruit) => ( + + {fruit} + + + + + ))} + + + ); +}); +``` + +The above example uses the `matchSorter` function from the `match-sorter` library to filter the items. + +#### `bind:value` instead of `bind:selectedIndex` + +bind:value has been added in favor of what was previously used to reactively control the combobox, bind:selectedIndex. + +> This change was needed to create a more consistent API across components, but also keeping the state in the case of custom filters. + +`onChange$` has been added to the `Combobox.Root` component so that you can listen to when the selected value changes. + +#### Add initial values to the combobox + +The `value` prop has been added to the `Combobox.Root` component to select the initial value of the combobox when it is rendered. + +> `defaultLabel` has been removed, as it does not reflect the selected state of the combobox. + +#### Input state management + +`bind:inputValue` (on the Root) has been replaced by using the `bind:value` prop on the `` component instead. + +You can also now listen to when the input value changes by using the `onInput$` prop on the `` component. + +#### Passing refs to the combobox + +The combobox is the first component to support passing refs! You can now pass a ref of your own to any component inside the combobox. + +```tsx +const inputRef = useSignal(); + + + +``` + +#### Multiple selections + +You can now select multiple items by passing the `multiple` prop to the `` component. + +#### removeOnBackspace + +When in multiple selection mode, and the `removeOnBackspace` prop has been added to the `Combobox.Root` component, selected items can be removed by pressing the backspace key. (when the input is empty) + +#### Managing display values + +`bind:displayValue` has been added to the `Combobox.Root` component to allow for grabbing the updated display values of the combobox. + +> This allows for full control over each display item. For example, a couple of display values shown as pills. + +#### Item indicators + +The item indicator shows when the item is selected. Inside can be the UI of choice. + +#### `bind:open` instead of `bind:isListboxOpen` + +bind:open has been added to control the open state of the listbox, replacing bind:isListboxOpen. + +`onOpenChange$` has been added to the `Combobox.Root` component so that you can listen to when the popup opens or closes. + +#### Focus State Management + +bind:isInputFocused has been deprecated. Instead, if you decide to manage focus state using event handlers like onFocus$ and onBlur$. OR pass a ref to the `Combobox.Input` component. + +#### Placeholders + +The placeholder prop has been added to the `Combobox.Root` component to allow for a custom placeholder. + +#### Environment + +Like many of the latest components in Qwik UI, each function of the Combobox has been optimized to run in both SSR or CSR automatically depending on the environment. + +#### Looping + +Looping is now opt-in by default. To enable looping, add the `loop` prop to the `Combobox.Root` component. + +#### Scrolling + +When a scrollbar is present, the combobox will now properly scroll to the selected item. The scroll behavior can be customized using the `scrollOptions` prop on the `Combobox.Root` component. + +#### Forms + +The Combobox now supports form submissions. To enable this: + +1. Add the `name` prop to the `Combobox.Root` component, with the name of the Combobox form field. + +2. Add the `` component inside of the `` component. + +#### Validation + +The Combobox now supports validation. It was a design decision to make validation as easy as possible to integrate with other tools and libraries, such as Modular Forms. + +A component is invalid when the `Combobox.ErrorMessage` component is rendered. This component provides an accessible description of the error to assistive technologies. + +### Floating / Top layer items + +The props previously on the `Combobox.Listbox`, have been moved to the `Combobox.Popover` component to be more consistent with the rest of the Qwik UI components. + +`placement` has been deprecated in favor of `floating`, which can be a boolean or the direction of the floating element. + +`autoPlacement` has been removed, as `flip` should be used instead. + +Ex: `floating={true}` or `floating="top"` + +### Keyboard interactions + +Key +Description + +| Key | Description | +| --------- | ---------------------------------------------------- | +| Enter | Selects a highlighted item when open. | +| ArrowDown | Opens the combobox or moves focus down. | +| ArrowUp | Opens the combobox or moves focus up. | +| Home | When focus is on an item, moves focus to first item. | +| End | When focus is on an item, moves focus to last item. | +| Esc | Closes the combobox and moves focus to the trigger. | +| Tab | Moves focus to the next focusable element. | + +The Enter key will toggle the selection of the highlighted item without closing the combobox if an item is already selected, otherwise it will close the popup. + +### Multi Select + +When in multi select mode, additional keyboard interactions are available. + +| Key | Description | +| ----- | --------------------------------------------------------------------------- | +| Enter | Toggles the selection of the highlighted item without closing the combobox. | + +### Data Attributes + +- `data-invalid` is added to the combobox when the combobox is invalid. +- `data-open` is added to the combobox when the combobox is open. +- `data-closed` is added to the combobox when the combobox is closed. +- `data-highlighted` is added to the combobox item when the item is highlighted. +- `data-selected` is added to the combobox item when the item is selected. +- `data-disabled` is added to the combobox item when the item is disabled. + +### Accessibility + +Announcements to the Combobox are more consistent and follow the [WAI-ARIA Combobox design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/). + +So far, the Combobox has been tested with VoiceOver, Axe, and NVDA. + +## Select + +The select component also includes some improvments + +### Accessibility + +Announcements to the Select are more consistent and follow the [WAI-ARIA Listbox design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/). + +So far, the Select has been tested with VoiceOver, Axe, and NVDA. + +## Dropdown + +A new component has been added to Qwik UI, the Dropdown. It is currently in a draft state, and is not yet ready for production use. We will be working on it more deeply in the near future. + +### Anatomy + +Here is the initial API: + +```tsx +import { component$ } from '@builder.io/qwik'; +import { Dropdown } from '@qwik-ui/headless'; +export default component$(() => { +return ( + + + Open Dropdown + + + + + + + Group 1 + + + + + + +``` + +Feel free to play around with it! Feedback is very appreciated. + +## Progress Bar + +The progress bar has been around for a while, it has finally reached a **beta state**, make sure to open an issue on the [Qwik UI repo](https://github.com/qwikifiers/qwik-ui/issues) if you run into any problems. diff --git a/apps/website/adapters/cloudflare-pages/vite.config.ts b/apps/website/adapters/cloudflare-pages/vite.config.ts index 7a8c8e541..8f3036425 100644 --- a/apps/website/adapters/cloudflare-pages/vite.config.ts +++ b/apps/website/adapters/cloudflare-pages/vite.config.ts @@ -14,7 +14,7 @@ export default extendConfig(baseConfig, () => { cloudflarePagesAdapter({ ssg: { include: ['/*'], - origin: 'https://qwikui.com' + origin: 'https://qwikui.com', }, }), ], diff --git a/apps/website/src/components/feature-list/feature-list.tsx b/apps/website/src/components/feature-list/feature-list.tsx index 14048627b..87bce4830 100644 --- a/apps/website/src/components/feature-list/feature-list.tsx +++ b/apps/website/src/components/feature-list/feature-list.tsx @@ -2,6 +2,7 @@ import { component$ } from '@builder.io/qwik'; import { CheckIcon } from '../icons/Check'; import { Roadmap } from '../icons/Roadmap'; import { IssueIcon } from '../icons/Issues'; +import { Note } from '../note/note'; type FeatureListProps = { features: string[]; @@ -11,57 +12,69 @@ type FeatureListProps = { export const FeatureList = component$((props: FeatureListProps) => { return ( -
    - {props.features && ( - <> - {props.features.map((descriptor, index) => { - return ( -
  • - - {descriptor} -
  • - ); - })} - - )} - {props.roadmap && ( - <> -

    Roadmap

    - {props.roadmap.map((descriptor) => { - return ( - <> -
  • - - {descriptor} -
  • - - ); - })} - - )} - {props.issues && ( - <> - {props.issues.map((descriptor) => { - return ( - <> + <> +
      + {props.features && ( + <> + {props.features.map((descriptor, index) => { + return (
    • - + {descriptor}
    • - - ); - })} - + ); + })} + + )} + {props.roadmap && ( + <> +

      Roadmap

      + {props.roadmap.map((descriptor) => { + return ( + <> +
    • + + {descriptor} +
    • + + ); + })} + + )} + {props.issues && ( + <> + {props.issues.map((descriptor) => { + return ( + <> +
    • + + {descriptor} +
    • + + ); + })} + + )} +
    + {!props.issues && ( + + Missing a feature? Check out the{' '} + + contributing guide + {' '} + and we'd be happy to review any relevant issues or PR's. Feel free to work on + any of the features listed above. + )} -
+ ); }); diff --git a/apps/website/src/routes/(main)/contributing/index.mdx b/apps/website/src/routes/(main)/contributing/index.mdx index e5ab9ae9b..28ec1059a 100644 --- a/apps/website/src/routes/(main)/contributing/index.mdx +++ b/apps/website/src/routes/(main)/contributing/index.mdx @@ -94,7 +94,7 @@ We strongly recommend TDD development for the headless library, and we are curre Currently, the component testing integration for Qwik & Playwright is in development, and we are using e2e tests for the time being. That said, most tests should be very easy to migrate later on. -#### Getting started w/ testing +### Getting started w/ testing Here's an example way of getting a testid of the `Hero` select docs example in `index.mdx`, without affecting any visible markup. diff --git a/apps/website/src/routes/docs/headless/combobox/examples/add-items.tsx b/apps/website/src/routes/docs/headless/combobox/examples/add-items.tsx new file mode 100644 index 000000000..4c3c547cc --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/add-items.tsx @@ -0,0 +1,55 @@ +import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuChevronDown, LuCheck } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const fruits = useSignal([ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]); + const hasAddedFruits = useSignal(false); + + return ( + <> + + Fruits + + + + + + + + {fruits.value.map((fruit) => ( + + {fruit} + + + + + ))} + + + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/change.tsx b/apps/website/src/routes/docs/headless/combobox/examples/change.tsx new file mode 100644 index 000000000..d687da231 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/change.tsx @@ -0,0 +1,51 @@ +import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const count = useSignal(0); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + const handleChange$ = $(() => { + count.value++; + }); + + return ( + <> + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + +

You have changed {count.value} times

+ + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/context.tsx b/apps/website/src/routes/docs/headless/combobox/examples/context.tsx deleted file mode 100644 index 2df1e06a2..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/context.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; - -import { createContextId, useContext, useContextProvider } from '@builder.io/qwik'; - -// Create a context ID -export const AnimalContext = createContextId('animal-context'); - -export default component$(() => { - const animals = ['Armadillo', 'Donkey', 'Baboon', 'Badger', 'Barracuda', 'Bat', 'Bear']; - // Provide the animals array to the context under the context ID - useContextProvider(AnimalContext, animals); - - return ; -}); - -export const ContextChild = component$(() => { - const animals = useContext(AnimalContext); - - return ( - - Animals 🐖 - - - - - - - - ( - - - {option.label} - - - )} - /> - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/csr.tsx b/apps/website/src/routes/docs/headless/combobox/examples/csr.tsx new file mode 100644 index 000000000..abbd14ca0 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/csr.tsx @@ -0,0 +1,51 @@ +import { component$, useStyles$, useSignal } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const renderCombobox = useSignal(false); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + <> + + {renderCombobox.value && ( + + Personal Trainers + + + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + + )} + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/custom-filter.tsx b/apps/website/src/routes/docs/headless/combobox/examples/custom-filter.tsx deleted file mode 100644 index 68bbe98b3..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/custom-filter.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - type Countries = { - value: string; - label: string; - }; - - const objectExample: Array = [ - { value: 'usa', label: 'United States' }, - { value: 'canada', label: 'Canada' }, - { value: 'mexico', label: 'Mexico' }, - { value: 'brazil', label: 'Brazil' }, - { value: 'uk', label: 'United Kingdom' }, - { value: 'germany', label: 'Germany' }, - { value: 'france', label: 'France' }, - { value: 'italy', label: 'Italy' }, - ]; - - return ( - - options.filter(({ option }) => { - return option.label.toLowerCase().startsWith(value.toLowerCase()); - }) - } - options={objectExample} - class="combobox-root" - > - Countries 🚩 - - - - - - - - { - return ( - - {option.label} - - ); - }} - /> - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/custom-keys.tsx b/apps/website/src/routes/docs/headless/combobox/examples/custom-keys.tsx deleted file mode 100644 index 45f867ebd..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/custom-keys.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$, useSignal } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - type Pokemon = { - pokedex: string; - pokemon: string; - isPokemonCaught: boolean; - }; - - const pokemonExample: Array = [ - { pokedex: '1', pokemon: 'Bulbasaur', isPokemonCaught: true }, - { pokedex: '2', pokemon: 'Ivysaur', isPokemonCaught: false }, - { pokedex: '3', pokemon: 'Venusaur', isPokemonCaught: false }, - { pokedex: '4', pokemon: 'Charmander', isPokemonCaught: true }, - { pokedex: '5', pokemon: 'Charmeleon', isPokemonCaught: true }, - { pokedex: '6', pokemon: 'Charizard', isPokemonCaught: true }, - { pokedex: '7', pokemon: 'Squirtle', isPokemonCaught: false }, - { pokedex: '8', pokemon: 'Wartortle', isPokemonCaught: false }, - ]; - - const isPokemonCaught = useSignal(false); - - return ( -
- {isPokemonCaught.value && ( -

- You've already caught this pokemon! -

- )} - - Pokemon 🦏 - - - - - - - - { - const pokemonOption = option.option as Pokemon; - return ( - { - if (pokemonOption.isPokemonCaught) { - isPokemonCaught.value = true; - } - }} - onMouseLeave$={() => { - isPokemonCaught.value = false; - }} - > - {option.label} - - ); - }} - /> - - -
- ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/default-label.tsx b/apps/website/src/routes/docs/headless/combobox/examples/default-label.tsx deleted file mode 100644 index c787cdc42..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/default-label.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - const names = ['Jim', 'Joanna', 'John', 'Jessica']; - - return ( - - Names - - - - - - - - { - return ( - - {option.label} - - ); - }} - /> - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/disable-blur.tsx b/apps/website/src/routes/docs/headless/combobox/examples/disable-blur.tsx deleted file mode 100644 index c3a587f6e..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/disable-blur.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - const planets = [ - 'Mercury', - 'Venus', - 'Earth', - 'Mars', - 'Jupiter', - 'Saturn', - 'Uranus', - 'Neptune', - ]; - - return ( -
-

I have blur disabled! Inspect me in the dev tools.

- - Planets - - - - - - - - { - return ( - - {option.label} - - ); - }} - /> - - -
- ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/disabled.tsx b/apps/website/src/routes/docs/headless/combobox/examples/disabled.tsx index b24042572..dd0ff4736 100644 --- a/apps/website/src/routes/docs/headless/combobox/examples/disabled.tsx +++ b/apps/website/src/routes/docs/headless/combobox/examples/disabled.tsx @@ -1,56 +1,49 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; export default component$(() => { - type DisabledExample = { - value: string; - label: string; - myDisabledKey: boolean; - }; + useStyles$(styles); - const disabledExample: Array = [ - { value: '0', label: 'Enabled', myDisabledKey: false }, - { value: '1', label: 'Enabled', myDisabledKey: false }, - { value: '2', label: 'Disabled', myDisabledKey: true }, - { value: '3', label: 'Enabled', myDisabledKey: false }, - { value: '4', label: 'Disabled', myDisabledKey: true }, - { value: '5', label: 'Disabled', myDisabledKey: true }, - { value: '6', label: 'Disabled', myDisabledKey: true }, - { value: '7', label: 'Enabled', myDisabledKey: false }, + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', ]; return ( - - Disabled ⛔ + + Personal Trainers - + - { - return ( - - {option.label} - - ); - }} - /> + {fruits.map((fruit, index) => ( + + {fruit} + + + + + ))} ); }); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/empty.tsx b/apps/website/src/routes/docs/headless/combobox/examples/empty.tsx new file mode 100644 index 000000000..6862bcf3d --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/empty.tsx @@ -0,0 +1,44 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + No items found + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/filter.tsx b/apps/website/src/routes/docs/headless/combobox/examples/filter.tsx new file mode 100644 index 000000000..3f43389b4 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/filter.tsx @@ -0,0 +1,53 @@ +import { component$, useSignal, useStyles$, useTask$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; +import { matchSorter } from 'match-sorter'; + +export default component$(() => { + useStyles$(styles); + + const inputValue = useSignal(''); + const filteredItems = useSignal([]); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + useTask$(({ track }) => { + track(() => inputValue.value); + + filteredItems.value = matchSorter(fruits, inputValue.value); + }); + + return ( + + Personal Trainers + + + + + + + + {filteredItems.value.map((fruit) => ( + + {fruit} + + + + + ))} + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/form.tsx b/apps/website/src/routes/docs/headless/combobox/examples/form.tsx new file mode 100644 index 000000000..b359ab7c5 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/form.tsx @@ -0,0 +1,62 @@ +import { component$, useStyles$, $, useSignal } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const submittedData = useSignal(); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + const formName = 'combobox-form-name'; + + const handleSubmit$ = $((e: SubmitEvent) => { + const formData = new FormData(e.target as HTMLFormElement); + const selectedFruit = formData.get(formName) as string; + submittedData.value = selectedFruit ?? undefined; + }); + + return ( +
+ + Personal Trainers + + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + + + {submittedData.value && ( +
+ You submitted: + {JSON.stringify(submittedData.value)} +
+ )} +
+ ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/group.tsx b/apps/website/src/routes/docs/headless/combobox/examples/group.tsx new file mode 100644 index 000000000..7d8ce215b --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/group.tsx @@ -0,0 +1,42 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const activeUsers = ['Tim', 'Ryan', 'Jim', 'Abby']; + const offlineUsers = ['Joey', 'Bob', 'Jack', 'John']; + + return ( + + User count + + + + + + + + + Active + {activeUsers.map((user) => ( + + {user} + + ))} + + + Offline + {offlineUsers.map((user) => ( + + {user} + + ))} + + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/hero.tsx b/apps/website/src/routes/docs/headless/combobox/examples/hero.tsx index ac3c48cf7..088288d38 100644 --- a/apps/website/src/routes/docs/headless/combobox/examples/hero.tsx +++ b/apps/website/src/routes/docs/headless/combobox/examples/hero.tsx @@ -1,61 +1,39 @@ -import { Combobox, type ResolvedOption } from '@qwik-ui/headless'; -import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; export default component$(() => { useStyles$(styles); - const selectedOptionIndexSig = useSignal(-1); - const objectExample = [ - { value: 'alice', label: 'Alice' }, - { value: 'joana', label: 'Joana' }, - { value: 'malcolm', label: 'Malcolm' }, - { value: 'zack', label: 'Zack' }, - { value: 'brian', label: 'Brian' }, - { value: 'ryan', label: 'Ryan' }, - { value: 'joe', label: 'Joe' }, - { value: 'randy', label: 'Randy' }, - { value: 'david', label: 'David' }, - { value: 'joseph', label: 'Joseph' }, + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', ]; - type MyData = { - value: string; - label: string; - disabled: boolean; - }; - return ( - - Personal Trainers ⚡ + + Personal Trainers - + - { - const myData = option.option as MyData; - return ( - - {myData.label} - {selectedOptionIndexSig.value === index && } - - ); - }} - /> + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} ); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/hide.tsx b/apps/website/src/routes/docs/headless/combobox/examples/hide.tsx deleted file mode 100644 index b97ec928f..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/hide.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; - -export default component$(() => { - const fruits = [ - 'Apple', - 'Apricot', - 'Avocado 🥑', - 'Banana', - 'Bilberry', - 'Blackberry', - 'Blackcurrant', - 'Blueberry', - 'Boysenberry', - 'Currant', - 'Cherry', - 'Coconut', - 'Cranberry', - 'Cucumber', - ]; - - return ( - <> -

☝️ Scroll up and down with me open! 👇

- - options.filter(({ option }) => { - return option.toLowerCase().startsWith(value.toLowerCase()); - }) - } - > - - - - - - - - ( - - {option.label} - - )} - /> - - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/highlighted-index.tsx b/apps/website/src/routes/docs/headless/combobox/examples/highlighted-index.tsx deleted file mode 100644 index db022dc78..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/highlighted-index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$, useSignal } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - const highlightedIndexSig = useSignal(2); - - const highlightedExample = [ - 'not highlighted', - 'not highlighted', - 'highlighted by default!', - 'not highlighted', - ]; - - return ( - <> -

Third option highlighted! 🚨

- - Highlighted 🚨 - - - - - - - - { - return ( - - {option.label} - - ); - }} - /> - - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/indicator.tsx b/apps/website/src/routes/docs/headless/combobox/examples/indicator.tsx new file mode 100644 index 000000000..0b24481f4 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/indicator.tsx @@ -0,0 +1,45 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + + Personal Trainers + + + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/initial.tsx b/apps/website/src/routes/docs/headless/combobox/examples/initial.tsx new file mode 100644 index 000000000..000d682a8 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/initial.tsx @@ -0,0 +1,43 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/input.tsx b/apps/website/src/routes/docs/headless/combobox/examples/input.tsx new file mode 100644 index 000000000..17be9a029 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/input.tsx @@ -0,0 +1,56 @@ +import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const count = useSignal(0); + const inputValue = useSignal(''); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + const handleInput$ = $((value: string) => { + count.value++; + inputValue.value = value; + }); + + return ( + <> + +
onInput$ value: {inputValue.value}
+ Personal Trainers + + + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + +
+

onInput$ was called {count.value} time(s)

+ + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/item-value.tsx b/apps/website/src/routes/docs/headless/combobox/examples/item-value.tsx new file mode 100644 index 000000000..5cc0ff466 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/item-value.tsx @@ -0,0 +1,48 @@ +import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; + +export default component$(() => { + useStyles$(styles); + const users = [ + { id: '0', label: 'Tim' }, + { id: '1', label: 'Ryan' }, + { id: '2', label: 'Jim' }, + { id: '3', label: 'Jessie' }, + { id: '4', label: 'Abby' }, + ]; + + const selected = useSignal(null); + + const handleChange$ = $((value: string) => { + selected.value = value; + }); + + return ( + <> + + Personal Trainers + + + + + + + + {users.map((user) => ( + + {user.label} + + + + + ))} + + +

The selected value is: {selected.value ?? 'null'}

+ + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/loop.tsx b/apps/website/src/routes/docs/headless/combobox/examples/loop.tsx new file mode 100644 index 000000000..7fc4a7d56 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/loop.tsx @@ -0,0 +1,43 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/multiple.tsx b/apps/website/src/routes/docs/headless/combobox/examples/multiple.tsx new file mode 100644 index 000000000..1d89d101d --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/multiple.tsx @@ -0,0 +1,81 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown, LuX } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + const displayValues = useSignal([]); + const selected = useSignal([]); + + const inputRef = useSignal(); + + return ( + + Personal Trainers + +
+ {displayValues.value.map((item) => ( + + {item} + { + selected.value = selected.value?.filter( + (selectedItem) => selectedItem !== item, + ); + inputRef.value?.focus(); + }} + > + + + ))} + {displayValues.value.length > 4 && ( + + )} +
+ + + + +
+ + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + +
+ ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/object.tsx b/apps/website/src/routes/docs/headless/combobox/examples/object.tsx index 1ddfeab53..3fc774011 100644 --- a/apps/website/src/routes/docs/headless/combobox/examples/object.tsx +++ b/apps/website/src/routes/docs/headless/combobox/examples/object.tsx @@ -1,47 +1,38 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; import { LuChevronDown } from '@qwikest/icons/lucide'; export default component$(() => { - type Jedi = { - value: string; - label: string; - }; + useStyles$(styles); - const objectExample: Array = [ - { value: 'anakin', label: 'Anakin Skywalker' }, - { value: 'obi-wan', label: 'Obi-Wan Kenobi' }, - { value: 'mace', label: 'Mace Windu' }, - { value: 'yoda', label: 'Yoda' }, + const users = [ + { name: 'Tim', status: '🟢' }, + { name: 'Ryan', status: '🔴' }, + { name: 'Jim', status: '🟡' }, + { name: 'Jessie', status: '🟢' }, + { name: 'Abby', status: '🟡' }, ]; return ( - - Star Wars 🧙‍♂️ + + Logged in users - + - { - return ( - - {option.label} - - ); - }} - /> + {users.map((user) => ( + + {user.name} + {user.status} + + ))} ); }); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/open-change.tsx b/apps/website/src/routes/docs/headless/combobox/examples/open-change.tsx new file mode 100644 index 000000000..5261b85d3 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/open-change.tsx @@ -0,0 +1,51 @@ +import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const count = useSignal(0); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + const handleOpenChange$ = $(() => { + count.value++; + }); + + return ( + <> + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + +

The listbox opened and closed {count.value} time(s)

+ + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/placeholder.tsx b/apps/website/src/routes/docs/headless/combobox/examples/placeholder.tsx new file mode 100644 index 000000000..67b358bd3 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/placeholder.tsx @@ -0,0 +1,45 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + + Personal Trainers + + + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/placement.tsx b/apps/website/src/routes/docs/headless/combobox/examples/placement.tsx deleted file mode 100644 index 8282aef2f..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/placement.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$, useSignal } from '@builder.io/qwik'; - -export default component$(() => { - const inputValueSig = useSignal(''); - type PlacementExample = { - value: string; - label: string; - }; - - const placementExample: Array = [ - { value: '0', label: 'Up' }, - { value: '1', label: 'Down' }, - { value: '2', label: 'Left' }, - { value: '3', label: 'Right' }, - ]; - - return ( - <> - - Positions - - - - - - - - ( - - {option.label} - - )} - /> - - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/programmatic.tsx b/apps/website/src/routes/docs/headless/combobox/examples/programmatic.tsx new file mode 100644 index 000000000..2c0dc19de --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/programmatic.tsx @@ -0,0 +1,42 @@ +import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +export default component$(() => { + useStyles$(styles); + const users = [ + { id: '0', name: 'Tim' }, + { id: '1', name: 'Ryan' }, // 👈 start with Ryan + { id: '2', name: 'Jim' }, + { id: '3', name: 'Jessie' }, + { id: '4', name: 'Abby' }, + ]; + const selectedId = useSignal('1'); + + return ( + <> + + Personal Trainers + + + + + + + + {users.map((user) => ( + + {user.name} + + + + + ))} + + + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; +import { LuChevronDown, LuCheck } from '@qwikest/icons/lucide'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/reactive-input.tsx b/apps/website/src/routes/docs/headless/combobox/examples/reactive-input.tsx new file mode 100644 index 000000000..631158b38 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/reactive-input.tsx @@ -0,0 +1,48 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const inputStr = useSignal('aba'); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + <> + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + +

Typed input string: {inputStr.value}

+ + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/reactive-open.tsx b/apps/website/src/routes/docs/headless/combobox/examples/reactive-open.tsx new file mode 100644 index 000000000..20beb2fc6 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/reactive-open.tsx @@ -0,0 +1,48 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const isOpen = useSignal(false); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + <> + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/reactive.tsx b/apps/website/src/routes/docs/headless/combobox/examples/reactive.tsx new file mode 100644 index 000000000..46e9b245d --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/reactive.tsx @@ -0,0 +1,48 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const selectedFruit = useSignal('Apricot'); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + <> + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + +

Your favorite fruit is: {selectedFruit.value}

+ + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/refs.tsx b/apps/website/src/routes/docs/headless/combobox/examples/refs.tsx new file mode 100644 index 000000000..64a023ca5 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/refs.tsx @@ -0,0 +1,58 @@ +import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + + const triggerRef = useSignal(); + + const handleClick$ = $(() => { + if (!triggerRef.value) return; + + triggerRef.value.style.backgroundColor = 'red'; + }); + + const fruits = [ + 'Apple', + 'Apricot', + 'Bilberry', + 'Blackberry', + 'Blackcurrant', + 'Currant', + 'Cherry', + 'Coconut', + ]; + + return ( + <> + + Personal Trainers + + + + + + + + {fruits.map((fruit) => ( + + {fruit} + + + + + ))} + + +

Click the trigger to change the background color

+ + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/scrollable.tsx b/apps/website/src/routes/docs/headless/combobox/examples/scrollable.tsx new file mode 100644 index 000000000..60ba309f6 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/scrollable.tsx @@ -0,0 +1,43 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; +import { LuChevronDown } from '@qwikest/icons/lucide'; + +export default component$(() => { + useStyles$(styles); + const activeUsers = ['Tim', 'Ryan', 'Jim', 'Abby']; + const offlineUsers = ['Joey', 'Bob', 'Jack', 'John']; + + return ( + + User count + + + + + + + + + Active + {activeUsers.map((user) => ( + + {user} + + ))} + + + + Offline + {offlineUsers.map((user) => ( + + {user} + + ))} + + + + ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/search-bar.tsx b/apps/website/src/routes/docs/headless/combobox/examples/search-bar.tsx deleted file mode 100644 index 449446d10..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/search-bar.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { PropsOf, component$, useSignal } from '@builder.io/qwik'; -import { statusByComponent } from '~/_state/component-statuses'; -import { StatusBadge } from '~/components/component-status-badge/component-status-badge'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - const inputValueSig = useSignal(''); - const highlightedIndexSig = useSignal(0); - const isListboxOpenSig = useSignal(false); - - type MyComponents = { - component: string; - label: string; - }; - - const docsPrefix = '/docs/headless'; - const components = [ - { component: 'accordion', label: 'Accordion' }, - { component: 'combobox', label: 'Combobox' }, - { component: 'popover', label: 'Popover' }, - { component: 'select', label: 'Select' }, - { component: 'separator', label: 'Separator' }, - { component: 'tabs', label: 'Tabs' }, - ]; - - return ( - - Qwik UI ⚡ - - (isListboxOpenSig.value = !isListboxOpenSig.value)} - onKeyDown$={(e: KeyboardEvent) => { - if (e.key === 'Enter') { - const inputElement = e.target as HTMLInputElement; - window.location.href = `${docsPrefix}/${inputElement.value.toLowerCase()}`; - } - }} - placeholder="Tabs" - class="combobox-input" - /> - - - - {inputValueSig.value.length > 0 && ( - // give separate id if two triggers - - )} - - - { - const searchOption = option.option as MyComponents; - return ( - - - {searchOption.label} - - - - - - ); - }} - /> - - - ); -}); - -export function SearchIcon(props: PropsOf<'svg'>, key: string) { - return ( - - - - ); -} - -export function ClearIcon(props: PropsOf<'svg'>, key: string) { - return ( - - - - ); -} diff --git a/apps/website/src/routes/docs/headless/combobox/examples/shift.tsx b/apps/website/src/routes/docs/headless/combobox/examples/shift.tsx deleted file mode 100644 index 1525c222f..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/shift.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$, useSignal } from '@builder.io/qwik'; - -export default component$(() => { - const shiftExample = ['Example1', 'Example2', 'Example3']; - const isListboxOpenSig = useSignal(true); - - return ( -
-
-
- - Fruits 🍓 - - - - - - - - - - ( - - {option.label} - - )} - class="left:0 top:0 absolute w-fit rounded-base border px-4 py-2" - /> - - -
-
- ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/signal-binds.tsx b/apps/website/src/routes/docs/headless/combobox/examples/signal-binds.tsx deleted file mode 100644 index a097cd2c5..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/signal-binds.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$, useSignal } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - const isListboxOpenSig = useSignal(false); - const highlightedIndexSig = useSignal(2); - - const signalsExample = [ - 'bind:isListboxOpen', - 'bind:isInputFocused', - 'bind:isTriggerFocused', - 'bind:inputValue', - ]; - - return ( - - I love signals! 🗼 - - (isListboxOpenSig.value = !isListboxOpenSig.value)} - placeholder="bind:isListboxOpen" - class="combobox-input" - /> - - - - - - { - return ( - - {option.label} - - ); - }} - /> - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/sort-filter.tsx b/apps/website/src/routes/docs/headless/combobox/examples/sort-filter.tsx deleted file mode 100644 index f7340f263..000000000 --- a/apps/website/src/routes/docs/headless/combobox/examples/sort-filter.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; -import { LuChevronDown } from '@qwikest/icons/lucide'; - -export default component$(() => { - type Countries = { - value: string; - label: string; - }; - - const objectExample: Array = [ - { value: 'usa', label: 'United States' }, - { value: 'canada', label: 'Canada' }, - { value: 'mexico', label: 'Mexico' }, - { value: 'brazil', label: 'Brazil' }, - { value: 'uk', label: 'United Kingdom' }, - { value: 'germany', label: 'Germany' }, - { value: 'france', label: 'France' }, - { value: 'italy', label: 'Italy' }, - ]; - - return ( - - options - .filter(({ option }) => { - return option.label.toLowerCase().startsWith(value.toLowerCase()); - }) - .sort((country1, country2) => - country1.option.label.localeCompare(country2.option.label), - ) - } - options={objectExample} - class="combobox-root" - > - Countries 🚩 - - - - - - - - { - return ( - - {option.label} - - ); - }} - /> - - - ); -}); diff --git a/apps/website/src/routes/docs/headless/combobox/examples/string.tsx b/apps/website/src/routes/docs/headless/combobox/examples/string.tsx index 84c5bbaab..6d4a07403 100644 --- a/apps/website/src/routes/docs/headless/combobox/examples/string.tsx +++ b/apps/website/src/routes/docs/headless/combobox/examples/string.tsx @@ -1,53 +1,33 @@ -import { Combobox, ResolvedOption } from '@qwik-ui/headless'; - -import { component$ } from '@builder.io/qwik'; +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Combobox } from '@qwik-ui/headless'; import { LuChevronDown } from '@qwikest/icons/lucide'; export default component$(() => { - const fruits = [ - 'Apple', - 'Apricot', - 'Avocado 🥑', - 'Banana', - 'Bilberry', - 'Blackberry', - 'Blackcurrant', - 'Blueberry', - 'Boysenberry', - 'Currant', - 'Cherry', - 'Coconut', - 'Cranberry', - 'Cucumber', - ]; + useStyles$(styles); return ( - - Fruits 🍓 + + Personal Trainers + - + + - { - const myData = option.option as string; - return ( - - {myData} - - ); - }} - /> + + Option 1 + + + + Option 2 + ); }); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/examples/validation.tsx b/apps/website/src/routes/docs/headless/combobox/examples/validation.tsx new file mode 100644 index 000000000..07ae2c505 --- /dev/null +++ b/apps/website/src/routes/docs/headless/combobox/examples/validation.tsx @@ -0,0 +1,65 @@ +import { component$, useStyles$, $ } from '@builder.io/qwik'; +import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide'; +import { Combobox } from '@qwik-ui/headless'; +import { useForm, required } from '@modular-forms/qwik'; + +type Users = { + firstName: string; +}; + +export default component$(() => { + const users = ['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']; + const [, { Form, Field }] = useForm({ + loader: { value: { firstName: '' } }, + }); + + useStyles$(styles); + + const handleSubmit$ = $(() => { + console.log('submitted!'); + }); + + return ( +
+ ('Make sure to select an option')]} + > + {(field, props) => { + return ( + + Logged in users + + + + + + + + {field.error && ( + + {field.error} + + )} + + {users.map((user) => ( + + {user} + + + + + ))} + + + ); + }} + + +
+ ); +}); + +// internal +import styles from '../snippets/combobox.css?inline'; diff --git a/apps/website/src/routes/docs/headless/combobox/index.mdx b/apps/website/src/routes/docs/headless/combobox/index.mdx index 34772493e..44b192d1e 100644 --- a/apps/website/src/routes/docs/headless/combobox/index.mdx +++ b/apps/website/src/routes/docs/headless/combobox/index.mdx @@ -9,46 +9,37 @@ import { FeatureList } from '~/components/feature-list/feature-list'; # Combobox -A customizable text field with a listbox, enabling users to constrain a list of choices based on their search criteria. +Users can either type a value or pick one from a dropdown list. - - The Combobox component is about to undergo a **major refactor**. The API and behavior will - change drastically in the coming weeks. - -
You can expect it to match the conventions, developer - experience, and performance of the Select component.
-
- -Qwik UI's Combobox implementation follows the [WAI-Aria Combobox specifications](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/), along with some additional API's that enhance the flexibility, types, and performance. - -
-[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/combobox) - -[Report an issue 🚨](https://github.com/qwikifiers/qwik-ui/issues) - -[Edit This Page 🗒️]() - -
- ## ✨ Features