feat(components): button component added

This commit is contained in:
Junior Garcia 2022-10-08 18:32:20 -03:00
parent 6991692525
commit 54d26fed91
22 changed files with 2171 additions and 3 deletions

View File

@ -0,0 +1,24 @@
# @nextui-org/button
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/button
# or
npm i @nextui-org/button
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,72 @@
import * as React from "react";
import {render} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {Button} from "../src";
describe("Button", () => {
it("should render correctly", () => {
const wrapper = render(<Button />);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLButtonElement>();
render(<Button ref={ref} />);
expect(ref.current).not.toBeNull();
});
it("should trigger onPress function", () => {
const onPress = jest.fn();
const {getByRole} = render(<Button onPress={onPress} />);
getByRole("button").click();
expect(onPress).toHaveBeenCalled();
});
it("should show warning message when onClick is being used", () => {
const onClick = jest.fn();
const spy = jest.spyOn(console, "warn").mockImplementation(() => {});
const wrapper = render(<Button onClick={onClick} />);
let button = wrapper.getByRole("button");
userEvent.click(button);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it("should ignore events when disabled", () => {
const onPress = jest.fn();
const {getByRole} = render(<Button disabled onPress={onPress} />);
getByRole("button").click();
expect(onPress).not.toHaveBeenCalled();
});
it("should renders with left icon", () => {
const wrapper = render(
<Button icon={<span data-testid="left-icon">Icon</span>}>Button</Button>,
);
expect(wrapper.getByTestId("left-icon")).toBeInTheDocument();
});
it("should renders with right icon", () => {
const wrapper = render(
<Button iconRight={<span data-testid="right-icon">Icon</span>}>Button</Button>,
);
expect(wrapper.getByTestId("right-icon")).toBeInTheDocument();
});
it("should have the proper type attribute", () => {
const wrapper = render(<Button type="submit" />);
expect(wrapper.getByRole("button")).toHaveAttribute("type", "submit");
});
});

View File

@ -0,0 +1,75 @@
import React from "react";
import {render} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {Button} from "../src";
describe("ButtonGroup", () => {
it("should render correctly", () => {
const wrapper = render(
<Button.Group>
<Button>action</Button>
</Button.Group>,
);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Button.Group ref={ref} />);
expect(ref.current).not.toBeNull();
});
it("should ignore events when group disabled", () => {
const handler = jest.fn();
const wrapper = render(
<Button.Group disabled>
<Button data-testid="button-test" onClick={handler}>
action
</Button>
</Button.Group>,
);
let button = wrapper.getByTestId("button-test");
userEvent.click(button);
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">
light
</Button>
<Button flat color="success">
button
</Button>
<Button flat color="warning">
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>,
);
expect(() => wrapper.unmount()).not.toThrow();
});
});

View File

@ -0,0 +1,3 @@
{ "replace": { "main": "dist/index.cjs.js", "module": "dist/index.esm.js",
"types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js" }, "./package.json": "./package.json" } } }

View File

