mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(root): more components refactored to have better data management and focus visible
This commit is contained in:
parent
8ce412c508
commit
5f1d889d8c
@ -82,7 +82,6 @@ export function useAccordionItem<T extends object = {}>(props: UseAccordionItemP
|
||||
isDisabled,
|
||||
hideDivider,
|
||||
hideIndicator,
|
||||
isFocusVisible,
|
||||
disableAnimation,
|
||||
disableIndicatorAnimation,
|
||||
}),
|
||||
@ -91,7 +90,6 @@ export function useAccordionItem<T extends object = {}>(props: UseAccordionItemP
|
||||
isDisabled,
|
||||
hideDivider,
|
||||
hideIndicator,
|
||||
isFocusVisible,
|
||||
disableAnimation,
|
||||
disableIndicatorAnimation,
|
||||
],
|
||||
@ -116,8 +114,8 @@ export function useAccordionItem<T extends object = {}>(props: UseAccordionItemP
|
||||
return {
|
||||
ref: domRef,
|
||||
"data-open": dataAttr(isOpen),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-focused": dataAttr(isFocused),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
className: slots.trigger({class: classNames?.trigger}),
|
||||
onFocus: callAllHandlers(
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@nextui-org/use-image": "workspace:*",
|
||||
"@react-aria/interactions": "^3.15.0",
|
||||
"@react-aria/focus": "^3.12.0",
|
||||
"@react-aria/utils": "^3.16.0"
|
||||
},
|
||||
|
||||
@ -12,7 +12,6 @@ const Avatar = forwardRef<AvatarProps, "span">((props, ref) => {
|
||||
src,
|
||||
icon = <AvatarIcon />,
|
||||
alt,
|
||||
domRef,
|
||||
classNames,
|
||||
slots,
|
||||
name,
|
||||
@ -55,7 +54,7 @@ const Avatar = forwardRef<AvatarProps, "span">((props, ref) => {
|
||||
}, [showFallback, src, fallbackComponent, name, classNames]);
|
||||
|
||||
return (
|
||||
<Component ref={domRef} {...getAvatarProps()}>
|
||||
<Component {...getAvatarProps()}>
|
||||
{src && <img {...getImageProps()} alt={alt} />}
|
||||
{fallback}
|
||||
</Component>
|
||||
|
||||
@ -4,10 +4,11 @@ import {avatar} from "@nextui-org/theme";
|
||||
import {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {ReactRef, clsx, safeText} from "@nextui-org/shared-utils";
|
||||
import {ReactRef, clsx, safeText, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {useMemo, useCallback} from "react";
|
||||
import {useImage} from "@nextui-org/use-image";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
|
||||
import {useAvatarGroupContext} from "./avatar-group-context";
|
||||
|
||||
@ -119,7 +120,8 @@ export function useAvatar(props: UseAvatarProps) {
|
||||
const domRef = useDOMRef(ref);
|
||||
const imgRef = useDOMRef(imgRefProp);
|
||||
|
||||
const {isFocusVisible, focusProps} = useFocusRing();
|
||||
const {isFocusVisible, isFocused, focusProps} = useFocusRing();
|
||||
const {isHovered, hoverProps} = useHover({isDisabled});
|
||||
|
||||
const imageStatus = useImage({src, onError, ignoreFallback});
|
||||
const isImgLoaded = imageStatus === "loaded";
|
||||
@ -140,12 +142,11 @@ export function useAvatar(props: UseAvatarProps) {
|
||||
radius,
|
||||
size,
|
||||
isBordered,
|
||||
isFocusVisible,
|
||||
isDisabled,
|
||||
isInGroup,
|
||||
isInGridGroup: groupContext?.isGrid ?? false,
|
||||
}),
|
||||
[color, radius, size, isBordered, isFocusVisible, isDisabled, isInGroup, groupContext?.isGrid],
|
||||
[color, radius, size, isBordered, isDisabled, isInGroup, groupContext?.isGrid],
|
||||
);
|
||||
|
||||
const buttonStyles = useMemo(() => {
|
||||
@ -163,11 +164,15 @@ export function useAvatar(props: UseAvatarProps) {
|
||||
|
||||
const getAvatarProps = useCallback<PropGetter>(
|
||||
() => ({
|
||||
ref: domRef,
|
||||
tabIndex: canBeFocused ? 0 : -1,
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
className: slots.base({
|
||||
class: clsx(baseStyles, buttonStyles),
|
||||
}),
|
||||
...mergeProps(otherProps, canBeFocused ? focusProps : {}),
|
||||
...mergeProps(otherProps, hoverProps, canBeFocused ? focusProps : {}),
|
||||
}),
|
||||
[canBeFocused, slots, baseStyles, buttonStyles, focusProps, otherProps],
|
||||
);
|
||||
@ -188,7 +193,6 @@ export function useAvatar(props: UseAvatarProps) {
|
||||
alt,
|
||||
icon,
|
||||
name,
|
||||
domRef,
|
||||
imgRef,
|
||||
slots,
|
||||
classNames,
|
||||
|
||||
@ -12,6 +12,7 @@ import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {button} from "@nextui-org/theme";
|
||||
import {isValidElement, cloneElement, useMemo} from "react";
|
||||
import {useAriaButton} from "@nextui-org/use-aria-button";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
|
||||
import {useButtonGroupContext} from "./button-group-context";
|
||||
|
||||
@ -87,7 +88,6 @@ export function useButton(props: UseButtonProps) {
|
||||
fullWidth,
|
||||
isDisabled,
|
||||
isInGroup,
|
||||
isFocusVisible,
|
||||
disableAnimation,
|
||||
isIconOnly,
|
||||
className,
|
||||
@ -100,7 +100,6 @@ export function useButton(props: UseButtonProps) {
|
||||
fullWidth,
|
||||
isDisabled,
|
||||
isInGroup,
|
||||
isFocusVisible,
|
||||
isIconOnly,
|
||||
disableAnimation,
|
||||
className,
|
||||
@ -114,7 +113,7 @@ export function useButton(props: UseButtonProps) {
|
||||
domRef.current && onDripClickHandler(e);
|
||||
};
|
||||
|
||||
const {buttonProps: ariaButtonProps} = useAriaButton(
|
||||
const {buttonProps: ariaButtonProps, isPressed} = useAriaButton(
|
||||
{
|
||||
elementType: as,
|
||||
isDisabled,
|
||||
@ -124,13 +123,16 @@ export function useButton(props: UseButtonProps) {
|
||||
} as AriaButtonProps,
|
||||
domRef,
|
||||
);
|
||||
const {isHovered, hoverProps} = useHover({isDisabled});
|
||||
|
||||
const getButtonProps: PropGetter = useCallback(
|
||||
() => ({
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-pressed": dataAttr(isPressed),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-focused": dataAttr(isFocused),
|
||||
...mergeProps(ariaButtonProps, focusProps, otherProps),
|
||||
"data-hover": dataAttr(isHovered),
|
||||
...mergeProps(ariaButtonProps, focusProps, hoverProps, otherProps),
|
||||
}),
|
||||
[ariaButtonProps, focusProps, otherProps],
|
||||
);
|
||||
|
||||
@ -104,7 +104,7 @@ export function useCard(originalProps: UseCardProps) {
|
||||
...otherProps,
|
||||
});
|
||||
|
||||
const {isFocusVisible, focusProps} = useFocusRing({
|
||||
const {isFocusVisible, isFocused, focusProps} = useFocusRing({
|
||||
autoFocus,
|
||||
});
|
||||
|
||||
@ -112,9 +112,8 @@ export function useCard(originalProps: UseCardProps) {
|
||||
() =>
|
||||
card({
|
||||
...variantProps,
|
||||
isFocusVisible,
|
||||
}),
|
||||
[...Object.values(variantProps), isFocusVisible],
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
|
||||
const context = useMemo<ContextType>(
|
||||
@ -145,6 +144,7 @@ export function useCard(originalProps: UseCardProps) {
|
||||
tabIndex: originalProps.isPressable ? 0 : -1,
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-pressed": dataAttr(isPressed),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-disabled": dataAttr(originalProps.isDisabled),
|
||||
...mergeProps(
|
||||
|
||||
@ -2,11 +2,11 @@ import type {CheckboxVariantProps, CheckboxSlots, SlotsToClasses} from "@nextui-
|
||||
import type {AriaCheckboxProps} from "@react-types/checkbox";
|
||||
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
|
||||
import {ReactNode, Ref, useCallback, useId} from "react";
|
||||
import {ReactNode, Ref, useCallback, useId, useState} from "react";
|
||||
import {useMemo, useRef} from "react";
|
||||
import {useToggleState} from "@react-stately/toggle";
|
||||
import {checkbox} from "@nextui-org/theme";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
import {useHover, usePress} from "@react-aria/interactions";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {chain, mergeProps} from "@react-aria/utils";
|
||||
import {useFocusableRef} from "@nextui-org/dom-utils";
|
||||
@ -82,14 +82,14 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
icon,
|
||||
name,
|
||||
isRequired = false,
|
||||
isReadOnly = false,
|
||||
isReadOnly: isReadOnlyProp = false,
|
||||
autoFocus = false,
|
||||
isSelected: isSelectedProp,
|
||||
size = groupContext?.size ?? "md",
|
||||
color = groupContext?.color ?? "primary",
|
||||
radius = groupContext?.radius ?? "md",
|
||||
lineThrough = groupContext?.lineThrough ?? false,
|
||||
isDisabled = groupContext?.isDisabled ?? false,
|
||||
isDisabled: isDisabledProp = groupContext?.isDisabled ?? false,
|
||||
disableAnimation = groupContext?.disableAnimation ?? false,
|
||||
isIndeterminate = false,
|
||||
validationState,
|
||||
@ -133,11 +133,11 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
children,
|
||||
autoFocus,
|
||||
defaultSelected,
|
||||
isSelected: isSelectedProp,
|
||||
isDisabled,
|
||||
isIndeterminate,
|
||||
isRequired,
|
||||
isReadOnly,
|
||||
isSelected: isSelectedProp,
|
||||
isDisabled: isDisabledProp,
|
||||
isReadOnly: isReadOnlyProp,
|
||||
validationState,
|
||||
"aria-label": ariaLabel,
|
||||
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
|
||||
@ -150,7 +150,8 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
children,
|
||||
autoFocus,
|
||||
isIndeterminate,
|
||||
isDisabled,
|
||||
isDisabledProp,
|
||||
isReadOnlyProp,
|
||||
isSelectedProp,
|
||||
defaultSelected,
|
||||
validationState,
|
||||
@ -159,7 +160,7 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
onValueChange,
|
||||
]);
|
||||
|
||||
const {inputProps} = isInGroup
|
||||
const {inputProps, isSelected, isDisabled, isReadOnly, isPressed: isPressedKeyboard} = isInGroup
|
||||
? // eslint-disable-next-line
|
||||
useReactAriaCheckboxGroupItem(
|
||||
{
|
||||
@ -171,8 +172,27 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
)
|
||||
: useReactAriaCheckbox(ariaCheckboxProps, useToggleState(ariaCheckboxProps), inputRef); // eslint-disable-line
|
||||
|
||||
const isSelected = inputProps.checked;
|
||||
const isInvalid = useMemo(() => validationState === "invalid", [validationState]);
|
||||
const isInteractionDisabled = isDisabled || isReadOnly;
|
||||
|
||||
// Handle press state for full label. Keyboard press state is returned by useCheckbox
|
||||
// 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;
|
||||
|
||||
if (isRequired) {
|
||||
inputProps.required = true;
|
||||
@ -194,10 +214,9 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
radius,
|
||||
lineThrough,
|
||||
isDisabled,
|
||||
isFocusVisible,
|
||||
disableAnimation,
|
||||
}),
|
||||
[color, size, radius, lineThrough, isDisabled, isFocusVisible, disableAnimation],
|
||||
[color, size, radius, lineThrough, isDisabled, disableAnimation],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
@ -209,22 +228,21 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
...mergeProps(hoverProps, otherProps),
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-pressed": dataAttr(pressed),
|
||||
"data-readonly": dataAttr(inputProps.readOnly),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-indeterminate": dataAttr(isIndeterminate),
|
||||
...mergeProps(hoverProps, pressProps, otherProps),
|
||||
};
|
||||
};
|
||||
|
||||
const getWrapperProps: PropGetter = () => {
|
||||
const getWrapperProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
|
||||
"data-indeterminate": dataAttr(isIndeterminate),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
"data-readonly": dataAttr(inputProps.readOnly),
|
||||
...props,
|
||||
"aria-hidden": true,
|
||||
className: clsx(slots.wrapper({class: classNames?.wrapper})),
|
||||
className: clsx(slots.wrapper({class: clsx(classNames?.wrapper, props?.className)})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -239,9 +257,6 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
const getLabelProps: PropGetter = useCallback(
|
||||
() => ({
|
||||
id: labelId,
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
className: slots.label({class: classNames?.label}),
|
||||
}),
|
||||
[slots, classNames?.label, isDisabled, isSelected, isInvalid],
|
||||
@ -250,7 +265,6 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
const getIconProps = useCallback(
|
||||
() =>
|
||||
({
|
||||
"data-checked": dataAttr(isSelected),
|
||||
isSelected: isSelected,
|
||||
isIndeterminate: !!isIndeterminate,
|
||||
disableAnimation: !!disableAnimation,
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type {AriaRadioProps} from "@react-types/radio";
|
||||
import type {RadioVariantProps, RadioSlots, SlotsToClasses} from "@nextui-org/theme";
|
||||
|
||||
import {Ref, ReactNode, useCallback, useId} from "react";
|
||||
import {Ref, ReactNode, useCallback, useId, useState} from "react";
|
||||
import {useMemo, useRef} from "react";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
import {useHover, usePress} from "@react-aria/interactions";
|
||||
import {radio} from "@nextui-org/theme";
|
||||
import {useRadio as useReactAriaRadio} from "@react-aria/radio";
|
||||
import {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
@ -88,7 +88,6 @@ export function useRadio(props: UseRadioProps) {
|
||||
|
||||
const labelId = useId();
|
||||
|
||||
const isDisabled = useMemo(() => !!isDisabledProp, [isDisabledProp]);
|
||||
const isRequired = useMemo(() => groupContext.isRequired ?? false, [groupContext.isRequired]);
|
||||
const isInvalid = useMemo(() => groupContext.validationState === "invalid", [
|
||||
groupContext.validationState,
|
||||
@ -104,15 +103,15 @@ export function useRadio(props: UseRadioProps) {
|
||||
|
||||
return {
|
||||
id,
|
||||
isDisabled,
|
||||
isRequired,
|
||||
isDisabled: isDisabledProp,
|
||||
"aria-label": ariaLabel,
|
||||
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
|
||||
"aria-describedby": ariaDescribedBy,
|
||||
};
|
||||
}, [labelId, id, isDisabled, isRequired]);
|
||||
}, [labelId, id, isDisabledProp, isRequired]);
|
||||
|
||||
const {inputProps} = useReactAriaRadio(
|
||||
const {inputProps, isDisabled, isSelected, isPressed: isPressedKeyboard} = useReactAriaRadio(
|
||||
{
|
||||
value,
|
||||
children,
|
||||
@ -123,14 +122,35 @@ export function useRadio(props: UseRadioProps) {
|
||||
inputRef,
|
||||
);
|
||||
|
||||
const isSelected = useMemo(() => inputProps.checked, [inputProps.checked]);
|
||||
|
||||
const {hoverProps, isHovered} = useHover({isDisabled});
|
||||
|
||||
const {focusProps, isFocused, isFocusVisible} = useFocusRing({
|
||||
autoFocus,
|
||||
});
|
||||
|
||||
const interactionDisabled = isDisabled || inputProps.readOnly;
|
||||
|
||||
// Handle press state for full label. Keyboard press state is returned by useCheckbox
|
||||
// since it is handled on the <input> element itself.
|
||||
const [isPressed, setPressed] = useState(false);
|
||||
const {pressProps} = usePress({
|
||||
isDisabled: interactionDisabled,
|
||||
onPressStart(e) {
|
||||
if (e.pointerType !== "keyboard") {
|
||||
setPressed(true);
|
||||
}
|
||||
},
|
||||
onPressEnd(e) {
|
||||
if (e.pointerType !== "keyboard") {
|
||||
setPressed(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const {hoverProps, isHovered} = useHover({
|
||||
isDisabled: interactionDisabled,
|
||||
});
|
||||
|
||||
const pressed = interactionDisabled ? false : isPressed || isPressedKeyboard;
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
radio({
|
||||
@ -139,10 +159,9 @@ export function useRadio(props: UseRadioProps) {
|
||||
radius,
|
||||
isInvalid,
|
||||
isDisabled,
|
||||
isFocusVisible,
|
||||
disableAnimation,
|
||||
}),
|
||||
[color, size, radius, isDisabled, isInvalid, isFocusVisible, disableAnimation],
|
||||
[color, size, radius, isDisabled, isInvalid, disableAnimation],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
@ -153,27 +172,24 @@ export function useRadio(props: UseRadioProps) {
|
||||
ref: domRef,
|
||||
className: slots.base({class: baseStyles}),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-checked": dataAttr(inputProps.checked),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
...mergeProps(hoverProps, otherProps),
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-pressed": dataAttr(pressed),
|
||||
"data-hover-unchecked": dataAttr(isHovered && !isSelected),
|
||||
"data-readonly": dataAttr(inputProps.readOnly),
|
||||
"aria-required": dataAttr(isRequired),
|
||||
...mergeProps(hoverProps, pressProps, otherProps),
|
||||
};
|
||||
};
|
||||
|
||||
const getWrapperProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
...props,
|
||||
"data-active": dataAttr(isSelected),
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-hover-unchecked": dataAttr(isHovered && !isSelected),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
"data-readonly": dataAttr(inputProps.readOnly),
|
||||
"aria-required": dataAttr(isRequired),
|
||||
"aria-hidden": true,
|
||||
className: clsx(slots.wrapper({class: classNames?.wrapper})),
|
||||
className: clsx(slots.wrapper({class: clsx(classNames?.wrapper, props.className)})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -182,9 +198,6 @@ export function useRadio(props: UseRadioProps) {
|
||||
...props,
|
||||
ref: inputRef,
|
||||
required: isRequired,
|
||||
"aria-required": dataAttr(isRequired),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
"data-readonly": dataAttr(inputProps.readOnly),
|
||||
...mergeProps(inputProps, focusProps),
|
||||
onChange: chain(inputProps.onChange, onChange),
|
||||
};
|
||||
@ -194,9 +207,6 @@ export function useRadio(props: UseRadioProps) {
|
||||
(props = {}) => ({
|
||||
...props,
|
||||
id: labelId,
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
className: slots.label({class: classNames?.label}),
|
||||
}),
|
||||
[slots, classNames, isDisabled, isSelected, isInvalid],
|
||||
@ -213,9 +223,6 @@ export function useRadio(props: UseRadioProps) {
|
||||
const getControlProps: PropGetter = useCallback(
|
||||
(props = {}) => ({
|
||||
...props,
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-checked": dataAttr(isSelected),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
className: slots.control({class: classNames?.control}),
|
||||
}),
|
||||
[slots, isDisabled, isSelected, isInvalid],
|
||||
|
||||
@ -2,7 +2,6 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {ringClasses} from "../utils";
|
||||
/**
|
||||
* AccordionItem wrapper **Tailwind Variants** component
|
||||
*
|
||||
@ -39,7 +38,16 @@ const accordionItem = tv({
|
||||
"group-[.is-splitted]:border-neutral-100",
|
||||
],
|
||||
heading: "",
|
||||
trigger: "py-2 flex w-full gap-3 outline-none items-center",
|
||||
trigger: [
|
||||
"py-2 flex w-full gap-3 outline-none items-center",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:!ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
],
|
||||
startContent: "flex-shrink-0",
|
||||
indicator: "text-neutral-400",
|
||||
titleWrapper: "flex-1 flex flex-col text-left",
|
||||
@ -63,11 +71,6 @@ const accordionItem = tv({
|
||||
base: "opacity-50 pointer-events-none",
|
||||
},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
trigger: [...ringClasses],
|
||||
},
|
||||
},
|
||||
hideDivider: {
|
||||
true: {
|
||||
base: "!border-b-0",
|
||||
|
||||
@ -2,7 +2,7 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {translateCenterClasses, ringClasses, colorVariants} from "../utils";
|
||||
import {translateCenterClasses, colorVariants} from "../utils";
|
||||
|
||||
/**
|
||||
* Avatar wrapper **Tailwind Variants** component
|
||||
@ -10,7 +10,7 @@ import {translateCenterClasses, ringClasses, colorVariants} from "../utils";
|
||||
* const {base, img, icon, name } = avatar({...})
|
||||
*
|
||||
* @example
|
||||
* <div className={base())}>
|
||||
* <div className={base())} data-hover={true/false} data-focus={true/false} data-focus-visible={true/false}>
|
||||
* <img className={img()} src="https://picsum.photos/200/300" alt="your avatar" />
|
||||
* <div role="img" aria-label="your name" className={name()}>your name</div>
|
||||
* <span role="img" aria-label="your icon" className={icon()}>your icon</span>
|
||||
@ -28,6 +28,13 @@ const avatar = tv({
|
||||
"align-middle",
|
||||
"text-white",
|
||||
"z-10",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:!ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
],
|
||||
img: [
|
||||
"flex",
|
||||
@ -128,19 +135,17 @@ const avatar = tv({
|
||||
base: "opacity-50",
|
||||
},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
base: [...ringClasses],
|
||||
},
|
||||
},
|
||||
isInGroup: {
|
||||
true: {
|
||||
base: "-ml-2 hover:-translate-x-3 transition-transform",
|
||||
base: [
|
||||
"-ml-2 data-[hover=true]:-translate-x-3 transition-transform",
|
||||
"data-[focus-visible=true]:-translate-x-3",
|
||||
],
|
||||
},
|
||||
},
|
||||
isInGridGroup: {
|
||||
true: {
|
||||
base: "m-0 hover:translate-x-0",
|
||||
base: "m-0 data-[hover=true]:translate-x-0",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -199,13 +204,6 @@ const avatar = tv({
|
||||
base: "ring",
|
||||
},
|
||||
},
|
||||
{
|
||||
isInGroup: true,
|
||||
isFocusVisible: true,
|
||||
class: {
|
||||
base: "-translate-x-3",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {ringClasses, colorVariants} from "../utils";
|
||||
import {colorVariants} from "../utils";
|
||||
|
||||
/**
|
||||
* Button wrapper **Tailwind Variants** component
|
||||
@ -10,7 +10,13 @@ import {ringClasses, colorVariants} from "../utils";
|
||||
* const classNames = button({...})
|
||||
*
|
||||
* @example
|
||||
* <button className={classNames())}>
|
||||
* <button
|
||||
* className={classNames())}
|
||||
* data-pressed={true/false}
|
||||
* data-hover={true/false}
|
||||
* data-focus={true/false}
|
||||
* data-focus-visible={true/false}
|
||||
* >
|
||||
* Button
|
||||
* </button>
|
||||
*/
|
||||
@ -27,11 +33,18 @@ const button = tv({
|
||||
"select-none",
|
||||
"font-medium",
|
||||
"subpixel-antialiased",
|
||||
"active:scale-95",
|
||||
"data-[pressed=true]:scale-95",
|
||||
"overflow-hidden",
|
||||
"gap-3",
|
||||
// svg icon
|
||||
"[&>svg]:max-w-[2em]",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:!ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
],
|
||||
variants: {
|
||||
variant: {
|
||||
@ -75,9 +88,7 @@ const button = tv({
|
||||
isDisabled: {
|
||||
true: "opacity-50 pointer-events-none",
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: [...ringClasses],
|
||||
},
|
||||
|
||||
isInGroup: {
|
||||
true: "[&:not(:first-child):not(:last-child)]:rounded-none",
|
||||
},
|
||||
|
||||
@ -2,8 +2,6 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {ringClasses} from "../utils";
|
||||
|
||||
/**
|
||||
* Card **Tailwind Variants** component
|
||||
*
|
||||
@ -32,6 +30,13 @@ const card = tv({
|
||||
"box-border",
|
||||
"dark:bg-content1",
|
||||
"border border-neutral-100",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:!ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
],
|
||||
header: [
|
||||
"flex",
|
||||
@ -149,17 +154,12 @@ const card = tv({
|
||||
},
|
||||
isHoverable: {
|
||||
true: {
|
||||
base: "hover:bg-content2 dark:hover:bg-content2",
|
||||
base: "data-[hover=true]:bg-content2 dark:data-[hover=true]:bg-content2",
|
||||
},
|
||||
},
|
||||
isPressable: {
|
||||
true: {base: "cursor-pointer"},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
base: [...ringClasses],
|
||||
},
|
||||
},
|
||||
isFooterBlurred: {
|
||||
true: {
|
||||
footer: "backdrop-blur-xl backdrop-saturate-200",
|
||||
@ -179,7 +179,7 @@ const card = tv({
|
||||
{
|
||||
isPressable: true,
|
||||
disableAnimation: false,
|
||||
class: "active:scale-95",
|
||||
class: "data-[pressed=true]:scale-95",
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
@ -188,7 +188,6 @@ const card = tv({
|
||||
fullWidth: false,
|
||||
isHoverable: false,
|
||||
isPressable: false,
|
||||
isFocusVisible: false,
|
||||
isDisabled: false,
|
||||
disableAnimation: false,
|
||||
isFooterBlurred: false,
|
||||
|
||||
@ -2,8 +2,6 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {ringClasses} from "../utils";
|
||||
|
||||
/**
|
||||
* Checkbox wrapper **Tailwind Variants** component
|
||||
*
|
||||
@ -22,7 +20,7 @@ import {ringClasses} from "../utils";
|
||||
*/
|
||||
const checkbox = 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",
|
||||
wrapper: [
|
||||
"relative",
|
||||
"inline-flex",
|
||||
@ -45,13 +43,20 @@ const checkbox = tv({
|
||||
"after:scale-50",
|
||||
"after:opacity-0",
|
||||
"after:origin-center",
|
||||
"data-[checked=true]:after:scale-100",
|
||||
"data-[checked=true]:after:opacity-100",
|
||||
"group-data-[checked=true]:after:scale-100",
|
||||
"group-data-[checked=true]:after:opacity-100",
|
||||
// hover
|
||||
"hover:before:bg-neutral-100",
|
||||
"data-[hover=true]:before:bg-neutral-100",
|
||||
"group-data-[hover=true]:before:bg-neutral-100",
|
||||
"group-data-[hover=true]:before:bg-neutral-100",
|
||||
// focus ring
|
||||
"group-data-[focus-visible=true]:outline-none",
|
||||
"group-data-[focus-visible=true]:ring-2",
|
||||
"group-data-[focus-visible=true]:!ring-primary",
|
||||
"group-data-[focus-visible=true]:ring-offset-2",
|
||||
"group-data-[focus-visible=true]:ring-offset-background",
|
||||
"group-data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
],
|
||||
icon: "z-10 w-4 h-3 opacity-0 data-[checked=true]:opacity-100",
|
||||
icon: "z-10 w-4 h-3 opacity-0 group-data-[checked=true]:opacity-100",
|
||||
label: "relative text-foreground select-none",
|
||||
},
|
||||
variants: {
|
||||
@ -136,8 +141,8 @@ const checkbox = tv({
|
||||
"before:bg-foreground",
|
||||
"before:w-0",
|
||||
"before:h-0.5",
|
||||
"data-[checked=true]:opacity-60",
|
||||
"data-[checked=true]:before:w-full",
|
||||
"group-data-[checked=true]:opacity-60",
|
||||
"group-data-[checked=true]:before:w-full",
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -146,11 +151,6 @@ const checkbox = tv({
|
||||
base: "opacity-50 pointer-events-none",
|
||||
},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
wrapper: [...ringClasses],
|
||||
},
|
||||
},
|
||||
disableAnimation: {
|
||||
true: {
|
||||
wrapper: "transition-none",
|
||||
|
||||
@ -2,15 +2,18 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {ringClasses} from "../utils";
|
||||
|
||||
/**
|
||||
* Radio wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const {base, wrapper, point, labelWrapper, label, description} = radio({...})
|
||||
*
|
||||
* @example
|
||||
* <label className={base())}>
|
||||
* <label
|
||||
* className={base())}
|
||||
* data-checked={boolean}
|
||||
* data-hover-unchecked={boolean}
|
||||
* data-focus-visible={boolean}
|
||||
* >
|
||||
* // input
|
||||
* <span className={wrapper()} aria-hidden="true" data-checked={checked} data-hover-unchecked={hoverUnchecked}>
|
||||
* <span className={point()}/>
|
||||
@ -23,7 +26,7 @@ import {ringClasses} from "../utils";
|
||||
*/
|
||||
const radio = 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",
|
||||
wrapper: [
|
||||
"relative",
|
||||
"inline-flex",
|
||||
@ -35,7 +38,14 @@ const radio = tv({
|
||||
"border-2",
|
||||
"box-border",
|
||||
"border-neutral",
|
||||
"data-[hover-unchecked=true]:bg-neutral-100",
|
||||
"group-data-[hover-unchecked=true]:bg-neutral-100",
|
||||
// focus ring
|
||||
"group-data-[focus-visible=true]:outline-none",
|
||||
"group-data-[focus-visible=true]:ring-2",
|
||||
"group-data-[focus-visible=true]:!ring-primary",
|
||||
"group-data-[focus-visible=true]:ring-offset-2",
|
||||
"group-data-[focus-visible=true]:ring-offset-background",
|
||||
"group-data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
],
|
||||
labelWrapper: "flex flex-col ml-1",
|
||||
control: [
|
||||
@ -45,8 +55,8 @@ const radio = tv({
|
||||
"opacity-0",
|
||||
"scale-0",
|
||||
"origin-center",
|
||||
"data-[checked=true]:opacity-100",
|
||||
"data-[checked=true]:scale-100",
|
||||
"group-data-[checked=true]:opacity-100",
|
||||
"group-data-[checked=true]:scale-100",
|
||||
],
|
||||
label: "relative text-foreground select-none",
|
||||
description: "relative text-neutral-400",
|
||||
@ -55,27 +65,27 @@ const radio = tv({
|
||||
color: {
|
||||
neutral: {
|
||||
control: "bg-neutral-500 text-neutral-contrastText",
|
||||
wrapper: "data-[checked=true]:border-neutral-500",
|
||||
wrapper: "group-data-[checked=true]:border-neutral-500",
|
||||
},
|
||||
primary: {
|
||||
control: "bg-primary text-primary-contrastText",
|
||||
wrapper: "data-[checked=true]:border-primary",
|
||||
wrapper: "group-data-[checked=true]:border-primary",
|
||||
},
|
||||
secondary: {
|
||||
control: "bg-secondary text-secondary-contrastText",
|
||||
wrapper: "data-[checked=true]:border-secondary",
|
||||
wrapper: "group-data-[checked=true]:border-secondary",
|
||||
},
|
||||
success: {
|
||||
control: "bg-success text-success-contrastText",
|
||||
wrapper: "data-[checked=true]:border-success",
|
||||
wrapper: "group-data-[checked=true]:border-success",
|
||||
},
|
||||
warning: {
|
||||
control: "bg-warning text-warning-contrastText",
|
||||
wrapper: "data-[checked=true]:border-warning",
|
||||
wrapper: "group-data-[checked=true]:border-warning",
|
||||
},
|
||||
danger: {
|
||||
control: "bg-danger text-danger-contrastText",
|
||||
wrapper: "data-[checked=true]:border-danger",
|
||||
wrapper: "group-data-[checked=true]:border-danger",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
@ -158,11 +168,6 @@ const radio = tv({
|
||||
description: "text-danger-300",
|
||||
},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
wrapper: [...ringClasses],
|
||||
},
|
||||
},
|
||||
disableAnimation: {
|
||||
true: {},
|
||||
false: {
|
||||
|
||||
@ -10,11 +10,11 @@ import {tv} from "tailwind-variants";
|
||||
* @example
|
||||
* <label
|
||||
* className={base())}
|
||||
* data-checked={checked}
|
||||
* data-pressed={pressed}
|
||||
* data-focus={focus}
|
||||
* data-hover={hover}
|
||||
* data-focus-visible={focusVisible}
|
||||
* data-checked={true/false}
|
||||
* data-pressed={true/false}
|
||||
* data-focus={true/false}
|
||||
* data-hover={true/false}
|
||||
* data-focus-visible={true/false}
|
||||
* >
|
||||
* <input/> // hidden input
|
||||
* <span className={wrapper()} aria-hidden="true">
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -526,6 +526,9 @@ importers:
|
||||
'@react-aria/focus':
|
||||
specifier: ^3.12.0
|
||||
version: 3.12.0(react@18.2.0)
|
||||
'@react-aria/interactions':
|
||||
specifier: ^3.15.0
|
||||
version: 3.15.0(react@18.2.0)
|
||||
'@react-aria/utils':
|
||||
specifier: ^3.16.0
|
||||
version: 3.16.0(react@18.2.0)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user