From 86935fa59f337399ff8b8d72b8bd45ed6f36644c Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Mon, 25 Apr 2022 17:47:40 +0700 Subject: [PATCH] feat(ui-components): add support for link to Button component (#45671) * add active style * add disable state * add support for full-width * add custom focus outline * feat(ui-components): add support for link to Button component * Add onClick action * center content --- .../src/button/button.stories.tsx | 26 +++++++- .../ui-components/src/button/button.test.tsx | 28 +++++++++ tools/ui-components/src/button/button.tsx | 60 ++++++++++++++----- tools/ui-components/src/button/types.ts | 2 + 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/tools/ui-components/src/button/button.stories.tsx b/tools/ui-components/src/button/button.stories.tsx index f47e6cbf93d..1650aecfeaf 100644 --- a/tools/ui-components/src/button/button.stories.tsx +++ b/tools/ui-components/src/button/button.stories.tsx @@ -8,7 +8,16 @@ const story = { component: Button, parameters: { controls: { - include: ['children', 'variant', 'size', 'disabled', 'block'] + include: [ + 'children', + 'variant', + 'size', + 'disabled', + 'block', + 'to', + 'target', + 'onClick' + ] } }, argTypes: { @@ -25,6 +34,15 @@ const story = { block: { options: [true, false], control: { type: 'radio' } + }, + target: { + options: ['_self', '_blank', '_parent', '_top'] + }, + onClick: { + action: 'clicked' + }, + to: { + control: { type: 'text' } } } }; @@ -74,4 +92,10 @@ FullWidth.args = { block: true }; +export const AsALink = Template.bind({}); +AsALink.args = { + children: "I'm a link that looks like a button", + to: 'https://www.freecodecamp.org' +}; + export default story; diff --git a/tools/ui-components/src/button/button.test.tsx b/tools/ui-components/src/button/button.test.tsx index aff4df5472b..689a29cc0a8 100644 --- a/tools/ui-components/src/button/button.test.tsx +++ b/tools/ui-components/src/button/button.test.tsx @@ -71,4 +71,32 @@ describe('Button', () => { expect(onClick).not.toBeCalled(); }); + + it('should render an anchor element if the `to` prop is defined', () => { + render(); + + const link = screen.getByRole('link', { name: /freeCodeCamp/i }); + const button = screen.queryByRole('button', { name: /freeCodeCamp/i }); + + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://www.freecodecamp.org'); + // Ensure that a button element is not rendered + expect(button).not.toBeInTheDocument(); + }); + + it('should render a button element if the `to` and `disabled` props are both defined', () => { + render( + + ); + + const button = screen.getByRole('button', { name: /freeCodeCamp/i }); + const link = screen.queryByRole('link', { name: /freeCodeCamp/i }); + + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-disabled', 'true'); + // Ensure that a link element is not rendered + expect(link).not.toBeInTheDocument(); + }); }); diff --git a/tools/ui-components/src/button/button.tsx b/tools/ui-components/src/button/button.tsx index e85e1200997..d618f5b5df7 100644 --- a/tools/ui-components/src/button/button.tsx +++ b/tools/ui-components/src/button/button.tsx @@ -6,6 +6,7 @@ const defaultClassNames = [ 'cursor-pointer', 'inline-block', 'border-3', + 'text-center', 'active:before:w-full', 'active:before:h-full', 'active:before:absolute', @@ -38,7 +39,6 @@ const computeClassNames = ({ classNames.push('block', 'w-full'); } - // TODO: support 'link' variant switch (variant) { case 'danger': classNames.push( @@ -96,7 +96,10 @@ const computeClassNames = ({ return classNames.join(' '); }; -export const Button = React.forwardRef( +export const Button = React.forwardRef< + HTMLButtonElement | HTMLAnchorElement, + ButtonProps +>( ( { variant = 'primary', @@ -105,7 +108,9 @@ export const Button = React.forwardRef( onClick, children, disabled, - block + block, + to, + target }, ref ) => { @@ -128,17 +133,44 @@ export const Button = React.forwardRef( [onClick] ); - return ( - - ); + const renderButton = useCallback(() => { + return ( + + ); + }, [children, classes, ref, type, handleClick, disabled]); + + const renderLink = useCallback(() => { + // Render a `button` tag if `disabled` is defined to keep the component semantically correct + // as a link cannot be disabled. + if (disabled) { + return renderButton(); + } + + return ( + } + className={classes} + href={to} + target={target} + > + {children} + + ); + }, [children, classes, ref, disabled, to, target, renderButton]); + + if (to) { + return renderLink(); + } else { + return renderButton(); + } } ); diff --git a/tools/ui-components/src/button/types.ts b/tools/ui-components/src/button/types.ts index 582a83ec900..c38498008c1 100644 --- a/tools/ui-components/src/button/types.ts +++ b/tools/ui-components/src/button/types.ts @@ -13,4 +13,6 @@ export interface ButtonProps type?: 'submit' | 'button'; disabled?: boolean; block?: boolean; + to?: string; + target?: React.HTMLAttributeAnchorTarget; }