@ -0,0 +1,58 @@
{
"name": "@nextui-org/button",
"version": "1.0.0-beta.11",
"description": "Buttons allow users to perform actions and choose with a single tap.",
"keywords": [
"button"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/components/button"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src/index.ts --format=esm,cjs --dts",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"build:fast": "tsup src/index.ts --format=esm,cjs",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"dependencies": {
"@nextui-org/system": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/shared-css": "workspace:*",
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/drip": "workspace:*",
"@react-aria/button": "^3.6.2",
"@react-aria/interactions": "^3.12.0",
"@react-aria/utils": "^3.14.0",
"@react-aria/focus": "^3.9.0"
},
"devDependencies": {
"@nextui-org/spacer": "workspace:*",
"@nextui-org/icons-utils": "workspace:*",
"@react-types/shared": "^3.14.1",
"@react-types/button": "^3.6.2",
"clean-package": "2.1.1",
"react": "^17.0.2"
}
}

View File

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

View File

@ -0,0 +1,120 @@
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

@ -0,0 +1,35 @@
import {forwardRef} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __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 {}
const ButtonGroup = forwardRef<ButtonGroupProps, "div">((props, ref) => {
const {context, children, className, ...otherProps} = useButtonGroup(props);
const domRef = useDOMRef(ref);
return (
<ButtonGroupProvider value={context}>
<StyledButtonGroup
ref={domRef}
className={clsx("nextui-button-group", className)}
{...otherProps}
>
{children}
</StyledButtonGroup>
</ButtonGroupProvider>
);
});
if (__DEV__) {
ButtonGroup.displayName = "NextUI.ButtonGroup";
}
ButtonGroup.toString = () => ".nextui-button-group";
export default ButtonGroup;

View File

@ -0,0 +1,130 @@
import {styled, forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
export interface ButtonIconProps extends HTMLNextUIProps<"span"> {
isAuto?: boolean;
isRight?: boolean;
isSingle?: boolean;
isGradientButtonBorder?: boolean;
}
const StyledButtonIcon = styled("span", {
dflex: "center",
position: "absolute",
left: "$$buttonPadding",
right: "auto",
top: "50%",
transform: "translateY(-50%)",
color: "inherit",
zIndex: "$1",
"& svg": {
background: "transparent",
},
variants: {
isAuto: {
true: {
position: "relative",
transform: "none",
top: "0%",
},
},
isRight: {
true: {
right: "$$buttonPadding",
left: "auto",
},
},
isSingle: {
true: {
position: "static",
transform: "none",
},
},
isGradientButtonBorder: {
true: {},
},
},
compoundVariants: [
// isAuto && isRight
{
isAuto: true,
isRight: true,
isSingle: false,
css: {
order: 2,
ml: "calc($$buttonPadding / 2)",
right: "0%",
left: "0%",
},
},
// isAuto && !isRight
{
isAuto: true,
isRight: false,
isSingle: false,
css: {
order: 0,
mr: "calc($$buttonPadding / 2)",
right: "0%",
left: "0%",
},
},
// isSingle && isRight
{
isSingle: true,
isRight: false,
css: {
ml: 0,
},
},
// isSingle && !isRight
{
isSingle: true,
isRight: true,
css: {
mr: 0,
},
},
// isSingle && !isRight && hasButttonBorder
{
isSingle: true,
isRight: false,
isGradientButtonBorder: true,
css: {
mr: "calc($$buttonPadding / 2)",
},
},
],
});
const ButtonIcon = forwardRef<ButtonIconProps, "span">((props, ref) => {
const {children, className, ...otherProps} = props;
const domRef = useDOMRef(ref);
return (
<StyledButtonIcon
ref={domRef}
className={clsx(
"nextui-button-icon",
{
"nextui-button-icon-right": props.isRight,
"nextui-button-icon-single": props.isSingle,
},
className,
)}
{...otherProps}
>
{children}
</StyledButtonIcon>
);
});
if (__DEV__) {
ButtonIcon.displayName = "NextUI.ButtonIcon";
}
ButtonIcon.toString = () => ".nextui-button-icon";
export default ButtonIcon;

View File

@ -0,0 +1,52 @@
import type {UseButtonProps} from "./use-button";
export const getColors = (props: UseButtonProps) => {
if (!props.disabled) {
if (props.auto && props.color === "gradient" && (props.bordered || props.ghost)) {
return {
px: "$$buttonBorderWeight",
py: "$$buttonBorderWeight",
};
}
return {};
}
const defaultDisabledCss = {
bg: "$accents1",
color: "$accents7",
transform: "none",
boxShadow: "none",
pe: "none",
};
if (!props.bordered && !props.flat && !props.ghost && !props.light) {
return defaultDisabledCss;
}
if (props.color === "gradient" && (props.bordered || props.ghost)) {
return {
color: "$accents4",
backgroundImage:
"linear-gradient($background, $background), linear-gradient($accents2, $accents2)",
transform: "none",
boxShadow: "none",
pe: "none",
pl: "$$buttonBorderWeight",
pr: "$$buttonBorderWeight",
};
}
if (props.bordered || props.ghost || props.light) {
return {
...defaultDisabledCss,
bg: "transparent",
borderColor: "$accents4",
};
}
if (props.flat) {
return {
...defaultDisabledCss,
bg: "$accents1",
};
}
return {};
};

View File

@ -0,0 +1,815 @@
import {styled} from "@nextui-org/system";
import {cssFocusVisible} from "@nextui-org/shared-css";
export const StyledButton = styled(
"button",
{
$$buttonBorderRadius: "$radii$md",
$$buttonPressedScale: 0.97,
dflex: "center",
appearance: "none",
boxSizing: "border-box",
fontWeight: "$medium",
us: "none",
lineHeight: "$sm",
ta: "center",
whiteSpace: "nowrap",
transition: "$button",
position: "relative",
overflow: "hidden",
border: "none",
cursor: "pointer",
pe: "auto",
p: 0,
br: "$$buttonBorderRadius",
"@motion": {
transition: "none",
},
".nextui-button-text": {
dflex: "center",
zIndex: "$2",
"p, pre, div": {
margin: 0,
},
},
".nextui-drip": {
zIndex: "$1",
".nextui-drip-filler": {
opacity: 0.25,
fill: "$accents2",
},
},
variants: {
bordered: {
true: {
bg: "transparent",
borderStyle: "solid",
color: "$text",
},
},
ghost: {
true: {},
},
color: {
default: {
bg: "$primary",
color: "$primarySolidContrast",
},
primary: {
bg: "$primary",
color: "$primarySolidContrast",
},
secondary: {
bg: "$secondary",
color: "$secondarySolidContrast",
},
success: {
bg: "$success",
color: "$successSolidContrast",
},
warning: {
bg: "$warning",
color: "$warningSolidContrast",
},
error: {
bg: "$error",
color: "$errorSolidContrast",
},
gradient: {
bg: "$gradient",
color: "$primarySolidContrast",
},
},
size: {
xs: {
$$buttonPadding: "$space$3",
$$buttonBorderRadius: "$radii$xs",
$$buttonHeight: "$space$10",
px: "$3",
height: "$$buttonHeight",
lh: "$space$10",
width: "auto",
minWidth: "$20",
fontSize: "$xs",
},
sm: {
$$buttonPadding: "$space$5",
$$buttonBorderRadius: "$radii$sm",
$$buttonHeight: "$space$12",
px: "$5",
height: "$$buttonHeight",
lh: "$space$14",
width: "auto",
minWidth: "$36",
fontSize: "$sm",
},
md: {
$$buttonPadding: "$space$7",
$$buttonBorderRadius: "$radii$md",
$$buttonHeight: "$space$14",
px: "$7",
height: "$$buttonHeight",
lh: "$space$14",
width: "auto",
minWidth: "$48",
fontSize: "$sm",
},
lg: {
$$buttonPadding: "$space$9",
$$buttonBorderRadius: "$radii$base",
$$buttonHeight: "$space$16",
px: "$9",
height: "$$buttonHeight",
lh: "$space$15",
width: "auto",
minWidth: "$60",
fontSize: "$md",
},
xl: {
$$buttonPadding: "$space$10",
$$buttonBorderRadius: "$radii$xl",
$$buttonHeight: "$space$18",
px: "$10",
height: "$$buttonHeight",
lh: "$space$17",
width: "auto",
minWidth: "$72",
fontSize: "$lg",
},
},
borderWeight: {
light: {
bw: "$light",
$$buttonBorderWeight: "$borderWeights$light",
},
normal: {
bw: "$normal",
$$buttonBorderWeight: "$borderWeights$normal",
},
bold: {
bw: "$bold",
$$buttonBorderWeight: "$borderWeights$bold",
},
extrabold: {
bw: "$extrabold",
$$buttonBorderWeight: "$borderWeights$extrabold",
},
black: {
bw: "$black",
$$buttonBorderWeight: "$borderWeights$black",
},
},
flat: {
true: {
color: "$text",
},
},
light: {
true: {
bg: "transparent",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.8,
fill: "$accents2",
},
},
},
},
shadow: {
true: {
bs: "$sm",
},
},
animated: {
false: {
transition: "none",
},
},
auto: {
true: {
width: "auto",
minWidth: "min-content",
},
},
rounded: {
true: {
$$buttonBorderRadius: "$radii$pill",
},
},
isPressed: {
true: {},
},
isHovered: {
true: {},
},
isChildLess: {
true: {
p: 0,
width: "$$buttonHeight",
height: "$$buttonHeight",
},
},
},
compoundVariants: [
// isPressed && animated
{
isPressed: true,
animated: true,
css: {
transform: "scale($$buttonPressedScale)",
},
},
// size / auto / isChildLess
{
auto: true,
isChildLess: false,
size: "xs",
css: {
px: "$5",
minWidth: "min-content",
},
},
{
auto: true,
isChildLess: false,
size: "sm",
css: {
px: "$8",
minWidth: "min-content",
},
},
{
auto: true,
isChildLess: false,
size: "md",
css: {
px: "$9",
minWidth: "min-content",
},
},
{
auto: true,
isChildLess: false,
size: "lg",
css: {
px: "$10",
minWidth: "min-content",
},
},
{
auto: true,
isChildLess: false,
size: "xl",
css: {
px: "$11",
minWidth: "min-content",
},
},
// shadow / color
{
shadow: true,
color: "default",
css: {
normalShadow: "$primaryShadow",
},
},
{
shadow: true,
color: "primary",
css: {
normalShadow: "$primaryShadow",
},
},
{
shadow: true,
color: "secondary",
css: {
normalShadow: "$secondaryShadow",
},
},
{
shadow: true,
color: "warning",
css: {
normalShadow: "$warningShadow",
},
},
{
shadow: true,
color: "success",
css: {
normalShadow: "$successShadow",
},
},
{
shadow: true,
color: "error",
css: {
normalShadow: "$errorShadow",
},
},
{
shadow: true,
color: "gradient",
css: {
normalShadow: "$primaryShadow",
},
},
// light / color
{
light: true,
color: "default",
css: {
bg: "transparent",
color: "$text",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.8,
fill: "$primaryLightActive",
},
},
},
},
{
light: true,
color: "primary",
css: {
bg: "transparent",
color: "$primary",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.8,
fill: "$primaryLightActive",
},
},
},
},
{
light: true,
color: "secondary",
css: {
bg: "transparent",
color: "$secondary",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.8,
fill: "$secondaryLightActive",
},
},
},
},
{
light: true,
color: "warning",
css: {
bg: "transparent",
color: "$warning",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.8,
fill: "$warningLightActive",
},
},
},
},
{
light: true,
color: "success",
css: {
bg: "transparent",
color: "$success",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.8,
fill: "$successLightActive",
},
},
},
},
{
light: true,
color: "error",
css: {
bg: "transparent",
color: "$error",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.8,
fill: "$errorLightActive",
},
},
},
},
// bordered / color
{
bordered: true,
color: "default",
css: {
bg: "transparent",
borderColor: "$primary",
color: "$primary",
".nextui-drip": {
".nextui-drip-filler": {
fill: "$primary",
},
},
},
},
{
bordered: true,
color: "primary",
css: {
bg: "transparent",
borderColor: "$primary",
color: "$primary",
".nextui-drip": {
".nextui-drip-filler": {
fill: "$primary",
},
},
},
},
{
bordered: true,
color: "secondary",
css: {
bg: "transparent",
borderColor: "$secondary",
color: "$secondary",
".nextui-drip": {
".nextui-drip-filler": {
fill: "$secondary",
},
},
},
},
{
bordered: true,
color: "success",
css: {
bg: "transparent",
borderColor: "$success",
color: "$success",
".nextui-drip": {
".nextui-drip-filler": {
fill: "$success",
},
},
},
},
{
bordered: true,
color: "warning",
css: {
bg: "transparent",
borderColor: "$warning",
color: "$warning",
".nextui-drip": {
".nextui-drip-filler": {
fill: "$warning",
},
},
},
},
{
bordered: true,
color: "error",
css: {
bg: "transparent",
borderColor: "$error",
color: "$error",
".nextui-drip": {
".nextui-drip-filler": {
fill: "$error",
},
},
},
},
{
bordered: true,
color: "gradient",
css: {
bg: "transparent",
color: "$text",
padding: "$$buttonBorderWeight",
bgClip: "content-box, border-box",
borderColor: "$primary",
backgroundImage: "linear-gradient($background, $background), $gradient",
border: "none",
".nextui-drip": {
".nextui-drip-filler": {
fill: "$secondary",
},
},
},
},
// ghost / color && isHovered
{
ghost: true,
isHovered: true,
color: "default",
css: {
bg: "$primary",
color: "$primarySolidContrast",
},
},
{
ghost: true,
isHovered: true,
color: "primary",
css: {
bg: "$primary",
color: "$primarySolidContrast",
},
},
{
ghost: true,
isHovered: true,
color: "secondary",
css: {
bg: "$secondary",
color: "$secondarySolidContrast",
},
},
{
ghost: true,
isHovered: true,
color: "success",
css: {
bg: "$success",
color: "$successSolidContrast",
},
},
{
ghost: true,
isHovered: true,
color: "warning",
css: {
bg: "$warning",
color: "$warningSolidContrast",
},
},
{
ghost: true,
isHovered: true,
color: "error",
css: {
bg: "$error",
color: "$errorSolidContrast",
},
},
{
ghost: true,
color: "gradient",
isHovered: true,
css: {
bg: "$gradient",
color: "$white",
},
},
// flat / color
{
flat: true,
color: "default",
css: {
bg: "$primaryLight",
color: "$primaryLightContrast",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.4,
fill: "$primary",
},
},
},
},
{
flat: true,
color: "primary",
css: {
bg: "$primaryLight",
color: "$primaryLightContrast",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.4,
fill: "$primary",
},
},
},
},
{
flat: true,
color: "secondary",
css: {
bg: "$secondaryLight",
color: "$secondaryLightContrast",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.4,
fill: "$secondary",
},
},
},
},
{
flat: true,
color: "success",
css: {
bg: "$successLight",
color: "$successLightContrast",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.4,
fill: "$success",
},
},
},
},
{
flat: true,
color: "warning",
css: {
bg: "$warningLight",
color: "$warningLightContrast",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.4,
fill: "$warning",
},
},
},
},
{
flat: true,
color: "error",
css: {
bg: "$errorLight",
color: "$errorLightContrast",
".nextui-drip": {
".nextui-drip-filler": {
opacity: 0.4,
fill: "$error",
},
},
},
},
// flat / isHovered / color
{
flat: true,
isHovered: true,
color: "default",
css: {
bg: "$primaryLightHover",
},
},
{
flat: true,
isHovered: true,
color: "primary",
css: {
bg: "$primaryLightHover",
},
},
{
flat: true,
isHovered: true,
color: "secondary",
css: {
bg: "$secondaryLightHover",
},
},
{
flat: true,
isHovered: true,
color: "success",
css: {
bg: "$successLightHover",
},
},
{
flat: true,
isHovered: true,
color: "warning",
css: {
bg: "$warningLightHover",
},
},
{
flat: true,
isHovered: true,
color: "error",
css: {
bg: "$errorLightHover",
},
},
// flat / isPressed / color
{
flat: true,
isPressed: true,
color: "default",
css: {
bg: "$primaryLightActive",
},
},
{
flat: true,
isPressed: true,
color: "primary",
css: {
bg: "$primaryLightActive",
},
},
{
flat: true,
isPressed: true,
color: "secondary",
css: {
bg: "$secondaryLightActive",
},
},
{
flat: true,
isPressed: true,
color: "success",
css: {
bg: "$successLightActive",
},
},
{
flat: true,
isPressed: true,
color: "warning",
css: {
bg: "$warningLightActive",
},
},
{
flat: true,
isPressed: true,
color: "error",
css: {
bg: "$errorLightActive",
},
},
// auto / gradient-color / bordered
{
auto: true,
color: "gradient",
bordered: true,
css: {
".nextui-button-text": {
px: "$$buttonPadding",
},
".nextui-button-icon": {
ml: "$$buttonPadding",
},
".nextui-button-icon-right": {
mr: "$$buttonPadding",
},
".nextui-button-text-left": {
pl: 0,
},
".nextui-button-text-right": {
pr: 0,
},
},
},
// rounded && size
{
rounded: true,
size: "xs",
css: {
br: "$pill",
},
},
{
rounded: true,
size: "sm",
css: {
br: "$pill",
},
},
{
rounded: true,
size: "md",
css: {
br: "$pill",
},
},
{
rounded: true,
size: "lg",
css: {
br: "$pill",
},
},
{
rounded: true,
size: "xl",
css: {
br: "$pill",
},
},
],
},
cssFocusVisible,
);

