From a9bbed0e9f387b7b41aede4212b1af362ceeda1e Mon Sep 17 00:00:00 2001 From: Yoshiharu KAMATA Date: Thu, 22 Jun 2023 16:06:05 +0900 Subject: [PATCH 1/9] feat(Button): support different html tags --- src/Button/Button.stories.tsx | 426 ++++++++++++++++++++++++++++------ src/Button/Button.test.tsx | 9 +- src/Button/Button.tsx | 68 ++++-- src/Button/index.tsx | 4 +- 4 files changed, 406 insertions(+), 101 deletions(-) diff --git a/src/Button/Button.stories.tsx b/src/Button/Button.stories.tsx index 4e7354d8..3affd33b 100644 --- a/src/Button/Button.stories.tsx +++ b/src/Button/Button.stories.tsx @@ -16,62 +16,55 @@ export default { }, } as Meta -const Template: Story = (args) => { - return } -export const Colors: Story = (args) => { +export const BrandColors: Story = (args) => { return ( -
-
- - - - - - -
-
- - - - -
+
+ + + + + + +
) } -Colors.args = { - className: 'm-1', -} +BrandColors.args = {} -export const Variants: Story = (args) => { +export const ActiveButtons: Story = (args) => { return ( -
+
- + + + +
) } +ActiveButtons.args = { active: true } -export const Icons: Story = (args) => { - const favoriteIcon = ( - - - +export const StateColors: Story = (args) => { + return ( +
+ + + + +
) +} +StateColors.args = {} +export const OutlineButtons: Story = (args) => { return ( -
- + - +
) } +OutlineButtons.args = { + variant: 'outline', +} -export const AsHref: Story = (args) => { +export const OutlineButtonsWithStateColors: Story = (args) => { return ( -
- + + + -
) } +OutlineButtonsWithStateColors.args = { + variant: 'outline', +} + +export const ButtonSizes: Story = (args) => { + return ( +
+ + + + +
+ ) +} +ButtonSizes.args = {} + +export const ResponsiveButton: Story = (args) => { + return +} +ResponsiveButton.args = { responsive: true } + +export const WideButton: Story = (args) => { + return +} +WideButton.args = { wide: true } export const Glass: Story = (args) => { return ( @@ -140,3 +180,237 @@ export const Glass: Story = (args) => { Glass.args = { glass: true, } + +export const DifferentHtmlTags: Story< + ButtonProps> +> = (args) => { + return ( +
+ > + {...args} + tag="a" + role="button" + > + Link + + + > + {...args} + tag="input" + type="button" + value="Input" + /> + > + {...args} + tag="input" + type="submit" + value="Submit" + /> + > + {...args} + tag="input" + type="radio" + aria-label="Radio" + /> + > + {...args} + tag="input" + type="checkbox" + aria-label="Checkbox" + /> + > + {...args} + tag="input" + type="reset" + value="Reset" + /> +
+ ) +} +DifferentHtmlTags.args = {} + +export const DisabledButtons: Story = (args) => { + return ( +
+ + +
+ ) +} +DisabledButtons.args = { + disabled: true, +} + +export const SquareButton: Story = (args) => { + return ( +
+ + +
+ ) +} +SquareButton.args = { + shape: 'square', +} + +export const CircleButton: Story = (args) => { + return ( +
+ + +
+ ) +} +CircleButton.args = { + shape: 'circle', +} + +export const IconAtStart: Story = (args) => { + return ( + + ) +} + +export const IconAtEnd: Story = (args) => { + return ( + + ) +} + +export const ButtonBlock: Story = (args) => { + return +} +ButtonBlock.args = { + fullWidth: true, +} + +export const LoadingSpinner: Story = (args) => { + return +} +LoadingSpinnerAndText.args = { + loading: true, +} + +export const WithoutClickAnimation: Story = (args) => { + return +} +WithoutClickAnimation.args = { + animation: false, +} diff --git a/src/Button/Button.test.tsx b/src/Button/Button.test.tsx index 9b78f937..51bd4f41 100644 --- a/src/Button/Button.test.tsx +++ b/src/Button/Button.test.tsx @@ -26,7 +26,14 @@ describe('Button', () => { }) it('Renders an anchor tag when an href exists', () => { - render() + render( + > + tag="a" + href="/home" + > + Home + + ) expect(screen.getByRole('link')).toBeTruthy() expect(screen.getByRole('link')).toHaveAttribute('href', '/home') diff --git a/src/Button/Button.tsx b/src/Button/Button.tsx index 0692d9d0..f6dcee2c 100644 --- a/src/Button/Button.tsx +++ b/src/Button/Button.tsx @@ -1,8 +1,8 @@ -import React, { forwardRef, ReactNode } from 'react' +import React, { forwardRef, ReactNode, ElementType } from 'react' import clsx from 'clsx' import { twMerge } from 'tailwind-merge' -import Loading from '../Loading'; +import Loading from '../Loading' import { IComponentBaseProps, ComponentColor, @@ -10,12 +10,10 @@ import { ComponentSize, } from '../types' -export type ButtonProps = Omit< - React.ButtonHTMLAttributes, - 'color' -> & +export type ButtonProps< + T extends React.HTMLAttributes = React.ButtonHTMLAttributes +> = Omit & IComponentBaseProps & { - href?: string shape?: ComponentShape size?: ComponentSize variant?: 'outline' | 'link' @@ -25,17 +23,34 @@ export type ButtonProps = Omit< fullWidth?: boolean responsive?: boolean animation?: boolean - loading?: boolean + loading?: boolean active?: boolean startIcon?: ReactNode endIcon?: ReactNode + tag?: ElementType } - +// https://developer.mozilla.org/en-US/docs/Glossary/Void_element +const VoidElementList: ElementType[] = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'keygen', + 'meta', + 'param', + 'source', + 'track', + 'wbr', +] const Button = forwardRef( ( { children, - href, shape, size, variant, @@ -53,10 +68,12 @@ const Button = forwardRef( dataTheme, className, style, + tag = 'button', ...props }, ref ): JSX.Element => { + const Tag = tag const classes = twMerge( 'btn', className, @@ -81,24 +98,26 @@ const Button = forwardRef( glass: glass, 'btn-wide': wide, 'btn-block': fullWidth, - 'btn-xs md:btn-sm lg:btn-md xl:btn-lg': responsive, + 'btn-xs sm:btn-sm md:btn-md lg:btn-lg': responsive, 'no-animation': !animation, 'btn-active': active, 'btn-disabled': disabled, }) ) - - if (href) { + if (VoidElementList.includes(Tag)) { return ( -
- {startIcon && startIcon} - {children} - {endIcon && endIcon} - + ) } else { return ( - + ) } } @@ -118,4 +137,9 @@ const Button = forwardRef( Button.displayName = 'Button' -export default Button +export default Button as < + E extends HTMLElement = HTMLButtonElement, + A extends React.HTMLAttributes = React.ButtonHTMLAttributes +>( + props: ButtonProps & { ref?: React.Ref } +) => JSX.Element diff --git a/src/Button/index.tsx b/src/Button/index.tsx index 6af1fe4a..897b29f9 100644 --- a/src/Button/index.tsx +++ b/src/Button/index.tsx @@ -1,3 +1,3 @@ -import Button, { ButtonProps as TButtonProps } from './Button' -export type ButtonProps = TButtonProps +import Button from './Button' +export type { ButtonProps } from './Button' export default Button From 6962d2111650546ad7f0b0c2903f7dd245802a9a Mon Sep 17 00:00:00 2001 From: Yoshiharu KAMATA Date: Fri, 23 Jun 2023 12:14:07 +0900 Subject: [PATCH 2/9] feat(Button): improve type system --- src/Button/Button.stories.tsx | 48 ++++++++++++--------------------- src/Button/Button.test.tsx | 5 +--- src/Button/Button.tsx | 50 ++++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/src/Button/Button.stories.tsx b/src/Button/Button.stories.tsx index 3affd33b..e65515b6 100644 --- a/src/Button/Button.stories.tsx +++ b/src/Button/Button.stories.tsx @@ -182,50 +182,26 @@ Glass.args = { } export const DifferentHtmlTags: Story< - ButtonProps> + ButtonProps<'main', React.HtmlHTMLAttributes> > = (args) => { return (
- > - {...args} - tag="a" - role="button" - > + {...args} tag="a" role="button"> Link - > - {...args} - tag="input" - type="button" - value="Input" - /> - > - {...args} - tag="input" - type="submit" - value="Submit" - /> - > - {...args} - tag="input" - type="radio" - aria-label="Radio" - /> - > + {...args} tag="input" type="button" value="Input" /> + {...args} tag="input" type="submit" value="Submit" /> + {...args} tag="input" type="radio" aria-label="Radio" /> + {...args} tag="input" type="checkbox" aria-label="Checkbox" /> - > - {...args} - tag="input" - type="reset" - value="Reset" - /> + {...args} tag="input" type="reset" value="Reset" />
) } @@ -414,3 +390,13 @@ export const WithoutClickAnimation: Story = (args) => { WithoutClickAnimation.args = { animation: false, } + +export const LinkButton: Story> = (args) => { + return +} +LinkButton.args = { + tag: 'a', + target: '_blank', + rel: 'noopener', + href: 'https://daisyui.com/', +} diff --git a/src/Button/Button.test.tsx b/src/Button/Button.test.tsx index 51bd4f41..91bb7571 100644 --- a/src/Button/Button.test.tsx +++ b/src/Button/Button.test.tsx @@ -27,10 +27,7 @@ describe('Button', () => { it('Renders an anchor tag when an href exists', () => { render( - > - tag="a" - href="/home" - > + tag="a" href="/home"> Home ) diff --git a/src/Button/Button.tsx b/src/Button/Button.tsx index f6dcee2c..ee2c5ebb 100644 --- a/src/Button/Button.tsx +++ b/src/Button/Button.tsx @@ -10,9 +10,45 @@ import { ComponentSize, } from '../types' +type ITagProps = { + a: { + attr: React.AnchorHTMLAttributes + ele: HTMLAnchorElement + } + button: { + attr: React.ButtonHTMLAttributes + ele: HTMLButtonElement + } + div: { + attr: React.HTMLAttributes + ele: HTMLDivElement + } + img: { + attr: React.ImgHTMLAttributes + ele: HTMLImageElement + } + input: { + attr: React.InputHTMLAttributes + ele: HTMLInputElement + } + label: { + attr: React.LabelHTMLAttributes + ele: HTMLLabelElement + } + span: { + attr: React.HTMLAttributes + ele: HTMLSpanElement + } +} + +type GetTagProps = T extends keyof ITagProps + ? ITagProps[T] + : ITagProps['button'] + export type ButtonProps< - T extends React.HTMLAttributes = React.ButtonHTMLAttributes -> = Omit & + T extends ElementType = 'button', + A extends React.HTMLAttributes = GetTagProps['attr'] +> = Omit & IComponentBaseProps & { shape?: ComponentShape size?: ComponentSize @@ -27,7 +63,8 @@ export type ButtonProps< active?: boolean startIcon?: ReactNode endIcon?: ReactNode - tag?: ElementType + disabled?: boolean + tag?: T } // https://developer.mozilla.org/en-US/docs/Glossary/Void_element const VoidElementList: ElementType[] = [ @@ -138,8 +175,9 @@ const Button = forwardRef( Button.displayName = 'Button' export default Button as < - E extends HTMLElement = HTMLButtonElement, - A extends React.HTMLAttributes = React.ButtonHTMLAttributes + T extends ElementType = 'button', + E extends HTMLElement = GetTagProps['ele'], + A extends React.HTMLAttributes = GetTagProps['attr'] >( - props: ButtonProps
& { ref?: React.Ref } + props: ButtonProps & { ref?: React.Ref } ) => JSX.Element From 20977423c9803274b10bfc33f5e8e070a639f304 Mon Sep 17 00:00:00 2001 From: Yoshiharu KAMATA Date: Fri, 23 Jun 2023 13:59:51 +0900 Subject: [PATCH 3/9] test(Button): Omit generics from the test code. --- src/Button/Button.stories.tsx | 17 ++++++----------- src/Button/Button.test.tsx | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Button/Button.stories.tsx b/src/Button/Button.stories.tsx index e65515b6..7e194818 100644 --- a/src/Button/Button.stories.tsx +++ b/src/Button/Button.stories.tsx @@ -186,22 +186,17 @@ export const DifferentHtmlTags: Story< > = (args) => { return (
- {...args} tag="a" role="button"> + - {...args} tag="input" type="button" value="Input" /> - {...args} tag="input" type="submit" value="Submit" /> - {...args} tag="input" type="radio" aria-label="Radio" /> - - {...args} - tag="input" - type="checkbox" - aria-label="Checkbox" - /> - {...args} tag="input" type="reset" value="Reset" /> +
) } diff --git a/src/Button/Button.test.tsx b/src/Button/Button.test.tsx index 91bb7571..7d0bff07 100644 --- a/src/Button/Button.test.tsx +++ b/src/Button/Button.test.tsx @@ -27,7 +27,7 @@ describe('Button', () => { it('Renders an anchor tag when an href exists', () => { render( - tag="a" href="/home"> + ) From 77262d958febe3bfcb0eb15a6a27f60ec2fe9a74 Mon Sep 17 00:00:00 2001 From: Yoshiharu KAMATA Date: Wed, 14 Jun 2023 14:17:24 +0900 Subject: [PATCH 4/9] feat(Dropdown): Add Details Element --- src/Dropdown/Dropdown.stories.tsx | 24 +++++-- src/Dropdown/Dropdown.tsx | 92 +++++++++++++++++------- src/Dropdown/DropdownDetails.stories.tsx | 24 +++++++ src/Dropdown/DropdownDetails.tsx | 50 +++++++++++++ src/Dropdown/DropdownToggle.tsx | 13 +++- src/Dropdown/index.tsx | 2 + 6 files changed, 169 insertions(+), 36 deletions(-) create mode 100644 src/Dropdown/DropdownDetails.stories.tsx create mode 100644 src/Dropdown/DropdownDetails.tsx diff --git a/src/Dropdown/Dropdown.stories.tsx b/src/Dropdown/Dropdown.stories.tsx index ad65366e..f226064e 100644 --- a/src/Dropdown/Dropdown.stories.tsx +++ b/src/Dropdown/Dropdown.stories.tsx @@ -48,8 +48,11 @@ export const AsCard: Story = (args) => { export const InNavbar: Story = ({ dataTheme, ...args }) => { return ( - - + + daisyUI @@ -57,8 +60,10 @@ export const InNavbar: Story = ({ dataTheme, ...args }) => { Button - Dropdown - + + Dropdown + + Item 1 Item 2 @@ -67,13 +72,19 @@ export const InNavbar: Story = ({ dataTheme, ...args }) => { ) } +InNavbar.args = { + end: true, +} export const Helper: Story = (args) => { return (
A normal text and a helper dropdown - + You needed more info? @@ -98,3 +109,4 @@ export const Helper: Story = (args) => {
) } +Helper.args = { end: true } diff --git a/src/Dropdown/Dropdown.tsx b/src/Dropdown/Dropdown.tsx index cb04484c..41f4c6dc 100644 --- a/src/Dropdown/Dropdown.tsx +++ b/src/Dropdown/Dropdown.tsx @@ -4,55 +4,93 @@ import { twMerge } from 'tailwind-merge' import { IComponentBaseProps } from '../types' +import DropdownDetails from './DropdownDetails' import DropdownMenu from './DropdownMenu' import DropdownItem from './DropdownItem' import DropdownToggle from './DropdownToggle' -export type DropdownProps = React.HTMLAttributes & - IComponentBaseProps & { - item?: ReactNode - horizontal?: 'left' | 'right' - vertical?: 'top' | 'bottom' - end?: boolean - hover?: boolean - open?: boolean - } +export type DropdownProps = + React.HTMLAttributes & + IComponentBaseProps & { + item?: ReactNode + horizontal?: 'left' | 'right' + vertical?: 'top' | 'bottom' + end?: boolean + hover?: boolean + open?: boolean + } + +export const classesFn = ({ + className, + horizontal, + vertical, + end, + hover, + open, +}: Pick< + DropdownProps, + 'className' | 'horizontal' | 'vertical' | 'end' | 'hover' | 'open' +>) => + twMerge( + 'dropdown', + className, + clsx({ + 'dropdown-left': horizontal === 'left', + 'dropdown-right': horizontal === 'right', + 'dropdown-top': vertical === 'top', + 'dropdown-bottom': vertical === 'bottom', + 'dropdown-end': end, + 'dropdown-hover': hover, + 'dropdown-open': open, + }) + ) const Dropdown = React.forwardRef( ( - { children, className, item, horizontal, vertical, end, hover, open, dataTheme, ...props }, + { + children, + className, + item, + horizontal, + vertical, + end, + hover, + open, + dataTheme, + ...props + }, ref ): JSX.Element => { - const classes = twMerge( - 'dropdown', - className, - clsx({ - 'dropdown-left': horizontal === 'left', - 'dropdown-right': horizontal === 'right', - 'dropdown-top': vertical === 'top', - 'dropdown-bottom': vertical === 'bottom', - 'dropdown-end': end, - 'dropdown-hover': hover, - 'dropdown-open': open, - }) - ) - return (
- -
    {item}
+ {item ? ( + <> + +
    {item}
+ + ) : ( + <>{children} + )}
) } ) export default Object.assign(Dropdown, { + Details: DropdownDetails, Toggle: DropdownToggle, Menu: DropdownMenu, Item: DropdownItem, diff --git a/src/Dropdown/DropdownDetails.stories.tsx b/src/Dropdown/DropdownDetails.stories.tsx new file mode 100644 index 00000000..007f0d8a --- /dev/null +++ b/src/Dropdown/DropdownDetails.stories.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { StoryFn as Story, Meta } from '@storybook/react' + +import Dropdown, { DetailsProps } from '.' + +export default { + title: 'Actions/Dropdown/Details', + component: Dropdown.Details, +} as Meta + +export const Default: Story = (args) => { + return ( +
+ + Click + + Item 1 + Item 2 + + +
+ ) +} +Default.args = {} diff --git a/src/Dropdown/DropdownDetails.tsx b/src/Dropdown/DropdownDetails.tsx new file mode 100644 index 00000000..4b9b4ae5 --- /dev/null +++ b/src/Dropdown/DropdownDetails.tsx @@ -0,0 +1,50 @@ +import React from 'react' + +import { classesFn, DropdownProps } from './Dropdown' +import DropdownMenu from './DropdownMenu' +import DropdownItem from './DropdownItem' +import { Summary } from './DropdownToggle' + +export type DetailsProps = Omit< + DropdownProps, + 'item' | 'hover' +> +const Details = React.forwardRef( + ( + { + children, + className, + horizontal, + vertical, + end, + dataTheme, + open, + ...props + }, + ref + ): JSX.Element => { + return ( +
+ {children} +
+ ) + } +) + +Details.displayName = 'Details' +export default Object.assign(Details, { + Toggle: Summary, +}) diff --git a/src/Dropdown/DropdownToggle.tsx b/src/Dropdown/DropdownToggle.tsx index 62f637ac..c2d34ea2 100644 --- a/src/Dropdown/DropdownToggle.tsx +++ b/src/Dropdown/DropdownToggle.tsx @@ -1,8 +1,8 @@ -import React from 'react' +import React, { forwardRef } from 'react' import { ComponentColor, ComponentSize, IComponentBaseProps } from '../types' -import Button from '../Button' +import Button, { ButtonProps } from '../Button' export type DropdownToggleProps = Omit< React.LabelHTMLAttributes, @@ -29,7 +29,7 @@ const DropdownToggle = ({