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

feat: [checkbox] implements open wc form control #6645

Closed
wants to merge 5 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "add open-wc form-control to fast-foundation",
"packageName": "@microsoft/fast-foundation",
"email": "jes@microsoft.com",
"dependentChangeType": "patch"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"dependentChangeType": "patch"
"dependentChangeType": "prerelease"

}
54 changes: 46 additions & 8 deletions packages/web-components/fast-foundation/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@

import { CaptureType } from '@microsoft/fast-element';
import { Constructable } from '@microsoft/fast-element';
import { Constructor } from '@open-wc/form-control';
import { CSSDirective } from '@microsoft/fast-element';
import { Direction } from '@microsoft/fast-web-utilities';
import type { ElementsFilter } from '@microsoft/fast-element';
import { ElementStyles } from '@microsoft/fast-element';
import { ElementViewTemplate } from '@microsoft/fast-element';
import { FASTElement } from '@microsoft/fast-element';
import { FASTElementDefinition } from '@microsoft/fast-element';
import { FormControlInterface } from '@open-wc/form-control';
import { HostBehavior } from '@microsoft/fast-element';
import { HostController } from '@microsoft/fast-element';
import { HTMLDirective } from '@microsoft/fast-element';
import { Orientation } from '@microsoft/fast-web-utilities';
import { PartialFASTElementDefinition } from '@microsoft/fast-element';
import type { SyntheticViewTemplate } from '@microsoft/fast-element';
import { Validator } from '@open-wc/form-control';
import { ViewTemplate } from '@microsoft/fast-element';

