@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
184 lines (183 loc) • 5.62 kB
JavaScript
"use client";
import { rootVariants, styles, thumbVariants } from "./style.mjs";
import { createContext, use, useEffect, useMemo, useRef, useState } from "react";
import { jsx } from "react/jsx-runtime";
import { cx } from "antd-style";
import useMergeState from "use-merge-value";
import { Switch } from "@base-ui/react/switch";
import { animate, motionValue } from "motion";
//#region src/base-ui/Switch/atoms.tsx
const THUMB_METRICS = {
default: {
checkedX: 14,
pressedCheckedX: 10,
pressedWidth: 22,
width: 18
},
small: {
checkedX: 12,
pressedCheckedX: 8,
pressedWidth: 16,
width: 12
}
};
const THUMB_SPRING = {
damping: 24,
stiffness: 360,
type: "spring"
};
const SwitchContext = createContext(null);
const useSwitchContext = () => {
const context = use(SwitchContext);
if (!context) throw new Error("useSwitchContext must be used within a SwitchRoot");
return context;
};
const SwitchRoot = ({ checked, className, defaultChecked, onCheckedChange, onClick, onKeyDown, onKeyUp, onPointerCancel, onPointerDown, onPointerLeave, onPointerUp, size = "default", children, disabled, readOnly, required, inputRef, id, name, ...rest }) => {
const [isPressed, setIsPressed] = useState(false);
const lastEventRef = useRef(null);
const [isChecked, setIsChecked] = useMergeState(defaultChecked ?? false, {
defaultValue: defaultChecked,
onChange: (value) => {
if (lastEventRef.current) onCheckedChange?.(value, lastEventRef.current);
},
value: checked
});
const baseClassName = rootVariants({ size });
const contextValue = useMemo(() => ({
isChecked: Boolean(isChecked),
isPressed,
setIsChecked: (value) => setIsChecked(value),
setIsPressed
}), [
isChecked,
isPressed,
setIsChecked
]);
const handleClick = (event) => {
lastEventRef.current = event;
onClick?.(!isChecked, event);
};
const isInteractive = !disabled && !readOnly;
const handleKeyDown = (event) => {
if (event.key === "Enter" || event.key === " ") lastEventRef.current = event;
if (event.key === " " && isInteractive) setIsPressed(true);
onKeyDown?.(event);
};
const handleKeyUp = (event) => {
if (event.key === " ") setIsPressed(false);
onKeyUp?.(event);
};
const handlePointerDown = (event) => {
if (isInteractive) setIsPressed(true);
onPointerDown?.(event);
};
const handlePointerUp = (event) => {
setIsPressed(false);
onPointerUp?.(event);
};
const handlePointerCancel = (event) => {
setIsPressed(false);
onPointerCancel?.(event);
};
const handlePointerLeave = (event) => {
setIsPressed(false);
onPointerLeave?.(event);
};
return /* @__PURE__ */ jsx(SwitchContext, {
value: contextValue,
children: /* @__PURE__ */ jsx(Switch.Root, {
nativeButton: true,
checked: isChecked,
defaultChecked,
disabled,
id,
inputRef,
name,
readOnly,
required,
render: /* @__PURE__ */ jsx("button", {
...rest,
className: cx(baseClassName, className),
onClick: handleClick,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
onPointerCancel: handlePointerCancel,
onPointerDown: handlePointerDown,
onPointerLeave: handlePointerLeave,
onPointerUp: handlePointerUp
}),
onCheckedChange: setIsChecked,
children
})
});
};
SwitchRoot.displayName = "SwitchRoot";
const SwitchThumb = ({ className, size = "default", style, children, ...rest }) => {
const { isChecked, isPressed } = useSwitchContext();
const ref = useRef(null);
const baseClassName = thumbVariants({ size });
const metrics = THUMB_METRICS[size];
const targetX = isChecked ? isPressed ? metrics.pressedCheckedX : metrics.checkedX : 0;
const targetWidth = isPressed ? metrics.pressedWidth : metrics.width;
const [values] = useState(() => ({
width: motionValue(targetWidth),
x: motionValue(targetX)
}));
const [initialStyle] = useState(() => ({
"--switch-x": `${targetX}px`,
"width": targetWidth
}));
useEffect(() => {
const el = ref.current;
if (!el) return;
const unsubscribeX = values.x.on("change", (x) => {
el.style.setProperty("--switch-x", `${x}px`);
});
const unsubscribeWidth = values.width.on("change", (width) => {
el.style.setProperty("width", `${width}px`);
});
return () => {
unsubscribeX();
unsubscribeWidth();
};
}, [values]);
useEffect(() => {
const transition = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false ? { duration: 0 } : THUMB_SPRING;
const animations = [animate(values.x, targetX, transition), animate(values.width, targetWidth, transition)];
return () => {
for (const animation of animations) animation.stop();
};
}, [
values,
targetX,
targetWidth
]);
return /* @__PURE__ */ jsx(Switch.Thumb, { render: /* @__PURE__ */ jsx("span", {
...rest,
className: cx(baseClassName, className),
ref,
style: {
...initialStyle,
...style
},
children
}) });
};
SwitchThumb.displayName = "SwitchThumb";
const getIconPositionClass = (position, size) => {
if (position === "thumb") return styles.iconThumb;
if (position === "left") return size === "small" ? styles.iconLeftSmall : styles.iconLeft;
return size === "small" ? styles.iconRightSmall : styles.iconRight;
};
const SwitchIcon = ({ children, className, position, size = "default", ...rest }) => {
const positionClass = getIconPositionClass(position, size);
return /* @__PURE__ */ jsx("span", {
className: cx(styles.icon, positionClass, className),
...rest,
children
});
};
SwitchIcon.displayName = "SwitchIcon";
//#endregion
export { SwitchIcon, SwitchRoot, SwitchThumb, styles as switchStyles, useSwitchContext };
//# sourceMappingURL=atoms.mjs.map