UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

464 lines (457 loc) • 16.4 kB
import { createDomCollectionItem, createDomCollection } from '../chunk/7CVNMTYF.js'; import { useLocale } from '../chunk/7TPOQ36Z.js'; import { FormControlLabel } from '../chunk/7ZHN3PYD.js'; import { createFormResetListener } from '../chunk/ANN3A2QM.js'; import { FormControlErrorMessage } from '../chunk/ICNSTULC.js'; export { FormControlErrorMessage as ErrorMessage } from '../chunk/ICNSTULC.js'; import { FormControlDescription, useFormControlContext, FORM_CONTROL_PROP_NAMES, createFormControl, FormControlContext } from '../chunk/YKGT7A57.js'; export { FormControlDescription as Description } from '../chunk/YKGT7A57.js'; import { createRegisterId } from '../chunk/E4R2EMM4.js'; import { createControllableSignal } from '../chunk/BLN63FDC.js'; import { Polymorphic } from '../chunk/6Y7B2NEO.js'; import { createComponent, mergeProps, spread, effect, style, memo, template } from 'solid-js/web'; import { mergeDefaultProps, visuallyHiddenStyles, createGenerateId, mergeRefs, isFunction, access, callHandler, EventKey } from '@kobalte/utils'; import { createContext, splitProps, createUniqueId, createSignal, onMount, createMemo, children, createEffect, onCleanup, useContext } from 'solid-js'; import { combineStyle } from '@solid-primitives/props'; var RatingContext = createContext(); function useRatingContext() { const context = useContext(RatingContext); if (context === void 0) { throw new Error("[kobalte]: `useRatingContext` must be used within a `Rating` component"); } return context; } // src/rating/rating-control.tsx function RatingControl(props) { const formControlContext = useFormControlContext(); const context = useRatingContext(); const defaultId = `${formControlContext.generateId("control")}`; const mergedProps = mergeDefaultProps({ id: defaultId }, props); const [local, others] = splitProps(mergedProps, ["onPointerLeave"]); const onPointerLeave = (e) => { if (formControlContext.isDisabled() || formControlContext.isReadOnly()) return; callHandler(e, local.onPointerLeave); if (e.pointerType === "touch") { return; } context.setHoveredValue(-1); }; return createComponent(Polymorphic, mergeProps({ as: "div", role: "presentation", onPointerLeave }, others)); } var _tmpl$ = /* @__PURE__ */ template(`<input type="text" tabindex="-1">`); function RatingHiddenInput(props) { const formControlContext = useFormControlContext(); const context = useRatingContext(); return (() => { const _el$ = _tmpl$(); spread(_el$, mergeProps({ get name() { return formControlContext.name(); }, get value() { return context.value(); }, get required() { return formControlContext.isRequired(); }, get disabled() { return formControlContext.isDisabled(); }, get readOnly() { return formControlContext.isReadOnly(); } }, props), false, false); effect((_$p) => style(_el$, visuallyHiddenStyles, _$p)); return _el$; })(); } var RatingItemContext = createContext(); function useRatingItemContext() { const context = useContext(RatingItemContext); if (context === void 0) { throw new Error("[kobalte]: `useRatingItemContext` must be used within a `Rating.Item` component"); } return context; } // src/rating/utils.ts function clamp(value) { return Math.max(0, Math.min(1, value)); } function pointFromTouch(e, type = "client") { const point = e.touches[0] || e.changedTouches[0]; return { x: point[`${type}X`], y: point[`${type}Y`] }; } function pointFromMouse(point, type = "client") { return { x: point[`${type}X`], y: point[`${type}Y`] }; } var isTouchEvent = (event) => "touches" in event && event.touches.length > 0; function getEventPoint(event, type = "client") { return isTouchEvent(event) ? pointFromTouch(event, type) : pointFromMouse(event, type); } function getRelativePoint(point, element) { const { left, top, width, height } = element.getBoundingClientRect(); const offset = { x: point.x - left, y: point.y - top }; const percent = { x: clamp(offset.x / width), y: clamp(offset.y / height) }; function getPercentValue(options = {}) { const { dir = "ltr", orientation = "horizontal", inverted } = options; const invertX = typeof inverted === "object" ? inverted.x : inverted; const invertY = typeof inverted === "object" ? inverted.y : inverted; if (orientation === "horizontal") { return dir === "rtl" || invertX ? 1 - percent.x : percent.x; } return invertY ? 1 - percent.y : percent.y; } return { offset, percent, getPercentValue }; } // src/rating/rating-item.tsx function RatingItem(props) { let ref; const formControlContext = useFormControlContext(); const RatingContext2 = useRatingContext(); const defaultId = `${formControlContext.generateId("item")}-${createUniqueId()}`; const mergedProps = mergeDefaultProps({ id: defaultId }, props); const [local, others] = splitProps(mergedProps, ["ref", "aria-labelledby", "aria-describedby", "onClick", "onKeyDown", "onPointerMove"]); createDomCollectionItem({ getItem: () => ({ ref: () => ref, disabled: formControlContext.isDisabled(), key: others.id, textValue: "", type: "item" }) }); const ariaLabelledBy = () => { return [local["aria-labelledby"], labelId(), local["aria-labelledby"] != null && others["aria-label"] != null ? others.id : void 0].filter(Boolean).join(" ") || void 0; }; const ariaDescribedBy = () => { return [local["aria-describedby"], descriptionId(), RatingContext2.ariaDescribedBy()].filter(Boolean).join(" ") || void 0; }; const { direction } = useLocale(); const isLTR = () => direction() === "ltr"; const [labelId, setLabelId] = createSignal(); const [descriptionId, setDescriptionId] = createSignal(); const index = () => ref ? RatingContext2.items().findIndex((v) => v.ref() === ref) : -1; const [value, setValue] = createSignal(); const newValue = () => RatingContext2.isHovering() ? RatingContext2.hoveredValue() : RatingContext2.value(); const equal = () => Math.ceil(newValue()) === value(); const highlighted = () => value() <= newValue() || equal(); const half = () => equal() && Math.abs(newValue() - value()) === 0.5; onMount(() => { setValue(direction() === "ltr" ? index() + 1 : RatingContext2.items().length - index()); }); const tabIndex = () => { if (formControlContext.isDisabled()) return void 0; if (formControlContext.isReadOnly()) equal() ? 0 : void 0; return equal() ? 0 : -1; }; const focusItem = (index2) => RatingContext2.items()[Math.round(index2)].ref().focus(); const setPrevValue = () => { const factor = RatingContext2.allowHalf() ? 0.5 : 1; const value2 = Math.max(0, RatingContext2.value() - factor); RatingContext2.setValue(value2); focusItem(Math.max(value2 - 1, 0)); }; const setNextValue = () => { const factor = RatingContext2.allowHalf() ? 0.5 : 1; const value2 = Math.min(RatingContext2.items().length, (RatingContext2.value() === -1 ? 0 : RatingContext2.value()) + factor); RatingContext2.setValue(value2); focusItem(value2 - 1); }; const onClick = (e) => { callHandler(e, local.onClick); const value2 = RatingContext2.hoveredValue() === -1 ? index() + 1 : RatingContext2.hoveredValue(); RatingContext2.setValue(value2); RatingContext2.setHoveredValue(-1); focusItem(value2 - 1); }; const onPointerMove = (e) => { if (formControlContext.isDisabled() || formControlContext.isReadOnly()) return; callHandler(e, local.onPointerMove); const point = getEventPoint(e); const relativePoint = getRelativePoint(point, e.currentTarget); const percentX = relativePoint.getPercentValue({ orientation: RatingContext2.orientation(), dir: direction() }); const isMidway = percentX < 0.5; const half2 = RatingContext2.allowHalf() && isMidway; const factor = half2 ? 0.5 : 0; RatingContext2.setHoveredValue(value() - factor); }; const onKeyDown = (e) => { callHandler(e, local.onKeyDown); switch (e.key) { case EventKey.ArrowLeft: case EventKey.ArrowUp: e.preventDefault(); if (isLTR()) { setPrevValue(); } else { setNextValue(); } break; case EventKey.ArrowRight: case EventKey.ArrowDown: e.preventDefault(); if (isLTR()) { setNextValue(); } else { setPrevValue(); } break; case EventKey.Space: e.preventDefault(); RatingContext2.setValue(newValue()); break; case EventKey.Home: e.preventDefault(); RatingContext2.setValue(1); break; case EventKey.End: e.preventDefault(); RatingContext2.setValue(RatingContext2.items().length); break; } }; const dataset = createMemo(() => ({ ...formControlContext.dataset(), "data-checked": equal() ? "" : void 0, "data-half": half() ? "" : void 0, "data-highlighted": highlighted() ? "" : void 0 })); const context = { state: { highlighted, half }, dataset, generateId: createGenerateId(() => others.id), itemId: () => others.id, registerLabel: createRegisterId(setLabelId), registerDescription: createRegisterId(setDescriptionId) }; return createComponent(RatingItemContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs((el) => ref = el, local.ref); typeof _ref$ === "function" && _ref$(r$); }, role: "radio", get tabIndex() { return tabIndex(); }, get ["aria-checked"]() { return equal(); }, get ["aria-required"]() { return formControlContext.isRequired() || void 0; }, get ["aria-disabled"]() { return formControlContext.isDisabled() || void 0; }, get ["aria-readonly"]() { return formControlContext.isReadOnly() || void 0; }, get ["aria-labelledby"]() { return ariaLabelledBy(); }, get ["aria-describedby"]() { return ariaDescribedBy(); }, onClick, onPointerMove, onKeyDown }, dataset, others)); } }); } function RatingItemControl(props) { const context = useRatingItemContext(); const defaultId = `${context.generateId("control")}`; const mergedProps = mergeDefaultProps({ id: defaultId }, props); const [local, others] = splitProps(mergedProps, ["children"]); return createComponent(Polymorphic, mergeProps({ as: "div", role: "presentation" }, others, { get children() { return createComponent(RatingItemControlChild, { get state() { return { highlighted: context.state.highlighted, half: context.state.half }; }, get children() { return local.children; } }); } })); } function RatingItemControlChild(props) { const resolvedChildren = children(() => { const body = props.children; return isFunction(body) ? body(props.state) : body; }); return memo(resolvedChildren); } function RatingItemDescription(props) { const context = useRatingItemContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("description") }, props); createEffect(() => onCleanup(context.registerDescription(mergedProps.id))); return createComponent(Polymorphic, mergeProps({ as: "div" }, () => context.dataset(), mergedProps)); } function RatingItemLabel(props) { const context = useRatingItemContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("label") }, props); const [local, others] = splitProps(mergedProps, ["style"]); createEffect(() => onCleanup(context.registerLabel(others.id))); return createComponent(Polymorphic, mergeProps({ as: "label", get ["for"]() { return context.itemId(); }, get style() { return combineStyle(visuallyHiddenStyles, local.style); } }, () => context.dataset(), others)); } function RatingLabel(props) { return createComponent(FormControlLabel, mergeProps({ as: "span" }, props)); } function RatingRoot(props) { let ref; const defaultId = `Rating-${createUniqueId()}`; const mergedProps = mergeDefaultProps({ id: defaultId, orientation: "horizontal" }, props); const [local, formControlProps, others] = splitProps(mergedProps, ["ref", "value", "defaultValue", "onChange", "allowHalf", "orientation", "aria-labelledby", "aria-describedby"], FORM_CONTROL_PROP_NAMES); const [items, setItems] = createSignal([]); const { DomCollectionProvider } = createDomCollection({ items, onItemsChange: setItems }); const [hoveredValue, setHoveredValue] = createSignal(-1); const [value, setValue] = createControllableSignal({ value: () => local.value, defaultValue: () => local.defaultValue ?? 0, onChange: (value2) => local.onChange?.(value2) }); const { formControlContext } = createFormControl(formControlProps); createFormResetListener(() => ref, () => setValue(local.defaultValue)); const ariaLabelledBy = () => { return formControlContext.getAriaLabelledBy(access(formControlProps.id), others["aria-label"], local["aria-labelledby"]); }; const ariaDescribedBy = () => { return formControlContext.getAriaDescribedBy(local["aria-describedby"]); }; const context = { value, setValue: (newValue) => { if (formControlContext.isReadOnly() || formControlContext.isDisabled()) { return; } setValue(newValue); }, allowHalf: () => local.allowHalf, orientation: () => local.orientation, hoveredValue, setHoveredValue, isHovering: () => hoveredValue() > -1, ariaDescribedBy, items, setItems }; return createComponent(DomCollectionProvider, { get children() { return createComponent(FormControlContext.Provider, { value: formControlContext, get children() { return createComponent(RatingContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs((el) => ref = el, local.ref); typeof _ref$ === "function" && _ref$(r$); }, role: "radiogroup", get id() { return access(formControlProps.id); }, get ["aria-invalid"]() { return formControlContext.validationState() === "invalid" || void 0; }, get ["aria-required"]() { return formControlContext.isRequired() || void 0; }, get ["aria-disabled"]() { return formControlContext.isDisabled() || void 0; }, get ["aria-readonly"]() { return formControlContext.isReadOnly() || void 0; }, get ["aria-orientation"]() { return local.orientation; }, get ["aria-labelledby"]() { return ariaLabelledBy(); }, get ["aria-describedby"]() { return ariaDescribedBy(); } }, () => formControlContext.dataset(), others)); } }); } }); } }); } // src/rating/index.tsx var Rating = Object.assign(RatingRoot, { Description: FormControlDescription, ErrorMessage: FormControlErrorMessage, Control: RatingControl, HiddenInput: RatingHiddenInput, ItemControl: RatingItemControl, ItemDescription: RatingItemDescription, ItemLabel: RatingItemLabel, Item: RatingItem, Label: RatingLabel }); export { RatingControl as Control, RatingHiddenInput as HiddenInput, RatingItem as Item, RatingItemControl as ItemControl, RatingItemDescription as ItemDescription, RatingItemLabel as ItemLabel, RatingLabel as Label, Rating, RatingRoot as Root };