feat(root): more components refactored to have better data management and focus visible

This commit is contained in:
Junior Garcia 2023-04-15 16:44:46 -03:00
parent 8ce412c508
commit 5f1d889d8c
16 changed files with 201 additions and 157 deletions

View File

@ -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(

View File

@ -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"
},

View File

@ -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>

View File

@ -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,

View File

@ -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],
);

View File

@ -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(

View File

@ -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,

View File

@ -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],

View File

@ -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",

View File

@ -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",
},
},
],
});

View File

@ -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",
},

View File

@ -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,

View File

@ -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",

View File

@ -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: {

View File

@ -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
View File

@ -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)