From 96842f8151fec3cf09044231229965d046e99f78 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 29 Apr 2026 05:15:37 +1000 Subject: [PATCH 01/19] foundation of shared select component --- src/v2/components/selectShared/keyboard.ts | 0 .../components/selectShared/listboxStyles.ts | 78 +++++++++++++++++++ .../selectShared/listboxTemplate.ts | 54 +++++++++++++ src/v2/components/selectShared/optionTypes.ts | 5 ++ 4 files changed, 137 insertions(+) create mode 100644 src/v2/components/selectShared/keyboard.ts create mode 100644 src/v2/components/selectShared/listboxStyles.ts create mode 100644 src/v2/components/selectShared/listboxTemplate.ts create mode 100644 src/v2/components/selectShared/optionTypes.ts diff --git a/src/v2/components/selectShared/keyboard.ts b/src/v2/components/selectShared/keyboard.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/v2/components/selectShared/listboxStyles.ts b/src/v2/components/selectShared/listboxStyles.ts new file mode 100644 index 000000000..ea342eaf1 --- /dev/null +++ b/src/v2/components/selectShared/listboxStyles.ts @@ -0,0 +1,78 @@ +import { css } from 'lit' + +export const listboxStyles = css` + :host { // default theme + display: inline-block; + position: relative; + z-index: 200; + --input-background: var(--color-background, #F8F9FB); + --item-text: var(--color-text, #1A1A1A); + --item-hover-background: var(--lavender-900, #7c4cff); + } + + :host([theme='dark']) { + display: inline-block; + position: relative; + z-index: 200; + --input-background: var(--color-background, #F8F9FB); + --item-text: var(--color-text, #1A1A1A); + --item-hover-background: var(--lavender-900, #7c4cff); + } + + .listbox { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + border: 1px solid var(--color-border, #E5E7EB); + border-top: none; + border-radius: 0 0 var(--border-radius-base, 0.3125rem) var(--border-radius-base, 0.3125rem); + background: var(--input-background); + overflow: visible; + z-index: 10; + box-shadow: 0 4px 12px rgba(124, 77, 255, 0.12); + } + + .listbox-item { + display: block; + width: 100%; + padding: 0.625rem 0.75rem; + border: none; + border-bottom: 1px solid var(--color-border, #E5E7EB); + background: transparent; + color: var(--item-text); + cursor: pointer; + font: inherit; + text-align: left; + box-sizing: border-box; + } + + .listbox-item:last-child { + border-bottom: none; + } + + .listbox-item:hover { + background: var(--item-hover-background); + border-radius: var(--border-radius-base-md, 0.5rem); + } + + .listbox-item-active { + background: var(--item-hover-background); + border-radius: var(--border-radius-base-md, 0.5rem); + outline: none; + } + + .listbox-item-selected { + font-weight: var(--font-weight-bold, 600); + } + + .listbox-item-disabled { + opacity: 0.55; + cursor: not-allowed; + } + + .listbox-item-disabled:hover { + background: transparent; + border-radius: 0; + } +` diff --git a/src/v2/components/selectShared/listboxTemplate.ts b/src/v2/components/selectShared/listboxTemplate.ts new file mode 100644 index 000000000..b441d14c3 --- /dev/null +++ b/src/v2/components/selectShared/listboxTemplate.ts @@ -0,0 +1,54 @@ +import { html } from 'lit' +import type { SelectOption } from './optionTypes' + +export interface RenderListboxArgs { + options: SelectOption[] + selectedOption?: SelectOption + activeOption?: SelectOption + listboxId?: string + getOptionId?: (option: SelectOption, index: number) => string + onOptionSelect: (option: SelectOption) => void +} + +export function renderListbox(args: RenderListboxArgs) { + const { + options, + selectedOption, + activeOption, + listboxId, + getOptionId, + onOptionSelect + } = args + + return html` + + ` +} \ No newline at end of file diff --git a/src/v2/components/selectShared/optionTypes.ts b/src/v2/components/selectShared/optionTypes.ts new file mode 100644 index 000000000..7aaa36d8d --- /dev/null +++ b/src/v2/components/selectShared/optionTypes.ts @@ -0,0 +1,5 @@ +export interface SelectOption { + label: string + value: string + disabled?: boolean +} From 21d4030f0e7e80767321e928c2ec5730e27d3a24 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 29 Apr 2026 14:06:53 +1000 Subject: [PATCH 02/19] Add a select webcomponent --- README.md | 4 +- src/v2/components/select/README.md | 202 ++++++++++ src/v2/components/select/Select.test.ts | 150 +++++++ src/v2/components/select/Select.ts | 376 ++++++++++++++++++ src/v2/components/select/index.ts | 9 + src/v2/components/selectShared/keyboard.ts | 82 ++++ .../components/selectShared/listboxStyles.ts | 54 +-- .../selectShared/listboxTemplate.ts | 2 + webpack.config.mjs | 3 + 9 files changed, 858 insertions(+), 24 deletions(-) create mode 100644 src/v2/components/select/README.md create mode 100644 src/v2/components/select/Select.test.ts create mode 100644 src/v2/components/select/Select.ts create mode 100644 src/v2/components/select/index.ts diff --git a/README.md b/README.md index 6d2e8d53c..0ca9b43b0 100644 --- a/README.md +++ b/README.md @@ -411,4 +411,6 @@ You are logged in as nameOfLoggedIn user. * Raptor mini: add a readme to the Footer component with example. -* Claude Sonnet 4.6: Make the dop down as a list under the input field and entlarge the pop up, make it higher, adjustable to fit the drop down. And make the drop down arrow area larger \ No newline at end of file +* Claude Sonnet 4.6: Make the dop down as a list under the input field and entlarge the pop up, make it higher, adjustable to fit the drop down. And make the drop down arrow area larger + +* GPT-5.4: can you wire up the keyboard interactions and aria attributes for Select. \ No newline at end of file diff --git a/src/v2/components/select/README.md b/src/v2/components/select/README.md new file mode 100644 index 000000000..41c31d1a1 --- /dev/null +++ b/src/v2/components/select/README.md @@ -0,0 +1,202 @@ +# solid-ui-select component + +A Lit-based custom element that renders a styled select control with a custom popup listbox. It supports keyboard navigation, emits a `change` event when the selected value changes, and keeps the currently selected option at the top of the popup when opened. + +## Installation + +```bash +npm install solid-ui +``` + +## Usage in a bundled project (webpack, Vite, Rollup, etc.) + +```javascript +import { Select } from 'solid-ui/components/select' +``` + +```html + + + +``` + +## Usage in a plain HTML page (CDN / script tag) + +```html + + + + + +``` + +## TypeScript + +```typescript +import { Select } from 'solid-ui/components/select' + +const select = document.querySelector('solid-ui-select') as Select +select.options = [ + { label: 'English', value: 'en' }, + { label: 'French', value: 'fr', disabled: false } +] + +select.addEventListener('change', (e: CustomEvent<{ value: string }>) => { + console.log(e.detail.value) +}) +``` + +`options` expects an array of: + +```typescript +type SelectOption = { + label: string + value: string + disabled?: boolean +} +``` + +## API + +### Properties / attributes + +| Property | Attribute | Type | Default | Description | +|----------|-----------|------|---------|-------------| +| `label` | `label` | `string` | `Select an option` | Fallback label shown when there is no selected value and no options are available. | +| `theme` | `theme` | `'light' \| 'dark'` | `'light'` | Sets the colour theme. | +| `options` | `options` | `SelectOption[]` | `[]` | Array of selectable options. In practice this should be set as a property from JavaScript rather than as an HTML attribute. | +| `layout` | `layout` | `'desktop' \| 'mobile'` | `'desktop'` | Layout mode reserved for integration with other responsive components. | +| `value` | `value` | `string` | `''` | The currently selected option value. If it matches an option, that option is shown in the trigger and moved to the top of the popup when opened. | + +### Events + +| Event | Detail | Description | +|-------|--------|-------------| +| `change` | `{ value: string }` | Fired when the user selects an option from the popup or confirms a keyboard selection. | + +### CSS custom properties + +These can be set on `solid-ui-select`, on a container element, or on `:root`. + +| Variable | Fallback | Description | +|----------|----------|-------------| +| `--select-z-index` | `400` / `900` in dark theme | Base host stacking level before the popup opens. | +| `--select-open-z-index` | `1000` | Host stacking level while the popup is open. | +| `--select-popup-z-index` | `1001` | Popup stacking level inside the open host. | +| `--select-popup-extra-width` | `2px` | Extra popup width beyond the trigger width. | +| `--select-popup-width` | `100%` | Base popup width before extra width is applied. | +| `--select-popup-background` | `--color-background` | Popup surface background. | +| `--select-trigger-background` | `--color-background` | Trigger background. | +| `--select-trigger-border` | `1px solid var(--gray-400, #99A1AF)` | Trigger border. | +| `--select-trigger-text` | `--color-text-subheading` | Trigger text colour. | +| `--select-trigger-height` | `--min-touch-target` / `44px` | Height of the trigger and option rows. | +| `--popup-border` | `--color-border` / `#E5E7EB` | Popup border colour. | +| `--popup-text` | `--color-text` | Popup text colour. | +| `--popup-shadow` | `--box-shadow-sm` / `0 1px 4px ...` | Popup shadow. | +| `--input-background` | `--color-background` | Listbox and option row background. | +| `--item-text` | `--color-text` | Option text colour. | +| `--item-selected-text` | `--color-primary` / `#7c4dff` | Active option text colour. | +| `--item-hover-background` | `--lavender-300` / `#e6dcff` | Hover background for option rows. | +| `--item-selected-background` | `--lavender-400` / `#cbb9ff` | Active option background. | + +The component also inherits common design-system tokens such as `--border-radius-base`, `--border-radius-sm`, `--spacing-xxs`, `--spacing-xs`, `--font-size-sm`, `--font-weight-md`, `--font-weight-bold`, `--gray-400`, `--color-background`, `--color-text`, `--color-text-subheading`, `--color-border`, `--color-primary`, `--lavender-300`, `--lavender-400`, `--box-shadow-sm`, and `--min-touch-target`. + +### CSS shadow parts + +These parts can be styled from a consuming repo using `::part(...)`. + +| Part | Description | +|------|-------------| +| `select-trigger` | The trigger button. | +| `trigger-label` | The text label inside the trigger. | +| `trigger-icon` | The down-arrow icon wrapper inside the trigger. | +| `popup-box` | The popup container that wraps the listbox. | +| `listbox` | The `