import { createEffect, createMemo, createSignal, mergeProps, onCleanup, splitProps, untrack, type Accessor, type Component, type JSX, type Signal } from "solid-js"; import { FilledField, type Field } from "../field"; import { createEventListenerMap, createEventSignal, type EventMapOf, type TargetWithEventMap } from "@solid-primitives/event-listener"; import { access, type MaybeAccessor } from "@solid-primitives/utils"; import type { Override } from "@material-solid/utils/types"; import { Dynamic } from "solid-js/web"; import { styles } from "./text-field.css"; import type { Ref, RefCallback } from "@material-solid/utils/refs"; const createMatches = < Target extends Element, Events extends keyof EventMapOf, // HandlersMap extends Partial> >( target: MaybeAccessor, selectors: MaybeAccessor, event: MaybeAccessor, initialValue: boolean = false, ): Accessor => { const [matches, setMatches] = createSignal(initialValue); createEffect(() => { const element = access(target); const events = access(event); const selector = access(selectors); const listener = () => { setMatches(element.matches(selector)); } events.forEach(event => element.addEventListener(event as string, listener)); onCleanup(() => { events.forEach(event => element.removeEventListener(event as string, listener)) }); }); return matches; } const createFocus = (target: MaybeAccessor): Accessor => { const [focused, setFocused] = createSignal(false); const handleFocusChange = () => { const element = access(target); setFocused(element.matches(":focus")); } createEventListenerMap( target, { focus: handleFocusChange, blur: handleFocusChange, }, ); return focused; } const createHover = (target: MaybeAccessor): Accessor => { const [hovered, setHovered] = createSignal(false); const handlePointerEvent = () => { const element = access(target); setHovered(element.matches(":hover")); } createEventListenerMap( target, { pointerenter: handlePointerEvent, pointerleave: handlePointerEvent, }, ); return hovered; } export type TextFieldType = | "email" | "number" | "password" | "search" | "tel" | "text" | "url" | "textarea"; type ResolvableProps< Resolved extends boolean = false, SharedProps = {}, UnresolvedProps = {}, ResolvedProps = {}, > = Resolved extends true ? Override : Override; type TextFieldInputElement = HTMLInputElement | HTMLTextAreaElement; export namespace TextField { export type Props = & ProtectedProps & PublicProps; export type ProtectedProps = { fieldComponent: Component; } export type PublicProps< Resolved extends boolean = false > = ResolvableProps< Resolved, { ref?: Ref; onChange?: JSX.ChangeEventHandlerUnion< TextFieldInputElement, Event >; onInput?: JSX.InputEventHandlerUnion< TextFieldInputElement, InputEvent >; onSelect?: JSX.EventHandlerUnion< TextFieldInputElement, Event>; }, { type?: TextFieldType; value?: string; label?: string; placeholder?: string; disabled?: boolean; required?: boolean; }, { type: TextFieldType; value: string; placeholder: string; label: string; disabled: boolean; required: boolean; } >; export interface Element { focus(): void; } } export const TextField = < Resolved extends boolean = false >( props: TextField.Props, ) => { const mergedProps = mergeProps( { type: "text", value: "", placeholder: "", label: "", required: false, disabled: false, } as TextField.Props, props, ) as TextField.Props; const [, local] = splitProps( mergedProps, [], ); const [inputRef, setInputRef] = createSignal() as Signal; const focused = createFocus(inputRef); const useTextarea = createMemo(() => local.type === "textarea"); createEffect(() => { const element: TextField.Element = { focus: () => inputRef()?.focus(), }; (local.ref as RefCallback | undefined) ?.(element); }); return ( > component={local.fieldComponent} focused={focused()} start={} end={} label={local.label} disabled={local.disabled} required={local.required} populated={!!local.value} content={
{/* // TODO: prefix */} {/* //TODO: suffix */}
} />
); }