Skip to content

Commit

Permalink
feat: introduce more customizable skip-nav (#208) (GAUD-5025)
Browse files Browse the repository at this point in the history
  • Loading branch information
dlockhart authored Jan 30, 2024
1 parent b939cfe commit 4bc4801
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .vdiff.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"browsers": [
{
"name": "Chromium",
"version": 120
"version": 121
}
]
}
31 changes: 31 additions & 0 deletions d2l-navigation-skip-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import './d2l-navigation-skip.js';
import { html, LitElement } from 'lit';
import { FocusMixin } from '@brightspace-ui/core/mixins/focus/focus-mixin.js';
import { LocalizeNavigationElement } from './components/localize-navigation-element.js';
import { querySelectorComposed } from '@brightspace-ui/core/helpers/dom.js';

class NavigationSkipMain extends FocusMixin(LocalizeNavigationElement(LitElement)) {

static get focusElementSelector() {
return 'd2l-navigation-skip';
}

render() {
return html`<d2l-navigation-skip text="${this.localize('skipNav')}" @click="${this._handleSkipNav}" class="vdiff-target"></d2l-navigation-skip>`;
}

_handleSkipNav() {
const elem = querySelectorComposed(document, 'main') ||
querySelectorComposed(document, '[role="main"]') ||
querySelectorComposed(document, 'h1');
if (elem) {
elem.tabIndex = -1;
elem.focus();
} else {
this.dispatchEvent(new CustomEvent('d2l-navigation-skip-fail', { bubbles: false, composed: false }));
}
}

}

customElements.define('d2l-navigation-skip-main', NavigationSkipMain);
47 changes: 18 additions & 29 deletions d2l-navigation-skip.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { css, html, LitElement } from 'lit';
import { LocalizeNavigationElement } from './components/localize-navigation-element.js';
import { querySelectorComposed } from '@brightspace-ui/core/helpers/dom.js';
import { RtlMixin } from '@brightspace-ui/core/mixins/rtl-mixin.js';
import { FocusMixin } from '@brightspace-ui/core/mixins/focus/focus-mixin.js';
import { PropertyRequiredMixin } from '@brightspace-ui/core/mixins/property-required/property-required-mixin.js';

class NavigationSkip extends LocalizeNavigationElement(RtlMixin(LitElement)) {
class NavigationSkip extends FocusMixin(PropertyRequiredMixin(LitElement)) {

static get properties() {
return {
text: { required: true, type: String }
};
}

static get styles() {
return css`
a {
left: -10000px;
inset-inline-start: -10000px;
overflow: hidden;
position: absolute;
width: 1px;
}
:host([dir="rtl"]) a {
left: auto;
right: -10000px;
}
a:active,
a:focus {
background-color: rgba(0, 0, 0, 0.7);
Expand All @@ -25,43 +26,31 @@ class NavigationSkip extends LocalizeNavigationElement(RtlMixin(LitElement)) {
cursor: pointer;
display: block;
font-weight: bold;
left: 25%;
inset-block-start: 0;
inset-inline-start: 25%;
margin: 0 auto;
outline: none;
padding: 0.3em;
text-align: center;
text-decoration: none;
top: 0;
width: 50%;
z-index: 10000;
}
:host([dir="rtl"]) a:active,
:host([dir="rtl"]) a:focus {
right: 25%;
}
`;
}

static get focusElementSelector() {
return 'a';
}

render() {
return html`<a tabindex="0" @click="${this._handleSkipNav}" @keydown="${this._handleKeyDown}">${this.localize('skipNav')}</a>`;
return html`<a tabindex="0" @keydown="${this._handleKeyDown}" class="vdiff-target">${this.text}</a>`;
}

_handleKeyDown(e) {
if (e.keyCode === 13) {
e.preventDefault();
this._handleSkipNav();
}
}

_handleSkipNav() {
const elem = querySelectorComposed(document, 'main') ||
querySelectorComposed(document, '[role="main"]') ||
querySelectorComposed(document, 'h1');
if (elem) {
elem.tabIndex = -1;
elem.focus();
} else {
this.dispatchEvent(new CustomEvent('d2l-navigation-skip-fail', { bubbles: false, composed: false }));
this.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true }));
}
}

Expand Down
4 changes: 2 additions & 2 deletions d2l-navigation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import './d2l-navigation-band.js';
import './d2l-navigation-skip.js';
import './d2l-navigation-skip-main.js';
import { css, html, LitElement, nothing } from 'lit';
import { getNextFocusable } from '@brightspace-ui/core/helpers/focus.js';

