import { createContext, createEffect, createSignal, createUniqueId, onCleanup, onMount, splitProps, useContext, Show, type ComponentProps, type ParentProps, } from "solid-js" import { TooltipV2 } from "./tooltip-v2" import "./field-v2.css" type FieldContextValue = { controlId: string labelId: string prefixId: string suffixId: string invalid: () => boolean registerPrefix: () => void unregisterPrefix: () => void registerSuffix: () => void unregisterSuffix: () => void getDescribedBy: () => string | undefined } const FieldContext = createContext() function useField() { const ctx = useContext(FieldContext) if (!ctx) { throw new Error("Field subcomponents must be used within ") } return ctx } const CONTROL_SELECTOR = [ "[data-slot='text-input-v2-input']", "[data-slot='textarea-v2-textarea']", "[data-slot='inline-input-v2-input']", ].join(", ") export interface FieldV2Props extends ComponentProps<"div"> { invalid?: boolean } function FieldV2Root(props: ParentProps) { const [local, rest] = splitProps(props, ["invalid", "class", "classList", "children"]) const controlId = `field-control-${createUniqueId()}` const labelId = `field-label-${createUniqueId()}` const prefixId = `field-prefix-${createUniqueId()}` const suffixId = `field-suffix-${createUniqueId()}` const [prefixCount, setPrefixCount] = createSignal(0) const [suffixCount, setSuffixCount] = createSignal(0) let rootRef: HTMLDivElement | undefined const ctx: FieldContextValue = { controlId, labelId, prefixId, suffixId, invalid: () => !!local.invalid, registerPrefix: () => setPrefixCount((n) => n + 1), unregisterPrefix: () => setPrefixCount((n) => Math.max(0, n - 1)), registerSuffix: () => setSuffixCount((n) => n + 1), unregisterSuffix: () => setSuffixCount((n) => Math.max(0, n - 1)), getDescribedBy: () => { const ids: string[] = [] if (prefixCount() > 0) ids.push(prefixId) if (suffixCount() > 0) ids.push(suffixId) return ids.length > 0 ? ids.join(" ") : undefined }, } const syncControlA11y = () => { const root = rootRef if (!root) return const control = root.querySelector(CONTROL_SELECTOR) as HTMLInputElement | HTMLTextAreaElement | null if (!control) return const shell = control.closest( "[data-component='text-input-v2'], [data-component='textarea-v2'], [data-component='inline-input-v2']", ) as HTMLElement | null control.id = controlId control.setAttribute("aria-labelledby", labelId) const describedBy = ctx.getDescribedBy() if (describedBy) { control.setAttribute("aria-describedby", describedBy) } else { control.removeAttribute("aria-describedby") } if (ctx.invalid()) { control.setAttribute("aria-invalid", "true") shell?.setAttribute("data-invalid", "") } else { control.removeAttribute("aria-invalid") shell?.removeAttribute("data-invalid") } } onMount(() => { syncControlA11y() }) createEffect(() => { prefixCount() suffixCount() local.invalid syncControlA11y() }) return (
{local.children}
) } function FieldLabelInfoIcon() { return ( ) } export interface FieldLabelProps extends ComponentProps<"label"> { /** When set, shows the info icon with a tooltip containing this text. */ tooltip?: string } function FieldLabel(props: ParentProps) { const [local, rest] = splitProps(props, ["class", "classList", "children", "tooltip"]) const field = useField() return ( ) } function FieldPrefix(props: ParentProps>) { const [local, rest] = splitProps(props, ["class", "classList", "children"]) const field = useField() onMount(() => { field.registerPrefix() onCleanup(() => field.unregisterPrefix()) }) return (
{local.children}
) } function FieldSuffix(props: ParentProps>) { const [local, rest] = splitProps(props, ["class", "classList", "children"]) const field = useField() onMount(() => { field.registerSuffix() onCleanup(() => field.unregisterSuffix()) }) return (
{local.children}
) } /** Optional layout wrapper around the control. */ function FieldControl(props: ParentProps>) { const [local, rest] = splitProps(props, ["class", "classList", "children"]) return (
{local.children}
) } export const FieldV2 = Object.assign(FieldV2Root, { Label: FieldLabel, Prefix: FieldPrefix, Suffix: FieldSuffix, Control: FieldControl, }) export const Field = FieldV2