feat(button-group): button-group migrated to tw

This commit is contained in:
Junior Garcia 2023-02-19 18:23:03 -03:00
parent dd30f6c34d
commit 08949f5c72
12 changed files with 269 additions and 353 deletions

View File

@ -82,15 +82,17 @@ export function useAvatarGroup(props: UseAvatarGroupProps) {
const Component = as || "div";
const context: ContextType = {
size,
color,
radius,
isGrid,
isBordered,
isDisabled,
};
const context = useMemo(
() => ({
size,
color,
radius,
isGrid,
isBordered,
isDisabled,
}),
[size, color, radius, isGrid, isBordered, isDisabled],
);
const styles = useMemo(() => avatarGroup({className, isGrid}), [className, isGrid]);
const validChildren = getValidChildren(children);

View File

@ -2,14 +2,14 @@ import React from "react";
import {render} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {Button} from "../src";
import {ButtonGroup, Button} from "../src";
describe("ButtonGroup", () => {
it("should render correctly", () => {
const wrapper = render(
<Button.Group>
<ButtonGroup>
<Button>action</Button>
</Button.Group>,
</ButtonGroup>,
);
expect(() => wrapper.unmount()).not.toThrow();
@ -18,18 +18,18 @@ describe("ButtonGroup", () => {
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Button.Group ref={ref} />);
render(<ButtonGroup ref={ref} />);
expect(ref.current).not.toBeNull();
});
it("should ignore events when group disabled", () => {
const handler = jest.fn();
const wrapper = render(
<Button.Group disabled>
<ButtonGroup isDisabled={true}>
<Button data-testid="button-test" onClick={handler}>
action
</Button>
</Button.Group>,
</ButtonGroup>,
);
let button = wrapper.getByTestId("button-test");
@ -38,36 +38,20 @@ describe("ButtonGroup", () => {
expect(handler).toBeCalledTimes(0);
});
it("buttons should be displayed vertically", () => {
const wrapper = render(
<Button.Group vertical>
<Button>action1</Button>
<Button>action2</Button>
</Button.Group>,
);
expect(() => wrapper.unmount()).not.toThrow();
});
it("should render different variants", () => {
const wrapper = render(
<Button.Group>
<Button flat>button</Button>
<Button light color="warning">
<ButtonGroup>
<Button variant="flat">button</Button>
<Button color="warning" variant="light">
light
</Button>
<Button flat color="success">
<Button color="success" variant="light">
button
</Button>
<Button flat color="warning">
<Button color="warning" variant="bordered">
button
</Button>
<Button rounded>button</Button>
<Button flat>button</Button>
<Button shadow>button</Button>
<Button auto>button</Button>
<Button animated={false}>button</Button>
</Button.Group>,
</ButtonGroup>,
);
expect(() => wrapper.unmount()).not.toThrow();

View File

@ -1,8 +1,8 @@
import {createContext} from "@nextui-org/shared-utils";
import {UseButtonProps} from "./use-button";
import {ContextType} from "./use-button-group";
export const [ButtonGroupProvider, useButtonGroupContext] = createContext<UseButtonProps>({
export const [ButtonGroupProvider, useButtonGroupContext] = createContext<ContextType>({
name: "ButtonGroupContext",
strict: false,
});

View File

@ -1,120 +0,0 @@
import {styled} from "@nextui-org/system";
import {StyledButton} from "./button.styles";
export const StyledButtonGroup = styled("div", {
display: "inline-flex",
margin: "$3",
backgroundColor: "transparent",
height: "min-content",
[`& ${StyledButton}`]: {
".nextui-button-text": {
top: 0,
},
},
variants: {
size: {
xs: {
br: "$xs",
},
sm: {
br: "$sm",
},
md: {
br: "$md",
},
lg: {
br: "$base",
},
xl: {
br: "$xl",
},
},
isVertical: {
true: {
fd: "column",
[`& ${StyledButton}`]: {
"&:not(:first-child)": {
btlr: 0, // top-left
btrr: 0, // top-right
},
"&:not(:last-child)": {
bblr: 0,
bbrr: 0,
},
},
},
false: {
fd: "row",
[`& ${StyledButton}`]: {
"&:not(:first-child)": {
btlr: 0, // top-left
bblr: 0, // bottom-left
},
"&:not(:last-child)": {
btrr: 0, // top-right
bbrr: 0, // bottom-right
},
},
},
},
isRounded: {
true: {
br: "$pill",
},
},
isBordered: {
true: {
bg: "transparent",
},
},
isGradient: {
true: {
pl: 0,
},
},
},
compoundVariants: [
// isBordered / isVertical:true
{
isBordered: true,
isVertical: true,
css: {
[`& ${StyledButton}`]: {
"&:not(:last-child)": {
borderBottom: "none",
paddingBottom: "0",
},
},
},
},
// isBordered / isVertical:false
{
isBordered: true,
isVertical: false,
css: {
[`& ${StyledButton}`]: {
"&:not(:first-child)": {
borderLeft: "none",
},
},
},
},
// isBordered & isVertical:false & isGradient
{
isBordered: true,
isVertical: false,
isGradient: true,
css: {
[`& ${StyledButton}`]: {
"&:not(:last-child)&:not(:first-child)": {
pl: 0,
},
"&:last-child": {
pl: 0,
},
},
},
},
],
});

View File

@ -1,27 +1,22 @@
import {forwardRef} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {__DEV__} from "@nextui-org/shared-utils";
import {ButtonGroupProvider} from "./button-group-context";
import {UseButtonGroupProps, useButtonGroup} from "./use-button-group";
import {StyledButtonGroup} from "./button-group.styles";
export interface ButtonGroupProps extends UseButtonGroupProps {}
export interface ButtonGroupProps extends Omit<UseButtonGroupProps, "ref"> {}
const ButtonGroup = forwardRef<ButtonGroupProps, "div">((props, ref) => {
const {context, children, className, ...otherProps} = useButtonGroup(props);
const domRef = useDOMRef(ref);
const {Component, domRef, context, children, styles, getButtonGroupProps} = useButtonGroup({
ref,
...props,
});
return (
<ButtonGroupProvider value={context}>
<StyledButtonGroup
ref={domRef}
className={clsx("nextui-button-group", className)}
{...otherProps}
>
<Component ref={domRef} className={styles} {...getButtonGroupProps()}>
{children}
</StyledButtonGroup>
</Component>
</ButtonGroupProvider>
);
});

View File

@ -1,12 +1,13 @@
import Button from "./button";
// import ButtonGroup from "./button-group";
import ButtonGroup from "./button-group";
// export types
export type {ButtonProps} from "./button";
export type {ButtonGroupProps} from "./button-group";
// export hooks
export {useButton} from "./use-button";
// export {useButtonGroup} from "./use-button-group";
export {useButtonGroup} from "./use-button-group";
// export component
export {Button};
export {Button, ButtonGroup};

View File

@ -1,66 +1,100 @@
import type {UseButtonProps} from "./use-button";
import type {ButtonProps} from "./index";
import type {ReactRef} from "@nextui-org/shared-utils";
import type {ButtonGroupVariantProps} from "@nextui-org/theme";
import {useMemo} from "react";
import {HTMLNextUIProps} from "@nextui-org/system";
export interface UseButtonGroupProps extends HTMLNextUIProps<"div", Omit<UseButtonProps, "ref">> {
import {buttonGroup} from "@nextui-org/theme";
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {useMemo, useCallback} from "react";
export interface UseButtonGroupProps
extends HTMLNextUIProps<"div", Omit<ButtonProps, "ref" | "fullWidth">>,
ButtonGroupVariantProps {
/**
* Whether the buttons should be stacked vertically.
* @default false
* Ref to the DOM node.
*/
vertical?: boolean;
ref?: ReactRef<HTMLDivElement | null>;
}
export function useButtonGroup(props: UseButtonGroupProps) {
export type ContextType = {
size?: ButtonProps["size"];
color?: ButtonProps["color"];
radius?: ButtonProps["radius"];
variant?: ButtonProps["variant"];
isDisabled?: ButtonProps["isDisabled"];
disableAnimation?: ButtonProps["disableAnimation"];
disableRipple?: ButtonProps["disableRipple"];
fullWidth?: boolean;
};
export function useButtonGroup(originalProps: UseButtonGroupProps) {
const [props, variantProps] = mapPropsVariants(originalProps, buttonGroup.variantKeys);
const {
color = "default",
ref,
as,
children,
color = "neutral",
size = "md",
borderWeight = "normal",
disabled = false,
bordered = false,
light = false,
ghost = false,
flat = false,
shadow = false,
auto = true,
animated = true,
rounded = false,
ripple = true,
vertical = false,
variant = "solid",
radius = "xl",
isDisabled = false,
disableAnimation = false,
disableRipple = false,
className,
...otherProps
} = props;
const context = useMemo<UseButtonProps>(
() => ({
disabled,
size,
color,
bordered,
light,
ghost,
flat,
shadow,
auto,
borderWeight,
animated,
rounded,
ripple,
isButtonGroup: true,
}),
[disabled, animated, size, ripple, color, bordered, light, ghost, flat, borderWeight],
const Component = as || "div";
const domRef = useDOMRef(ref);
const styles = useMemo(
() =>
buttonGroup({
...variantProps,
className,
}),
[variantProps, className],
);
const isGradient = color === "gradient";
const context = useMemo<ContextType>(
() => ({
size,
color,
variant,
radius,
isDisabled,
disableAnimation,
disableRipple,
fullWidth: !!originalProps?.fullWidth,
}),
[
size,
color,
variant,
radius,
isDisabled,
disableAnimation,
disableRipple,
originalProps?.fullWidth,
],
);
const getButtonGroupProps = useCallback(
() => ({
role: "group",
...otherProps,
}),
[otherProps],
);
// TODO: the idea is to migrate the boolean names from "disable" to "isDisabled" (v12)
return {
Component,
children,
domRef,
context,
size,
isRounded: rounded,
isBordered: bordered || ghost,
isVertical: vertical,
isGradient,
...otherProps,
styles,
getButtonGroupProps,
};
}

View File

@ -19,9 +19,9 @@ import {useButtonGroupContext} from "./button-group-context";
export interface UseButtonProps
extends HTMLNextUIProps<"button", Omit<AriaButtonProps, keyof ButtonVariantProps>>,
Omit<ButtonVariantProps, "isFocusVisible"> {
Omit<ButtonVariantProps, "isFocusVisible" | "isInGroup" | "isInVerticalGroup"> {
/**
* the button ref.
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLButtonElement | null>;
/**
@ -47,6 +47,7 @@ export interface UseButtonProps
export function useButton(props: UseButtonProps) {
const groupContext = useButtonGroupContext();
const isInGroup = !!groupContext;
const {
ref,
@ -90,6 +91,7 @@ export function useButton(props: UseButtonProps) {
radius,
fullWidth,
isDisabled,
isInGroup,
isFocusVisible,
disableAnimation,
className,
@ -101,6 +103,7 @@ export function useButton(props: UseButtonProps) {
radius,
fullWidth,
isDisabled,
isInGroup,
isFocusVisible,
disableAnimation,
className,
@ -115,6 +118,8 @@ export function useButton(props: UseButtonProps) {
};
const handlePress = (e: PressEvent) => {
if (isDisabled) return;
if (e.pointerType === "keyboard" || e.pointerType === "virtual") {
handleDrip(e);
} else if (typeof window !== "undefined" && window.event) {

View File

@ -1,133 +1,69 @@
// import React from "react";
// import {Meta} from "@storybook/react";
// import {Grid} from "@nextui-org/grid";
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {button, buttonGroup} from "@nextui-org/theme";
// import {Button} from "../src";
import {Button, ButtonGroup, ButtonGroupProps} from "../src";
// export default {
// title: "General/ButtonGroup",
// component: Button,
// decorators: [
// (Story) => (
// <Grid.Container direction="column" gap={2} justify="center">
// <Story />
// </Grid.Container>
// ),
// ],
// } as Meta;
export default {
title: "General/ButtonGroup",
component: ButtonGroup,
argTypes: {
variant: {
control: {
type: "select",
options: ["solid", "bordered", "light", "flat", "shadow", "ghost"],
},
},
color: {
control: {
type: "select",
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "2xl", "3xl", "full"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
fullWidth: {
control: {
type: "boolean",
},
},
isDisabled: {
control: {
type: "boolean",
},
},
disableAnimation: {
control: {
type: "boolean",
},
},
},
} as ComponentMeta<typeof ButtonGroup>;
// export const Default = () => (
// <Button.Group>
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// );
const defaultProps = {
...button.defaultVariants,
...buttonGroup.defaultVariants,
};
// export const Loading = () => (
// <Button.Group>
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// );
const Template: ComponentStory<typeof ButtonGroup> = (args: ButtonGroupProps) => (
<ButtonGroup {...args}>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
);
// export const Variants = () => (
// <>
// <Button.Group color="success">
// <Button>One</Button>
// <Button disabled>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// <Button.Group color="gradient">
// <Button>One</Button>
// <Button disabled>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// <Button.Group color="error">
// <Button>One</Button>
// <Button>Two</Button>
// <Button disabled>Three</Button>
// </Button.Group>
// <Button.Group bordered color="primary">
// <Button disabled>Action1</Button>
// <Button>Action2</Button>
// <Button>Action3</Button>
// </Button.Group>
// <Button.Group bordered color="gradient">
// <Button disabled>Action1</Button>
// <Button>Action2</Button>
// <Button>Action3</Button>
// </Button.Group>
// <Button.Group flat color="warning">
// <Button>Action1</Button>
// <Button disabled>Action2</Button>
// <Button>Action2</Button>
// </Button.Group>
// <Button.Group color="secondary" size="sm">
// <Button disabled>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// <Button.Group light color="secondary">
// <Button>One</Button>
// <Button disabled>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// <Button.Group ghost color="gradient">
// <Button>One</Button>
// <Button disabled>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// </>
// );
// export const Sizes = () => (
// <>
// <Button.Group size="xs">
// <Button>One</Button>
// <Button>Two</Button>
// </Button.Group>
// <Button.Group size="sm">
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// <Button.Group size="md">
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// <Button.Group size="lg">
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// <Button.Group size="xl">
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// </>
// );
// export const Vertical = () => (
// <>
// <Button.Group vertical size="sm">
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// <Button>Four</Button>
// </Button.Group>
// </>
// );
// export const Disabled = () => (
// <>
// <Button.Group disabled size="sm">
// <Button>One</Button>
// <Button>Two</Button>
// <Button>Three</Button>
// </Button.Group>
// </>
// );
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};

View File

@ -0,0 +1,27 @@
import {tv, VariantProps} from "tailwind-variants";
/**
* ButtonGroup wrapper **Tailwind Variants** component
*
* const styles = buttonGroup({...})
*
* @example
* <div role="group" className={styles())}>
* // button elements
* </div>
*/
const buttonGroup = tv({
base: "inline-flex items-center justify-center h-auto w-max-content",
variants: {
fullWidth: {
true: "w-full",
},
},
defaultVariants: {
fullWidth: false,
},
});
export type ButtonGroupVariantProps = VariantProps<typeof buttonGroup>;
export {buttonGroup};

View File

@ -72,6 +72,9 @@ const button = tv({
isFocusVisible: {
true: [...ringClasses],
},
isInGroup: {
true: "[&:not(:first-child):not(:last-child)]:rounded-none",
},
disableAnimation: {
false: "transition-transform",
true: "!transition-none",
@ -84,6 +87,7 @@ const button = tv({
radius: "xl",
fullWidth: false,
isDisabled: false,
isInGroup: false,
disableAnimation: false,
},
compoundVariants: [
@ -248,6 +252,53 @@ const button = tv({
disableAnimation: false,
class: "transition-[transform,background]",
},
// isInGroup / radius
{
isInGroup: true,
radius: "base",
class: "rounded-none first:rounded-l last:rounded-r",
},
{
isInGroup: true,
radius: "sm",
class: "rounded-none first:rounded-l-sm last:rounded-r-sm",
},
{
isInGroup: true,
radius: "md",
class: "rounded-none first:rounded-l-md last:rounded-r-md",
},
{
isInGroup: true,
radius: "lg",
class: "rounded-none first:rounded-l-lg last:rounded-r-lg",
},
{
isInGroup: true,
radius: "xl",
class: "rounded-none first:rounded-l-xl last:rounded-r-xl",
},
{
isInGroup: true,
radius: "2xl",
class: "rounded-none first:rounded-l-2xl last:rounded-r-2xl",
},
{
isInGroup: true,
radius: "3xl",
class: "rounded-none first:rounded-l-3xl last:rounded-r-3xl",
},
{
isInGroup: true,
radius: "full",
class: "rounded-none first:rounded-l-full last:rounded-r-full",
},
// isInGroup / bordered
{
isInGroup: true,
variant: "bordered",
class: "[&:not(:first-child)]:border-l-0",
},
],
});

View File

@ -3,5 +3,6 @@ export * from "./avatar";
export * from "./avatar-group";
export * from "./user";
export * from "./button";
export * from "./button-group";
export * from "./drip";
export * from "./spinner";