Skip to content

Commit 89a91e6

Browse files
committed
πŸ›(react) fix button link disabled state
When disabled, a button rendered as a link does not have disabled styles applied.
1 parent 3615ff8 commit 89a91e6

5 files changed

Lines changed: 47 additions & 25 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gouvfr-lasuite/cunningham-react": patch
3+
---
4+
5+
Fix disabled state for button rendered as link

β€Žpackages/react/src/components/Button/_index.scssβ€Ž

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222

2323
}
2424

25-
&:disabled {
25+
&:disabled,
26+
&[aria-disabled="true"] {
2627
background-color: var(--c--contextuals--background--semantic--disabled--primary);
2728
color: var(--c--contextuals--content--semantic--disabled--primary);
2829
}
@@ -44,7 +45,8 @@
4445
);
4546
}
4647

47-
&:disabled {
48+
&:disabled,
49+
&[aria-disabled="true"] {
4850
background-color: var(--c--contextuals--background--semantic--disabled--secondary);
4951
color: var(--c--contextuals--content--semantic--disabled--primary);
5052
}
@@ -64,7 +66,8 @@
6466
background-color: var(--c--contextuals--background--semantic--contextual--primary);
6567
}
6668

67-
&:disabled {
69+
&:disabled,
70+
&[aria-disabled="true"] {
6871
background-color: transparent;
6972
color: var(--c--contextuals--content--semantic--disabled--primary);
7073
}
@@ -85,7 +88,8 @@
8588
background-color: var(--c--contextuals--background--semantic--contextual--primary);
8689
}
8790

88-
&:disabled {
91+
&:disabled,
92+
&[aria-disabled="true"] {
8993
background-color: transparent;
9094
color: var(--c--contextuals--content--semantic--disabled--primary);
9195
border-color: var(--c--contextuals--border--semantic--disabled--primary);

β€Žpackages/react/src/components/Button/index.spec.tsxβ€Ž

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,28 @@ describe("<Button/>", () => {
6666
expect(handleClick).not.toHaveBeenCalled();
6767
});
6868

69-
it("renders disabled link", async () => {
69+
it("renders disabled link with aria-disabled and preserves href", async () => {
7070
render(
7171
<Button href="https://perdu.com" disabled={true}>
7272
Test button link
7373
</Button>,
7474
);
7575
const button = screen.getByRole("link", { name: "Test button link" });
76-
expect(button.classList).toContain("c__button--disabled");
76+
expect(button).toHaveAttribute("aria-disabled", "true");
77+
expect(button).toHaveAttribute("href", "https://perdu.com");
78+
});
79+
80+
it("does not call onClick when click occurs on a disabled link", async () => {
81+
const user = userEvent.setup();
82+
const handleClick = vi.fn();
83+
render(
84+
<Button href="https://perdu.com" disabled={true} onClick={handleClick}>
85+
Test button link
86+
</Button>,
87+
);
88+
const button = screen.getByRole("link", { name: "Test button link" });
89+
await act(async () => user.click(button));
90+
expect(handleClick).not.toHaveBeenCalled();
7791
});
7892

7993
it("renders as link when href is used", () => {

β€Žpackages/react/src/components/Button/index.stories.tsxβ€Ž

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ type Story = StoryObj<typeof Button>;
1212

1313
type AllButtonsProps = ButtonProps & {
1414
color: ButtonProps["color"];
15+
asLink?: boolean;
1516
};
1617

17-
const AllButtons = ({ color = "brand" }: AllButtonsProps) => {
18+
const AllButtons = ({ color = "brand", asLink = false }: AllButtonsProps) => {
19+
const href = asLink ? "#" : undefined;
1820
return (
1921
<div
2022
style={{
@@ -34,23 +36,23 @@ const AllButtons = ({ color = "brand" }: AllButtonsProps) => {
3436
}}
3537
>
3638
<h4 className={`clr-content-semantic-${color}-primary`}>Primary</h4>
37-
<Button {...Primary.args} color={color} />
38-
<Button {...PrimaryDisabled.args} color={color} />
39+
<Button {...Primary.args} color={color} href={href} />
40+
<Button {...PrimaryDisabled.args} color={color} href={href} />
3941
</div>
4042
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
4143
<h4 className={`clr-content-semantic-${color}-primary`}>Secondary</h4>
42-
<Button {...Secondary.args} color={color} />
43-
<Button {...SecondaryDisabled.args} color={color} />
44+
<Button {...Secondary.args} color={color} href={href} />
45+
<Button {...SecondaryDisabled.args} color={color} href={href} />
4446
</div>
4547
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
4648
<h4 className={`clr-content-semantic-${color}-primary`}>Tertiary</h4>
47-
<Button {...BrandTertiary.args} color={color} />
48-
<Button {...TertiaryDisabled.args} color={color} />
49+
<Button {...BrandTertiary.args} color={color} href={href} />
50+
<Button {...TertiaryDisabled.args} color={color} href={href} />
4951
</div>
5052
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
5153
<h4 className={`clr-content-semantic-${color}-primary`}>Bordered</h4>
52-
<Button {...Bordered.args} color={color} />
53-
<Button {...BrandBorderedDisabled.args} color={color} />
54+
<Button {...Bordered.args} color={color} href={href} />
55+
<Button {...BrandBorderedDisabled.args} color={color} href={href} />
5456
</div>
5557
</div>
5658
);
@@ -226,12 +228,7 @@ export const IconOnly: Story = {
226228
};
227229

228230
export const AsLink: Story = {
229-
args: {
230-
children: "Go to fun-mooc.fr",
231-
icon: <span className="material-icons">link</span>,
232-
variant: "primary",
233-
href: "https://www.fun-mooc.fr/",
234-
target: "_blank",
235-
rel: "noopener noreferrer",
231+
render: () => {
232+
return <AllButtons color="brand" asLink />;
236233
},
237234
};

β€Žpackages/react/src/components/Button/index.tsxβ€Ž

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,18 @@ export const Button = ({
6060
if (fullWidth) {
6161
classes.push("c__button--full-width");
6262
}
63-
if (props.href && props.disabled) {
64-
classes.push("c__button--disabled");
65-
}
6663
const iconElement = <span className="c__button__icon">{icon}</span>;
64+
const isDisabledLink = !!props.href && !!props.disabled;
6765
const tagName = props.href ? "a" : "button";
6866
return createElement(
6967
tagName,
7068
{
7169
className: classes.join(" "),
7270
...props,
71+
...(isDisabledLink && {
72+
"aria-disabled": true,
73+
onClick: (e: React.MouseEvent) => e.preventDefault(),
74+
}),
7375
ref,
7476
},
7577
<>

0 commit comments

Comments
Β (0)