import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { createEffect, createMemo, omit, onCleanup } from "solid-js"; import { composeRefs, userRef } from "../../internals/compose-refs"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps, NativeButtonProps, } from "../../internals/types"; import { useButton } from "../../internals/use-button/use-button"; import { Timeout } from "../../internals/use-timeout"; import { useSelectRootContext } from "../select-root-context"; const SELECTED_DELAY = 400; const UNSELECTED_DELAY = 200; export interface SelectTriggerState { open: boolean; disabled: boolean; // biome-ignore lint/suspicious/noExplicitAny: select is value-generic. value: any; readOnly: boolean; placeholder: boolean; } export interface SelectTriggerProps extends NativeButtonProps, BaseUIComponentProps<"button", SelectTriggerState> { as?: ValidComponent; disabled?: boolean | undefined; } export function SelectTrigger(props: SelectTriggerProps): JSX.Element { const root = useSelectRootContext(); const disabled = (): boolean => Boolean(props.disabled) || root.disabled(); const nativeButton = (): boolean => props.nativeButton ?? true; const { props: buttonProps, ref: buttonRef } = useButton({ disabled, focusableWhenDisabled: () => false, native: nativeButton, }); const setTriggerRef = (el: HTMLElement | null): void => { root.setTriggerElement(el); }; const hasSelectedValue = createMemo(() => { const v = root.value(); if (v == null) return false; if (root.multiple() && Array.isArray(v)) return v.length > 0; return true; }); const state = createMemo(() => ({ open: root.open(), disabled: disabled(), value: root.value(), readOnly: root.readOnly(), placeholder: !hasSelectedValue(), })); const selectedDelayTimeout = Timeout.create(); const unselectedDelayTimeout = Timeout.create(); onCleanup(() => { selectedDelayTimeout.clear(); unselectedDelayTimeout.clear(); }); createEffect( () => ({ isOpen: root.open(), hasSelected: hasSelectedValue() }), ({ isOpen, hasSelected }) => { if (isOpen) { if (!hasSelected) { selectedDelayTimeout.start(SELECTED_DELAY, () => { root.selectionRef.current.allowUnselectedMouseUp = true; root.selectionRef.current.allowSelectedMouseUp = true; }); } else { unselectedDelayTimeout.start(UNSELECTED_DELAY, () => { root.selectionRef.current.allowUnselectedMouseUp = true; selectedDelayTimeout.start(UNSELECTED_DELAY, () => { root.selectionRef.current.allowSelectedMouseUp = true; }); }); } return; } selectedDelayTimeout.clear(); unselectedDelayTimeout.clear(); }, ); const internal = { get id() { return root.id(); }, role: "combobox", "aria-haspopup": "listbox", get "aria-expanded"() { return root.open() ? "true" : "false"; }, get "aria-controls"() { if (!root.open()) return ""; const listEl = root.listElement(); if (listEl?.id) return listEl.id; return `${root.id() ?? ""}-list`; }, get "aria-labelledby"() { return root.labelId() ?? ""; }, get "aria-readonly"() { return root.readOnly() ? "true" : undefined; }, get "aria-required"() { return root.required() ? "true" : undefined; }, get tabindex() { return disabled() ? -1 : 0; }, // pressableTriggerOpenStateMapping: open → data-popup-open + data-pressed. // Default projection covers the rest of state (P3 — manually enumerated). get "data-popup-open"() { return state().open ? "" : undefined; }, get "data-pressed"() { return state().open ? "" : undefined; }, get "data-disabled"() { return state().disabled ? "" : undefined; }, get "data-readonly"() { return state().readOnly ? "" : undefined; }, get "data-placeholder"() { return state().placeholder ? "" : undefined; }, get "data-value"() { const v = state().value; if (v == null || v === false) return undefined; return String(v); }, }; const userClassStyle = resolveClassStyle(props, () => state()); const userProps = omit( props, "as", "class", "style", "attrs", "disabled", "nativeButton", ); let merged: Record = mergeRootProps( root.interactions.getReferenceProps(), buttonProps, ); merged = mergeRootProps(merged, internal); merged = mergeRootProps(merged, userClassStyle); merged = mergeRootProps(merged, userProps); const Tag = dynamic(() => props.as ?? "button"); return ( ); }