diff --git a/README.md b/README.md index 392721af..9b17a7bd 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,21 @@ Add the `d2l-navigation-main-footer` component, and provide elements for the `ma --- +### d2l-navigation-button-icon + +`` provides a button with an icon and optional text. + +### Properties + +| Property | Type | Description | +|--|--|--| +| `disabled` | Boolean | Disables the button | +| `text` | String, required | Text for the button | +| `icon` | String | Preset icon key (e.g. `tier1:gear`) | +| `text-hidden` | Boolean | Visually hides the text | + +--- + ### d2l-navigation-link-icon Similar to ``, a link that comes with an icon and optional text. @@ -253,11 +268,11 @@ Then run the tests: ```shell # run visual-diff tests -npx mocha './test/**/*.visual-diff.mjs' -t 10000 +npx mocha './test/**/*.visual-diff.js' -t 10000 # subset of visual-diff tests: -npx mocha './test/**/*.visual-diff.mjs' -t 10000 -g some-pattern +npx mocha './test/**/*.visual-diff.js' -t 10000 -g some-pattern # update visual-diff goldens -npx mocha './test/**/*.visual-diff.mjs' -t 10000 --golden +npx mocha './test/**/*.visual-diff.js' -t 10000 --golden ``` ### Running the demos diff --git a/d2l-navigation-button-icon.js b/d2l-navigation-button-icon.js new file mode 100644 index 00000000..d8746fa8 --- /dev/null +++ b/d2l-navigation-button-icon.js @@ -0,0 +1,126 @@ +import '@brightspace-ui/core/components/colors/colors.js'; +import '@brightspace-ui/core/components/icons/icon.js'; +import '@brightspace-ui/core/components/tooltip/tooltip.js'; +import { css, html, LitElement, nothing } from 'lit'; +import { FocusMixin } from '@brightspace-ui/core/mixins/focus-mixin.js'; +import { getUniqueId } from '@brightspace-ui/core/helpers/uniqueId.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { highlightBorderStyles } from './d2l-navigation-styles.js'; + +/** + * Navigation button with an icon and text. + */ +class NavigationButtonIcon extends FocusMixin(LitElement) { + + static get properties() { + return { + /** + * Disables the button + * @type {boolean} + */ + disabled: { reflect: true, type: Boolean }, + /** + * REQUIRED: Preset icon key (e.g. "tier1:gear") + * @type {string} + */ + icon: { type: String }, + /** + * REQUIRED: Text for the button + * @type {string} + */ + text: { type: String }, + /** + * Visually hides the text but still accessible + * @type {boolean} + */ + textHidden: { attribute: 'text-hidden', type: Boolean } + }; + } + + static get styles() { + return [highlightBorderStyles, css` + :host { + display: inline-block; + height: 100%; + } + :host([hidden]) { + display: none; + } + button { + align-items: center; + background: transparent; + border: none; + color: var(--d2l-color-ferrite); + cursor: pointer; + font-family: inherit; + font-size: inherit; + height: 100%; + margin: 0; + min-height: 40px; + outline: none; + overflow: visible; + padding: 0; + position: relative; + white-space: nowrap; + } + /* Firefox includes a hidden border which messes up button dimensions */ + button::-moz-focus-inner { + border: 0; + } + button:not([disabled]):hover, + button:not([disabled]):focus { + --d2l-icon-fill-color: var(--d2l-color-celestine); + color: var(--d2l-color-celestine); + outline: none; + } + button[disabled] { + cursor: default; + opacity: 0.5; + } + `]; + } + + constructor() { + super(); + this.disabled = false; + this.textHidden = false; + this._buttonId = getUniqueId(); + } + + static get focusElementSelector() { + return 'button'; + } + + render() { + const { ariaLabel, id, text, tooltip } = this._getRenderSettings(); + const highlightBorder = !this.disabled ? html`` : nothing; + return html` + + ${tooltip} + `; + } + + _getRenderSettings() { + if (this.textHidden) { + return { + ariaLabel: this.text, + id: this._buttonId, + text: nothing, + tooltip: html`${this.text}` + }; + } + return { + ariaLabel: undefined, + id: undefined, + text: this.text, + tooltip: nothing + }; + } + +} + +customElements.define('d2l-navigation-button-icon', NavigationButtonIcon); diff --git a/d2l-navigation-link-icon.js b/d2l-navigation-link-icon.js index a20e609f..17fee9b1 100644 --- a/d2l-navigation-link-icon.js +++ b/d2l-navigation-link-icon.js @@ -1,7 +1,8 @@ import '@brightspace-ui/core/components/icons/icon.js'; -import { html, LitElement } from 'lit'; -import { classMap } from 'lit/directives/class-map.js'; +import '@brightspace-ui/core/components/tooltip/tooltip.js'; +import { html, LitElement, nothing } from 'lit'; import { FocusMixin } from '@brightspace-ui/core/mixins/focus-mixin.js'; +import { getUniqueId } from '@brightspace-ui/core/helpers/uniqueId.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { offscreenStyles } from '@brightspace-ui/core/components/offscreen/offscreen.js'; import { highlightBorderStyles, highlightLinkStyles } from './d2l-navigation-styles.js'; @@ -24,7 +25,7 @@ class NavigationLinkIcon extends FocusMixin(LitElement) { */ icon: { type: String }, /** - * REQUIRED: Accessible text for the button + * REQUIRED: Text for the link * @type {string} */ text: { type: String }, @@ -43,6 +44,7 @@ class NavigationLinkIcon extends FocusMixin(LitElement) { constructor() { super(); this.textHidden = false; + this._linkId = getUniqueId(); this._missingHrefErrorHasBeenThrown = false; this._validatingHrefTimeout = null; } @@ -57,15 +59,14 @@ class NavigationLinkIcon extends FocusMixin(LitElement) { } render() { - const textClasses = { - 'd2l-offscreen': this.textHidden - }; + const { ariaLabel, id, text, tooltip } = this._getRenderSettings(); return html` - + - ${this.text} + ${text} + ${tooltip} `; } @@ -77,6 +78,23 @@ class NavigationLinkIcon extends FocusMixin(LitElement) { } + _getRenderSettings() { + if (this.textHidden) { + return { + ariaLabel: this.text, + id: this._linkId, + text: nothing, + tooltip: html`${this.text}` + }; + } + return { + ariaLabel: undefined, + id: undefined, + text: this.text, + tooltip: nothing + }; + } + _validateHref() { clearTimeout(this._validatingHrefTimeout); // don't error immediately in case it doesn't get set immediately diff --git a/demo/button-link.html b/demo/button-link.html index 941c4c24..5c185b25 100644 --- a/demo/button-link.html +++ b/demo/button-link.html @@ -7,6 +7,7 @@ import '@brightspace-ui/core/components/icons/icon.js'; import '../d2l-navigation-button.js'; import '../d2l-navigation-button-close.js'; + import '../d2l-navigation-button-icon.js'; import '../d2l-navigation-button-notification-icon.js'; import '../d2l-navigation-link.js'; import '../d2l-navigation-link-back.js'; @@ -44,9 +45,11 @@

