Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(checkbox): no longer depend on foundation (VIV-2020) #1963

Merged
merged 2 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions libs/components/src/lib/checkbox/checkbox.form-associated.ts
Original file line number Diff line number Diff line change
@@ -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');
}
98 changes: 92 additions & 6 deletions libs/components/src/lib/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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 () {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
});
});

Expand Down Expand Up @@ -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';
Expand Down
55 changes: 47 additions & 8 deletions libs/components/src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
Loading