View File

@ -0,0 +1,97 @@
import {forwardRef} from "@nextui-org/system";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {Drip} from "@nextui-org/drip";
import {Children, useMemo} from "react";
import ButtonGroup from "./button-group";
import ButtonIcon from "./button-icon";
import {StyledButton} from "./button.styles";
import {UseButtonProps, useButton} from "./use-button";
export interface ButtonProps extends Omit<UseButtonProps, "ref"> {}
type CompoundButton = {
Group: typeof ButtonGroup;
};
const Button = forwardRef<ButtonProps, "button", CompoundButton>((props, ref) => {
const {
buttonRef,
children,
state,
as,
css,
hasIcon,
dripBindings,
isRightIcon,
cssColors,
getIconCss,
className,
isGradientButtonBorder,
getButtonProps,
} = useButton({
ref,
...props,
});
const buttonProps = useMemo(() => getButtonProps(), [getButtonProps]);
return (
<StyledButton
ref={buttonRef}
as={as}
className={clsx("nextui-button", className)}
css={{
...cssColors,
...css,
}}
data-state={state}
{...buttonProps}
>
{Children.count(children) === 0 ? (
<ButtonIcon
isSingle
css={getIconCss}
isAuto={buttonProps.auto}
isGradientButtonBorder={isGradientButtonBorder}
isRight={isRightIcon}
>
{hasIcon}
</ButtonIcon>
) : hasIcon ? (
<>
<ButtonIcon
css={getIconCss}
isAuto={buttonProps.auto}
isGradientButtonBorder={isGradientButtonBorder}
isRight={isRightIcon}
isSingle={false}
>
{hasIcon}
</ButtonIcon>
<div
className={clsx("nextui-button-text", {
"nextui-button-text-right": isRightIcon,
"nextui-button-text-left": !isRightIcon,
})}
>
{children}
</div>
</>
) : (
<span className="nextui-button-text">{children}</span>
)}
<Drip color="white" {...dripBindings} />
</StyledButton>
);
});
Button.Group = ButtonGroup;
if (__DEV__) {
Button.displayName = "NextUI.Button";
}
Button.toString = () => ".nextui-button";
export default Button;

