Buckets:
| import { | |
| createContext, | |
| createMemo, | |
| createSignal, | |
| mergeProps, | |
| splitProps, | |
| useContext, | |
| type Accessor, | |
| type JSX, | |
| type ParentProps, | |
| } from "solid-js" | |
| import type { ComponentProps } from "solid-js" | |
| import "./segmented-control-v2.css" | |
| type OnChange = (value: string | null) => void | |
| type SegmentedControlContextValue = { | |
| selected: Accessor<string | null> | |
| groupDisabled: Accessor<boolean> | |
| select: (value: string) => void | |
| clearIfAllowed: (value: string) => void | |
| focusNext: (from: HTMLButtonElement, direction: 1 | -1) => void | |
| } | |
| const SegmentedControlContext = createContext<SegmentedControlContextValue>() | |
| function useSegmentedControlContext() { | |
| const ctx = useContext(SegmentedControlContext) | |
| if (!ctx) throw new Error("SegmentedControlItemV2 must be used inside SegmentedControlV2") | |
| return ctx | |
| } | |
| export type SegmentedControlV2Props = Omit<ComponentProps<"div">, "onChange"> & | |
| ParentProps<{ | |
| /** Selected value when controlled (including `null` when empty). Omit key for uncontrolled. */ | |
| value?: string | null | |
| /** Initial value when uncontrolled. */ | |
| defaultValue?: string | |
| onChange?: OnChange | |
| /** When true, clicking the active segment clears selection (`onChange(null)`). Default false. */ | |
| allowDeselect?: boolean | |
| disabled?: boolean | |
| }> | |
| export function SegmentedControlV2(props: SegmentedControlV2Props) { | |
| const isControlled = createMemo(() => Object.hasOwn(props as object, "value")) | |
| const merged = mergeProps({ allowDeselect: false, disabled: false }, props) | |
| const [local, rest] = splitProps(merged, [ | |
| "class", | |
| "classList", | |
| "children", | |
| "value", | |
| "defaultValue", | |
| "onChange", | |
| "allowDeselect", | |
| "disabled", | |
| "ref", | |
| ]) | |
| const [internal, setInternal] = createSignal<string | null>(local.defaultValue ?? null) | |
| const selected = createMemo(() => (isControlled() ? (local.value ?? null) : internal())) | |
| const setSelected = (next: string | null) => { | |
| if (!isControlled()) setInternal(next) | |
| local.onChange?.(next) | |
| } | |
| const select = (value: string) => { | |
| setSelected(value) | |
| } | |
| const clearIfAllowed = (value: string) => { | |
| if (!local.allowDeselect || selected() !== value) return | |
| setSelected(null) | |
| } | |
| const focusNext = (from: HTMLButtonElement, direction: 1 | -1) => { | |
| const root = from.closest(`[data-slot="segmented-control-v2"]`) | |
| if (!root) return | |
| const buttons = Array.from( | |
| root.querySelectorAll<HTMLButtonElement>(`button[data-slot="segmented-control-v2-item"]`), | |
| ).filter((b) => !b.disabled) | |
| const i = buttons.indexOf(from) | |
| const next = buttons[i + direction] | |
| next?.focus() | |
| } | |
| const ctx: SegmentedControlContextValue = { | |
| selected, | |
| groupDisabled: () => !!local.disabled, | |
| select, | |
| clearIfAllowed, | |
| focusNext, | |
| } | |
| const assignRef = (el: HTMLDivElement | undefined) => { | |
| const r = local.ref | |
| if (typeof r === "function") (r as (el: HTMLDivElement | undefined) => void)(el) | |
| else if (r != null && typeof r === "object" && "value" in r) (r as { value: HTMLDivElement | undefined }).value = el | |
| } | |
| return ( | |
| <SegmentedControlContext.Provider value={ctx}> | |
| <div | |
| {...rest} | |
| ref={assignRef} | |
| role="group" | |
| data-component="segmented-control-v2" | |
| data-slot="segmented-control-v2" | |
| classList={{ | |
| ...local.classList, | |
| [local.class ?? ""]: !!local.class, | |
| }} | |
| > | |
| {local.children} | |
| </div> | |
| </SegmentedControlContext.Provider> | |
| ) | |
| } | |
| export type SegmentedControlItemV2Props = Omit<ComponentProps<"button">, "type" | "children"> & | |
| ParentProps<{ | |
| value: string | |
| children: JSX.Element | |
| }> | |
| function invokeButtonHandler<E extends Event>( | |
| handler: JSX.EventHandlerUnion<HTMLButtonElement, E> | undefined, | |
| e: E & { currentTarget: HTMLButtonElement }, | |
| ) { | |
| if (typeof handler === "function") (handler as (ev: typeof e) => void)(e) | |
| } | |
| export function SegmentedControlItemV2(props: SegmentedControlItemV2Props) { | |
| const merged = mergeProps({ disabled: false }, props) | |
| const [local, rest] = splitProps(merged, [ | |
| "class", | |
| "classList", | |
| "children", | |
| "value", | |
| "disabled", | |
| "onClick", | |
| "onKeyDown", | |
| ]) | |
| const ctx = useSegmentedControlContext() | |
| const pressed = createMemo(() => ctx.selected() === local.value) | |
| const disabled = createMemo(() => ctx.groupDisabled() || !!local.disabled) | |
| const onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => { | |
| invokeButtonHandler(local.onClick, e) | |
| if (e.defaultPrevented || disabled()) return | |
| if (pressed()) ctx.clearIfAllowed(local.value) | |
| else ctx.select(local.value) | |
| } | |
| const onKeyDown: JSX.EventHandlerUnion<HTMLButtonElement, KeyboardEvent> = (e) => { | |
| invokeButtonHandler(local.onKeyDown, e) | |
| if (e.defaultPrevented || disabled()) return | |
| const t = e.currentTarget | |
| if (e.key === "ArrowRight") { | |
| e.preventDefault() | |
| ctx.focusNext(t, 1) | |
| } else if (e.key === "ArrowLeft") { | |
| e.preventDefault() | |
| ctx.focusNext(t, -1) | |
| } | |
| // accessibility stuff | |
| else if (e.key === "Home") { | |
| e.preventDefault() | |
| const root = t.closest(`[data-slot="segmented-control-v2"]`) | |
| const first = root?.querySelector<HTMLButtonElement>( | |
| `button[data-slot="segmented-control-v2-item"]:not(:disabled)`, | |
| ) | |
| first?.focus() | |
| } else if (e.key === "End") { | |
| e.preventDefault() | |
| const root = t.closest(`[data-slot="segmented-control-v2"]`) | |
| const buttons = root?.querySelectorAll<HTMLButtonElement>( | |
| `button[data-slot="segmented-control-v2-item"]:not(:disabled)`, | |
| ) | |
| const last = buttons?.[buttons.length - 1] | |
| last?.focus() | |
| } | |
| } | |
| return ( | |
| <button | |
| {...rest} | |
| type="button" | |
| data-slot="segmented-control-v2-item" | |
| data-pressed={pressed() ? "" : undefined} | |
| aria-pressed={pressed()} | |
| disabled={disabled()} | |
| classList={{ | |
| ...local.classList, | |
| [local.class ?? ""]: !!local.class, | |
| }} | |
| onClick={onClick} | |
| onKeyDown={onKeyDown} | |
| > | |
| <span data-slot="segmented-control-v2-item-label">{local.children}</span> | |
| </button> | |
| ) | |
| } | |
Xet Storage Details
- Size:
- 6.2 kB
- Xet hash:
- 5defbfddbd57009fe8ac17e9f72c5e529bd60f87e0acd3aeede13dd8110a77a9
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.