Skip to content

Commit 37aee22

Browse files
authored
fix(link): Emit click event on enter key when the component doesn't have href defined (#12191)
**Related Issue:** #10728 ## Summary - Changes the internal element from a `span` to a `button` to handle enter key events to trigger click event - Removes setting role on `span` as a `link` since its a `button` now - Updates tests - Adds test for use case - Updates styles
1 parent 888301b commit 37aee22

File tree

3 files changed

+46
-35
lines changed

3 files changed

+46
-35
lines changed

packages/calcite-components/src/components/link/link.e2e.ts

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,19 @@ describe("calcite-link", () => {
6565
expect(elementAsLink).not.toHaveAttribute("download");
6666
});
6767

68-
it("renders as a span with default props", async () => {
68+
it("renders as a button with default props", async () => {
6969
const page = await newE2EPage();
7070
await page.setContent(`<calcite-link>Continue</calcite-link>`);
7171

7272
const element = await page.find("calcite-link");
73-
const elementAsSpan = await page.find("calcite-link >>> span");
73+
const elementAsButton = await page.find("calcite-link >>> button");
7474
const elementAsLink = await page.find("calcite-link >>> a");
7575
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
7676
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
7777

7878
expect(element).not.toHaveAttribute("icon-flip-rtl");
7979
expect(elementAsLink).toBeNull();
80-
expect(elementAsSpan).not.toBeNull();
80+
expect(elementAsButton).not.toBeNull();
8181
expect(iconStart).toBeNull();
8282
expect(iconEnd).toBeNull();
8383
});
@@ -86,62 +86,62 @@ describe("calcite-link", () => {
8686
const page = await newE2EPage({ html: `<calcite-link>Continue</calcite-link>` });
8787
const link = await page.find("calcite-link");
8888
let elementAsLink: E2EElement;
89-
let elementAsSpan: E2EElement;
89+
let elementAsButton: E2EElement;
9090

91-
elementAsSpan = await page.find("calcite-link >>> span");
91+
elementAsButton = await page.find("calcite-link >>> button");
9292
elementAsLink = await page.find("calcite-link >>> a");
93-
expect(elementAsSpan).not.toBeNull();
93+
expect(elementAsButton).not.toBeNull();
9494
expect(elementAsLink).toBeNull();
9595

9696
link.setProperty("href", "/");
9797
await page.waitForChanges();
9898

99-
elementAsSpan = await page.find("calcite-link >>> span");
99+
elementAsButton = await page.find("calcite-link >>> button");
100100
elementAsLink = await page.find("calcite-link >>> a");
101-
expect(elementAsSpan).toBeNull();
101+
expect(elementAsButton).toBeNull();
102102
expect(elementAsLink).not.toBeNull();
103103
});
104104

105105
it("renders as a link with default props", async () => {
106106
const page = await newE2EPage();
107107
await page.setContent(`<calcite-link href="/">Continue</calcite-link>`);
108108
const element = await page.find("calcite-link");
109-
const elementAsSpan = await page.find("calcite-link >>> span");
109+
const elementAsButton = await page.find("calcite-link >>> button");
110110
const elementAsLink = await page.find("calcite-link >>> a");
111111
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
112112
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
113113

114114
expect(element).not.toHaveAttribute("icon-flip-rtl");
115115
expect(elementAsLink).not.toBeNull();
116-
expect(elementAsSpan).toBeNull();
116+
expect(elementAsButton).toBeNull();
117117
expect(iconStart).toBeNull();
118118
expect(iconEnd).toBeNull();
119119
});
120120

121-
it("renders as a span with requested props", async () => {
121+
it("renders as a button with requested props", async () => {
122122
const page = await newE2EPage();
123123
await page.setContent(`<calcite-link>Continue</calcite-link>`);
124-
const elementAsSpan = await page.find("calcite-link >>> span");
124+
const elementAsButton = await page.find("calcite-link >>> button");
125125
const elementAsLink = await page.find("calcite-link >>> a");
126126
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
127127
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
128128

129129
expect(elementAsLink).toBeNull();
130-
expect(elementAsSpan).not.toBeNull();
130+
expect(elementAsButton).not.toBeNull();
131131
expect(iconStart).toBeNull();
132132
expect(iconEnd).toBeNull();
133133
});
134134

135135
it("renders as a link with requested props", async () => {
136136
const page = await newE2EPage();
137137
await page.setContent(`<calcite-link href="/">Continue</calcite-link>`);
138-
const elementAsSpan = await page.find("calcite-link >>> span");
138+
const elementAsButton = await page.find("calcite-link >>> button");
139139
const elementAsLink = await page.find("calcite-link >>> a");
140140
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
141141
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
142142

143143
expect(elementAsLink).not.toBeNull();
144-
expect(elementAsSpan).toBeNull();
144+
expect(elementAsButton).toBeNull();
145145
expect(iconStart).toBeNull();
146146
expect(iconEnd).toBeNull();
147147
});
@@ -151,13 +151,13 @@ describe("calcite-link", () => {
151151
await page.setContent(
152152
`<calcite-link rel="noopener noreferrer" target="_blank" class="my-custom-class" href="google.com">Continue</calcite-link>`,
153153
);
154-
const elementAsSpan = await page.find("calcite-link >>> span");
154+
const elementAsButton = await page.find("calcite-link >>> button");
155155
const elementAsLink = await page.find("calcite-link >>> a");
156156
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
157157
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
158158

159159
expect(elementAsLink).not.toBeNull();
160-
expect(elementAsSpan).toBeNull();
160+
expect(elementAsButton).toBeNull();
161161
expect(elementAsLink).not.toHaveClass("my-custom-class");
162162
expect(elementAsLink).toEqualAttribute("href", "google.com");
163163
expect(elementAsLink).toEqualAttribute("rel", "noopener noreferrer");
@@ -169,38 +169,38 @@ describe("calcite-link", () => {
169169
it("renders with an icon-start", async () => {
170170
const page = await newE2EPage();
171171
await page.setContent(`<calcite-link icon-start='plus'>Continue</calcite-link>`);
172-
const elementAsSpan = await page.find("calcite-link >>> span");
172+
const elementAsButton = await page.find("calcite-link >>> button");
173173
const elementAsLink = await page.find("calcite-link >>> a");
174174
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
175175
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
176176
expect(elementAsLink).toBeNull();
177-
expect(elementAsSpan).not.toBeNull();
177+
expect(elementAsButton).not.toBeNull();
178178
expect(iconStart).not.toBeNull();
179179
expect(iconEnd).toBeNull();
180180
});
181181

182182
it("renders with an icon-end", async () => {
183183
const page = await newE2EPage();
184184
await page.setContent(`<calcite-link icon-end='plus'>Continue</calcite-link>`);
185-
const elementAsSpan = await page.find("calcite-link >>> span");
185+
const elementAsButton = await page.find("calcite-link >>> button");
186186
const elementAsLink = await page.find("calcite-link >>> a");
187187
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
188188
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
189189
expect(elementAsLink).toBeNull();
190-
expect(elementAsSpan).not.toBeNull();
190+
expect(elementAsButton).not.toBeNull();
191191
expect(iconStart).toBeNull();
192192
expect(iconEnd).not.toBeNull();
193193
});
194194

195195
it("renders with an icon-start and icon-end", async () => {
196196
const page = await newE2EPage();
197197
await page.setContent(`<calcite-link icon-start='plus' icon-end='plus'>Continue</calcite-link>`);
198-
const elementAsSpan = await page.find("calcite-link >>> span");
198+
const elementAsButton = await page.find("calcite-link >>> button");
199199
const elementAsLink = await page.find("calcite-link >>> a");
200200
const iconStart = await page.find("calcite-link >>> .calcite-link--icon.icon-start");
201201
const iconEnd = await page.find("calcite-link >>> .calcite-link--icon.icon-end");
202202
expect(elementAsLink).toBeNull();
203-
expect(elementAsSpan).not.toBeNull();
203+
expect(elementAsButton).not.toBeNull();
204204
expect(iconStart).not.toBeNull();
205205
expect(iconEnd).not.toBeNull();
206206
});
@@ -231,6 +231,18 @@ describe("calcite-link", () => {
231231
expect(page.url()).toBe(targetUrl);
232232
});
233233

234+
it("keyboard without href", async () => {
235+
const element = await page.find("calcite-link");
236+
element.setProperty("href", undefined);
237+
const clickEvent = await element.spyOnEvent("click");
238+
239+
await element.callMethod("setFocus");
240+
await page.waitForChanges();
241+
await page.keyboard.press("Enter");
242+
await page.waitForChanges();
243+
expect(clickEvent).toHaveReceivedEventTimes(1);
244+
});
245+
234246
it("mouse", async () => {
235247
// workaround for https://github.com/puppeteer/puppeteer/issues/2977
236248
await page.$eval("calcite-link", (link: HTMLElement): void => {

packages/calcite-components/src/components/link/link.scss

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
// link base
1414
:host a,
15-
:host span {
15+
:host button {
1616
@apply font-inherit
1717
relative
1818
flex
@@ -34,7 +34,7 @@
3434

3535
// focus styles
3636
:host a,
37-
:host span {
37+
:host button {
3838
@apply focus-base;
3939
&:focus {
4040
@apply focus-outset;
@@ -63,7 +63,7 @@ calcite-icon {
6363
}
6464

6565
:host {
66-
span,
66+
button,
6767
a {
6868
@apply relative
6969
inline
@@ -73,7 +73,8 @@ calcite-icon {
7373
color: var(--calcite-link-text-color, var(--calcite-color-text-link));
7474
line-height: inherit;
7575
white-space: initial;
76-
background-image: linear-gradient(currentColor, currentColor),
76+
background-image:
77+
linear-gradient(currentColor, currentColor),
7778
linear-gradient(var(--calcite-color-brand-underline), var(--calcite-color-brand-underline));
7879
background-position-x: 0%, 100%;
7980
background-position-y: min(1.5em, 100%);

packages/calcite-components/src/components/link/link.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ declare global {
2222
/**
2323
* Any attributes placed on <calcite-link> component will propagate to the rendered child
2424
*
25-
* Passing a 'href' will render an anchor link, instead of a span. Role will be set to link, or link, depending on this.
25+
* Passing a 'href' will render an anchor link, instead of a button.
2626
*
2727
* It is the consumers responsibility to add aria information, rel, target, for links, and any link attributes for form submission
2828
*
@@ -38,7 +38,7 @@ export class Link extends LitElement implements InteractiveComponent {
3838
// #region Private Properties
3939

4040
/** the rendered child element */
41-
private childEl: HTMLAnchorElement | HTMLSpanElement;
41+
private childEl: HTMLAnchorElement | HTMLButtonElement;
4242

4343
// #endregion
4444

@@ -131,7 +131,7 @@ export class Link extends LitElement implements InteractiveComponent {
131131
override render(): JsxNode {
132132
const { download, el } = this;
133133
const dir = getElementDir(el);
134-
const childElType = this.href ? "a" : "span";
134+
const childElType = this.href ? "a" : "button";
135135
const iconStartEl = (
136136
<calcite-icon
137137
class="calcite-link--icon icon-start"
@@ -151,11 +151,10 @@ export class Link extends LitElement implements InteractiveComponent {
151151
);
152152

153153
const DynamicHtmlTag =
154-
childElType === "span"
155-
? (literal`span` as unknown as "span")
154+
childElType === "button"
155+
? (literal`button` as unknown as "button")
156156
: (literal`a` as unknown as "a");
157-
const role = childElType === "span" ? "link" : null;
158-
const tabIndex = childElType === "span" ? 0 : null;
157+
const tabIndex = childElType === "button" ? 0 : null;
159158
/* TODO: [MIGRATION] This used <Host> before. In Stencil, <Host> props overwrite user-provided props. If you don't wish to overwrite user-values, replace "=" here with "??=" */
160159
this.el.role = "presentation";
161160

@@ -178,7 +177,6 @@ export class Link extends LitElement implements InteractiveComponent {
178177
onClick={this.childElClickHandler}
179178
ref={this.storeTagRef}
180179
rel={childElType === "a" && this.rel}
181-
role={role}
182180
tabIndex={tabIndex}
183181
target={childElType === "a" && this.target}
184182
>

0 commit comments

Comments
 (0)