diff --git a/src/Badge/Badge.stories.tsx b/src/Badge/Badge.stories.tsx index e50ec903..1b764334 100644 --- a/src/Badge/Badge.stories.tsx +++ b/src/Badge/Badge.stories.tsx @@ -10,74 +10,56 @@ export default { } as Meta export const Default: Story = (args) => { - return Example + return Badge } Default.args = {} -export const Colors: Story = (args) => { +export const BrandColors: Story = (args) => { return ( -
-
- neutral - - primary - - - secondary - - - accent - - - ghost - -
-
- - success - - - info - - - warning - - - error - -
+
+ default + + neutral + + + primary + + + secondary + + + accent + + + ghost +
) } -Colors.args = { - className: 'm-1' -} export const Outline: Story = (args) => { return ( -
- - neutral - - +
+ default + primary - + secondary - + accent
) } Outline.args = { - className: 'm-1' + variant: 'outline', } export const Sizes: Story = (args) => { return ( -
+
987,654 @@ -93,9 +75,6 @@ export const Sizes: Story = (args) => {
) } -Sizes.args = { - className: 'm-1' -} export const Empty: Story = (args) => { return ( @@ -107,17 +86,112 @@ export const Empty: Story = (args) => {
) } -Empty.args = {} +Empty.args = { + color: 'primary', +} -export const BadgeInText: Story = (args) => { +export const StateColors: Story = (args) => { return (
-

- Heading{' '} + + + + + info + + + + + + success + + + + + + warning + + + + + + error + +

+ ) +} +StateColors.args = { + className: 'gap-2', +} + +export const BadgeInText: Story = (args) => { + return ( +
+

+ Heading NEW

+

+ Heading + + NEW + +

+

+ Heading + + NEW + +

+ +
+ Heading + + NEW + +
) } @@ -126,13 +200,13 @@ BadgeInText.args = {} export const BadgeInAButton: Story = (args) => { return (
- - diff --git a/src/Badge/Badge.tsx b/src/Badge/Badge.tsx index 48436142..26d97276 100644 --- a/src/Badge/Badge.tsx +++ b/src/Badge/Badge.tsx @@ -37,6 +37,7 @@ const Badge = forwardRef( 'badge-sm': size === 'sm', 'badge-xs': size === 'xs', 'badge-outline': variant === 'outline' || outline, + 'badge-neutral': color === 'neutral', 'badge-primary': color === 'primary', 'badge-secondary': color === 'secondary', 'badge-accent': color === 'accent', diff --git a/src/Button/Button.stories.tsx b/src/Button/Button.stories.tsx index 4e7354d8..7e194818 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 OutlineButtonsWithStateColors: Story = (args) => { return ( -
- + - +
) } +OutlineButtonsWithStateColors.args = { + variant: 'outline', +} -export const AsHref: Story = (args) => { +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,218 @@ export const Glass: Story = (args) => { Glass.args = { glass: true, } + +export const DifferentHtmlTags: Story< + ButtonProps<'main', React.HtmlHTMLAttributes> +> = (args) => { + return ( +
+ + +
+ ) +} +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, +} + +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 9b78f937..7d0bff07 100644 --- a/src/Button/Button.test.tsx +++ b/src/Button/Button.test.tsx @@ -26,7 +26,11 @@ describe('Button', () => { }) it('Renders an anchor tag when an href exists', () => { - render() + render( + + ) 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..ee2c5ebb 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,46 @@ import { ComponentSize, } from '../types' -export type ButtonProps = Omit< - React.ButtonHTMLAttributes, - 'color' -> & +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 ElementType = 'button', + A extends React.HTMLAttributes = GetTagProps['attr'] +> = Omit & IComponentBaseProps & { - href?: string shape?: ComponentShape size?: ComponentSize variant?: 'outline' | 'link' @@ -25,17 +59,35 @@ export type ButtonProps = Omit< fullWidth?: boolean responsive?: boolean animation?: boolean - loading?: boolean + loading?: boolean active?: boolean startIcon?: ReactNode endIcon?: ReactNode + disabled?: boolean + tag?: T } - +// 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 +105,12 @@ const Button = forwardRef( dataTheme, className, style, + tag = 'button', ...props }, ref ): JSX.Element => { + const Tag = tag const classes = twMerge( 'btn', className, @@ -81,24 +135,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 +174,10 @@ const Button = forwardRef( Button.displayName = 'Button' -export default Button +export default Button as < + T extends ElementType = 'button', + E extends HTMLElement = GetTagProps['ele'], + A extends React.HTMLAttributes = GetTagProps['attr'] +>( + 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 diff --git a/src/Collapse/Collapse.stories.tsx b/src/Collapse/Collapse.stories.tsx index c6e48258..20692786 100644 --- a/src/Collapse/Collapse.stories.tsx +++ b/src/Collapse/Collapse.stories.tsx @@ -6,6 +6,9 @@ import Collapse, { CollapseProps } from '.' export default { title: 'Data Display/Collapse', component: Collapse, + args: { + className: 'bg-base-200', + }, } as Meta const Template: Story = (args) => { @@ -24,15 +27,13 @@ const Template: Story = (args) => { export const Default = Template.bind({}) Default.args = {} -export const Checkbox = (args) => { +export const Checkbox: Story = (args) => { return ( Click me to show/hide content - - hello - + hello ) } @@ -42,22 +43,22 @@ Checkbox.args = { export const WithBorderAndBackground = Template.bind({}) WithBorderAndBackground.args = { - className: 'border border-base-300 bg-base-100 rounded-box', + className: 'border border-base-300 bg-base-200', } export const WithArrow = Template.bind({}) WithArrow.args = { - className: 'border border-base-300 bg-base-100 rounded-box', + className: 'border border-base-300 bg-base-200', icon: 'arrow', } export const WithPlusMinus = Template.bind({}) WithPlusMinus.args = { - className: 'border border-base-300 bg-base-100 rounded-box', + className: 'border border-base-300 bg-base-200', icon: 'plus', } -export const ForceOpen = (args) => { +export const ForceOpen: Story = (args) => { return ( @@ -70,11 +71,11 @@ export const ForceOpen = (args) => { ) } ForceOpen.args = { - className: 'border border-base-300 bg-base-100 rounded-box', + className: 'border border-base-300 bg-base-200', open: true, } -export const ForceClose = (args) => { +export const ForceClose: Story = (args) => { return ( @@ -87,7 +88,7 @@ export const ForceClose = (args) => { ) } ForceClose.args = { - className: 'border border-base-300 bg-base-100 rounded-box', + className: 'border border-base-300 bg-base-200', open: false, } diff --git a/src/Collapse/Collapse.tsx b/src/Collapse/Collapse.tsx index da7e0cdf..ff344fe5 100644 --- a/src/Collapse/Collapse.tsx +++ b/src/Collapse/Collapse.tsx @@ -4,18 +4,36 @@ import { twMerge } from 'tailwind-merge' import { IComponentBaseProps } from '../types' +import CollapseDetails from './CollapseDetails' import CollapseTitle from './CollapseTitle' import CollapseContent from './CollapseContent' -export type CollapseProps = React.HTMLAttributes & - IComponentBaseProps & { - checkbox?: boolean - icon?: 'arrow' | 'plus' - open?: boolean - onOpen?: () => void - onClose?: () => void - onToggle?: () => void - } +export type CollapseProps = + React.HTMLAttributes & + IComponentBaseProps & { + checkbox?: boolean + icon?: 'arrow' | 'plus' + open?: boolean + onOpen?: () => void + onClose?: () => void + onToggle?: () => void + } + +export const classesFn = ({ + className, + icon, + open, +}: Pick) => + twMerge( + 'collapse', + className, + clsx({ + 'collapse-arrow': icon === 'arrow', + 'collapse-plus': icon === 'plus', + 'collapse-open': open === true, + 'collapse-close': open === false, + }) + ) const Collapse = React.forwardRef( ( @@ -33,17 +51,6 @@ const Collapse = React.forwardRef( }, ref ): JSX.Element => { - const classes = twMerge( - 'collapse', - className, - clsx({ - 'collapse-arrow': icon === 'arrow', - 'collapse-plus': icon === 'plus', - 'collapse-open': open === true, - 'collapse-close': open === false, - }) - ) - const [isChecked, setIsChecked] = useState(open) const checkboxRef = useRef(null) @@ -57,7 +64,7 @@ const Collapse = React.forwardRef( } else if (onClose && !checkboxRef.current?.checked) { onClose() } - + setIsChecked(checkboxRef.current?.checked) } @@ -82,7 +89,7 @@ const Collapse = React.forwardRef( ref={ref} tabIndex={isChecked === true ? undefined : 0} data-theme={dataTheme} - className={classes} + className={classesFn({ className, icon, open })} onBlur={handleBlur} onFocus={handleFocus} > @@ -102,6 +109,7 @@ const Collapse = React.forwardRef( ) export default Object.assign(Collapse, { + Details: CollapseDetails, Title: CollapseTitle, Content: CollapseContent, }) diff --git a/src/Collapse/CollapseDetails.stories.tsx b/src/Collapse/CollapseDetails.stories.tsx new file mode 100644 index 00000000..c3070aee --- /dev/null +++ b/src/Collapse/CollapseDetails.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { StoryFn as Story, Meta } from '@storybook/react' + +import Collapse, { DetailsProps } from '.' + +export default { + title: 'Data Display/Collapse/Details', + component: Collapse.Details, +} as Meta + +export const Default: Story = (args) => { + return ( + + + Click to open/close + + +

content

+
+
+ ) +} +Default.args = { + className: 'bg-base-200', +} diff --git a/src/Collapse/CollapseDetails.tsx b/src/Collapse/CollapseDetails.tsx new file mode 100644 index 00000000..dd591d62 --- /dev/null +++ b/src/Collapse/CollapseDetails.tsx @@ -0,0 +1,30 @@ +import React from 'react' + +import { classesFn, CollapseProps } from './Collapse' +import { Summary } from './CollapseTitle' + +export type DetailsProps = Omit< + CollapseProps, + 'checkbox' | 'onOpen' | 'onClose' | 'onToggle' +> +const Details = React.forwardRef( + ( + { children, icon, open, dataTheme, className, ...props }, + ref + ): JSX.Element => { + return ( +
+ {children} +
+ ) + } +) + +Details.displayName = 'Details' +export default Object.assign(Details, { Title: Summary }) diff --git a/src/Collapse/CollapseTitle.tsx b/src/Collapse/CollapseTitle.tsx index 6ba78771..4f395145 100644 --- a/src/Collapse/CollapseTitle.tsx +++ b/src/Collapse/CollapseTitle.tsx @@ -3,21 +3,33 @@ import { twMerge } from 'tailwind-merge' import { IComponentBaseProps } from '../types' -export type CollapseTitleProps = React.HTMLAttributes & - IComponentBaseProps +export type CollapseTitleProps = + React.HTMLAttributes & IComponentBaseProps + +const classesFn = ({ className }: Pick) => + twMerge('collapse-title', className) const CollapseTitle = ({ children, className, ...props }: CollapseTitleProps): JSX.Element => { - const classes = twMerge('collapse-title', className) - return ( -
+
{children}
) } +export type SummaryProps = CollapseTitleProps +export const Summary = React.forwardRef( + ({ children, className }, ref): JSX.Element => { + return ( + + {children} + + ) + } +) + export default CollapseTitle diff --git a/src/Collapse/index.tsx b/src/Collapse/index.tsx index cbe59409..15bb239b 100644 --- a/src/Collapse/index.tsx +++ b/src/Collapse/index.tsx @@ -1,3 +1,5 @@ import Collapse, { CollapseProps as TCollapseProps } from './Collapse' + +export type { DetailsProps } from './CollapseDetails' export type CollapseProps = TCollapseProps export default Collapse 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..abbef514 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 = ({