mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(switch): data management improved, animation improved
This commit is contained in:
parent
af7d07ab66
commit
da5aa53a6c
@ -3,9 +3,9 @@ import type {FocusableRef} from "@react-types/shared";
|
||||
import type {AriaSwitchProps} from "@react-aria/switch";
|
||||
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
|
||||
import {ReactNode, Ref, useCallback, useId, useRef} from "react";
|
||||
import {ReactNode, Ref, useCallback, useId, useRef, useState} from "react";
|
||||
import {mapPropsVariants} from "@nextui-org/system";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
import {useHover, usePress} from "@react-aria/interactions";
|
||||
import {toggle} from "@nextui-org/theme";
|
||||
import {chain, mergeProps} from "@react-aria/utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
@ -82,7 +82,7 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
as,
|
||||
name,
|
||||
value = "",
|
||||
isReadOnly = false,
|
||||
isReadOnly: isReadOnlyProp = false,
|
||||
autoFocus = false,
|
||||
startIcon,
|
||||
endIcon,
|
||||
@ -116,7 +116,7 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
defaultSelected,
|
||||
isSelected: isSelectedProp,
|
||||
isDisabled: !!originalProps.isDisabled,
|
||||
isReadOnly,
|
||||
isReadOnly: isReadOnlyProp,
|
||||
"aria-label": ariaLabel,
|
||||
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
|
||||
onChange: onValueChange,
|
||||
@ -127,6 +127,7 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
labelId,
|
||||
children,
|
||||
autoFocus,
|
||||
isReadOnlyProp,
|
||||
isSelectedProp,
|
||||
defaultSelected,
|
||||
originalProps.isDisabled,
|
||||
@ -136,12 +137,38 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
]);
|
||||
|
||||
const state = useToggleState(ariaSwitchProps);
|
||||
const {inputProps} = useReactAriaSwitch(ariaSwitchProps, state, inputRef);
|
||||
|
||||
const {inputProps, isPressed: isPressedKeyboard, isReadOnly} = useReactAriaSwitch(
|
||||
ariaSwitchProps,
|
||||
state,
|
||||
inputRef,
|
||||
);
|
||||
const {focusProps, isFocused, isFocusVisible} = useFocusRing({autoFocus: inputProps.autoFocus});
|
||||
const {hoverProps, isHovered} = useHover({
|
||||
isDisabled: inputProps.disabled,
|
||||
});
|
||||
|
||||
const isInteractionDisabled = ariaSwitchProps.isDisabled || isReadOnly;
|
||||
|
||||
// Handle press state for full label. Keyboard press state is returned by useSwitch
|
||||
// since it is handled on the <input> element itself.
|
||||
const [isPressed, setPressed] = useState(false);
|
||||
const {pressProps} = usePress({
|
||||
isDisabled: isInteractionDisabled,
|
||||
onPressStart(e) {
|
||||
if (e.pointerType !== "keyboard") {
|
||||
setPressed(true);
|
||||
}
|
||||
},
|
||||
onPressEnd(e) {
|
||||
if (e.pointerType !== "keyboard") {
|
||||
setPressed(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const pressed = isInteractionDisabled ? false : isPressed || isPressedKeyboard;
|
||||
|
||||
const isSelected = inputProps.checked;
|
||||
const isDisabled = inputProps.disabled;
|
||||
|
||||
@ -149,80 +176,64 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
() =>
|
||||
toggle({
|
||||
...variantProps,
|
||||
isFocusVisible,
|
||||
}),
|
||||
[...Object.values(variantProps), isFocusVisible],
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
|
||||
const getBaseProps: PropGetter = () => {
|
||||
const getBaseProps: PropGetter = (props) => {
|
||||
return {
|
||||
...mergeProps(hoverProps, pressProps, otherProps, props),
|
||||
ref: domRef,
|
||||
className: slots.base({class: baseStyles}),
|
||||
className: slots.base({class: clsx(baseStyles, props?.className)}),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
...mergeProps(hoverProps, otherProps),
|
||||
"data-readonly": dataAttr(isReadOnly),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-pressed": dataAttr(pressed),
|
||||
style: {
|
||||
...props?.style,
|
||||
WebkitTapHighlightColor: "transparent",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getWrapperProps: PropGetter = useCallback(() => {
|
||||
return {
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-readonly": dataAttr(inputProps.readOnly),
|
||||
"aria-hidden": true,
|
||||
className: clsx(slots.wrapper({class: classNames?.wrapper})),
|
||||
};
|
||||
}, [
|
||||
slots,
|
||||
classNames?.wrapper,
|
||||
isHovered,
|
||||
isSelected,
|
||||
isFocused,
|
||||
isFocusVisible,
|
||||
isDisabled,
|
||||
inputProps.readOnly,
|
||||
]);
|
||||
const getWrapperProps: PropGetter = useCallback(
|
||||
(props = {}) => {
|
||||
return {
|
||||
...props,
|
||||
"aria-hidden": true,
|
||||
className: clsx(slots.wrapper({class: clsx(classNames?.wrapper, props?.className)})),
|
||||
};
|
||||
},
|
||||
[slots, classNames?.wrapper],
|
||||
);
|
||||
|
||||
const getInputProps: PropGetter = () => {
|
||||
const getInputProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
...mergeProps(inputProps, focusProps, props),
|
||||
ref: inputRef,
|
||||
id: inputProps.id,
|
||||
...mergeProps(inputProps, focusProps),
|
||||
onChange: chain(onChange, inputProps.onChange),
|
||||
};
|
||||
};
|
||||
|
||||
const getThumbProps: PropGetter = useCallback(
|
||||
() => ({
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-readonly": dataAttr(inputProps.readOnly),
|
||||
className: slots.thumb({class: classNames?.thumb}),
|
||||
(props = {}) => ({
|
||||
...props,
|
||||
className: slots.thumb({class: clsx(classNames?.thumb, props?.className)}),
|
||||
}),
|
||||
[
|
||||
slots,
|
||||
classNames?.thumb,
|
||||
isSelected,
|
||||
isFocused,
|
||||
isFocusVisible,
|
||||
isDisabled,
|
||||
inputProps.readOnly,
|
||||
],
|
||||
[slots, classNames?.thumb],
|
||||
);
|
||||
|
||||
const getLabelProps: PropGetter = useCallback(
|
||||
() => ({
|
||||
(props = {}) => ({
|
||||
...props,
|
||||
id: labelId,
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
className: slots.label({class: classNames?.label}),
|
||||
className: slots.label({class: clsx(classNames?.label, props?.className)}),
|
||||
}),
|
||||
[slots, classNames?.label, isDisabled, isSelected],
|
||||
);
|
||||
@ -237,8 +248,7 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
{
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
"data-checked": dataAttr(isSelected),
|
||||
className: slots.thumbIcon({class: classNames?.thumbIcon}),
|
||||
className: slots.thumbIcon({class: clsx(classNames?.thumbIcon)}),
|
||||
},
|
||||
props.includeStateProps
|
||||
? {
|
||||
@ -246,25 +256,25 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
}
|
||||
: {},
|
||||
) as unknown) as SwitchThumbIconProps,
|
||||
[slots, classNames?.thumbIcon, isSelected, originalProps.disableAnimation],
|
||||
[slots, classNames?.thumbIcon, isSelected],
|
||||
);
|
||||
|
||||
const getStartIconProps = useCallback(
|
||||
() => ({
|
||||
const getStartIconProps = useCallback<PropGetter>(
|
||||
(props = {}) => ({
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
"data-checked": dataAttr(isSelected),
|
||||
className: slots.startIcon({class: classNames?.startIcon}),
|
||||
...props,
|
||||
className: slots.startIcon({class: clsx(classNames?.startIcon, props?.className)}),
|
||||
}),
|
||||
[slots, classNames?.startIcon, isSelected],
|
||||
);
|
||||
|
||||
const getEndIconProps = useCallback(
|
||||
() => ({
|
||||
const getEndIconProps = useCallback<PropGetter>(
|
||||
(props = {}) => ({
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
"data-checked": dataAttr(isSelected),
|
||||
className: slots.endIcon({class: classNames?.endIcon}),
|
||||
...props,
|
||||
className: slots.endIcon({class: clsx(classNames?.endIcon, props?.className)}),
|
||||
}),
|
||||
[slots, classNames?.endIcon, isSelected],
|
||||
);
|
||||
@ -280,6 +290,7 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
endIcon,
|
||||
isHovered,
|
||||
isSelected,
|
||||
isPressed: pressed,
|
||||
isFocused,
|
||||
isFocusVisible,
|
||||
isDisabled,
|
||||
|
||||
@ -2,17 +2,22 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {ringClasses} from "../utils";
|
||||
|
||||
/**
|
||||
* Toggle (Switch) wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const {base, wrapper, thumb, thumbIcon, label, startIcon, endIcon} = toggle({...})
|
||||
*
|
||||
* @example
|
||||
* <label className={base())}>
|
||||
* // hidden input
|
||||
* <span className={wrapper()} aria-hidden="true" data-checked={checked}>
|
||||
* <label
|
||||
* className={base())}
|
||||
* data-checked={checked}
|
||||
* data-pressed={pressed}
|
||||
* data-focus={focus}
|
||||
* data-hover={hover}
|
||||
* data-focus-visible={focusVisible}
|
||||
* >
|
||||
* <input/> // hidden input
|
||||
* <span className={wrapper()} aria-hidden="true">
|
||||
* <svg className={startIcon()}>...</svg>
|
||||
* <span className={thumb()}>
|
||||
* <svg className={thumbIcon()}>...</svg>
|
||||
@ -24,9 +29,9 @@ import {ringClasses} from "../utils";
|
||||
*/
|
||||
const toggle = tv({
|
||||
slots: {
|
||||
base: "relative max-w-fit inline-flex items-center justify-start cursor-pointer",
|
||||
base:
|
||||
"group relative max-w-fit inline-flex items-center justify-start cursor-pointer touch-none",
|
||||
wrapper: [
|
||||
"group",
|
||||
"px-1",
|
||||
"relative",
|
||||
"inline-flex",
|
||||
@ -36,6 +41,13 @@ const toggle = tv({
|
||||
"overflow-hidden",
|
||||
"bg-neutral-200",
|
||||
"rounded-full",
|
||||
// focus ring
|
||||
"group-data-[focus-visible]:outline-none",
|
||||
"group-data-[focus-visible]:ring-2",
|
||||
"group-data-[focus-visible]:!ring-primary",
|
||||
"group-data-[focus-visible]:ring-offset-2",
|
||||
"group-data-[focus-visible]:ring-offset-background",
|
||||
"group-data-[focus-visible]:dark:ring-offset-background-dark",
|
||||
],
|
||||
thumb: [
|
||||
"z-10",
|
||||
@ -45,7 +57,7 @@ const toggle = tv({
|
||||
"bg-white",
|
||||
"shadow-sm",
|
||||
"rounded-full",
|
||||
"data-[checked=true]:translate-x-full",
|
||||
"origin-right",
|
||||
],
|
||||
startIcon: "z-0 absolute left-1.5 text-current",
|
||||
endIcon: "z-0 absolute right-1.5 text-neutral-600",
|
||||
@ -56,42 +68,52 @@ const toggle = tv({
|
||||
color: {
|
||||
neutral: {
|
||||
wrapper: [
|
||||
"data-[checked=true]:bg-neutral-400",
|
||||
"data-[checked=true]:text-neutral-contrastText",
|
||||
"group-data-[checked=true]:bg-neutral-400",
|
||||
"group-data-[checked=true]:text-neutral-contrastText",
|
||||
],
|
||||
},
|
||||
primary: {
|
||||
wrapper: [
|
||||
"data-[checked=true]:bg-primary",
|
||||
"data-[checked=true]:text-primary-contrastText",
|
||||
"group-data-[checked=true]:bg-primary",
|
||||
"group-data-[checked=true]:text-primary-contrastText",
|
||||
],
|
||||
},
|
||||
secondary: {
|
||||
wrapper: [
|
||||
"data-[checked=true]:bg-secondary",
|
||||
"data-[checked=true]:text-secondary-contrastText",
|
||||
"group-data-[checked=true]:bg-secondary",
|
||||
"group-data-[checked=true]:text-secondary-contrastText",
|
||||
],
|
||||
},
|
||||
success: {
|
||||
wrapper: [
|
||||
"data-[checked=true]:bg-success",
|
||||
"data-[checked=true]:text-success-contrastText",
|
||||
"group-data-[checked=true]:bg-success",
|
||||
"group-data-[checked=true]:text-success-contrastText",
|
||||
],
|
||||
},
|
||||
warning: {
|
||||
wrapper: [
|
||||
"data-[checked=true]:bg-warning",
|
||||
"data-[checked=true]:text-warning-contrastText",
|
||||
"group-data-[checked=true]:bg-warning",
|
||||
"group-data-[checked=true]:text-warning-contrastText",
|
||||
],
|
||||
},
|
||||
danger: {
|
||||
wrapper: ["data-[checked=true]:bg-danger", "data-[checked=true]:text-danger-contrastText"],
|
||||
wrapper: [
|
||||
"group-data-[checked=true]:bg-danger",
|
||||
"data-[checked=true]:text-danger-contrastText",
|
||||
],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
wrapper: "px-0.5 w-7 h-4 mr-1",
|
||||
thumb: "w-3 h-3 text-[0.5rem]",
|
||||
thumb: [
|
||||
"w-3 h-3 text-[0.5rem]",
|
||||
//checked
|
||||
"group-data-[checked=true]:ml-3",
|
||||
// pressed
|
||||
"group-data-[pressed=true]:w-4",
|
||||
"group-data-[checked]:group-data-[pressed]:ml-2",
|
||||
],
|
||||
startIcon: "text-[0.5rem] left-1",
|
||||
endIcon: "text-[0.5rem] right-1",
|
||||
right: "text-[0.5rem]",
|
||||
@ -99,38 +121,61 @@ const toggle = tv({
|
||||
},
|
||||
sm: {
|
||||
wrapper: "w-8 h-5 mr-1",
|
||||
thumb: "w-3 h-3 text-[0.6rem]",
|
||||
thumb: [
|
||||
"w-3 h-3 text-[0.6rem]",
|
||||
//checked
|
||||
"group-data-[checked=true]:ml-3",
|
||||
// pressed
|
||||
"group-data-[pressed=true]:w-4",
|
||||
"group-data-[checked]:group-data-[pressed]:ml-2",
|
||||
],
|
||||
startIcon: "text-[0.6rem] left-1",
|
||||
endIcon: "text-[0.6rem] right-1",
|
||||
label: "text-sm",
|
||||
},
|
||||
md: {
|
||||
wrapper: "w-10 h-6 mr-2",
|
||||
thumb: "w-4 h-4 text-xs",
|
||||
thumb: [
|
||||
"w-4 h-4 text-xs",
|
||||
//checked
|
||||
"group-data-[checked=true]:ml-4",
|
||||
// pressed
|
||||
"group-data-[pressed=true]:w-5",
|
||||
"group-data-[checked]:group-data-[pressed]:ml-4",
|
||||
],
|
||||
endIcon: "text-xs",
|
||||
startIcon: "text-xs",
|
||||
label: "text-base",
|
||||
},
|
||||
lg: {
|
||||
wrapper: "w-12 h-7 mr-2",
|
||||
thumb: "w-5 h-5 text-sm",
|
||||
thumb: [
|
||||
"w-5 h-5 text-sm",
|
||||
//checked
|
||||
"group-data-[checked=true]:ml-5",
|
||||
// pressed
|
||||
"group-data-[pressed=true]:w-6",
|
||||
"group-data-[checked]:group-data-[pressed]:ml-4",
|
||||
],
|
||||
endIcon: "text-sm",
|
||||
startIcon: "text-sm",
|
||||
label: "text-lg",
|
||||
},
|
||||
xl: {
|
||||
wrapper: "w-14 h-8 mr-2",
|
||||
thumb: "w-6 h-6 text-base",
|
||||
thumb: [
|
||||
"w-6 h-6 text-base",
|
||||
//checked
|
||||
"group-data-[checked=true]:ml-6",
|
||||
// pressed
|
||||
"group-data-[pressed=true]:w-7",
|
||||
"group-data-[checked]:group-data-[pressed]:ml-5",
|
||||
],
|
||||
endIcon: "text-base",
|
||||
startIcon: "text-base",
|
||||
label: "text-xl",
|
||||
},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
wrapper: [...ringClasses],
|
||||
},
|
||||
},
|
||||
isDisabled: {
|
||||
true: {
|
||||
base: "opacity-50 pointer-events-none",
|
||||
@ -142,20 +187,20 @@ const toggle = tv({
|
||||
thumb: "transition-none",
|
||||
},
|
||||
false: {
|
||||
wrapper: "transition-background",
|
||||
thumb: "transition-transform !ease-soft-spring !duration-500",
|
||||
wrapper: "transition-background !duration-500",
|
||||
thumb: "transition-all !ease-soft-spring !duration-500",
|
||||
startIcon: [
|
||||
"opacity-0",
|
||||
"scale-50",
|
||||
"transition-transform-opacity",
|
||||
"data-[checked=true]:scale-100",
|
||||
"data-[checked=true]:opacity-100",
|
||||
"group-data-[checked=true]:scale-100",
|
||||
"group-data-[checked=true]:opacity-100",
|
||||
],
|
||||
endIcon: [
|
||||
"opacity-100",
|
||||
"transition-transform-opacity",
|
||||
"data-[checked=true]:translate-x-3",
|
||||
"data-[checked=true]:opacity-0",
|
||||
"group-data-[checked=true]:translate-x-3",
|
||||
"group-data-[checked=true]:opacity-0",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user