View File

@ -0,0 +1,5 @@
// export types
export type {ButtonProps} from "./button";
// export component
export {default as Button} from "./button";

View File

@ -0,0 +1,67 @@
import type {UseButtonProps} from "./use-button";
import {useMemo} from "react";
import {HTMLNextUIProps} from "@nextui-org/system";
export interface UseButtonGroupProps extends HTMLNextUIProps<"div", Omit<UseButtonProps, "ref">> {
/**
* Whether the buttons should be stacked vertically.
* @default false
*/
vertical?: boolean;
}
export function useButtonGroup(props: UseButtonGroupProps) {
const {
color = "default",
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,
...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 isGradient = color === "gradient";
// TODO: the idea is to migrate the boolean names from "disable" to "isDisabled" (v12)
return {
context,
size,
isRounded: rounded,
isBordered: bordered || ghost,
isVertical: vertical,
isGradient,
...otherProps,
};
}
export type UseButtonGroupReturn = ReturnType<typeof useButtonGroup>;

View File

@ -0,0 +1,276 @@
import type {AriaButtonProps} from "@react-types/button";
import type {PressEvent} from "@react-types/shared";
import type {ReactRef, NormalColors, NormalSizes, NormalWeights} from "@nextui-org/shared-utils";
import type {HTMLNextUIProps, CSS} from "@nextui-org/system";
import {MouseEventHandler, ReactNode, useCallback, useMemo, Children} from "react";
import {useButton as useAriaButton} from "@react-aria/button";
import {useFocusRing} from "@react-aria/focus";
import {mergeProps} from "@react-aria/utils";
import {useDrip} from "@nextui-org/drip";
import {useHover} from "@react-aria/interactions";
import {IFocusRingAria, useDOMRef} from "@nextui-org/dom-utils";
import {__DEV__} from "@nextui-org/shared-utils";
import {getColors} from "./button-utils";
import {useButtonGroupContext} from "./button-group-context";
export interface UseButtonProps extends HTMLNextUIProps<"button", AriaButtonProps> {
/**
* the button ref.
*/
ref?: ReactRef<HTMLButtonElement | null>;
/**
* The button color.
* @default "default"
*/
color?: NormalColors;
/**
* The button size.
* @default "md"
*/
size?: NormalSizes;
/**
* The border weight of the border button.
* @default "normal"
*/
borderWeight?: NormalWeights;
/**
* Whether the button should autoscale its width to fit its content.
* @default false
*/
auto?: boolean;
/**
* Whether the button is disabled.
* @default false
*/
disabled?: boolean;
/**
* Whether the button should be rounded.
* @default false
*/
bordered?: boolean;
/**
* Whether the button should be light.
* @default false
*/
light?: boolean;
/**
* Whether the button should be flat.
* @default false
*/
flat?: boolean;
/**
* Whether the button should display a shadow.
* @default false
*/
shadow?: boolean;
/**
* Whether the button should be rounded.
* @default false
*/
rounded?: boolean;
/**
* Whether the button should have a ghost look.
* @default false
*/
ghost?: boolean;
/**
* Whether the button have animations.
* @default true
*/
animated?: boolean;
/**
* Whether the button should display a ripple effect on press.
* @default true
*/
ripple?: boolean;
/**
* The button left content.
*/
icon?: ReactNode;
/**
* The button right content.
*/
iconRight?: ReactNode;
/**
* The button left content css object.
*/
iconLeftCss?: CSS;
/**
* The button right content css object.
*/
iconRightCss?: CSS;
/**
* The native button click event handler.
* @deprecated - use `onPress` instead.
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
}
export function useButton(props: UseButtonProps) {
const groupContext = useButtonGroupContext();
const {
ref,
as,
css,
children,
iconLeftCss,
iconRightCss,
autoFocus,
icon,
iconRight,
className,
auto = groupContext?.auto ?? false,
size = groupContext?.size ?? "md",
color = groupContext?.color ?? "default",
shadow = groupContext?.shadow ?? false,
flat = groupContext?.flat ?? false,
ghost = groupContext?.ghost ?? false,
light = groupContext?.light ?? false,
bordered = groupContext?.bordered ?? false,
borderWeight = groupContext?.borderWeight ?? "normal",
animated = groupContext?.animated ?? true,
rounded = groupContext?.rounded ?? false,
ripple = groupContext?.ripple ?? true,
disabled = groupContext?.disabled ?? false,
onClick: deprecatedOnClick,
onPress,
onPressStart,
onPressEnd,
onPressChange,
onPressUp,
...otherProps
} = props;
const buttonRef = useDOMRef(ref);
const hasIcon = icon || iconRight;
const isChildLess = Children.count(children) === 0;
const isRightIcon = Boolean(iconRight);
const isGradientButtonBorder = useMemo(
() => color === "gradient" && (bordered || ghost),
[color, bordered, ghost],
);
/* eslint-enable @typescript-eslint/no-unused-vars */
if (__DEV__ && color === "gradient" && (flat || light)) {
console.warn("Using the gradient color on flat and light buttons will have no effect.");
}
const buttonProps = {
auto,
size,
color,
shadow,
flat,
ghost,
light,
bordered,
borderWeight,
animated,
rounded,
disabled,
};
const cssColors = getColors(buttonProps) as CSS;
const {onClick: onDripClickHandler, ...dripBindings} = useDrip(false, buttonRef);
const handleDrip = (e: React.MouseEvent<HTMLButtonElement> | PressEvent | Event) => {
if (animated && ripple && buttonRef.current) {
onDripClickHandler(e);
}
};
const handlePress = (e: PressEvent) => {
if (e.pointerType === "keyboard" || e.pointerType === "virtual") {
handleDrip(e);
} else if (typeof window !== "undefined" && window.event) {
handleDrip(window.event);
}
if (deprecatedOnClick) {
deprecatedOnClick(e as any);
console.warn("onClick is deprecated, please use onPress");
}
onPress?.(e);
};
const {buttonProps: buttonAriaProps, isPressed} = useAriaButton(
{
...otherProps,
elementType: as,
onPress: handlePress,
onPressStart,
onPressEnd,
onPressChange,
onPressUp,
} as AriaButtonProps,
buttonRef,
);
const {hoverProps, isHovered} = useHover({isDisabled: disabled});
const {isFocused, isFocusVisible, focusProps}: IFocusRingAria<UseButtonProps> = useFocusRing({
autoFocus,
});
const getButtonProps = useCallback(() => {
return mergeProps(buttonAriaProps, hoverProps, focusProps, otherProps, {
...buttonProps,
bordered: buttonProps.bordered || buttonProps.ghost,
isFocusVisible: isFocusVisible && !buttonProps.disabled,
isHovered: isHovered || (buttonProps.ghost && isFocused),
isChildLess,
isPressed,
});
}, [
buttonProps,
buttonAriaProps,
hoverProps,
focusProps,
isFocusVisible,
isHovered,
isFocused,
isPressed,
isChildLess,
otherProps,
]);
const state = useMemo(() => {
if (isPressed) return "pressed";
if (isHovered) return "hovered";
return disabled ? "disabled" : "ready";
}, [disabled, isHovered, isPressed]);
const getIconCss = useMemo<any>(() => {
if (isRightIcon) return iconRightCss;
return iconLeftCss;
}, [isRightIcon, iconRightCss, iconLeftCss]);
return {
as,
css,
state,
icon,
children,
buttonRef,
className,
cssColors,
hasIcon,
iconRight,
isFocused,
isRightIcon,
isFocusVisible,
isGradientButtonBorder,
dripBindings,
getIconCss,
getButtonProps,
};
}
export type UseButtonReturn = ReturnType<typeof useButton>;

View File

@ -0,0 +1,260 @@
import React from "react";
import {Meta} from "@storybook/react";
import {Spacer} from "@nextui-org/spacer";
import {Lock, Notification, User, Camera, Activity} from "@nextui-org/icons-utils";
import {Button} from "../src";
export default {
title: "General/Button",
component: Button,
decorators: [
(Story) => (
<div style={{}}>
<Story />
</div>
),
],
} as Meta;
export const Default = () => <Button>Action</Button>;
export const Sizes = () => (
<div>
<Button size="xs">Mini</Button>
<Spacer y={0.5} />
<Button color="secondary" size="sm">
Small
</Button>
<Spacer y={0.5} />
<Button color="success" size="md">
Medium
</Button>
<Spacer y={0.5} />
<Button color="warning" size="lg">
Large
</Button>
<Spacer y={0.5} />
<Button color="error" size="xl">
Extra Large
</Button>
<Spacer y={0.5} />
<Button auto color="gradient">
Auto width
</Button>
</div>
);
export const Colors = () => (
<>
<Button color="primary">Primary</Button>
<Spacer y={0.5} />
<Button color="secondary">Secondary</Button>
<Spacer y={0.5} />
<Button color="success">Success</Button>
<Spacer y={0.5} />
<Button color="warning">Warning</Button>
<Spacer y={0.5} />
<Button color="error">Error</Button>
<Spacer y={0.5} />
<Button color="gradient">Gradient</Button>
<Spacer y={0.5} />
</>
);
export const Ghost = () => (
<>
<Button ghost color="primary">
Primary
</Button>
<Spacer y={0.5} />
<Button ghost color="secondary">
Secondary
</Button>
<Spacer y={0.5} />
<Button ghost color="success">
Success
</Button>
<Spacer y={0.5} />
<Button ghost color="warning">
Warning
</Button>
<Spacer y={0.5} />
<Button ghost color="error">
Error
</Button>
<Spacer y={0.5} />
<Button ghost color="gradient">
Gradient
</Button>
<Spacer y={0.5} />
</>
);
export const Disabled = () => <Button disabled>Action</Button>;
export const Shadow = () => (
<>
<Button shadow color="primary">
Primary
</Button>
<Spacer y={1} />
<Button shadow color="secondary">
Secondary
</Button>
<Spacer y={1} />
<Button shadow color="success">
Success
</Button>
<Spacer y={1} />
<Button shadow color="warning">
Warning
</Button>
<Spacer y={1} />
<Button shadow color="error">
Error
</Button>
<Spacer y={1} />
<Button shadow color="gradient">
Gradient
</Button>
</>
);
export const Bordered = () => (
<>
<Button bordered color="primary">
Primary
</Button>
<Spacer y={0.5} />
<Button bordered color="secondary">
Secondary
</Button>
<Spacer y={0.5} />
<Button bordered color="success">
Success
</Button>
<Spacer y={0.5} />
<Button bordered color="warning">
Warning
</Button>
<Spacer y={0.5} />
<Button bordered color="error">
Error
</Button>
<Spacer y={0.5} />
<Button bordered color="gradient">
Gradient
</Button>
</>
);
export const Flat = () => (
<>
<Button flat color="primary">
Primary
</Button>
<Spacer y={0.5} />
<Button flat color="secondary">
Secondary
</Button>
<Spacer y={0.5} />
<Button flat color="success">
Success
</Button>
<Spacer y={0.5} />
<Button flat color="warning">
Warning
</Button>
<Spacer y={0.5} />
<Button flat color="error">
Error
</Button>
</>
);
export const Rounded = () => (
<>
<Button rounded color="primary">
Primary
</Button>
<Spacer y={0.5} />
<Button rounded color="secondary">
Secondary
</Button>
<Spacer y={0.5} />
<Button rounded color="success">
Success
</Button>
<Spacer y={0.5} />
<Button rounded color="warning">
Warning
</Button>
<Spacer y={0.5} />
<Button rounded color="error">
Error
</Button>
<Spacer y={0.5} />
<Button rounded color="gradient">
Action
</Button>
</>
);
export const Light = () => (
<>
<Button light>Default</Button>
<Spacer y={0.5} />
<Button light color="primary">
Primary
</Button>
<Spacer y={0.5} />
<Button light color="secondary">
Secondary
</Button>
<Spacer y={0.5} />
<Button light color="success">
Success
</Button>
<Spacer y={0.5} />
<Button light color="warning">
Warning
</Button>
<Spacer y={0.5} />
<Button light color="error">
Error
</Button>
</>
);
export const Icons = () => {
return (
<>
<Button auto color="secondary" icon={<Activity fill="currentColor" />} />
<Spacer y={0.5} />
<Button auto iconRight={<Camera fill="currentColor" />}>
Right Icon
</Button>
<Spacer y={0.5} />
<Button auto bordered color="gradient" icon={<Camera fill="currentColor" />}>
Left Icon
</Button>
<Spacer y={0.5} />
<Button color="success" icon={<Lock fill="currentColor" />}>
Lock
</Button>
<Spacer y={0.5} />
<Button color="secondary" icon={<Notification fill="currentColor" />}>
Notifications
</Button>
<Spacer y={0.5} />
<Button flat color="error" icon={<User fill="currentColor" />}>
Delete User
</Button>
<Spacer y={0.5} />
<Button disabled icon={<User fill="currentColor" />}>
Delete User
</Button>
</>
);
};

View File

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"paths": {
"@stitches/react": ["../../../node_modules/@stitches/react"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -0,0 +1,13 @@
import {defineConfig} from "tsup";
import {findUpSync} from "find-up";
export default defineConfig({
clean: true,
minify: false,
treeshake: true,
format: ["cjs", "esm"],
outExtension(ctx) {
return {js: `.${ctx.format}.js`};
},
inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined,
});

View File

@ -3,7 +3,7 @@ import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";
import {CheckboxGroupProvider} from "./checkbox-context";
import {CheckboxGroupProvider} from "./checkbox-group-context";
import {StyledCheckboxGroup, StyledCheckboxGroupContainer} from "./checkbox-group.styles";
import {UseCheckboxGroupProps, useCheckboxGroup} from "./use-checkbox-group";

View File

@ -12,7 +12,7 @@ import {
useCheckboxGroupItem as useReactAriaCheckboxGroupItem,
} from "@react-aria/checkbox";
import {useCheckboxGroupContext} from "./checkbox-context";
import {useCheckboxGroupContext} from "./checkbox-group-context";
export interface UseCheckboxProps extends HTMLNextUIProps<"label", AriaCheckboxProps> {
/**

51
pnpm-lock.yaml generated
View File

@ -342,6 +342,41 @@ importers:
clean-package: 2.1.1
react: 17.0.2
packages/components/button:
specifiers:
'@nextui-org/dom-utils': workspace:*
'@nextui-org/drip': workspace:*
'@nextui-org/icons-utils': workspace:*
'@nextui-org/shared-css': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/spacer': workspace:*
'@nextui-org/system': workspace:*
'@react-aria/button': ^3.6.2
'@react-aria/focus': ^3.9.0
'@react-aria/interactions': ^3.12.0
'@react-aria/utils': ^3.14.0
'@react-types/button': ^3.6.2
'@react-types/shared': ^3.14.1
clean-package: 2.1.1
react: ^17.0.2
dependencies:
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/drip': link:../drip
'@nextui-org/shared-css': link:../../utilities/shared-css
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@react-aria/button': 3.6.2_react@17.0.2
'@react-aria/focus': 3.9.0_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
devDependencies:
'@nextui-org/icons-utils': link:../../utilities/icons-utils
'@nextui-org/spacer': link:../spacer
'@react-types/button': 3.6.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
clean-package: 2.1.1
react: 17.0.2
packages/components/card:
specifiers:
'@nextui-org/code': workspace:*
@ -4653,6 +4688,21 @@ packages:
tslib: 2.4.0
dev: false
/@react-aria/button/3.6.2_react@17.0.2:
resolution: {integrity: sha512-8XRcPR5qXKNmnBO6u9FSY8vYa7P1FIgVix07EHBCTZiJYHYxcUsFYRWG9qf2aShGotJs4957unqGH1L1ncRYKQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@babel/runtime': 7.19.0
'@react-aria/focus': 3.9.0_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-stately/toggle': 3.4.2_react@17.0.2
'@react-types/button': 3.6.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
dev: false
/@react-aria/checkbox/3.6.0_react@17.0.2:
resolution: {integrity: sha512-E2MZoMZhtHtlS1mYjxTI29JRq4s3Y6d92KHj7tzvcC308d4Bzz0IHJd0bMz/8whoNteU02pQyZ3rDAcGf92bHQ==}
peerDependencies:
@ -4891,7 +4941,6 @@ packages:
dependencies:
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
dev: false
/@react-types/checkbox/3.4.0_react@17.0.2:
resolution: {integrity: sha512-ZDqbtAYWWSGPjL4ydinaWHrD65Qft9yEGA6BCKQTxdJCgxiXxgGkA3pI7Sxwk+OulR+O0CYJ1JROExM9cSJyyQ==}