(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

@@ -0,0 +1,5 @@
---
"@openfun/cunningham-react": minor
---
render Button as link

View File

@@ -4,7 +4,9 @@
border: thin solid transparent; border: thin solid transparent;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; 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-weight: var(--c--components--button--font-weight);
font-family: var(--c--components--button--font-family); font-family: var(--c--components--button--font-family);
transition: all var(--c--theme--transitions--duration) var(--c--theme--transitions--ease-out); 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(); 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 () => { it("uses custom token", async () => {
await buildTheme(); await buildTheme();
const tokens = await loadTokens(); const tokens = await loadTokens();

View File

@@ -103,3 +103,14 @@ export const IconOnly: Story = {
color: "primary", 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> { export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
color?: "primary" | "secondary" | "tertiary" | "danger"; AnchorHTMLAttributes<HTMLAnchorElement> & {
size?: "medium" | "small" | "nano"; color?: "primary" | "secondary" | "tertiary" | "danger";
icon?: ReactNode; size?: "medium" | "small" | "nano";
iconPosition?: "left" | "right"; icon?: ReactNode;
active?: boolean; iconPosition?: "left" | "right";
fullWidth?: boolean; active?: boolean;
} fullWidth?: boolean;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>( export type ButtonElement = HTMLButtonElement & HTMLAnchorElement;
export const Button = forwardRef<ButtonElement, ButtonProps>(
( (
{ {
children, children,
@@ -43,13 +52,19 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
classes.push("c__button--full-width"); classes.push("c__button--full-width");
} }
const iconElement = <span className="c__button__icon">{icon}</span>; const iconElement = <span className="c__button__icon">{icon}</span>;
// const iconElement = icon; const tagName = props.href ? "a" : "button";
return ( return createElement(
<button className={classes.join(" ")} {...props} ref={ref}> tagName,
{
className: classes.join(" "),
...props,
ref,
},
<>
{!!icon && iconPosition === "left" && iconElement} {!!icon && iconPosition === "left" && iconElement}
{children} {children}
{!!icon && iconPosition === "right" && iconElement} {!!icon && iconPosition === "right" && iconElement}
</button> </>,
); );
}, },
); );

View File

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