Skip to content

Commit

Permalink
feat(component): Allow Buttons to have href (#209)
Browse files Browse the repository at this point in the history
* feat(component): Allow `Button`s to have `href`

A pretty common use case is to use a button visual
style in combination with a link to another page.

However, HTML doesn't support nesting `<a>` inside
of `<button>`.

Adding an `onClick` hook to the `<button>` is fine,
but that only works with JavaScript enabled.

In this case, they will render as an anchor (`<a>`)
instead, which is the best scenario for SSR apps.

* test(component): Add unit tests for `<Button href="..">`

* fix(component): Prevent `<Button className="..">`

The prop is excluded already, but this change tells
TypeScript users `className` is not allowed.

* refactor(component): Rename `ButtonComponentProps` -> `ButtonProps`

* refactor: Update usages of `Button` in docs & stories

Replace `className` calls to a container, and update
out-of-date color references.
  • Loading branch information
tulup-conner authored Jun 8, 2022
1 parent 9cd1eab commit 2f9f345
Show file tree
Hide file tree
Showing 17 changed files with 126 additions and 113 deletions.
24 changes: 12 additions & 12 deletions src/docs/pages/ButtonGroupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ const ButtonGroupPage: FC = () => {
title: 'Default example',
code: (
<Button.Group>
<Button color="alternative">Profile</Button>
<Button color="alternative">Settings</Button>
<Button color="alternative">Messages</Button>
<Button color="gray">Profile</Button>
<Button color="gray">Settings</Button>
<Button color="gray">Messages</Button>
</Button.Group>
),
},
{
title: 'Group buttons with icons',
code: (
<Button.Group>
<Button color="alternative">
<Button color="gray">
<HiUserCircle className="mr-3 h-4 w-4" /> Profile
</Button>
<Button color="alternative">
<Button color="gray">
<HiAdjustments className="mr-3 h-4 w-4" /> Settings
</Button>
<Button color="alternative">
<Button color="gray">
<HiCloudDownload className="mr-3 h-4 w-4" /> Messages
</Button>
</Button.Group>
Expand Down Expand Up @@ -60,9 +60,9 @@ const ButtonGroupPage: FC = () => {
code: (
<div className="flex flex-wrap gap-2">
<Button.Group outline>
<Button color="alternative">Profile</Button>
<Button color="alternative">Settings</Button>
<Button color="alternative">Messages</Button>
<Button color="gray">Profile</Button>
<Button color="gray">Settings</Button>
<Button color="gray">Messages</Button>
</Button.Group>
<Button.Group outline>
<Button gradientMonochrome="info">Profile</Button>
Expand All @@ -82,13 +82,13 @@ const ButtonGroupPage: FC = () => {
code: (
<div className="flex flex-wrap gap-2">
<Button.Group outline>
<Button color="alternative">
<Button color="gray">
<HiUserCircle className="mr-3 h-4 w-4" /> Profile
</Button>
<Button color="alternative">
<Button color="gray">
<HiAdjustments className="mr-3 h-4 w-4" /> Settings
</Button>
<Button color="alternative">
<Button color="gray">
<HiCloudDownload className="mr-3 h-4 w-4" /> Messages
</Button>
</Button.Group>
Expand Down
33 changes: 18 additions & 15 deletions src/docs/pages/ButtonsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ const ButtonsPage: FC = () => {
code: (
<div className="flex flex-wrap gap-2">
<Button>Default</Button>
<Button color="alternative">Alternative</Button>
<Button color="gray">Gray</Button>
<Button color="dark">Dark</Button>
<Button color="light">Light</Button>
<Button color="green">Green</Button>
<Button color="red">Red</Button>
<Button color="yellow">Yellow</Button>
<Button color="success">Success</Button>
<Button color="failure">Failure</Button>
<Button color="warning">Warning</Button>
<Button color="purple">Purple</Button>
</div>
),
Expand All @@ -27,24 +27,23 @@ const ButtonsPage: FC = () => {
title: 'Button pills',
code: (
<div className="flex flex-wrap gap-2">
<Button pill>Default</Button>
<Button color="alternative" pill>
Alternative
<Button color="gray" pill>
Gray
</Button>
<Button color="dark" pill>
Dark
</Button>
<Button color="light" pill>
Light
</Button>
<Button color="green" pill>
Green
<Button color="success" pill>
Success
</Button>
<Button color="red" pill>
Red
<Button color="failure" pill>
Failure
</Button>
<Button color="yellow" pill>
Yellow
<Button color="warning" pill>
Warning
</Button>
<Button color="purple" pill>
Purple
Expand Down Expand Up @@ -172,11 +171,15 @@ const ButtonsPage: FC = () => {
code: (
<div className="flex flex-wrap items-center gap-2">
<Button>
<Spinner className="mr-3" size="sm" light />
<div className="mr-3">
<Spinner size="sm" light />
</div>
Loading ...
</Button>
<Button outline>
<Spinner className="mr-3" size="sm" light />
<div className="mr-3">
<Spinner size="sm" light />
</div>
Loading ...
</Button>
</div>
Expand Down
12 changes: 7 additions & 5 deletions src/docs/pages/ModalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const ModalPage: FC = () => {
</Modal.Body>
<Modal.Footer>
<Button onClick={() => setOpenModal(undefined)}>I accept</Button>
<Button color="alternative" onClick={() => setOpenModal(undefined)}>
<Button color="gray" onClick={() => setOpenModal(undefined)}>
Decline
</Button>
</Modal.Footer>
Expand All @@ -58,7 +58,7 @@ const ModalPage: FC = () => {
<Button color="failure" onClick={() => setOpenModal(undefined)}>
{"Yes, I'm sure"}
</Button>
<Button color="alternative" onClick={() => setOpenModal(undefined)}>
<Button color="gray" onClick={() => setOpenModal(undefined)}>
No, cancel
</Button>
</div>
Expand Down Expand Up @@ -104,7 +104,9 @@ const ModalPage: FC = () => {
Lost Password?
</a>
</div>
<Button className="w-full">Log in to your account</Button>
<div className="w-full">
<Button>Log in to your account</Button>
</div>
<div className="text-sm font-medium text-gray-500 dark:text-gray-300">
Not registered?{' '}
<a href="/modal" className="text-blue-700 hover:underline dark:text-blue-500">
Expand Down Expand Up @@ -153,7 +155,7 @@ const ModalPage: FC = () => {
</Modal.Body>
<Modal.Footer>
<Button onClick={() => setOpenModal(undefined)}>I accept</Button>
<Button color="alternative" onClick={() => setOpenModal(undefined)}>
<Button color="gray" onClick={() => setOpenModal(undefined)}>
Decline
</Button>
</Modal.Footer>
Expand Down Expand Up @@ -196,7 +198,7 @@ const ModalPage: FC = () => {
</Modal.Body>
<Modal.Footer>
<Button onClick={() => setOpenModal(undefined)}>I accept</Button>
<Button color="alternative" onClick={() => setOpenModal(undefined)}>
<Button color="gray" onClick={() => setOpenModal(undefined)}>
Decline
</Button>
</Modal.Footer>
Expand Down
27 changes: 12 additions & 15 deletions src/docs/pages/SidebarPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,18 @@ const SidebarPage: FC = () => {
</Sidebar.Items>
<Sidebar.CTA>
<div className="mb-3 flex items-center">
<Badge color="yellow">Beta</Badge>
<Button
aria-label="Close"
className="-mx-1.5 -my-1.5 ml-auto !h-6 !w-6 bg-transparent !p-1 text-blue-900 hover:bg-blue-200 dark:!bg-blue-900 dark:text-blue-200 dark:hover:!bg-blue-800"
data-collapse-toggle="dropdown-cta"
>
<span className="sr-only">Close</span>
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
</svg>
</Button>
<Badge color="warning">Beta</Badge>
<div className="-mx-1.5 -my-1.5 ml-auto">
<Button aria-label="Close" outline>
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
clipRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
fillRule="evenodd"
/>
</svg>
</Button>
</div>
</div>
<p className="mb-3 text-sm text-blue-900 dark:text-blue-400">
Preview the new Flowbite dashboard navigation! You can turn the new navigation off for a limited time in
Expand Down
2 changes: 1 addition & 1 deletion src/docs/pages/SpinnersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const SpinnersPage: FC = () => {
<Spinner aria-label="Spinner button example" />
<span className="pl-3">Loading...</span>
</Button>
<Button color="alternative">
<Button color="gray">
<Spinner aria-label="Alternate spinner button example" />
<span className="pl-3">Loading...</span>
</Button>
Expand Down
4 changes: 2 additions & 2 deletions src/docs/pages/TimelinePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const TimelinePage: FC = () => {
Get access to over 20+ pages including a dashboard layout, charts, kanban board, calendar, and pre-order
E-commerce & Marketing pages.
</Timeline.Body>
<Button color="alternative">
<Button color="gray">
Learn More
<HiArrowNarrowRight className="ml-2 h-3 w-3" />
</Button>
Expand Down Expand Up @@ -63,7 +63,7 @@ const TimelinePage: FC = () => {
Get access to over 20+ pages including a dashboard layout, charts, kanban board, calendar, and pre-order
E-commerce & Marketing pages.
</Timeline.Body>
<Button color="alternative">
<Button color="gray">
Learn More
<HiArrowNarrowRight className="ml-2 h-3 w-3" />
</Button>
Expand Down
14 changes: 8 additions & 6 deletions src/docs/pages/ToastPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ const ToastPage: FC = () => {
<span className="mb-1 text-sm font-semibold text-gray-900 dark:text-white">Update available</span>
<div className="mb-2 text-sm font-normal">A new software version is available for download.</div>
<div className="flex gap-2">
<Button className="!w-full" size="xs">
Update
</Button>
<Button className="!w-full" color="light" size="xs">
Not now
</Button>
<div className="w-full">
<Button size="xs">Update</Button>
</div>
<div className="w-full">
<Button color="light" size="xs">
Not now
</Button>
</div>
</div>
</div>
<Toast.Toggle />
Expand Down
42 changes: 26 additions & 16 deletions src/lib/components/Button/Button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,17 @@ describe.concurrent('Components / Button', () => {
expect(button).toHaveAttribute('type', 'submit');
});

it('should be disabled given `disabled={true}`', () => {
const button = getButton(render(<Button disabled>Hi there</Button>));
describe('`disabled={true}`', () => {
it('should be disabled given', () => {
const button = getButton(render(<Button disabled>Hi there</Button>));

expect(button).toBeDisabled();
});

it('should ignore `className`', () => {
const button = getButton(render(<Button className="font-extralight">Hi there</Button>));

expect(button).not.toHaveClass('font-extralight');
expect(button).toBeDisabled();
});
});
});

describe('Rendering', () => {
it('should render', () => {
it('should render a `<button>`', () => {
const button = getButton(
render(
<Button color="gray" outline>
Expand All @@ -104,16 +100,28 @@ describe.concurrent('Components / Button', () => {
expect(button).toHaveTextContent('Hi there');
});

it('should render given `children={0}`', () => {
const button = getButton(render(<Button>0</Button>));
describe('`children={0}`', () => {
it('should render', () => {
const button = getButton(render(<Button>0</Button>));

expect(button).toHaveTextContent('0');
expect(button).toHaveTextContent('0');
});
});

it('should render without `children`', () => {
const button = getButton(render(<Button label="Something or other" />));
describe('`children={undefined}`', () => {
it('should render', () => {
const button = getButton(render(<Button label="Something or other" />));

expect(button).toHaveTextContent('Something or other');
expect(button).toHaveTextContent('Something or other');
});
});

describe('`href=".."`', () => {
it('should render an anchor `<a>`', () => {
const button = getButtonLink(render(<Button href="#" label="Something or other" />));

expect(button).toBeInTheDocument();
});
});
});

Expand Down Expand Up @@ -335,4 +343,6 @@ describe.concurrent('Components / Button', () => {

const getButton = ({ getByRole }: RenderResult): HTMLElement => getByRole('button');

const getButtonLink = ({ getByRole }: RenderResult): HTMLElement => getByRole('link');

const getButtons = ({ getAllByRole }: RenderResult): HTMLElement[] => getAllByRole('button');
4 changes: 2 additions & 2 deletions src/lib/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { Meta, Story } from '@storybook/react/types-6-0';

import type { ButtonComponentProps } from '.';
import type { ButtonProps } from '.';
import { Button } from '.';

export default {
title: 'Components/Button',
component: Button,
} as Meta;

const Template: Story<ButtonComponentProps> = (args) => <Button {...args} />;
const Template: Story<ButtonProps> = (args) => <Button {...args} />;

export const DefaultButton = Template.bind({});
DefaultButton.storyName = 'Default';
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/Button/ButtonGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export default {

const Template: Story<ButtonGroupProps> = (args) => (
<Button.Group {...args}>
<Button color="alternative">Profile</Button>
<Button color="alternative">Settings</Button>
<Button color="alternative">Messages</Button>
<Button color="gray">Profile</Button>
<Button color="gray">Settings</Button>
<Button color="gray">Messages</Button>
</Button.Group>
);

Expand Down
14 changes: 4 additions & 10 deletions src/lib/components/Button/ButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { ComponentProps, FC, PropsWithChildren, ReactElement } from 'react';
import { Children, cloneElement, useMemo } from 'react';

import type { ButtonComponentProps } from '.';
import type { ButtonProps } from '.';
import { excludeClassName } from '../../helpers/exclude';
import { useTheme } from '../Flowbite/ThemeContext';

export type ButtonGroupProps = PropsWithChildren<
ComponentProps<'div'> & Pick<ButtonComponentProps, 'outline' | 'pill'>
>;
export type ButtonGroupProps = PropsWithChildren<ComponentProps<'div'> & Pick<ButtonProps, 'outline' | 'pill'>>;

export interface PositionInButtonGroup {
none: string;
Expand All @@ -21,16 +19,12 @@ const ButtonGroup: FC<ButtonGroupProps> = ({ children, outline, pill, ...props }

const items = useMemo(
() =>
Children.map(children as ReactElement<ButtonComponentProps>[], (child, index) =>
Children.map(children as ReactElement<ButtonProps>[], (child, index) =>
cloneElement(child, {
outline,
pill,
positionInGroup:
index === 0
? 'start'
: index === (children as ReactElement<ButtonComponentProps>[]).length - 1
? 'end'
: 'middle',
index === 0 ? 'start' : index === (children as ReactElement<ButtonProps>[]).length - 1 ? 'end' : 'middle',
}),
),
[children, outline, pill],
Expand Down
Loading

0 comments on commit 2f9f345

Please sign in to comment.