Skip to content

Commit 7e38263

Browse files
authored
feat: Add visually hidden internal component (#2143)
1 parent cd9d9c1 commit 7e38263

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect, fixture, html } from '@open-wc/testing';
2+
import { defineComponents } from '../common/definitions/defineComponents.js';
3+
import IgcVisuallyHiddenComponent from './visually-hidden.js';
4+
5+
describe('VisuallyHidden', () => {
6+
before(() => {
7+
defineComponents(IgcVisuallyHiddenComponent);
8+
});
9+
10+
it('passes the a11y audit', async () => {
11+
const el = await fixture<IgcVisuallyHiddenComponent>(
12+
html`<igc-visually-hidden>Hidden text</igc-visually-hidden>`
13+
);
14+
15+
await expect(el).to.be.accessible();
16+
await expect(el).shadowDom.to.be.accessible();
17+
});
18+
19+
it('renders slotted content', async () => {
20+
const el = await fixture<IgcVisuallyHiddenComponent>(
21+
html`<igc-visually-hidden>Screen reader text</igc-visually-hidden>`
22+
);
23+
24+
expect(el).dom.to.have.text('Screen reader text');
25+
});
26+
27+
it('is hidden from visual layout when not focused', async () => {
28+
const el = await fixture<IgcVisuallyHiddenComponent>(
29+
html`<igc-visually-hidden>Hidden</igc-visually-hidden>`
30+
);
31+
32+
const styles = getComputedStyle(el);
33+
expect(styles.position).to.equal('absolute');
34+
expect(styles.width).to.equal('1px');
35+
expect(styles.height).to.equal('1px');
36+
});
37+
38+
it('becomes visible when focus is within', async () => {
39+
const el = await fixture<IgcVisuallyHiddenComponent>(
40+
html`<igc-visually-hidden><a href="#">Skip</a></igc-visually-hidden>`
41+
);
42+
43+
const link = el.querySelector('a')!;
44+
link.focus();
45+
46+
// When focused, :host(:not(:focus-within)) no longer matches,
47+
// so the element is no longer clipped to 1px
48+
const styles = getComputedStyle(el);
49+
expect(styles.position).to.not.equal('absolute');
50+
});
51+
52+
it('renders a slot element in shadow DOM', async () => {
53+
const el = await fixture<IgcVisuallyHiddenComponent>(
54+
html`<igc-visually-hidden>text</igc-visually-hidden>`
55+
);
56+
57+
expect(el).shadowDom.to.equal('<slot></slot>');
58+
});
59+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { css, html, LitElement } from 'lit';
2+
import { registerComponent } from '../common/definitions/register.js';
3+
4+
/* blazorSuppress */
5+
/**
6+
* A utility component that visually hides its content while keeping it
7+
* accessible to screen readers and other assistive technologies.
8+
*
9+
* The content is only visible when it receives focus, making it ideal for
10+
* skip navigation links and other focus-based accessibility patterns.
11+
*
12+
* @element igc-visually-hidden
13+
*
14+
* @slot - Default slot for the visually hidden content.
15+
*
16+
* @example
17+
* ```html
18+
* <!-- Hide a label visually while keeping it accessible -->
19+
* <igc-visually-hidden>
20+
* <label for="search">Search</label>
21+
* </igc-visually-hidden>
22+
* <input id="search" type="search" placeholder="Search..." />
23+
* ```
24+
*
25+
* @example
26+
* ```html
27+
* <!-- Skip navigation link that becomes visible on focus -->
28+
* <igc-visually-hidden>
29+
* <a href="#main-content">Skip to main content</a>
30+
* </igc-visually-hidden>
31+
* ```
32+
*
33+
* @example
34+
* ```html
35+
* <!-- Provide additional context for icon-only buttons -->
36+
* <button>
37+
* <igc-icon name="close"></igc-icon>
38+
* <igc-visually-hidden>Close dialog</igc-visually-hidden>
39+
* </button>
40+
* ```
41+
*/
42+
export default class IgcVisuallyHiddenComponent extends LitElement {
43+
public static readonly tagName = 'igc-visually-hidden';
44+
45+
public static override styles = css`
46+
:host(:not(:focus-within)) {
47+
position: absolute;
48+
width: 1px;
49+
height: 1px;
50+
border: 0;
51+
padding: 0;
52+
margin: -1px;
53+
overflow: hidden;
54+
white-space: nowrap;
55+
clip: rect(0, 0, 0, 0);
56+
clip-path: inset(50%);
57+
}
58+
`;
59+
60+
/* blazorSuppress */
61+
public static register(): void {
62+
registerComponent(IgcVisuallyHiddenComponent);
63+
}
64+
65+
protected override render() {
66+
return html`<slot></slot>`;
67+
}
68+
}
69+
70+
declare global {
71+
interface HTMLElementTagNameMap {
72+
'igc-visually-hidden': IgcVisuallyHiddenComponent;
73+
}
74+
}

0 commit comments

Comments
 (0)