From 32fcfaf36e8b7383defe891dcb51664f3069a725 Mon Sep 17 00:00:00 2001 From: m-akinc <7282195+m-akinc@users.noreply.github.com> Date: Tue, 21 May 2024 17:39:18 -0500 Subject: [PATCH 1/6] Support tabindex overriding for Button, MenuButton, ToggleButton, Checkbox, and Anchor (#2110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## ๐Ÿคจ Rationale Fixes #2094 ## ๐Ÿ‘ฉโ€๐Ÿ’ป Implementation For each of `Button`, `MenuButton`, `ToggleButton`, 'Checkbox', and `Anchor`, added `tabindex` attribute, updated the template to forward the `tabindex` value, and wrote automated tests. Additionally for `Button` and `Checkbox`, forked template from FAST and copied over the FAST tests. ## ๐Ÿงช Testing Ran tests. Manually tested in Storybook in Chrome and Firefox. ## โœ… Checklist - [x] I have updated the project documentation to reflect my changes or determined no changes are needed. --------- Co-authored-by: Milan Raj --- ...-573fc816-eaa7-4e07-b0ef-8bfd5ccff19e.json | 7 + packages/eslint-config-nimble/typescript.js | 9 + packages/nimble-components/CONTRIBUTING.md | 29 + .../nimble-components/src/anchor/index.ts | 10 +- .../nimble-components/src/anchor/template.ts | 1 + .../src/anchor/tests/anchor.spec.ts | 19 + .../nimble-components/src/button/index.ts | 12 +- .../nimble-components/src/button/template.ts | 59 ++ .../button/tests/button.foundation.spec.ts | 669 ++++++++++++++++++ .../src/button/tests/button.spec.ts | 45 ++ .../nimble-components/src/checkbox/index.ts | 23 +- .../src/checkbox/template.ts | 40 ++ .../tests/checkbox.foundation.spec.ts | 449 ++++++++++++ .../src/checkbox/tests/checkbox.spec.ts | 32 + .../src/menu-button/index.ts | 9 +- .../src/menu-button/template.ts | 1 + .../src/menu-button/tests/menu-button.spec.ts | 21 + .../src/toggle-button/index.ts | 18 +- .../src/toggle-button/template.ts | 28 +- .../toggle-button/tests/toggle-button.spec.ts | 20 +- 20 files changed, 1481 insertions(+), 20 deletions(-) create mode 100644 change/@ni-nimble-components-573fc816-eaa7-4e07-b0ef-8bfd5ccff19e.json create mode 100644 packages/nimble-components/src/button/template.ts create mode 100644 packages/nimble-components/src/button/tests/button.foundation.spec.ts create mode 100644 packages/nimble-components/src/checkbox/template.ts create mode 100644 packages/nimble-components/src/checkbox/tests/checkbox.foundation.spec.ts diff --git a/change/@ni-nimble-components-573fc816-eaa7-4e07-b0ef-8bfd5ccff19e.json b/change/@ni-nimble-components-573fc816-eaa7-4e07-b0ef-8bfd5ccff19e.json new file mode 100644 index 0000000000..7aae738c80 --- /dev/null +++ b/change/@ni-nimble-components-573fc816-eaa7-4e07-b0ef-8bfd5ccff19e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Support tabindex overriding for Button, MenuButton, ToggleButton, Checkbox, and Anchor", + "packageName": "@ni/nimble-components", + "email": "7282195+m-akinc@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/eslint-config-nimble/typescript.js b/packages/eslint-config-nimble/typescript.js index d02cc0cbe8..ec7876e9e8 100644 --- a/packages/eslint-config-nimble/typescript.js +++ b/packages/eslint-config-nimble/typescript.js @@ -117,6 +117,15 @@ module.exports = { ] } }, + { + files: ['*.foundation.spec.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { paths: restrictedImportsPaths() } + ] + } + }, { files: ['styles.ts'], rules: { diff --git a/packages/nimble-components/CONTRIBUTING.md b/packages/nimble-components/CONTRIBUTING.md index 707ac24b5d..7d3fe21b1d 100644 --- a/packages/nimble-components/CONTRIBUTING.md +++ b/packages/nimble-components/CONTRIBUTING.md @@ -375,6 +375,35 @@ const nimbleButton = Button.compose({ }); ``` +If delegating focus, you must forward the `tabindex` attribute to any focusable elements in the shadow DOM. While some browsers (e.g. Chrome) will work properly without forwarding, others (e.g. Firefox) won't. Override the `tabIndex` property and mark it as an attribute: + +```ts +export class MyComponent { + ... + @attr({ attribute: 'tabindex', converter: nullableNumberConverter }) + public override tabIndex!: number; +} +``` + +Then in the template, bind the focusable elements' `tabindex` to the host component's property: + + +```html +html` + + + // or for an element that isn't focusable by default: +
+
`; +``` + ### Leverage mixins for shared APIs across components TypeScript and the FAST library each offer patterns and/or mechanisms to alter the APIs for a component via a mixin. diff --git a/packages/nimble-components/src/anchor/index.ts b/packages/nimble-components/src/anchor/index.ts index 8c9da580bb..3d59c01542 100644 --- a/packages/nimble-components/src/anchor/index.ts +++ b/packages/nimble-components/src/anchor/index.ts @@ -1,4 +1,4 @@ -import { attr } from '@microsoft/fast-element'; +import { attr, nullableNumberConverter } from '@microsoft/fast-element'; import { DesignSystem, Anchor as FoundationAnchor, @@ -35,6 +35,14 @@ export class Anchor extends AnchorBase { @attr public appearance: AnchorAppearance; + /** + * @public + * @remarks + * HTML Attribute: tabindex + */ + @attr({ attribute: 'tabindex', converter: nullableNumberConverter }) + public override tabIndex!: number; + /** * @public * @remarks diff --git a/packages/nimble-components/src/anchor/template.ts b/packages/nimble-components/src/anchor/template.ts index 13cd273f22..2da977f94c 100644 --- a/packages/nimble-components/src/anchor/template.ts +++ b/packages/nimble-components/src/anchor/template.ts @@ -27,6 +27,7 @@ AnchorOptions rel="${x => x.rel}" target="${x => x.target}" type="${x => x.type}" + tabindex="${x => x.tabIndex}" aria-atomic="${x => x.ariaAtomic}" aria-busy="${x => x.ariaBusy}" aria-controls="${x => x.ariaControls}" diff --git a/packages/nimble-components/src/anchor/tests/anchor.spec.ts b/packages/nimble-components/src/anchor/tests/anchor.spec.ts index 8de3d11811..193380572e 100644 --- a/packages/nimble-components/src/anchor/tests/anchor.spec.ts +++ b/packages/nimble-components/src/anchor/tests/anchor.spec.ts @@ -80,6 +80,25 @@ describe('Anchor', () => { expect(element.control!.getAttribute(name)).toBe('foo'); }); }); + + it('for attribute tabindex', async () => { + await connect(); + + element.setAttribute('tabindex', '-1'); + await waitForUpdatesAsync(); + + expect(element.control!.getAttribute('tabindex')).toBe('-1'); + }); + }); + + it('should clear tabindex attribute from the internal control when cleared from the host', async () => { + element.setAttribute('tabindex', '-1'); + await connect(); + + element.removeAttribute('tabindex'); + await waitForUpdatesAsync(); + + expect(element.control!.hasAttribute('tabindex')).toBeFalse(); }); describe('contenteditable behavior', () => { diff --git a/packages/nimble-components/src/button/index.ts b/packages/nimble-components/src/button/index.ts index ad5c7f467e..8b702638f8 100644 --- a/packages/nimble-components/src/button/index.ts +++ b/packages/nimble-components/src/button/index.ts @@ -1,8 +1,7 @@ -import { attr } from '@microsoft/fast-element'; +import { attr, nullableNumberConverter } from '@microsoft/fast-element'; import { Button as FoundationButton, ButtonOptions, - buttonTemplate as template, DesignSystem } from '@microsoft/fast-foundation'; import type { @@ -10,6 +9,7 @@ import type { ButtonAppearanceVariantPattern } from '../patterns/button/types'; import { styles } from './styles'; +import { template } from './template'; import { ButtonAppearance, ButtonAppearanceVariant } from './types'; declare global { @@ -47,6 +47,14 @@ export class Button */ @attr({ attribute: 'content-hidden', mode: 'boolean' }) public contentHidden = false; + + /** + * @public + * @remarks + * HTML Attribute: tabindex + */ + @attr({ attribute: 'tabindex', converter: nullableNumberConverter }) + public override tabIndex!: number; } /** diff --git a/packages/nimble-components/src/button/template.ts b/packages/nimble-components/src/button/template.ts new file mode 100644 index 0000000000..fabe485441 --- /dev/null +++ b/packages/nimble-components/src/button/template.ts @@ -0,0 +1,59 @@ +import { html, ref, slotted } from '@microsoft/fast-element'; +import type { ViewTemplate } from '@microsoft/fast-element'; +import { + ButtonOptions, + endSlotTemplate, + FoundationElementTemplate, + startSlotTemplate +} from '@microsoft/fast-foundation'; +import type { Button } from '.'; + +export const template: FoundationElementTemplate< +ViewTemplate +`; diff --git a/packages/nimble-components/src/button/tests/button.foundation.spec.ts b/packages/nimble-components/src/button/tests/button.foundation.spec.ts new file mode 100644 index 0000000000..77ac6c4c40 --- /dev/null +++ b/packages/nimble-components/src/button/tests/button.foundation.spec.ts @@ -0,0 +1,669 @@ +/** + * Based on tests in FAST repo: https://github.com/microsoft/fast/blob/9c6dbb66615e6d229fc0ebf8065a67f109139f26/packages/web-components/fast-foundation/src/button/button.spec.ts + */ +import { DOM } from '@microsoft/fast-element'; +import { eventClick } from '@microsoft/fast-web-utilities'; +import { fixture } from '../../utilities/tests/fixture'; +import { Button } from '..'; +import { template } from '../template'; + +const button = Button.compose({ + baseName: 'button', + template +}); + +async function setup(): Promise<{ + element: Button, + connect: () => Promise, + disconnect: () => Promise, + parent: HTMLElement +}> { + const { connect, disconnect, element, parent } = await fixture(button()); + + return { connect, disconnect, element, parent }; +} + +describe('Button', () => { + it('should set the `autofocus` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + + element.autofocus = true; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.hasAttribute('autofocus') + ).toBeTrue(); + + await disconnect(); + }); + + it('should set the `disabled` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + + element.disabled = true; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.hasAttribute('disabled') + ).toBeTrue(); + + await disconnect(); + }); + + it('should set the `form` attribute on the internal button when `formId` is provided', async () => { + const { element, connect, disconnect } = await setup(); + const formId = 'testId'; + + element.formId = 'testId'; + + await connect(); + + expect( + element.shadowRoot?.querySelector('button')?.getAttribute('form') + ).toEqual(formId); + + await disconnect(); + }); + + it('should set the `formaction` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const formaction = 'test_action.asp'; + + element.formaction = formaction; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('formaction') + ).toEqual(formaction); + + await disconnect(); + }); + + it('should set the `formenctype` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const formenctype = 'text/plain'; + + element.formenctype = formenctype; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('formenctype') + ).toEqual(formenctype); + + await disconnect(); + }); + + it('should set the `formmethod` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const formmethod = 'post'; + + element.formmethod = formmethod; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('formmethod') + ).toEqual(formmethod); + + await disconnect(); + }); + + it('should set the `formnovalidate` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const formnovalidate = true; + + element.formnovalidate = formnovalidate; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('formnovalidate') + ).toEqual(formnovalidate.toString()); + + await disconnect(); + }); + + it('should set the `formtarget` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const formtarget = '_blank'; + + element.formtarget = formtarget; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('formtarget') + ).toEqual(formtarget); + + await disconnect(); + }); + + it('should set the `name` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const name = 'testName'; + + element.name = name; + + await connect(); + + expect( + element.shadowRoot?.querySelector('button')?.getAttribute('name') + ).toEqual(name); + + await disconnect(); + }); + + it('should set the `type` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const type = 'submit'; + + element.type = type; + + await connect(); + + expect( + element.shadowRoot?.querySelector('button')?.getAttribute('type') + ).toEqual(type); + + await disconnect(); + }); + + it('should set the `value` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const value = 'Reset'; + + element.value = value; + + await connect(); + + expect( + element.shadowRoot?.querySelector('button')?.getAttribute('value') + ).toEqual(value); + + await disconnect(); + }); + + describe('Delegates ARIA button', () => { + it('should set the `aria-atomic` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaAtomic = 'true'; + + element.ariaAtomic = ariaAtomic; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-atomic') + ).toEqual(ariaAtomic); + + await disconnect(); + }); + + it('should set the `aria-busy` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaBusy = 'false'; + + element.ariaBusy = ariaBusy; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-busy') + ).toEqual(ariaBusy); + + await disconnect(); + }); + + it('should set the `aria-controls` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaControls = 'testId'; + + element.ariaControls = ariaControls; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-controls') + ).toEqual(ariaControls); + + await disconnect(); + }); + + it('should set the `aria-current` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaCurrent = 'page'; + + element.ariaCurrent = ariaCurrent; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-current') + ).toEqual(ariaCurrent); + + await disconnect(); + }); + + it('should set the `aria-describedby` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaDescribedby = 'testId'; + + element.ariaDescribedby = ariaDescribedby; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-describedby') + ).toEqual(ariaDescribedby); + + await disconnect(); + }); + + it('should set the `aria-details` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaDetails = 'testId'; + + element.ariaDetails = ariaDetails; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-details') + ).toEqual(ariaDetails); + + await disconnect(); + }); + + it('should set the `aria-disabled` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaDisabled = 'true'; + + element.ariaDisabled = ariaDisabled; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-disabled') + ).toEqual(ariaDisabled); + + await disconnect(); + }); + + it('should set the `aria-errormessage` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaErrormessage = 'test'; + + element.ariaErrormessage = ariaErrormessage; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-errormessage') + ).toEqual(ariaErrormessage); + + await disconnect(); + }); + + it('should set the `aria-expanded` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaExpanded = 'true'; + + element.ariaExpanded = ariaExpanded; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-expanded') + ).toEqual(ariaExpanded); + + await disconnect(); + }); + + it('should set the `aria-flowto` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaFlowto = 'testId'; + + element.ariaFlowto = ariaFlowto; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-flowto') + ).toEqual(ariaFlowto); + + await disconnect(); + }); + + it('should set the `aria-haspopup` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaHaspopup = 'true'; + + element.ariaHaspopup = ariaHaspopup; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-haspopup') + ).toEqual(ariaHaspopup); + + await disconnect(); + }); + + it('should set the `aria-hidden` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaHidden = 'true'; + + element.ariaHidden = ariaHidden; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-hidden') + ).toEqual(ariaHidden); + + await disconnect(); + }); + + it('should set the `aria-invalid` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaInvalid = 'spelling'; + + element.ariaInvalid = ariaInvalid; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-invalid') + ).toEqual(ariaInvalid); + + await disconnect(); + }); + + it('should set the `aria-keyshortcuts` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaKeyshortcuts = 'F4'; + + element.ariaKeyshortcuts = ariaKeyshortcuts; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-keyshortcuts') + ).toEqual(ariaKeyshortcuts); + + await disconnect(); + }); + + it('should set the `aria-label` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaLabel = 'Foo label'; + + element.ariaLabel = ariaLabel; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-label') + ).toEqual(ariaLabel); + + await disconnect(); + }); + + it('should set the `aria-labelledby` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaLabelledby = 'testId'; + + element.ariaLabelledby = ariaLabelledby; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-labelledby') + ).toEqual(ariaLabelledby); + + await disconnect(); + }); + + it('should set the `aria-live` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaLive = 'polite'; + + element.ariaLive = ariaLive; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-live') + ).toEqual(ariaLive); + + await disconnect(); + }); + + it('should set the `aria-owns` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaOwns = 'testId'; + + element.ariaOwns = ariaOwns; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-owns') + ).toEqual(ariaOwns); + + await disconnect(); + }); + + it('should set the `aria-pressed` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaPressed = 'true'; + + element.ariaPressed = ariaPressed; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-pressed') + ).toEqual(ariaPressed); + + await disconnect(); + }); + + it('should set the `aria-relevant` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaRelevant = 'removals'; + + element.ariaRelevant = ariaRelevant; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-relevant') + ).toEqual(ariaRelevant); + + await disconnect(); + }); + + it('should set the `aria-roledescription` attribute on the internal button when provided', async () => { + const { element, connect, disconnect } = await setup(); + const ariaRoledescription = 'slide'; + + element.ariaRoledescription = ariaRoledescription; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('button') + ?.getAttribute('aria-roledescription') + ).toEqual(ariaRoledescription); + + await disconnect(); + }); + }); + + describe("of type 'submit'", () => { + it('should submit the parent form when clicked', async () => { + const { connect, disconnect, element, parent } = await setup(); + const form = document.createElement('form'); + element.setAttribute('type', 'submit'); + form.appendChild(element); + parent.appendChild(form); + + await connect(); + + const wasSumbitted = await new Promise(resolve => { + // Resolve as true when the event listener is handled + form.addEventListener('submit', (event: SubmitEvent) => { + event.preventDefault(); + expect(event.submitter).toEqual(element.proxy); + resolve(true); + }); + + element.click(); + + // Resolve false on the next update in case reset hasn't happened + DOM.queueUpdate(() => resolve(false)); + }); + + expect(wasSumbitted).toBeTrue(); + + await disconnect(); + }); + }); + + describe("of type 'reset'", () => { + it('should reset the parent form when clicked', async () => { + const { connect, disconnect, element, parent } = await setup(); + const form = document.createElement('form'); + element.setAttribute('type', 'reset'); + form.appendChild(element); + parent.appendChild(form); + + await connect(); + + const wasReset = await new Promise(resolve => { + // Resolve true when the event listener is handled + form.addEventListener('reset', () => resolve(true)); + + element.click(); + + // Resolve false on the next update in case reset hasn't happened + DOM.queueUpdate(() => resolve(false)); + }); + + expect(wasReset).toBeTrue(); + + await disconnect(); + }); + }); + + describe("of 'disabled'", () => { + it('should not propagate when clicked', async () => { + const { connect, disconnect, element, parent } = await setup(); + + element.disabled = true; + parent.appendChild(element); + + let wasClicked = false; + await connect(); + + parent.addEventListener(eventClick, () => { + wasClicked = true; + }); + + await DOM.nextUpdate(); + element.click(); + + expect(wasClicked).toEqual(false); + + await disconnect(); + }); + + it('should not propagate when spans within shadowRoot are clicked', async () => { + const { connect, disconnect, element, parent } = await setup(); + + element.disabled = true; + parent.appendChild(element); + + let wasClicked = false; + + await connect(); + + parent.addEventListener(eventClick, () => { + wasClicked = true; + }); + + await DOM.nextUpdate(); + + const elements = element.shadowRoot?.querySelectorAll('span'); + if (elements) { + const spans: HTMLSpanElement[] = Array.from(elements); + spans.forEach((span: HTMLSpanElement) => { + span.click(); + expect(wasClicked).toEqual(false); + }); + } + + await disconnect(); + }); + }); +}); diff --git a/packages/nimble-components/src/button/tests/button.spec.ts b/packages/nimble-components/src/button/tests/button.spec.ts index be7e1ac2a9..59b6e1a2e2 100644 --- a/packages/nimble-components/src/button/tests/button.spec.ts +++ b/packages/nimble-components/src/button/tests/button.spec.ts @@ -1,6 +1,13 @@ +import { html } from '@microsoft/fast-element'; import { Button, buttonTag } from '..'; +import { fixture, type Fixture } from '../../utilities/tests/fixture'; +import { waitForUpdatesAsync } from '../../testing/async-helpers'; describe('Button', () => { + async function setup(): Promise> { + return fixture