d2l-navigation-button / d2l-navigation-link

-

d2l-navigation-link-icon

+

d2l-navigation-button-icon / d2l-navigation-link-icon

+ +
@@ -93,13 +96,14 @@

Combined

href="https://www.example.org" src="./logo-image.png" text="Link Image Text"> + - + Settings - + Settings diff --git a/package.json b/package.json index 582b2afb..61a33ccc 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "/lang", "d2l-navigation-band.js", "d2l-navigation-button-close.js", + "d2l-navigation-button-icon.js", "d2l-navigation-button-notification-icon.js", "d2l-navigation-button.js", "d2l-navigation-highlight-styles.js", diff --git a/test/button.test.js b/test/button.test.js index d96679b1..ae9501f4 100644 --- a/test/button.test.js +++ b/test/button.test.js @@ -1,10 +1,9 @@ import '../d2l-navigation-button.js'; import '../d2l-navigation-button-close.js'; +import '../d2l-navigation-button-icon.js'; import '../d2l-navigation-button-notification-icon.js'; -import '../d2l-navigation-link.js'; -import '../d2l-navigation-link-back.js'; -import '../d2l-navigation-link-image.js'; import { expect, fixture, html, oneEvent } from '@open-wc/testing'; +import { getComposedActiveElement } from '@brightspace-ui/core/helpers/focus.js'; import { runConstructor } from '@brightspace-ui/core/tools/constructor-test-helper.js'; describe('Buttons', () => { @@ -74,61 +73,46 @@ describe('Buttons', () => { }); - describe('d2l-navigation-link', () => { + describe('d2l-navigation-button-icon', () => { describe('accessibility', () => { it('default', async() => { - const el = await fixture(html`D2L`); + const el = await fixture(html``); await expect(el).to.be.accessible(); }); - it('focused', async() => { - const el = await fixture(html`D2L`); - setTimeout(() => el.shadowRoot.querySelector('a').focus()); - await oneEvent(el, 'focus'); + it('text hidden', async() => { + const el = await fixture(html``); await expect(el).to.be.accessible(); }); - }); - - describe('constructor', () => { - it('should construct', () => { - runConstructor('d2l-navigation-link'); + it('disabled', async() => { + const el = await fixture(html``); + await expect(el).to.be.accessible(); }); - }); - - }); - describe('d2l-navigation-link-back', () => { - - describe('accessibility', () => { - it('should pass all aXe tests', async() => { - const el = await fixture(html`D2L`); + it('focused', async() => { + const el = await fixture(html``); + el.focus(); + const activeElem = getComposedActiveElement(); + expect(activeElem).to.equal(el.shadowRoot.querySelector('button')); await expect(el).to.be.accessible(); }); + }); describe('constructor', () => { it('should construct', () => { - runConstructor('d2l-navigation-link-back'); + runConstructor('d2l-navigation-button-icon'); }); }); - }); - - describe('d2l-navigation-link-image', () => { - - describe('accessibility', () => { - it('should pass all aXe tests', async() => { - const el = await fixture(html``); - await expect(el).to.be.accessible(); - }); - }); - - describe('constructor', () => { - it('should construct', () => { - runConstructor('d2l-navigation-link-image'); + describe('events', () => { + it('should trigger click event', async() => { + const el = await fixture(html``); + setTimeout(() => el.shadowRoot.querySelector('button').click()); + await oneEvent(el, 'click'); }); }); diff --git a/test/button.visual-diff.html b/test/button.visual-diff.html index 3b7cf03b..5fa74d29 100644 --- a/test/button.visual-diff.html +++ b/test/button.visual-diff.html @@ -10,6 +10,7 @@ import '@brightspace-ui/core/components/typography/typography.js'; import '../../d2l-navigation-button.js'; import '../../d2l-navigation-button-close.js'; + import '../../d2l-navigation-button-icon.js'; import '../../d2l-navigation-button-notification-icon.js'; import { forceFocusVisible } from '@brightspace-ui/core/helpers/focus.js'; window.forceFocusVisible = forceFocusVisible; @@ -25,6 +26,18 @@
+
+ +
+
+ +
+
+ +
+
+ +
diff --git a/test/button.visual-diff.js b/test/button.visual-diff.js index 186e29cd..437fbf61 100644 --- a/test/button.visual-diff.js +++ b/test/button.visual-diff.js @@ -28,6 +28,10 @@ describe('d2l-navigation-button', () => { { category: 'default', tests: ['normal', 'hover', 'focus'] }, { category: 'disabled', tests: ['normal', 'hover', 'focus'] }, { category: 'close', tests: ['normal', 'hover', 'focus'] }, + { category: 'icon-text', tests: ['normal', 'hover', 'focus'] }, + { category: 'icon-text-disabled', tests: ['normal', 'hover', 'focus'] }, + { category: 'icon-text-hidden', rectSelector: 'icon-text-hidden-container', tests: ['normal', 'hover', 'focus'] }, + { category: 'icon-text-hidden-disabled', tests: ['normal', 'hover', 'focus'] }, { category: 'notification-icon-off', tests: ['normal', 'hover', 'focus'] }, { category: 'notification-icon-on', tests: ['normal', 'hover', 'focus'] } ].forEach((entry) => { @@ -35,11 +39,17 @@ describe('d2l-navigation-button', () => { entry.tests.forEach((name) => { it(name, async function() { const selector = `#${entry.category}`; - - if (name === 'hover') await page.hover(selector); + const rectSelector = entry.rectSelector ? `#${entry.rectSelector}` : selector; + + if (name === 'hover') { + await page.hover(selector); + if (entry.category === 'icon-text-hidden') { + await new Promise(resolve => setTimeout(resolve, 350)); + } + } else if (name === 'focus') await page.$eval(selector, (elem) => forceFocusVisible(elem)); - const rect = await visualDiff.getRect(page, selector); + const rect = await visualDiff.getRect(page, rectSelector); await visualDiff.screenshotAndCompare(page, this.test.fullTitle(), { clip: rect }); }); }); diff --git a/test/link.test.js b/test/link.test.js new file mode 100644 index 00000000..0f0bc044 --- /dev/null +++ b/test/link.test.js @@ -0,0 +1,103 @@ +import '../d2l-navigation-link.js'; +import '../d2l-navigation-link-back.js'; +import '../d2l-navigation-link-icon.js'; +import '../d2l-navigation-link-image.js'; +import { expect, fixture, html, oneEvent } from '@open-wc/testing'; +import { getComposedActiveElement } from '@brightspace-ui/core/helpers/focus.js'; +import { runConstructor } from '@brightspace-ui/core/tools/constructor-test-helper.js'; + +describe('Links', () => { + + describe('d2l-navigation-link', () => { + + describe('accessibility', () => { + + it('default', async() => { + const el = await fixture(html`D2L`); + await expect(el).to.be.accessible(); + }); + + it('focused', async() => { + const el = await fixture(html`D2L`); + setTimeout(() => el.shadowRoot.querySelector('a').focus()); + await oneEvent(el, 'focus'); + await expect(el).to.be.accessible(); + }); + + }); + + describe('constructor', () => { + it('should construct', () => { + runConstructor('d2l-navigation-link'); + }); + }); + + }); + + describe('d2l-navigation-link-back', () => { + + describe('accessibility', () => { + it('should pass all aXe tests', async() => { + const el = await fixture(html`D2L`); + await expect(el).to.be.accessible(); + }); + }); + + describe('constructor', () => { + it('should construct', () => { + runConstructor('d2l-navigation-link-back'); + }); + }); + + }); + + describe('d2l-navigation-link-icon', () => { + + describe('accessibility', () => { + + it('default', async() => { + const el = await fixture(html``); + await expect(el).to.be.accessible(); + }); + + it('text hidden', async() => { + const el = await fixture(html``); + await expect(el).to.be.accessible(); + }); + + it('focused', async() => { + const el = await fixture(html``); + el.focus(); + const activeElem = getComposedActiveElement(); + expect(activeElem).to.equal(el.shadowRoot.querySelector('a')); + await expect(el).to.be.accessible(); + }); + + }); + + describe('constructor', () => { + it('should construct', () => { + runConstructor('d2l-navigation-link-icon'); + }); + }); + + }); + + describe('d2l-navigation-link-image', () => { + + describe('accessibility', () => { + it('should pass all aXe tests', async() => { + const el = await fixture(html``); + await expect(el).to.be.accessible(); + }); + }); + + describe('constructor', () => { + it('should construct', () => { + runConstructor('d2l-navigation-link-image'); + }); + }); + + }); + +}); diff --git a/test/link.visual-diff.html b/test/link.visual-diff.html index f683b12c..2565ec50 100644 --- a/test/link.visual-diff.html +++ b/test/link.visual-diff.html @@ -25,7 +25,7 @@
-
+
diff --git a/test/link.visual-diff.js b/test/link.visual-diff.js index e2a43892..25358bb6 100644 --- a/test/link.visual-diff.js +++ b/test/link.visual-diff.js @@ -28,18 +28,24 @@ describe('d2l-navigation-link', () => { { category: 'base', tests: ['normal', 'hover', 'focus'] }, { category: 'back', tests: ['normal', 'hover', 'focus'] }, { category: 'icon-text', tests: ['normal', 'hover', 'focus'] }, - { category: 'icon-text-hidden', tests: ['normal', 'hover', 'focus'] }, + { category: 'icon-text-hidden', rectSelector: 'icon-text-hidden-container', tests: ['normal', 'hover', 'focus'] }, { category: 'image', tests: ['normal', 'hover', 'focus'] } ].forEach((entry) => { describe(entry.category, () => { entry.tests.forEach((name) => { it(name, async function() { const selector = `#${entry.category}`; - - if (name === 'hover') await page.hover(selector); + const rectSelector = entry.rectSelector ? `#${entry.rectSelector}` : selector; + + if (name === 'hover') { + await page.hover(selector); + if (entry.category === 'icon-text-hidden') { + await new Promise(resolve => setTimeout(resolve, 350)); + } + } else if (name === 'focus') await page.$eval(selector, (elem) => forceFocusVisible(elem)); - const rect = await visualDiff.getRect(page, selector); + const rect = await visualDiff.getRect(page, rectSelector); await visualDiff.screenshotAndCompare(page, this.test.fullTitle(), { clip: rect }); }); }); diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-focus.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-focus.png new file mode 100644 index 00000000..d884493a Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-focus.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-hover.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-hover.png new file mode 100644 index 00000000..d884493a Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-hover.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-normal.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-normal.png new file mode 100644 index 00000000..d884493a Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-disabled-normal.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-focus.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-focus.png new file mode 100644 index 00000000..27a4d6ee Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-focus.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-focus.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-focus.png new file mode 100644 index 00000000..69db6fda Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-focus.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-hover.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-hover.png new file mode 100644 index 00000000..69db6fda Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-hover.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-normal.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-normal.png new file mode 100644 index 00000000..69db6fda Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-disabled-normal.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-focus.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-focus.png new file mode 100644 index 00000000..cc331126 Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-focus.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-hover.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-hover.png new file mode 100644 index 00000000..cc331126 Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-hover.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-normal.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-normal.png new file mode 100644 index 00000000..240b3b02 Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hidden-normal.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hover.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hover.png new file mode 100644 index 00000000..27a4d6ee Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-hover.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-normal.png b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-normal.png new file mode 100644 index 00000000..35216633 Binary files /dev/null and b/test/screenshots/ci/golden/d2l-navigation-button/d2l-navigation-button-icon-text-normal.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-focus.png b/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-focus.png index 58e40b13..f5562222 100644 Binary files a/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-focus.png and b/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-focus.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-hover.png b/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-hover.png index 58e40b13..f5562222 100644 Binary files a/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-hover.png and b/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-hover.png differ diff --git a/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-normal.png b/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-normal.png index f313f0e8..797d89ee 100644 Binary files a/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-normal.png and b/test/screenshots/ci/golden/d2l-navigation-link/d2l-navigation-link-icon-text-hidden-normal.png differ