(react) render Button as link

For a variety of reasons, such as accessibility or integration with
external react-router deps style we needed to be able to provide the
ability to render the Button component as link in order to be able
to provide link-specific attribute for rendering such as href.
This commit is contained in:
Nathan Vasse
2023-10-18 17:38:48 +02:00
committed by NathanVss
parent b86ba5cc8e
commit 01528b9377
6 changed files with 65 additions and 17 deletions

View File

@@ -4,7 +4,9 @@
border: thin solid transparent;
box-sizing: border-box;
cursor: pointer;
display: flex;
display: inline-flex;
// When button is rendered as link.
text-decoration: none;
font-weight: var(--c--components--button--font-weight);
font-family: var(--c--components--button--font-family);
transition: all var(--c--theme--transitions--duration) var(--c--theme--transitions--ease-out);

View File

@@ -65,6 +65,21 @@ describe("<Button/>", () => {
expect(handleClick).not.toHaveBeenCalled();
});
it("renders as link when href is used", () => {
render(
<Button
href="https://www.fun-mooc.fr/"
target="_blank"
rel="noopener noreferrer"
>
Open link
</Button>,
);
const button = screen.getByRole("link", { name: "Open link" });
expect(button).toHaveAttribute("target", "_blank");
expect(button).toHaveAttribute("rel", "noopener noreferrer");
});
it("uses custom token", async () => {
await buildTheme();
const tokens = await loadTokens();

View File

@@ -103,3 +103,14 @@ export const IconOnly: Story = {
color: "primary",
},
};
export const AsLink: Story = {
args: {
children: "Go to fun-mooc.fr",
icon: <span className="material-icons">link</span>,
color: "primary",
href: "https://www.fun-mooc.fr/",
target: "_blank",
rel: "noopener noreferrer",
},
};

View File

@@ -1,15 +1,24 @@
import React, { ButtonHTMLAttributes, forwardRef, ReactNode } from "react";
import React, {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
createElement,
forwardRef,
ReactNode,
} from "react";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
color?: "primary" | "secondary" | "tertiary" | "danger";
size?: "medium" | "small" | "nano";
icon?: ReactNode;
iconPosition?: "left" | "right";
active?: boolean;
fullWidth?: boolean;
}
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
AnchorHTMLAttributes<HTMLAnchorElement> & {
color?: "primary" | "secondary" | "tertiary" | "danger";
size?: "medium" | "small" | "nano";
icon?: ReactNode;
iconPosition?: "left" | "right";
active?: boolean;
fullWidth?: boolean;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
export type ButtonElement = HTMLButtonElement & HTMLAnchorElement;
export const Button = forwardRef<ButtonElement, ButtonProps>(
(
{
children,
@@ -43,13 +52,19 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
classes.push("c__button--full-width");
}
const iconElement = <span className="c__button__icon">{icon}</span>;
// const iconElement = icon;
return (
<button className={classes.join(" ")} {...props} ref={ref}>
const tagName = props.href ? "a" : "button";
return createElement(
tagName,
{
className: classes.join(" "),
...props,
ref,
},
<>
{!!icon && iconPosition === "left" && iconElement}
{children}
{!!icon && iconPosition === "right" && iconElement}
</button>
</>,
);
},
);

View File

@@ -8,7 +8,7 @@ import {
isToday,
} from "@internationalized/date";
import { CalendarState, RangeCalendarState } from "@react-stately/calendar";
import { Button } from ":/components/Button";
import { Button, ButtonElement } from ":/components/Button";
interface CalendarCellProps {
state: CalendarState | RangeCalendarState;
@@ -20,7 +20,7 @@ const isRangeCalendar = (object: any): object is RangeCalendarState => {
};
export const CalendarCell = ({ state, date }: CalendarCellProps) => {
const ref = useRef<HTMLButtonElement>(null);
const ref = useRef<ButtonElement>(null);
const {
cellProps,
buttonProps,