// @public
Expand Down Expand Up @@ -911,20 +915,54 @@ export interface FASTCalendar extends StartEnd {
export class FASTCard extends FASTElement {
}

// Warning: (ae-forgotten-export) The symbol "FormAssociatedCheckbox" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "FASTCheckbox_base" needs to be exported by the entry point index.d.ts
//
// @public
export class FASTCheckbox extends FormAssociatedCheckbox {
constructor();
// @internal (undocumented)
clickHandler: (e: MouseEvent) => void;
export class FASTCheckbox extends FASTCheckbox_base {
// (undocumented)
ariaChecked: string | null;
// (undocumented)
checked: boolean;
checkedAttribute: boolean;
// @internal
checkedAttributeChanged(prev: boolean | undefined, next: boolean): void;
// (undocumented)
checkedChanged(prev: boolean | undefined, next: boolean): void;
// @internal
clickHandler(e: Event): void;
currentChecked: boolean;
// (undocumented)
currentCheckedChanged(prev: boolean | undefined, next: boolean): void;
// (undocumented)
defaultChecked: boolean;
// @internal
defaultCheckedChanged(prev: boolean | undefined, next: boolean): void;
// @internal (undocumented)
defaultSlottedNodes: Node[];
indeterminate: boolean;
protected dirtyChecked: boolean;
// @internal
dirtyValue: boolean;
disabled: boolean;
// (undocumented)
static formControlValidators: Validator[];
indeterminate: boolean | undefined;
// @internal
indeterminateChanged(prev: boolean | undefined, next: boolean | undefined): void;
initialValue: string;
// @internal (undocumented)
keypressHandler: (e: KeyboardEvent) => void;
initialValueChanged(previous: string, next: string): void;
// @internal
keypressHandler(e: KeyboardEvent): void;
name: string;
required: boolean;
// (undocumented)
requiredChanged(): void;
// (undocumented)
resetFormControl(): void;
// (undocumented)
shouldFormValueUpdate(): boolean;
value: string;
// (undocumented)
valueChanged(previous: string, next: string): void;
}

// Warning: (ae-different-release-tags) This symbol has another declaration with a different release tag
Expand Down
2 changes: 2 additions & 0 deletions packages/web-components/fast-foundation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@storybook/html": "6.5.10",
"@storybook/manager-webpack5": "6.5.10",
"concurrently": "^7.3.0",
"element-internals-polyfill": "^1.1.17",
"esm": "^3.2.25",
"express": "^4.18.1",
"expect": "29.2.1",
Expand All @@ -97,6 +98,7 @@
"dependencies": {
"@floating-ui/dom": "^1.0.3",
"@microsoft/fast-element": "2.0.0-beta.23",
"@open-wc/form-control": "^0.7.0",
"@microsoft/fast-web-utilities": "^6.0.0",
"tabbable": "^5.2.0",
"tslib": "^2.4.0"
Expand Down
65 changes: 55 additions & 10 deletions packages/web-components/fast-foundation/src/checkbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,67 @@ export const myCheckbox = Checkbox.compose<CheckboxOptions>({

#### Superclass

| Name | Module | Package |
| ------------------------ | ----------------------------------------- | ------- |
| `FormAssociatedCheckbox` | /src/checkbox/checkbox.form-associated.js | |
| Name | Module | Package |
| ------------- | ------ | ----------------------- |
| `FASTElement` | | @microsoft/fast-element |

#### Mixins

| Name | Module | Package |
| ------------------ | ------ | --------------------- |
| `FormControlMixin` | | @open-wc/form-control |

#### Static Fields

| Name | Privacy | Type | Default | Description | Inherited From |
| ----------------------- | ------- | ------- | --------------------- | ----------- | -------------- |
| `formControlValidators` | | `array` | `[requiredValidator]` | | |

#### Fields

| Name | Privacy | Type | Default | Description | Inherited From |
| --------------- | ------- | --------- | ------- | -------------------------------------- | ---------------------- |
| `indeterminate` | public | `boolean` | `false` | The indeterminate state of the control | |
| `proxy` | | | | | FormAssociatedCheckbox |
| Name | Privacy | Type | Default | Description | Inherited From |
| ------------------ | --------- | ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `ariaChecked` | public | `string or null` | | | |
| `checked` | public | `boolean` | `false` | | |
| `checkedAttribute` | public | `boolean` | `false` | The initial state of the checkbox. | |
| `currentChecked` | public | `boolean` | | The current checkedness of the element. This property serves as a mechanism to set the \`checked\` property through both property assignment and the .setAttribute() method. This is useful for setting the field's checkedness in UI libraries that bind data through the .setAttribute() API and don't support IDL attribute binding. | |
| `defaultChecked` | public | `boolean` | | | |
| `dirtyChecked` | protected | `boolean` | `false` | Tracks whether the "checked" property has been changed. This is necessary to provide consistent behavior with normal input checkboxes | |
| `disabled` | public | `boolean` | `false` | Sets the element's disabled state. A disabled element will not be included during form submission. | |
| `indeterminate` | public | `boolean or undefined` | | The indeterminate state of the control | |
| `initialValue` | public | `string` | `"on"` | The initial value of the form. This value sets the \`value\` property only when the \`value\` property has not been explicitly set. | |
| `name` | public | `string` | | The name of the element. This element's value will be surfaced during form submission under the provided name. | |
| `required` | public | `boolean` | `false` | The required state of the element. If true, the element will be required to complete the form. | |
| `value` | public | `string` | | The value of the element. This element's value will be surfaced during form submission under the provided name. | |

#### Methods

| Name | Privacy | Description | Parameters | Return | Inherited From |
| ----------------------- | ------- | --------------------------------------------------- | ------------------------------------------- | --------- | -------------- |
| `checkedChanged` | public | | `prev: boolean or undefined, next: boolean` | `void` | |
| `currentCheckedChanged` | public | | `prev: boolean or undefined, next: boolean` | | |
| `initialValueChanged` | public | Invoked when the \`initialValue\` property changes. | `previous: string, next: string` | `void` | |
| `requiredChanged` | public | | | `void` | |
| `valueChanged` | public | | `previous: string, next: string` | `void` | |
| `resetFormControl` | | | | `void` | |
| `shouldFormValueUpdate` | | | | `boolean` | |

#### Events

| Name | Type | Description | Inherited From |
| -------- | ---- | ---------------------------------------------------------- | -------------- |
| `change` | | Emits a custom change event when the checked state changes | |
| Name | Type | Description | Inherited From |
| -------- | ---- | --------------------------------------------------------------------------------------------------------------- | -------------- |
| `change` | | Emits a custom change event when the checked state changes {@inheritDoc @open-wc/form-control#FormControlMixin} | |

#### Attributes

| Name | Field | Inherited From |
| ----------------- | ---------------- | -------------- |
| `checked` | checkedAttribute | |
| `current-checked` | currentChecked | |
| | disabled | |
| `value` | initialValue | |
| `name` | name | |
| | required | |

#### CSS Parts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ test.describe("Checkbox", () => {
);

// Playwright doesn't yet see our components as input elements
await expect(element).toHaveJSProperty("dirtyValue", false);
await expect(element).toHaveJSProperty("value", initialValue);
});

Expand Down Expand Up @@ -363,7 +364,7 @@ test.describe("Checkbox", () => {
await expect(element).toHaveJSProperty("checked", false);
});

test("should set its checked property to true if the checked attribute is set", async () => {
test("should set its `checked` and `defaultChecked` property to true if the checked attribute is set", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<form>
Expand All @@ -373,18 +374,21 @@ test.describe("Checkbox", () => {
});

await expect(element).toHaveJSProperty("checked", false);
await expect(element).toHaveJSProperty("defaultChecked", false);

await element.evaluate((node: FASTCheckbox) => {
node.setAttribute("checked", "");
});

await expect(element).toHaveJSProperty("checked", true);
await expect(element).toHaveJSProperty("defaultChecked", true);

await form.evaluate((node: HTMLFormElement) => {
node.reset();
});

await expect(element).toHaveJSProperty("checked", true);
await expect(element).toHaveJSProperty("defaultChecked", true);
});

test("should put the control into a clean state, where checked attribute modifications change the checked property prior to user or programmatic interaction", async () => {
Expand Down Expand Up @@ -415,4 +419,81 @@ test.describe("Checkbox", () => {

expect(await element.evaluate((node: FASTCheckbox) => node.value)).toBeTruthy();
});

test("should communicate if `checked` to the parent form", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<form>
<fast-checkbox checked name="test"></fast-checkbox>
</form>
`;
});

const form = page.locator("form");

expect(
await form.evaluate((node: HTMLFormElement) => {
const formData = new FormData(node);

return formData.get("test");
})
).toBe("on");
});

test("should not communicate when `disabled` and `checked` to the parent form", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<form>
<fast-checkbox checked disabled name="test"></fast-checkbox>
</form>
`;
});

const form = page.locator("form");

expect(
await form.evaluate((node: HTMLFormElement) => {
const formData = new FormData(node);

return formData.get("test");
})
).toBe(null);
});

test("should align the `checked` property and `current-checked` and `aria-checked` attribute changes", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-checkbox></fast-checkbox>
`;
});

