diff --git a/docs/pdrs/select-primitive.mdx b/docs/pdrs/select-primitive.mdx new file mode 100644 index 00000000..41fdf4f4 --- /dev/null +++ b/docs/pdrs/select-primitive.mdx @@ -0,0 +1,609 @@ +# Select Primitive PDR + +## Overview + +PDR describing the `Select` primitive built on React Aria's `useSelect` and `useSelectState` hooks, following the [W3C ARIA combobox (select-only) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/). + +### Unique Attributes + +- **Reuses ListBox selection patterns**: Uses existing [`ListStateContext`](../../packages/listbox/src/listbox.tsx#L107) from `@bento/listbox` to share selection state with `ListBox` and `ListBoxItem` components. Source: [`ListStateContext`](../../packages/listbox/src/listbox.tsx#L107) is exported and consumed by [`ListBoxItem`](../../packages/listbox/src/listbox-item.tsx#L133). +- **Form integration via HiddenSelect**: React Aria provides [`HiddenSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/HiddenSelect.tsx#L142) which renders a visually hidden native [`` path when `state.collection.size <= 300`). Source: [`HiddenSelect.tsx`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/HiddenSelect.tsx#L142). + +## Goals + +- Provide a single- and multi-select dropdown primitive built on React Aria's `useSelect` and `useSelectState` hooks. +- Reuse existing Bento primitives (`ListBox`, `ListBoxItem`, `Container`, slots) rather than introducing new option or popover primitives. +- Integrate with native HTML forms via React Aria's `HiddenSelect` using the `name` prop. +- Expose a small public API surface that forwards most props to React Aria `SelectProps` while using Bento's slot system for composition. + +## Constraints + +- Must not introduce new shared hooks or utilities if existing packages (`@bento/slots`, `@bento/use-props`, `@bento/use-data-attributes`, `@bento/listbox`) can satisfy the requirements. +- Must use `ListBoxItem` as the only option primitive so selection behavior stays aligned with `@bento/listbox`. +- Must rely on React Aria and React Stately for selection, overlay, and keyboard behavior rather than reimplementing this logic. +- Consumers handle portal rendering for the popover; Select passes overlay-related props to slots and does not depend on a dedicated popover primitive. +- Must treat other not-yet-implemented primitives (e.g., `Dismiss`) as optional dependencies, not hard requirements for core `Select` behavior. + +## API + +**Components**: +- `Select` — Coordinator component + +**Type Safety Exports**: +- `SelectProps` — Generic props interface for the `Select` component (`SelectProps`). +- `SelectionMode` — Type alias for selection modes: `'single' | 'multiple'`. +- `SelectSlots` — Type map for all slot prop types. Enables type-safe slot component implementations. +- `PropsFromSelectSlot` — Utility type for extracting individual slot prop types without importing each interface. +- `SelectRenderProps` — Type for render props passed to `renderEmptyState` function. +- Individual slot prop interfaces: `SelectTriggerSlotProps`, `SelectValueSlotProps`, `SelectPopoverSlotProps`, `SelectListSlotProps` — Available for direct import when needed (derivable from `PropsFromSelectSlot`). + +**Advanced type & hook exports (optional)**: +- `SelectState` — React Stately `SelectState` type (re-exported for advanced integrations; not required for normal usage). +- `Placement` — Popover placement type from React Aria (re-exported for convenience). +- `useSelectState` — React Stately hook for Select state management (re-exported for advanced control flows). + +**Dependencies**: +- `@bento/listbox` — Provides `ListBox`, `ListBoxItem`, and `ListStateContext`. Source: [`ListStateContext`](../../packages/listbox/src/listbox.tsx#L107) exists. +- `@bento/container` — Provides `Container` for slot-based composition. Source: [`Container`](../../packages/container/src/index.tsx#L84) exists. +- `@bento/slots` — Provides `withSlots` for slot system. Source: [`withSlots`](../../packages/slots/src/slots.tsx#L51) exists. +- `@bento/use-props` — Provides `useProps` for prop merging. Source: [`useProps`](../../packages/use-props/src/index.ts#L117) exists. +- `@bento/use-data-attributes` — Provides `useDataAttributes` for data attributes. Source: [`useDataAttributes`](../../packages/use-data-attributes/src/index.ts#L25) exists. +- `react-aria` — Provides `useSelect`, `useOverlay`, `useOverlayPosition`, `usePreventScroll`, `useFocusRing`, `useHover`. Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L74) exists. +- `react-stately` — Provides `useSelectState`. Source: [`useSelectState`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L81) exists. +- `@react-aria/collections` — Provides `CollectionBuilder`. Source: [`CollectionBuilder`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/collections/src/CollectionBuilder.tsx#L36) exists. + +**Using ListBoxItem with Select:** + +`ListBoxItem` from `@bento/listbox` is the option primitive for Select. The `id` and `textValue` props are optional: + +- **`id` prop** — Optional. Source: [`ListBoxItemProps.id`](../../packages/listbox/src/listbox-item.tsx#L90) is `readonly id?: Key`. If not provided, React Aria auto-generates a unique key. Source: [`Document.setProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/collections/src/Document.ts#L336) uses `id ?? \`react-aria-${++this.ownerDocument.nodeId}\`` +- **`textValue` prop** — Optional. Source: [`ListBoxItemProps.textValue`](../../packages/listbox/src/listbox-item.tsx#L94) is `readonly textValue?: string`. If not provided, React Aria auto-derives it from children. Source: [`Document.setProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/collections/src/Document.ts#L350) uses `textValue || (typeof props.children === 'string' ? props.children : '') || obj['aria-label'] || ''` + +**About `Node.value`**: React Aria populates `Node.value` with the original item object when using dynamic collections (`items` / `childItems`). Source: [`Item.getCollectionNode`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/collections/src/Item.ts#L32-L45) yields `{ type: 'item', value: child }` for each `childItems` entry. For **static JSX children**, `Node.value` is not meaningful and should not be relied upon. + +```tsx +// Simple usage - id auto-generated, textValue derived from children +Apple + +// Explicit id for selection state - state.value will be "apple" +Apple + +// Complex markup - explicit textValue for typeahead, id for selection + + + Apple Special + +``` + +### Working with item data + +**For dynamic collections**, use `items` on `Select`/`ListBox` and read `Node.value` from `selectedItem`: + +```tsx +import { withSlots } from '@bento/slots'; +import { useProps } from '@bento/use-props'; + +type Fruit = { id: string; name: string; calories: number }; + +const fruits: Fruit[] = [ + { id: 'apple', name: 'Apple', calories: 52 }, + { id: 'orange', name: 'Orange', calories: 47 } +]; + +// Value display component using withSlots pattern +const FruitValue = withSlots('FruitValue', function FruitValue(args: any) { + const { props } = useProps(args); + const { selectedItem, ...rest } = props; + const fruit = selectedItem?.value as Fruit | undefined; + + return ( + + {fruit ? `${fruit.name} (${fruit.calories} kcal)` : 'Select a fruit'} + + ); +}); + + items={fruits}> + +
+ slot="listbox"> + {(item) => {item.name}} + +
+ +``` + +**For static children**, map keys to your own data: + +```tsx +import { withSlots } from '@bento/slots'; +import { useProps } from '@bento/use-props'; + +const fruitsById = { + apple: { name: 'Apple', calories: 52 }, + orange: { name: 'Orange', calories: 47 } +} as const; + +// Value display component using withSlots pattern +const FruitValue = withSlots('FruitValue', function FruitValue(args: any) { + const { props } = useProps(args); + const { selectedItem, ...rest } = props; + const id = selectedItem?.key as keyof typeof fruitsById | undefined; + const fruit = id ? fruitsById[id] : undefined; + + return ( + + {fruit ? `${fruit.name} (${fruit.calories} kcal)` : 'Select a fruit'} + + ); +}); + + +``` + +**Pattern requirements**: +- If you want a **component** to receive slot props (like `selectedItem`, `selectedItems`) via `useProps`, it must be wrapped with `withSlots`. +- Plain elements like `` do not receive `selectedItem` themselves via `useProps`; they only act as slot markers for a slotted component (e.g. `ValueDisplay`) that actually consumes the props. + +## Implementation Notes + +| Concept | Implementation | +|---------|----------------| +| React Aria | [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L74), [`useSelectState`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L81), [`useOverlay`](https://react-spectrum.adobe.com/react-aria/useOverlay.html), [`useOverlayPosition`](https://react-spectrum.adobe.com/react-aria/useOverlayPosition.html), [`usePreventScroll`](https://react-spectrum.adobe.com/react-aria/usePreventScroll.html), [`useFocusRing`](https://react-spectrum.adobe.com/react-aria/useFocusRing.html), [`useHover`](https://react-spectrum.adobe.com/react-aria/useHover.html) | +| Overlay Management | [`useSelectOverlay`](../../packages/select/src/use-select-overlay.ts) custom hook encapsulates overlay dismiss behavior, positioning, and scroll prevention | +| Slots | [`withSlots`](../../packages/slots/src/slots.tsx#L51), [`Container`](../../packages/container/src/index.tsx#L84) with `slots` prop | +| Context | [`ListStateContext`](../../packages/listbox/src/listbox.tsx#L107) for selection state (consumed by `ListBox`/`ListBoxItem`) | +| CollectionBuilder | [`CollectionBuilder`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/collections/src/CollectionBuilder.tsx#L36) builds collection from children | +| data-\* Attributes | [`useDataAttributes`](../../packages/use-data-attributes/src/index.ts#L25) exists. Boolean conversion: `true` → `"true"` string, `false` → attribute not added (returns `undefined`). Source: [`to-attribute-value/src/index.ts:41`](../../packages/to-attribute-value/src/index.ts#L41) returns `value ? value.toString() : undefined` for booleans. | +| Event Handler Passing | Bento slots system passes event handlers (props matching `/^on[A-Z]/`) via context. Source: [`isEventListener`](../../packages/use-props/src/index.ts#L32) matches `/^on[A-Z]/`, [`withSlots`](../../packages/slots/src/slots.tsx#L51) wraps components to receive slot props via context. | + +## Internal Structure & Reuse Potential + +**Reuses existing patterns**: +- [`ListStateContext`](../../packages/listbox/src/listbox.tsx#L107) pattern from `@bento/listbox` (similar to [`CheckboxGroupStateContext`](../../packages/checkbox/src/checkbox-group-state.tsx#L4) in `@bento/checkbox`). Source: Both contexts exist and follow the same pattern. +- Bento slots system: [`withSlots`](../../packages/slots/src/slots.tsx#L51), [`Container`](../../packages/container/src/index.tsx#L84), [`useProps`](../../packages/use-props/src/index.ts#L117). Source: All exist. +- React Aria hooks: `useSelect`, `useSelectState`, `useOverlay`, `useOverlayPosition`, etc. Source: All exist in React Aria. + +**No new reusable utilities proposed**: All functionality provided by existing Bento packages and React Aria hooks. + +## React Aria or External Hook Integration + +**React Aria hooks used**: +- [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L74) — Returns `labelProps`, `triggerProps`, `valueProps`, `menuProps`, `descriptionProps`, `errorMessageProps`, `hiddenSelectProps`. Source: [`SelectAria` interface](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L35) defines return types. +- [`useSelectState`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L81) — Manages selection state, overlay open state, and collection. Source: [`SelectState` interface](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L23) extends `ListState` and `OverlayTriggerState`. +- [`useOverlay`](https://react-spectrum.adobe.com/react-aria/useOverlay.html) — Handles overlay dismiss behavior. Source: Hook exists in React Aria. +- [`useOverlayPosition`](https://react-spectrum.adobe.com/react-aria/useOverlayPosition.html) — Handles positioning relative to trigger. Source: [`useOverlayPosition.ts`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/useOverlayPosition.ts#L100). +- [`usePreventScroll`](https://react-spectrum.adobe.com/react-aria/usePreventScroll.html) — Prevents body scrolling when overlay is open. Source: Hook exists in React Aria. +- [`useFocusRing`](https://react-spectrum.adobe.com/react-aria/useFocusRing.html) — Manages focus visibility. Source: Hook exists in React Aria. +- [`useHover`](https://react-spectrum.adobe.com/react-aria/useHover.html) — Manages hover state. Source: Hook exists in React Aria. +- [`HiddenSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/HiddenSelect.tsx#L142) — Component for form integration. Source: Component exists and accepts `name` prop via [`AriaHiddenSelectProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/HiddenSelect.tsx#L22) (line 32). + +**CollectionBuilder usage**: [`CollectionBuilder`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/collections/src/CollectionBuilder.tsx#L36) builds a collection from the children passed to it. + +## Behaviors + +**State management**: +- Uses `useSelectState` which internally calls `useListState` from `@react-stately/list`. Source: [`useSelectState`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L113) calls `useListState` (line 113). +- `SelectState` extends `ListState` which provides `collection`, `disabledKeys`, and `selectionManager`. Source: [`SelectState`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L23) extends `ListState`. +- `SelectState` provides `selectedItems: Node[]`. Source: [`SelectState.selectedItems`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L58) is `readonly selectedItems: Node[]`. +- `SelectState.selectedKey` exists but is deprecated. Source: [`SelectState.selectedKey`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L28) is marked `@deprecated`. `SelectState.value` is the current API. Source: [`SelectState.value`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L43) is `readonly value: ValueType`. + +**Props from React Aria**: +- `value`/`defaultValue` — Type: `Key | null` (single) or `Key[]` (multiple). Selection state control. Source: [`ValueBase`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/shared/src/inputs.d.ts#L65) provides these props. +- `onChange` — Handler called when selection/value changes. Type: `(value: Key | null) => void` (single) or `(value: Key[]) => void` (multiple). Source: [`SelectProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/select/src/index.d.ts#L38) extends [`ValueBase>`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/shared/src/inputs.d.ts#L65) from `@react-types/shared`, which defines [`onChange?: (value: C) => void`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/shared/src/inputs.d.ts#L71). +- `selectionMode` — Type: `'single' | 'multiple'`, default `'single'`. Source: [`SelectProps.selectionMode`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/select/src/index.d.ts#L43) with default `'single'` from [`useSelectState`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L82). +- `isOpen` / `defaultOpen` / `onOpenChange` — Overlay trigger-style props controlling the open state of the menu. Source: [`SelectProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/select/src/index.d.ts) in `@react-types/select`. +- `name` — For form submission. Source: [`AriaSelectProps.name`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/select/src/index.d.ts#L77). +- `label` — From `LabelableProps`. Source: [`AriaSelectProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/select/src/index.d.ts#L69) extends `LabelableProps`. +- `isDisabled`, `isRequired`, `isInvalid` — From React Aria types. Source: [`SelectProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/select/src/index.d.ts#L38) includes these from `InputBase` and `Validation`. +- `placement`, `offset`, `crossOffset`, `shouldFlip`, `containerPadding` — Positioning props for `useOverlayPosition`. + - **React Aria defaults**: `placement='bottom'`, `offset=0`, `crossOffset=0`, `shouldFlip=true`, `containerPadding=12`. Source: [`useOverlayPosition.ts`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/useOverlayPosition.ts#L100-104). + - **Bento Select will override**: `placement='bottom start'` (aligns popover to start edge like React Spectrum's Picker). +- `renderEmptyState` — Render function or element to display when collection is empty. Receives `SelectRenderProps` with `{ isOpen, isDisabled, isInvalid, isRequired, selectedItem, selectedItems, isEmpty }`. Extracted before `useProps` to prevent render prop corruption (Bento invariant #16). +- `required` — Native HTML attribute available via `React.ComponentProps<'div'>`. Select combines `required` and `isRequired` when computing ARIA decoration (`aria-required`), but React Aria's `HiddenSelect` and validation behavior are driven by `isRequired` + `validationBehavior`, not `required`. +- `aria-labelledby` — ARIA attribute for custom label associations, inherited from `React.ComponentProps<'div'>`. Can override React Aria's auto-generated label associations when explicit control is needed. + +**Slot structure**: +- `trigger` — Receives `triggerProps` (type: `AriaButtonProps`), `focusProps`, `hoverProps`, `role="combobox"`, `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`, `aria-required`, `aria-invalid`, `aria-disabled`, `data-open`, `ref`. Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L40) returns `triggerProps: AriaButtonProps`. React Aria provides `aria-haspopup`, `aria-expanded` (boolean), `aria-controls` via `useOverlayTrigger`. Bento adds `role="combobox"`, converts `aria-expanded` to string, and adds `aria-required`, `aria-invalid`, `aria-disabled`. +- `label` — Receives `labelProps` (type: `DOMAttributes`). Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L37) returns `labelProps: DOMAttributes`. +- `value` / `trigger.value` — Receives `valueProps` (type: `DOMAttributes` with `id`), `selectedItem` (Node\ | null), `selectedItems` (Node\[]). Slot alias `trigger.value` allows nesting value display inside trigger. Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L215) returns `valueProps: { id: valueId }`. React Aria's [`SelectState.selectedItems`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/select/src/useSelectState.ts#L58) provides the Node array. For dynamic collections, `Node.value` holds the original item object. Consumers handle placeholder rendering using the provided selection data. +- `popover` — Receives `overlayProps`, `positionProps`, `isOpen`, `onClose`, `ref`, `data-open`. Consumer handles portal rendering and styling. +- `listbox` — Receives `menuProps` (type: `AriaListBoxOptions`), `ref`. Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L46) returns `menuProps: AriaListBoxOptions`. +- `description` — Receives `descriptionProps` (type: `DOMAttributes`). Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L49) returns `descriptionProps: DOMAttributes`. +- `error` — Receives `errorMessageProps` (type: `DOMAttributes`). Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L52) returns `errorMessageProps: DOMAttributes`. + +**Rendered DOM structure**: Consumer-controlled via slots. Select coordinator only distributes props via context. + +**Controlled vs Uncontrolled**: Supports both via `value` / `defaultValue` (selection) and `isOpen` / `defaultOpen` (menu open state). Source: [`SelectProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-types/select/src/index.d.ts#L38) extends `ValueBase>` and includes explicit open-state props. + +## TypeScript Considerations + +**Type Casts and Erasure**: +- `Select` uses `CollectionBuilder` which requires a render callback, necessitating split into `Select` and `SelectInner` components +- Generic type `T` is intentionally erased when passing to `SelectInner` - the inner component only needs keys and nodes, not specific item types +- `ListStateContext.Provider` requires type cast from `SelectState` to `ListState` - this is safe because `SelectState` extends `ListState` (Bento invariants #42, #43) + +**Ref Type Constraints**: + +Select's implementation uses specific ref types to ensure type compatibility with slotted primitives: + +| Slot | Ref Type | Reason | Could Be Relaxed? | +|------|----------|--------|-------------------| +| `trigger` | `HTMLButtonElement` | Button primitive expects this specific type | ❌ No - constrained by Button's forwardRef type | +| `popover` | `HTMLDivElement` internal, `HTMLElement` in public slot props | Internal implementation detail; React Aria only requires `Element` | 🔶 Internally could be generalized further if needed | +| `listbox` | `HTMLDivElement` | ListBox primitive renders `
` | ❌ No - constrained by ListBox implementation | + +**Note**: The internal `popoverRef` uses `HTMLDivElement` for now, but the public `SelectPopoverSlotProps.ref` is already `RefObject`, and React Aria's `useOverlayPosition` only requires `RefObject`. The `trigger` and `listbox` constraints are fundamental - Button expects `HTMLButtonElement` and ListBox renders a `div`. + +## Accessibility + +**W3C ARIA Combobox Pattern Conformance**: + +Select implements the [W3C ARIA combobox (select-only) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/) with the following ARIA attributes: + +| Attribute | Source | Notes | +|-----------|--------|-------| +| `role="combobox"` | Bento Select | Added explicitly (line 271). React Aria's `useSelect` doesn't provide this. | +| `aria-haspopup="listbox"` | React Aria | Provided by `useOverlayTrigger` (called by `useMenuTrigger`). Source: [`useOverlayTrigger.ts:57`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/useOverlayTrigger.ts#L57) | +| `aria-expanded` | React Aria + Bento | React Aria provides as boolean. Bento converts to string (`"true"`/`"false"`) for CSS attribute selector compatibility. | +| `aria-controls` | React Aria | Provided by `useOverlayTrigger`, references listbox `id`. Source: [`useOverlayTrigger.ts:65`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/useOverlayTrigger.ts#L65) | +| `aria-labelledby` | React Aria | Auto-generated by `useSelect` to concatenate label and value IDs. Can be overridden via prop. | +| `aria-required` | Bento Select | Added when `isRequired` or `required` prop is set. React Aria doesn't add this. | +| `aria-invalid` | Bento Select | Added when `isInvalid` prop is set. React Aria doesn't add this. | +| `aria-disabled` | Bento Select | Added when `isDisabled` prop is set. React Aria doesn't add this. | +| `role="listbox"` | React Aria | Provided by `useListBox` (via `@bento/listbox`). Source: [`useListBox.ts:121`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/listbox/src/useListBox.ts#L121) | + +**ARIA attributes**: Provided by React Aria's `useSelect` hook. Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts) returns props with ARIA attributes via `triggerProps` and `menuProps`. `useMenuTrigger` calls `useOverlayTrigger`, which sets `aria-haspopup`, `aria-expanded`, and `aria-controls` on the trigger. Sources: [`useMenuTrigger`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/menu/src/useMenuTrigger.ts#L55-L56) calls `useOverlayTrigger`, and [`useOverlayTrigger`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/useOverlayTrigger.ts#L62-L66) sets these attributes. + +**Keyboard interactions**: Handled by React Aria hooks and native button semantics: +- Arrow keys for navigation in the listbox (via `useListBox`/`useSelectableList`). Source: [`useListBox`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/listbox/src/useListBox.ts#L67) uses `useSelectableList` for keyboard navigation. +- Typeahead (via `useTypeSelect`). Source: [`useSelect`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L127) uses `useTypeSelect` (line 127). +- Enter/Space activate the trigger and commit selection via native ` + +``` + +**Note**: React Aria's `HiddenSelect` accepts `name` via [`AriaHiddenSelectProps`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/HiddenSelect.tsx#L22) (line 32) and uses it when constructing hidden form fields for form submission (lines 174–212). + +### With Empty State + +```tsx +import { withSlots } from '@bento/slots'; +import { useProps } from '@bento/use-props'; + +// Slottable value component +const ValueDisplay = withSlots('SelectValue', function SelectValue(args: any) { + const { props } = useProps(args); + const { selectedItem, ...rest } = props; + return {selectedItem?.textValue || 'Select...'}; +}); + + +``` + +**When to use `renderEmptyState`**: +- Use `renderEmptyState` when you need access to selection state (`isOpen`, `isDisabled`, etc.) via `SelectRenderProps` +- Use conditional rendering outside Select when you control visibility independently +- Empty state replaces all children when collection is empty (`state.collection.size === 0`) +- Does not affect overlay behavior - popover still opens/closes normally + +**Note**: `renderEmptyState` must be extracted before `useProps` (Bento invariant #16) to prevent `useProps` from executing it as a slot render prop with incorrect arguments. \ No newline at end of file diff --git a/packages/focus-lock/package.json b/packages/focus-lock/package.json index bec8af20..2e4b892d 100644 --- a/packages/focus-lock/package.json +++ b/packages/focus-lock/package.json @@ -57,7 +57,6 @@ "@bento/container": "*", "@bento/heading": "*", "@bento/listbox": "*", - "@bento/portal": "*", "@bento/radio": "*", "@bento/text": "*" },