{"_id":"@asnewyla/unstyled-select","name":"@asnewyla/unstyled-select","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@asnewyla/unstyled-select","version":"0.2.0","description":"Unstyled select/combobox primitive with single and multi-select, accessibility","license":"MIT","repository":{"type":"git","url":"git+https://github.com/xavierDelaFuente/xd-components.git","directory":"packages/unstyled-select"},"publishConfig":{"access":"public"},"main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./test-utils":{"types":"./dist/test-utils.d.ts","import":"./dist/test-utils.mjs","require":"./dist/test-utils.js"}},"sideEffects":false,"keywords":["react","select","combobox","listbox","unstyled","accessible"],"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","@testing-library/react":"^16.0.0","@testing-library/user-event":"^14.5.0"},"peerDependenciesMeta":{"@testing-library/react":{"optional":true},"@testing-library/user-event":{"optional":true}},"devDependencies":{"react":"^18.3.0","react-dom":"^18.3.0","tsup":"^8.1.0","vitest":"^2.0.0","@vitest/coverage-v8":"^2.0.0","@testing-library/react":"^16.0.0","@testing-library/user-event":"^14.5.0","@testing-library/jest-dom":"^6.4.0","jsdom":"^24.0.0","typescript":"^5.5.0"},"scripts":{"build":"tsup","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","type-check":"tsc --noEmit"},"_nodeVersion":"24.19.0","_id":"@asnewyla/unstyled-select@0.2.0","dist":{"integrity":"sha512-n/Tz8KzqzeU62IDPHC2ZQ46HHae6NFGv+7d+3ZoCRiIRWRQd7nI5RAUX3N6swpejGFexNO41LjeEDXkB9YblBQ==","shasum":"53af4eb52046c6c6173f80222d10e295e8097513","tarball":"https://registry.npmjs.org/@asnewyla/unstyled-select/-/unstyled-select-0.2.0.tgz","fileCount":15,"unpackedSize":82213,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD/VXRo1gGFfELOmslK9bpBZdKeVYbawPehIwt8bI75swIgBGPu6zVEJKLrwsf8YAN11MfTAPlD2Cm5QyyYTN+w59w="}]},"_npmUser":{"name":"asnewyla","email":"asnewyla@gmail.com"},"directories":{},"maintainers":[{"name":"asnewyla","email":"asnewyla@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/unstyled-select_0.2.0_1787691187428_0.18126896987268704"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-25T20:53:07.290Z","0.2.0":"2026-08-25T20:53:07.586Z","modified":"2026-08-25T20:53:07.765Z"},"maintainers":[{"name":"asnewyla","email":"asnewyla@gmail.com"}],"description":"Unstyled select/combobox primitive with single and multi-select, accessibility","keywords":["react","select","combobox","listbox","unstyled","accessible"],"repository":{"type":"git","url":"git+https://github.com/xavierDelaFuente/xd-components.git","directory":"packages/unstyled-select"},"license":"MIT","readme":"# @asnewyla/unstyled-select\n\nUnstyled, accessible select/combobox primitive. Supports single- and\nmulti-select, keyboard navigation, and full ARIA listbox semantics without\nimposing any visual styling.\n\n## Install\n\n```bash\nnpm install @asnewyla/unstyled-select\n```\n\n## Usage\n\n```tsx\nimport { UnstyledSelect } from '@asnewyla/unstyled-select';\n\nconst fruitOptions = [\n  { value: 'apple', label: 'Apple' },\n  { value: 'banana', label: 'Banana' },\n  { value: 'cherry', label: 'Cherry', disabled: true },\n];\n\n// Uncontrolled, single-select\n<UnstyledSelect\n  aria-label=\"Fruit\"\n  options={fruitOptions}\n  defaultValue=\"apple\"\n  placeholder=\"Choose a fruit\"\n/>\n\n// Controlled, single-select\n<UnstyledSelect\n  aria-label=\"Fruit\"\n  options={fruitOptions}\n  value={selected}\n  onChange={setSelected}\n/>\n\n// Multi-select — value/defaultValue/onChange become array-typed\n<UnstyledSelect\n  aria-label=\"Fruit\"\n  options={fruitOptions}\n  multiple\n  defaultValue={['apple', 'banana']}\n  onChange={(values) => console.log(values)}\n/>\n```\n\n### Props\n\n| Prop | Type | Default |\n|---|---|---|\n| `options` | `{ value: string; label: string; disabled?: boolean }[]` | — |\n| `multiple` | `boolean` | `false` |\n| `value` / `defaultValue` | `string` (single) or `string[]` (`multiple`) | — |\n| `onChange` | `(value: string) => void` (single) or `(value: string[]) => void` (`multiple`) | — |\n| `placeholder` | `string` | — |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `renderValue` | `(selectedOptions: SelectOption[], helpers: { removeOption: (option: SelectOption) => void }) => ReactNode` | — |\n\n`multiple` gates which shape `value`/`defaultValue`/`onChange` accept —\nthis is enforced at the type level (a discriminated union), not just at\nruntime, so passing an array `defaultValue` without `multiple` is a\ncompile error rather than a silent bug.\n\nUncontrolled by default: pass `value` + `onChange` to control it yourself.\nSingle-select closes the listbox and returns focus to the trigger on\nselection; multi-select keeps the listbox open so more options can be\ntoggled.\n\nBy default the trigger shows the selected option's label (or every\nselected label, comma-joined, for `multiple`), falling back to\n`placeholder` when nothing's selected. Pass `renderValue` to replace that\nwith your own rendering — e.g. removable chips for `multiple` — using the\n`selectedOptions` (real `SelectOption` objects, not just their values) and\na `removeOption` helper that deselects one of them (calls `onChange` with\nthe option removed, same as clicking it again in the listbox would).\n\n### Accessibility\n\nImplements the WAI-ARIA listbox pattern: the trigger is a `<div\nrole=\"combobox\">` with `aria-expanded`/`aria-haspopup` (not a `<button>` —\nsee \"Trigger element\" below), the popup is `role=\"listbox\"`\n(`aria-multiselectable` when `multiple`), and each option is\n`role=\"option\"` with `aria-selected`. State is also mirrored via\n`data-*` attributes (`data-open`, `data-selected`, `data-disabled`) for\nstyling and test hooks, same convention as every other `@asnewyla/*`\nprimitive.\n\nKeyboard support:\n\n| Key | Behavior |\n|---|---|\n| `Enter` / `Space` on the trigger | Opens the listbox and focuses the search input |\n| `ArrowDown` | Moves focus to the next enabled option, skipping disabled ones, clamped at the end (no wrap) |\n| `ArrowUp` | Moves focus to the previous enabled option; from the first option (or with nothing focused), moves focus back to the search input |\n| `Home` / `End` | Jumps to the first/last enabled option |\n| `Enter` / `Space` on a focused option | Selects it |\n| `Escape` | Clears the search query, closes the listbox, and returns focus to the trigger |\n| Click outside | Closes the listbox |\n\n### Search / filter\n\nOpening the listbox renders a search `<input>` (`aria-label=\"Search\noptions\"`) as the first child of the popup, above the option list — the\ntrigger itself does not become an editable input. Typing filters `options`\nclient-side by a case-insensitive substring match on `label`; there's no\nasync/remote search. Selecting an option or closing the listbox (`Escape`,\nclick outside) clears the query.\n\n### Trigger element\n\nThe trigger renders as a `<div role=\"combobox\" tabIndex={0}>`, not a\n`<button>`. A real `<button>` can't contain other interactive elements\n(nested buttons are invalid HTML), which `renderValue` needs to be able to\ndo — chip-style multi-select values need a real, focusable remove button\nper chip. Native `<button>` semantics (Enter/Space activates, disabled\nremoves it from the tab order) are reimplemented by hand for the div:\n`Enter`/`Space` open the listbox, and a `disabled` trigger gets\n`tabIndex={-1}` instead of the native `disabled` attribute.\n\n### Not yet supported\n\nThis primitive is still growing. Not implemented yet:\n\n- **Native form participation** — nothing here submits through\n  `FormData` the way `@asnewyla/form`'s `FormFieldInput` expects.\n- **Portal rendering** — the listbox renders inline, so it can be\n  visually clipped by an `overflow: hidden` or scrolling ancestor.\n\n## License\n\nMIT\n","readmeFilename":"","_rev":"1-7bcecbff2183722bcc93841945bb6ba1"}