mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
309 lines
7.9 KiB
TypeScript
309 lines
7.9 KiB
TypeScript
import type {ToggleVariantProps, ToggleSlots, SlotsToClasses} from "@nextui-org/theme";
|
|
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, useState} from "react";
|
|
import {mapPropsVariants} from "@nextui-org/system";
|
|
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";
|
|
import {useFocusableRef} from "@nextui-org/dom-utils";
|
|
import {useSwitch as useReactAriaSwitch} from "@react-aria/switch";
|
|
import {useMemo} from "react";
|
|
import {useToggleState} from "@react-stately/toggle";
|
|
import {useFocusRing} from "@react-aria/focus";
|
|
|
|
export type SwitchThumbIconProps = {
|
|
width: string;
|
|
height: string;
|
|
"data-checked": string;
|
|
isSelected: boolean;
|
|
className: string;
|
|
};
|
|
interface Props extends HTMLNextUIProps<"label"> {
|
|
/**
|
|
* Ref to the DOM node.
|
|
*/
|
|
ref?: Ref<HTMLElement>;
|
|
/**
|
|
* The label of the switch.
|
|
*/
|
|
children?: ReactNode;
|
|
/**
|
|
* Whether the switch is disabled.
|
|
* @default false
|
|
*/
|
|
isDisabled?: boolean;
|
|
/**
|
|
* The icon to be displayed inside the thumb.
|
|
*/
|
|
thumbIcon?: ReactNode | ((props: SwitchThumbIconProps) => ReactNode);
|
|
/**
|
|
* Start icon to be displayed inside the switch.
|
|
*/
|
|
startIcon?: ReactNode;
|
|
/**
|
|
* End icon to be displayed inside the switch.
|
|
*/
|
|
endIcon?: ReactNode;
|
|
/**
|
|
* Classname or List of classes to change the classNames of the element.
|
|
* if `className` is passed, it will be added to the base slot.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* <Switch classNames={{
|
|
* base:"base-classes",
|
|
* wrapper: "wrapper-classes",
|
|
* thumb: "thumb-classes",
|
|
* thumbIcon: "thumbIcon-classes",
|
|
* label: "label-classes",
|
|
* }} />
|
|
* ```
|
|
*/
|
|
classNames?: SlotsToClasses<ToggleSlots>;
|
|
/**
|
|
* React aria onChange event.
|
|
*/
|
|
onValueChange?: AriaSwitchProps["onChange"];
|
|
}
|
|
|
|
export type UseSwitchProps = Omit<Props, "defaultChecked"> &
|
|
Omit<AriaSwitchProps, keyof ToggleVariantProps | "onChange"> &
|
|
ToggleVariantProps;
|
|
|
|
export function useSwitch(originalProps: UseSwitchProps) {
|
|
const [props, variantProps] = mapPropsVariants(originalProps, toggle.variantKeys);
|
|
|
|
const {
|
|
ref,
|
|
as,
|
|
name,
|
|
value = "",
|
|
isReadOnly: isReadOnlyProp = false,
|
|
autoFocus = false,
|
|
startIcon,
|
|
endIcon,
|
|
defaultSelected,
|
|
isSelected: isSelectedProp,
|
|
children,
|
|
thumbIcon,
|
|
className,
|
|
classNames,
|
|
onChange,
|
|
onValueChange,
|
|
...otherProps
|
|
} = props;
|
|
|
|
const Component = as || "label";
|
|
|
|
const inputRef = useRef(null);
|
|
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
|
|
|
|
const labelId = useId();
|
|
|
|
const ariaSwitchProps = useMemo(() => {
|
|
const ariaLabel =
|
|
otherProps["aria-label"] || typeof children === "string" ? (children as string) : undefined;
|
|
|
|
return {
|
|
name,
|
|
value,
|
|
children,
|
|
autoFocus,
|
|
defaultSelected,
|
|
isSelected: isSelectedProp,
|
|
isDisabled: !!originalProps.isDisabled,
|
|
isReadOnly: isReadOnlyProp,
|
|
"aria-label": ariaLabel,
|
|
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
|
|
onChange: onValueChange,
|
|
};
|
|
}, [
|
|
value,
|
|
name,
|
|
labelId,
|
|
children,
|
|
autoFocus,
|
|
isReadOnlyProp,
|
|
isSelectedProp,
|
|
defaultSelected,
|
|
originalProps.isDisabled,
|
|
otherProps["aria-label"],
|
|
otherProps["aria-labelledby"],
|
|
onValueChange,
|
|
]);
|
|
|
|
const state = useToggleState(ariaSwitchProps);
|
|
|
|
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;
|
|
|
|
const slots = useMemo(
|
|
() =>
|
|
toggle({
|
|
...variantProps,
|
|
}),
|
|
[...Object.values(variantProps)],
|
|
);
|
|
|
|
const baseStyles = clsx(classNames?.base, className);
|
|
|
|
const getBaseProps: PropGetter = (props) => {
|
|
return {
|
|
...mergeProps(hoverProps, pressProps, otherProps, props),
|
|
ref: domRef,
|
|
className: slots.base({class: clsx(baseStyles, props?.className)}),
|
|
"data-disabled": dataAttr(isDisabled),
|
|
"data-checked": dataAttr(isSelected),
|
|
"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(
|
|
(props = {}) => {
|
|
return {
|
|
...props,
|
|
"aria-hidden": true,
|
|
className: clsx(slots.wrapper({class: clsx(classNames?.wrapper, props?.className)})),
|
|
};
|
|
},
|
|
[slots, classNames?.wrapper],
|
|
);
|
|
|
|
const getInputProps: PropGetter = (props = {}) => {
|
|
return {
|
|
...mergeProps(inputProps, focusProps, props),
|
|
ref: inputRef,
|
|
id: inputProps.id,
|
|
onChange: chain(onChange, inputProps.onChange),
|
|
};
|
|
};
|
|
|
|
const getThumbProps: PropGetter = useCallback(
|
|
(props = {}) => ({
|
|
...props,
|
|
className: slots.thumb({class: clsx(classNames?.thumb, props?.className)}),
|
|
}),
|
|
[slots, classNames?.thumb],
|
|
);
|
|
|
|
const getLabelProps: PropGetter = useCallback(
|
|
(props = {}) => ({
|
|
...props,
|
|
id: labelId,
|
|
className: slots.label({class: clsx(classNames?.label, props?.className)}),
|
|
}),
|
|
[slots, classNames?.label, isDisabled, isSelected],
|
|
);
|
|
|
|
const getThumbIconProps = useCallback(
|
|
(
|
|
props = {
|
|
includeStateProps: false,
|
|
},
|
|
) =>
|
|
(mergeProps(
|
|
{
|
|
width: "1em",
|
|
height: "1em",
|
|
className: slots.thumbIcon({class: clsx(classNames?.thumbIcon)}),
|
|
},
|
|
props.includeStateProps
|
|
? {
|
|
isSelected: isSelected,
|
|
}
|
|
: {},
|
|
) as unknown) as SwitchThumbIconProps,
|
|
[slots, classNames?.thumbIcon, isSelected],
|
|
);
|
|
|
|
const getStartIconProps = useCallback<PropGetter>(
|
|
(props = {}) => ({
|
|
width: "1em",
|
|
height: "1em",
|
|
...props,
|
|
className: slots.startIcon({class: clsx(classNames?.startIcon, props?.className)}),
|
|
}),
|
|
[slots, classNames?.startIcon, isSelected],
|
|
);
|
|
|
|
const getEndIconProps = useCallback<PropGetter>(
|
|
(props = {}) => ({
|
|
width: "1em",
|
|
height: "1em",
|
|
...props,
|
|
className: slots.endIcon({class: clsx(classNames?.endIcon, props?.className)}),
|
|
}),
|
|
[slots, classNames?.endIcon, isSelected],
|
|
);
|
|
|
|
return {
|
|
Component,
|
|
slots,
|
|
classNames,
|
|
domRef,
|
|
children,
|
|
thumbIcon,
|
|
startIcon,
|
|
endIcon,
|
|
isHovered,
|
|
isSelected,
|
|
isPressed: pressed,
|
|
isFocused,
|
|
isFocusVisible,
|
|
isDisabled,
|
|
getBaseProps,
|
|
getWrapperProps,
|
|
getInputProps,
|
|
getLabelProps,
|
|
getThumbProps,
|
|
getThumbIconProps,
|
|
getStartIconProps,
|
|
getEndIconProps,
|
|
};
|
|
}
|
|
|
|
export type UseSwitchReturn = ReturnType<typeof useSwitch>;
|