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;
}