From c5dd0335eaf7893baa2985d7be5c6bbf3571d3bf Mon Sep 17 00:00:00 2001 From: Richard Helm <86777660+RichardHelm@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:47:45 +0000 Subject: [PATCH] chore(checkbox): no longer depend on foundation (VIV-2020) (#1963) Decouple checkbox from FAST foundation --- .../lib/checkbox/checkbox.form-associated.ts | 12 +++ .../src/lib/checkbox/checkbox.spec.ts | 98 +++++++++++++++++-- libs/components/src/lib/checkbox/checkbox.ts | 55 +++++++++-- 3 files changed, 151 insertions(+), 14 deletions(-) create mode 100644 libs/components/src/lib/checkbox/checkbox.form-associated.ts diff --git a/libs/components/src/lib/checkbox/checkbox.form-associated.ts b/libs/components/src/lib/checkbox/checkbox.form-associated.ts new file mode 100644 index 0000000000..b3a1171b8b --- /dev/null +++ b/libs/components/src/lib/checkbox/checkbox.form-associated.ts @@ -0,0 +1,12 @@ +import { + CheckableFormAssociated, + FoundationElement, +} from '@microsoft/fast-foundation'; + +class _Checkbox extends FoundationElement {} +// eslint-disable-next-line @typescript-eslint/naming-convention +interface _Checkbox extends CheckableFormAssociated {} + +export class FormAssociatedCheckbox extends CheckableFormAssociated(_Checkbox) { + proxy = document.createElement('input'); +} diff --git a/libs/components/src/lib/checkbox/checkbox.spec.ts b/libs/components/src/lib/checkbox/checkbox.spec.ts index 0372845679..d627bda786 100644 --- a/libs/components/src/lib/checkbox/checkbox.spec.ts +++ b/libs/components/src/lib/checkbox/checkbox.spec.ts @@ -73,6 +73,56 @@ describe('vwc-checkbox', () => { getBaseElement(element).classList.contains('checked') ).toBeTruthy(); }); + + it('should toggle checked when clicked', () => { + getBaseElement(element).click(); + + expect(element.checked).toBe(true); + + getBaseElement(element).click(); + + expect(element.checked).toBe(false); + }); + + it('should not toggle checked when clicked while disabled', () => { + element.disabled = true; + + getBaseElement(element).click(); + + expect(element.checked).toBe(false); + }); + + it('should not toggle checked when clicked while readOnly', () => { + element.readOnly = true; + + getBaseElement(element).click(); + + expect(element.checked).toBe(false); + }); + + it('should toggle checked when pressing Space', () => { + getBaseElement(element).dispatchEvent( + new KeyboardEvent('keypress', { key: ' ' }) + ); + + expect(element.checked).toBe(true); + + getBaseElement(element).dispatchEvent( + new KeyboardEvent('keypress', { key: ' ' }) + ); + + expect(element.checked).toBe(false); + }); + + it('should not toggle checked when pressing Space while readOnly', () => { + element.readOnly = true; + + getBaseElement(element).dispatchEvent( + new KeyboardEvent('keypress', { key: ' ' }) + ); + + expect(element.checked).toBe(false); + }); }); describe('disabled', function () { @@ -82,6 +132,17 @@ describe('vwc-checkbox', () => { await elementUpdated(element); expect(element.shadowRoot?.querySelector('.disabled')).toBeTruthy(); }); + + it('should set aria-disabled attribute when disabled is true', async () => { + expect(getBaseElement(element).getAttribute('aria-disabled')).toBe( + 'false' + ); + element.disabled = true; + await elementUpdated(element); + expect(getBaseElement(element).getAttribute('aria-disabled')).toBe( + 'true' + ); + }); }); describe('readonly', function () { @@ -93,6 +154,19 @@ describe('vwc-checkbox', () => { }); }); + describe('required', function () { + it('should set aria-required attribute when required is true', async () => { + expect(getBaseElement(element).getAttribute('aria-required')).toBe( + 'false' + ); + element.required = true; + await elementUpdated(element); + expect(getBaseElement(element).getAttribute('aria-required')).toBe( + 'true' + ); + }); + }); + describe('indeterminate', () => { it('should set checked class when indeterminate is true', async () => { element.indeterminate = true; @@ -236,12 +310,8 @@ describe('vwc-checkbox', () => { const submitPromise = listenToFormSubmission(formElement); formElement.requestSubmit(); - (await submitPromise).forEach( - (formDataValue: any, formDataKey: string) => { - expect(formDataKey).toEqual(fieldName); - expect(formDataValue).toEqual(checked); - } - ); + const submitResult = await submitPromise; + expect(submitResult.get(fieldName)).toBe(checked); }); }); @@ -327,6 +397,22 @@ describe('vwc-checkbox', () => { expect(baseElement?.getAttribute('role')).toBe('checkbox'); }); + it('should set aria-required attribute when required is true', async () => { + element.required = true; + await elementUpdated(element); + expect(getBaseElement(element).getAttribute('aria-required')).toBe( + 'true' + ); + }); + + it('should set aria-readonly attribute when readOnly is true', async () => { + element.readOnly = true; + await elementUpdated(element); + expect(getBaseElement(element).getAttribute('aria-readonly')).toBe( + 'true' + ); + }); + describe('aria-label', () => { beforeEach(async () => { element.ariaLabel = 'Label'; diff --git a/libs/components/src/lib/checkbox/checkbox.ts b/libs/components/src/lib/checkbox/checkbox.ts index c0528be9f0..43ef6ef1cf 100644 --- a/libs/components/src/lib/checkbox/checkbox.ts +++ b/libs/components/src/lib/checkbox/checkbox.ts @@ -1,4 +1,3 @@ -import { Checkbox as FoundationCheckbox } from '@microsoft/fast-foundation'; import { attr, observable } from '@microsoft/fast-element'; import type { Connotation } from '../enums.js'; import { @@ -10,6 +9,7 @@ import { FormElementSuccessText, } from '../../shared/patterns'; import { applyMixinsWithObservables } from '../../shared/utils/applyMixinsWithObservables'; +import { FormAssociatedCheckbox } from './checkbox.form-associated'; export const keySpace: ' ' = ' ' as const; @@ -35,7 +35,7 @@ export type AriaCheckedStates = 'false' | 'true' | 'mixed' | 'undefined'; */ @errorText @formElements -export class Checkbox extends FoundationCheckbox { +export class Checkbox extends FormAssociatedCheckbox { @attr({ attribute: 'aria-label' }) override ariaLabel: string | null = null; @attr({ attribute: 'tabindex' }) tabindex: string | null = null; @@ -58,6 +58,49 @@ export class Checkbox extends FoundationCheckbox { @attr({ attribute: 'aria-checked' }) override ariaChecked: AriaCheckedStates | null = null; + /** + * When true, the control will be immutable by user interaction. See {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly | readonly HTML attribute} for more information. + * @public + * @remarks + * HTML Attribute: readonly + */ + @attr({ attribute: 'readonly', mode: 'boolean' }) + readOnly!: boolean; // Map to proxy element + /** + * @internal + */ + readOnlyChanged() { + if (this.proxy instanceof HTMLInputElement) { + this.proxy.readOnly = this.readOnly; + } + } + + /** + * The element's value to be included in form submission when checked. + * Default to "on" to reach parity with input[type="checkbox"] + * + * @internal + */ + override initialValue = 'on'; + + /** + * @internal + */ + @observable + defaultSlottedNodes: Node[] = []; + + /** + * The indeterminate state of the control + */ + @observable + indeterminate = false; + + constructor() { + super(); + + this.proxy.setAttribute('type', 'checkbox'); + } + indeterminateChanged(_: boolean, next: boolean) { this.checked = !next; } @@ -83,11 +126,9 @@ export class Checkbox extends FoundationCheckbox { } /** - * !remove method as will be implemented by fast-foundation in version after 2.46.9 - * * @internal */ - override keypressHandler = (event: KeyboardEvent): boolean => { + keypressHandler = (event: KeyboardEvent): boolean => { if (event.target instanceof HTMLAnchorElement) { return true; } @@ -106,11 +147,9 @@ export class Checkbox extends FoundationCheckbox { }; /** - * !remove method as will be implemented by fast-foundation in version after 2.46.9 - * * @internal */ - override clickHandler = (event: Event): boolean => { + clickHandler = (event: Event): boolean => { if (event.target instanceof HTMLAnchorElement) { return true; }