const element = page.locator("fast-checkbox");

await expect(element).toHaveJSProperty("checked", false);

await expect(element).toHaveAttribute("current-checked", "false");

await expect(element).toHaveAttribute("aria-checked", "false");

await element.evaluate((node: FASTCheckbox) => {
node.setAttribute("current-checked", "true");
});

await expect(element).toHaveJSProperty("checked", true);

await expect(element).toHaveAttribute("current-checked", "true");

await expect(element).toHaveAttribute("aria-checked", "true");

await element.evaluate((node: FASTCheckbox) => {
node.setAttribute("current-checked", "false");
});

await expect(element).toHaveJSProperty("checked", false);

await expect(element).toHaveAttribute("current-checked", "false");

await expect(element).toHaveAttribute("aria-checked", "false");
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ export function checkboxTemplate<T extends FASTCheckbox>(
return html<T>`
<template
role="checkbox"
aria-checked="${x => (x.indeterminate ? "mixed" : x.checked)}"
aria-required="${x => x.required}"
aria-checked="${x => x.ariaChecked}"
aria-disabled="${x => x.disabled}"
aria-required="${x => x.required}"
tabindex="${x => (x.disabled ? null : 0)}"
@keypress="${(x, c) => x.keypressHandler(c.event as KeyboardEvent)}"
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
@click="${(x, c) => x.clickHandler(c.event)}"
>
<div part="control" class="control">
<slot name="checked-indicator">
Expand Down
Loading