import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { createMemo, omit } from "solid-js"; import { createChangeEventDetails, REASONS, } from "../../internals/change-event-details"; import { composeRefs, userRef } from "../../internals/compose-refs"; import { useCompositeListItem } from "../../internals/composite/composite-list"; import { stopEvent } from "../../internals/floating-ui/utils/event"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps } from "../../internals/types"; import { useComboboxChipsContext } from "../chips/chips-context"; import { type ComboboxRootChangeEventDetails, useComboboxRootContext, } from "../root/combobox-root-context"; import { ComboboxChipContext } from "./chip-context"; export interface ComboboxChipState { disabled: boolean; } export interface ComboboxChipProps extends BaseUIComponentProps<"div", ComboboxChipState> { as?: ValidComponent; } /** * An individual chip that represents a value in a multiselectable input. * Renders a `
` element. */ export function ComboboxChip(props: ComboboxChipProps): JSX.Element { const root = useComboboxRootContext(); const chipsCtxRaw = useComboboxChipsContext(); if (chipsCtxRaw == null) { throw new Error( "Base UI: must be rendered inside .", ); } const chipsCtx = chipsCtxRaw; const disabled = (): boolean => root.disabled(); const readOnly = (): boolean => root.readOnly(); const listItem = useCompositeListItem(); const state = createMemo(() => ({ disabled: disabled(), })); function handleKeyDown(event: KeyboardEvent): number | undefined { const index = listItem.index(); const chipsCount = chipsCtx.chipsRef.current.length; const selected = root.selectedValue() as unknown; const selectedArr = Array.isArray(selected) ? (selected as unknown[]) : []; let nextIndex: number | undefined = index; if (event.key === "ArrowLeft") { event.preventDefault(); nextIndex = index > 0 ? index - 1 : undefined; } else if (event.key === "ArrowRight") { event.preventDefault(); nextIndex = index < chipsCount - 1 ? index + 1 : undefined; } else if (event.key === "Backspace" || event.key === "Delete") { const computedNext = index >= selectedArr.length - 1 ? selectedArr.length - 2 : index; nextIndex = computedNext >= 0 ? computedNext : undefined; stopEvent(event); root.setIndices({ activeIndex: null, selectedIndex: null, type: "keyboard", }); const details = createChangeEventDetails( REASONS.none, event, ) as ComboboxRootChangeEventDetails; root.setSelectedValue( selectedArr.filter((_, i) => i !== index) as never, details, ); } else if (event.key === "Enter" || event.key === " ") { stopEvent(event); nextIndex = undefined; } else if (event.key === "ArrowDown" || event.key === "ArrowUp") { stopEvent(event); const details = createChangeEventDetails( REASONS.listNavigation, event, ) as ComboboxRootChangeEventDetails; root.setOpen(true, details); nextIndex = undefined; } else if ( event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey ) { nextIndex = undefined; } return nextIndex; } function onKeyDown(event: KeyboardEvent): void { if (disabled() || readOnly()) return; const next = handleKeyDown(event); chipsCtx.setHighlightedChipIndex(next); if (next === undefined) { root.inputRef.current?.focus(); } else { chipsCtx.chipsRef.current[next]?.focus(); } } const internal = { tabIndex: -1, get "aria-disabled"() { return disabled() ? ("true" as const) : undefined; }, get "aria-readonly"() { return readOnly() ? ("true" as const) : undefined; }, onKeyDown, // Default state-attr projection: data-disabled present when true. get "data-disabled"() { return state().disabled ? "" : undefined; }, get children() { return props.children; }, }; const userClassStyle = resolveClassStyle(props, () => state()); const userProps = omit(props, "as", "class", "style", "attrs", "children"); let merged: Record = mergeRootProps( internal, userClassStyle, ); merged = mergeRootProps(merged, userProps); const Tag = dynamic(() => props.as ?? "div"); return ( ); }