Expand Down Expand Up @@ -40,7 +40,7 @@ class Navigation extends LitElement {
}

render() {
const skipNav = this.hasSkipNav ? html`<d2l-navigation-skip @d2l-navigation-skip-fail="${this._handleSkipNavFail}"></d2l-navigation-skip>` : nothing;
const skipNav = this.hasSkipNav ? html`<d2l-navigation-skip-main @d2l-navigation-skip-fail="${this._handleSkipNavFail}"></d2l-navigation-skip-main>` : nothing;
return html`
${skipNav}<d2l-navigation-band><slot name="navigation-band"></slot></d2l-navigation-band>
<slot></slot>
Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ <h1>Navigation Demo Pages</h1>
<li><a href="button-link.html">buttons and links</a></li>
<li><a href="immersive.html">navigation-immersive</a></li>
<li><a href="iterator.html">navigation-iterator</a></li>
<li><a href="skip.html">navigation-skip</a></li>
</ul>
</body>
</html>
32 changes: 32 additions & 0 deletions demo/skip.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/node_modules/@brightspace-ui/core/components/demo/styles.css" type="text/css">
<script type="module">
import '@brightspace-ui/core/components/demo/demo-page.js';
import '../d2l-navigation-skip.js';
import '../d2l-navigation-skip-main.js';
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
</head>
<body>
<d2l-demo-page page-title="d2l-navigation-skip">
<h2>Custom</h2>
<d2l-demo-snippet>
<d2l-navigation-skip text="Skip to custom place" id="custom"></d2l-navigation-skip>
<button id="custom-target">Skip to here</button>
<script>
document.querySelector('#custom').addEventListener('click', () => {
document.querySelector('#custom-target').focus();
});
</script>
</d2l-demo-snippet>

<h2>Main</h2>
<d2l-demo-snippet>
<d2l-navigation-skip-main></d2l-navigation-skip-main>
</d2l-demo-snippet>
</d2l-demo-page>
</body>
</html>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"d2l-navigation-separator.js",
"d2l-navigation-shared-styles.js",
"d2l-navigation-skip.js",
"d2l-navigation-skip-main.js",
"d2l-navigation-styles.js",
"d2l-navigation.js",
"navigation.serge.json"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/golden/navigation-skip-main/chromium/en.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/golden/navigation-skip/chromium/focus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test/navigation.vdiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('d2l-navigation', () => {

it('skip-nav', async() => {
const elem = await fixture(navigationDefaultFixture);
await focusElem(elem.shadowRoot.querySelector('d2l-navigation-skip').shadowRoot.querySelector('a'));
await focusElem(elem.shadowRoot.querySelector('d2l-navigation-skip-main'));
await expect(elem).to.be.golden();
});

Expand Down
91 changes: 91 additions & 0 deletions test/skip-main.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import '../d2l-navigation-skip-main.js';
import { clickElem, expect, fixture, focusElem, html, oneEvent, sendKeysElem } from '@brightspace-ui/testing';
import { getComposedActiveElement } from '@brightspace-ui/core/helpers/focus.js';

const mainFixture = html`<d2l-navigation-skip-main></d2l-navigation-skip-main>`;

function getAnchor(elem) {
return elem.shadowRoot.querySelector('d2l-navigation-skip').shadowRoot.querySelector('a');
}

describe('d2l-navigation-skip-main', () => {

describe('events', () => {

let elem, anchor;
beforeEach(async() => {
elem = await fixture(mainFixture);
anchor = getAnchor(elem);
});

it('should fire click event when clicked with mouse', async() => {
const p = oneEvent(elem, 'click');
await focusElem(anchor);
clickElem(anchor);
await p;
});

it('should fire click event when ENTER is pressed', async() => {
const p = oneEvent(elem, 'click');
sendKeysElem(anchor, 'press', 'Enter');
await p;
});

it('should delegate focus to anchor', async() => {
await focusElem(elem);
expect(getComposedActiveElement()).to.equal(anchor);
});

});

describe('skip logic', () => {

it('should focus on main element if present', async() => {
const elem = await fixture(html`
<div>
${mainFixture}
<main>main1</main>
<div role="main">main2</div>
<h1>heading</h1>
</div>
`);
const anchor = getAnchor(elem.querySelector('d2l-navigation-skip-main'));
await sendKeysElem(anchor, 'press', 'Enter');
expect(getComposedActiveElement()).to.equal(elem.querySelector('main'));
});

it('should focus on role="main" element if no main', async() => {
const elem = await fixture(html`
<div>
${mainFixture}
<div role="main">main2</div>
<h1>heading</h1>
</div>
`);
const anchor = getAnchor(elem.querySelector('d2l-navigation-skip-main'));
await sendKeysElem(anchor, 'press', 'Enter');
expect(getComposedActiveElement()).to.equal(elem.querySelector('[role="main"]'));
});

it('should focus on h1 element if no main or role="main"', async() => {
const elem = await fixture(html`
<div>
${mainFixture}
<h1>heading</h1>
</div>
`);
const anchor = getAnchor(elem.querySelector('d2l-navigation-skip-main'));
await sendKeysElem(anchor, 'press', 'Enter');
expect(getComposedActiveElement()).to.equal(elem.querySelector('h1'));
});

it('should dispatch "d2l-navigation-skip-fail" event if no focus targets are found', async() => {
const elem = await fixture(mainFixture);
const anchor = getAnchor(elem);
sendKeysElem(anchor, 'press', 'Enter');
await oneEvent(elem, 'd2l-navigation-skip-fail');
});

});

});
24 changes: 24 additions & 0 deletions test/skip-main.vdiff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import '../../d2l-navigation-skip-main.js';
import { expect, fixture, focusElem, html } from '@brightspace-ui/testing';

const mainFixture = html`
<div class="width: 600px;">
<d2l-navigation-skip-main class="vdiff-include"></d2l-navigation-skip-main>
<main>
<h1>Heading</h1>
<p>Some content</p>
</main>
</div>
`;

describe('d2l-navigation-skip-main', () => {

['en', 'ar'].forEach(lang => {
it(lang, async() => {
const elem = await fixture(mainFixture, { lang });
await focusElem(elem.querySelector('d2l-navigation-skip-main'));
await expect(elem).to.be.golden();
});
});

});
54 changes: 54 additions & 0 deletions test/skip.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import '../d2l-navigation-skip.js';
import { clickElem, expect, fixture, focusElem, html, oneEvent, sendKeysElem } from '@brightspace-ui/testing';
import { createMessage } from '@brightspace-ui/core/mixins/property-required/property-required-mixin.js';
import { getComposedActiveElement } from '@brightspace-ui/core/helpers/focus.js';

const customFixture = html`<d2l-navigation-skip text="Skip to custom place"></d2l-navigation-skip>`;

describe('d2l-navigation-skip', () => {

it('should throw if text is not provided', async() => {
const elem = await fixture(html`<d2l-navigation-skip><d2l-navigation-skip>`);
expect(() => elem.flushRequiredPropertyErrors())
.to.throw(TypeError, createMessage(elem, 'text'));
});

describe('accessibility', () => {

it('should pass all aXe tests', async() => {
const elem = await fixture(customFixture);
await focusElem(elem);
await expect(elem).to.be.accessible();
});

});

describe('events', () => {

let elem, anchor;
beforeEach(async() => {
elem = await fixture(customFixture);
anchor = elem.shadowRoot.querySelector('a');
});

it('should fire click event when clicked with mouse', async() => {
const p = oneEvent(elem, 'click');
await focusElem(anchor);
clickElem(anchor);
await p;
});

it('should fire click event when ENTER is pressed', async() => {
const p = oneEvent(elem, 'click');
sendKeysElem(anchor, 'press', 'Enter');
await p;
});

it('should delegate focus to anchor', async() => {
await focusElem(elem);
expect(getComposedActiveElement()).to.equal(anchor);
});

});

});
17 changes: 17 additions & 0 deletions test/skip.vdiff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import '../../d2l-navigation-skip.js';
import { expect, fixture, focusElem, html } from '@brightspace-ui/testing';

describe('d2l-navigation-skip', () => {

it('focus', async() => {
const elem = await fixture(html`
<div class="width: 600px;">
<d2l-navigation-skip text="Skip to custom place" class="vdiff-include"></d2l-navigation-skip>
<p>Some content</p>
</div>
`);
await focusElem(elem.querySelector('d2l-navigation-skip'));
await expect(elem).to.be.golden();
});

});

0 comments on commit 4bc4801

Please sign in to comment.