From 6631f7d204f322cb90bf19b70a4a724932d087f5 Mon Sep 17 00:00:00 2001 From: Chris Holt <13071055+chrisdholt@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:23:11 -0700 Subject: [PATCH 1/6] Update Switch to leverage Checkbox and ElementInternals --- packages/web-components/docs/api-report.md | 83 ++------ .../web-components/src/checkbox/checkbox.ts | 44 ++-- .../src/switch/switch.stories.ts | 192 +++++++++++------- .../src/switch/switch.styles.ts | 92 +++------ .../src/switch/switch.template.ts | 22 +- packages/web-components/src/switch/switch.ts | 76 +------ 6 files changed, 190 insertions(+), 319 deletions(-) diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index 75f9a62de863b..d0590f9e3f05c 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -588,58 +588,13 @@ export const ButtonType: { // @public export type ButtonType = ValuesOf; -// @public -export class Checkbox extends FASTElement { - constructor(); - autofocus: boolean; - get checked(): boolean; - set checked(state: boolean); - checkValidity(): boolean; - // @internal - clickHandler(e: MouseEvent): boolean | void; - // (undocumented) - connectedCallback(): void; - disabled: boolean; - // @internal - elementInternals: ElementInternals; - get form(): HTMLFormElement | null; - static formAssociated: boolean; - formAttribute?: string; - // @internal - formResetCallback(): void; - indeterminate?: boolean; - // @internal - indeterminateChanged(prev: boolean, next: boolean): void; - initialChecked?: boolean; - // @internal - initialCheckedChanged(prev: boolean | undefined, next: boolean): void; - initialValue: string; - // @internal - initialValueChanged(prev: string, next: string): void; - // @internal - inputHandler(e: Event): boolean | void; - // @internal - keydownHandler(e: KeyboardEvent): boolean | void; - // @internal - keyupHandler(e: KeyboardEvent): boolean | void; - get labels(): ReadonlyArray; - name: string; - reportValidity(): boolean; - required: boolean; - // @internal - requiredChanged(prev: boolean, next: boolean): void; - setCustomValidity(message: string): void; - // @internal - setFormValue(value: File | string | FormData | null, state?: File | string | FormData | null): void; - // @internal - setValidity(flags?: Partial, message?: string, anchor?: HTMLElement): void; +// Warning: (ae-forgotten-export) The symbol "BaseCheckbox" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Checkbox" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class Checkbox extends BaseCheckbox { shape: CheckboxShape; size?: CheckboxSize; - get validationMessage(): string; - get validity(): ValidityState; - get value(): string; - set value(value: string); - get willValidate(): boolean; } // @public @@ -2931,24 +2886,10 @@ const styles: ElementStyles; export { styles as ButtonStyles } export { styles as MenuButtonStyles } -// Warning: (ae-forgotten-export) The symbol "FormAssociatedSwitch" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Switch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Switch extends FormAssociatedSwitch { - constructor(); - // @internal (undocumented) - clickHandler: (e: MouseEvent) => void; - // @internal (undocumented) - defaultSlottedNodes: Node[]; - // @internal - initialValue: string; - // @internal (undocumented) - keypressHandler: (e: KeyboardEvent) => void; - labelPosition: SwitchLabelPosition | undefined; - readOnly: boolean; - // (undocumented) - protected readOnlyChanged(): void; +export class Switch extends BaseCheckbox { } // @public @@ -3451,7 +3392,17 @@ export const typographyTitle3Styles: CSSDirective; // @public export const ValidationFlags: { - [key in keyof ValidityState]: key; + readonly badInput: "bad-input"; + readonly customError: "custom-error"; + readonly patternMismatch: "pattern-mismatch"; + readonly rangeOverflow: "range-overflow"; + readonly rangeUnderflow: "range-underflow"; + readonly stepMismatch: "step-mismatch"; + readonly tooLong: "too-long"; + readonly tooShort: "too-short"; + readonly typeMismatch: "type-mismatch"; + readonly valueMissing: "value-missing"; + readonly valid: "valid"; }; // @public (undocumented) diff --git a/packages/web-components/src/checkbox/checkbox.ts b/packages/web-components/src/checkbox/checkbox.ts index 78fb62b465fd1..91de2ababfe2f 100644 --- a/packages/web-components/src/checkbox/checkbox.ts +++ b/packages/web-components/src/checkbox/checkbox.ts @@ -13,7 +13,7 @@ import { CheckboxShape, CheckboxSize } from './checkbox.options.js'; * * @public */ -export class Checkbox extends FASTElement { +export class BaseCheckbox extends FASTElement { /** * Indicates that the element should get focus after the page finishes loading. * @see The {@link https://developer.mozilla.org/docs/Web/HTML/Element/input#autofocus | `autofocus`} attribute @@ -160,26 +160,6 @@ export class Checkbox extends FASTElement { } } - /** - * Indicates the shape of the checkbox. - * - * @public - * @remarks - * HTML Attribute: `shape` - */ - @attr - public shape!: CheckboxShape; - - /** - * Indicates the size of the checkbox. - * - * @public - * @remarks - * HTML Attribute: `size` - */ - @attr - public size?: CheckboxSize; - /** * The internal checked state of the control. * @@ -477,3 +457,25 @@ export class Checkbox extends FASTElement { this.checked = force; } } + +export class Checkbox extends BaseCheckbox { + /** + * Indicates the shape of the checkbox. + * + * @public + * @remarks + * HTML Attribute: `shape` + */ + @attr + public shape!: CheckboxShape; + + /** + * Indicates the size of the checkbox. + * + * @public + * @remarks + * HTML Attribute: `size` + */ + @attr + public size?: CheckboxSize; +} diff --git a/packages/web-components/src/switch/switch.stories.ts b/packages/web-components/src/switch/switch.stories.ts index 57c9e2a9fee1b..f52dfa1a736fe 100644 --- a/packages/web-components/src/switch/switch.stories.ts +++ b/packages/web-components/src/switch/switch.stories.ts @@ -1,106 +1,140 @@ -import { html } from '@microsoft/fast-element'; -import type { Args, Meta } from '@storybook/html'; -import { renderComponent } from '../helpers.stories.js'; +import { html, repeat } from '@microsoft/fast-element'; +import { uniqueId } from '@microsoft/fast-web-utilities'; +import { LabelPosition, ValidationFlags } from '../field/field.options.js'; +import { Meta, renderComponent, Story, StoryArgs } from '../helpers.stories.js'; import type { Switch as FluentSwitch } from './switch.js'; + import './define.js'; -import { SwitchLabelPosition } from './switch.options.js'; +import '../button/define.js'; +import '../field/define.js'; -type SwitchStoryArgs = Args & FluentSwitch; -type SwitchStoryMeta = Meta; +const storyTemplate = html>` + +`; -const storyTemplate = html` -
- x.checked} - ?disabled=${x => x.disabled} - ?required=${x => x.required} - label-position=${x => x.labelPosition} - value="${x => x.value}" - > - ${x => x.value} - -
+const messageTemplate = html` + + ${x => x.message} + +`; + +const fieldStoryTemplate = html>` + + + ${x => x.storyContent} ${repeat(x => x.messages, messageTemplate)} + `; export default { title: 'Components/Switch', args: { - checked: false, - disabled: false, - required: false, - labelPosition: 'after', + name: 'switch', }, argTypes: { - labelPosition: { - options: Object.values(SwitchLabelPosition), - control: { - type: 'select', - }, - table: { - type: { - summary: 'Sets the position of label', - }, - defaultValue: { - summary: 'after', - }, - }, - }, checked: { + description: 'Sets the checked state of the switch', control: 'boolean', - table: { - type: { - summary: 'Sets checked state', - }, - defaultValue: { - summary: 'false', - }, - }, }, disabled: { + description: 'Sets the disabled state of the switch', control: 'boolean', - table: { - type: { - summary: 'Sets disabled state', - }, - defaultValue: { - summary: 'false', - }, - }, }, required: { + description: 'Sets the switch as required', control: 'boolean', - table: { - type: { - summary: 'Sets required state', - }, - defaultValue: { - summary: 'false', - }, - }, - }, - value: { - control: 'text', - defaultValue: 'This is a label', }, }, -} as SwitchStoryMeta; +} as Meta; -export const Switch = renderComponent(storyTemplate).bind({}); +export const Switch: Story = renderComponent(storyTemplate).bind({}); +Switch.args = { + id: uniqueId('switch-'), +}; -// -// Attribute stories -// +export const Checked: Story = renderComponent(storyTemplate).bind({}); +Checked.args = { + storyContent: storyTemplate, + slot: 'input', + labelPosition: LabelPosition.after, + id: uniqueId('switch-'), + checked: true, + label: 'Checked (default)', +}; -export const Checked = renderComponent(html`Checked`); +export const Disabled: Story = renderComponent(html>` + ${repeat(x => x.storyContent, html>`${fieldStoryTemplate}
`)} +`).bind({}); +Disabled.args = { + storyContent: [ + { + storyContent: storyTemplate, + id: uniqueId('switch-'), + disabled: true, + label: 'Disabled unchecked', + labelPosition: LabelPosition.after, + slot: 'input', + }, + { + storyContent: storyTemplate, + checked: true, + disabled: true, + id: uniqueId('switch-'), + label: 'Disabled checked', + labelPosition: LabelPosition.after, + slot: 'input', + }, + ], +}; -export const Disabled = renderComponent(html`Disabled`); +export const Required: Story = renderComponent(html>` +
+
+ + +
+ ${fieldStoryTemplate} + Submit +
+`).bind({}); +Required.args = { + storyContent: storyTemplate, + slot: 'input', + labelPosition: LabelPosition.after, + id: uniqueId('switch-'), + required: true, + label: 'Required', + messages: [{ message: 'This field is required', flag: ValidationFlags.valueMissing }], +}; -export const Required = renderComponent(html`Required`); +export const LabelBefore: Story = renderComponent(fieldStoryTemplate).bind({}); +LabelBefore.args = { + storyContent: storyTemplate, + id: uniqueId('switch-'), + labelPosition: LabelPosition.before, + label: 'Label before', + slot: 'input', +}; -export const LabelPosition = renderComponent(html` -
- before - above - after -
-`); +export const LabelWrapping: Story = renderComponent(fieldStoryTemplate).bind({}); +LabelWrapping.args = { + storyContent: storyTemplate, + id: uniqueId('switch-'), + labelPosition: LabelPosition.after, + label: 'Here is an example of a switch with a long label and it starts to wrap to a second line', + slot: 'input', +}; +LabelWrapping.decorators = [ + story => { + const storyElement = story() as HTMLElement; + storyElement.style.width = '400px'; + return storyElement; + }, +]; diff --git a/packages/web-components/src/switch/switch.styles.ts b/packages/web-components/src/switch/switch.styles.ts index 4677c871802a1..e1006c990b2a8 100644 --- a/packages/web-components/src/switch/switch.styles.ts +++ b/packages/web-components/src/switch/switch.styles.ts @@ -6,7 +6,6 @@ import { colorCompoundBrandBackgroundHover, colorCompoundBrandBackgroundPressed, colorNeutralBackgroundDisabled, - colorNeutralForeground1, colorNeutralForeground3, colorNeutralForeground3Hover, colorNeutralForeground3Pressed, @@ -23,94 +22,61 @@ import { colorTransparentStroke, curveEasyEase, durationNormal, - fontFamilyBase, - fontSizeBase300, - fontWeightRegular, - lineHeightBase300, shadow4, - spacingHorizontalS, - spacingHorizontalXS, spacingHorizontalXXS, - spacingVerticalS, - spacingVerticalXS, strokeWidthThick, } from '../theme/design-tokens.js'; +/** + * Selector for the `checked` state. + * @public + */ +const checkedState = css.partial`:is([state--checked], :state(checked))`; + export const styles = css` ${display('inline-flex')} :host { + box-sizing: border-box; align-items: center; - flex-direction: row-reverse; + flex-direction: row; outline: none; user-select: none; contain: content; - } - :host([label-position='before']) { - flex-direction: row; - } - :host([label-position='above']) { - flex-direction: column; - align-items: flex-start; - } - :host([disabled]) .label, - :host([readonly]) .label, - :host([readonly]) .switch, - :host([disabled]) .switch { - cursor: not-allowed; - } - .label { - position: relative; - color: ${colorNeutralForeground1}; - line-height: ${lineHeightBase300}; - font-size: ${fontSizeBase300}; - font-weight: ${fontWeightRegular}; - font-family: ${fontFamilyBase}; - padding: ${spacingVerticalXS} ${spacingHorizontalXS}; - cursor: pointer; - } - .label__hidden { - display: none; - } - .switch { - display: flex; - align-items: center; padding: 0 ${spacingHorizontalXXS}; - box-sizing: border-box; width: 40px; height: 20px; background-color: ${colorTransparentBackground}; border: 1px solid ${colorNeutralStrokeAccessible}; border-radius: ${borderRadiusCircular}; - outline: none; cursor: pointer; - margin: ${spacingVerticalS} ${spacingHorizontalS}; } - :host(:hover) .switch { + + :host(:hover) { background: none; border-color: ${colorNeutralStrokeAccessibleHover}; } - :host(:active) .switch { + :host(:active) { border-color: ${colorNeutralStrokeAccessiblePressed}; } - :host([disabled]) .switch, - :host([readonly]) .switch { + :host(:disabled), + :host([readonly]) { border: 1px solid ${colorNeutralStrokeDisabled}; background-color: none; pointer: default; } - :host([aria-checked='true']) .switch { + :host(${checkedState}) { background: ${colorCompoundBrandBackground}; } - :host([aria-checked='true']:hover) .switch { + :host(${checkedState}:hover) { background: ${colorCompoundBrandBackgroundHover}; border-color: ${colorCompoundBrandBackgroundHover}; } - :host([aria-checked='true']:active) .switch { + :host(${checkedState}:active) { background: ${colorCompoundBrandBackgroundPressed}; border-color: ${colorCompoundBrandBackgroundPressed}; } - :host([aria-checked='true'][disabled]) .switch { + :host(${checkedState}:disabled) { background: ${colorNeutralBackgroundDisabled}; border-color: ${colorNeutralStrokeDisabled}; } @@ -124,14 +90,14 @@ export const styles = css` transition-timing-function: ${curveEasyEase}; transition-property: margin-inline-start; } - :host([aria-checked='true']) .checked-indicator { + :host(${checkedState}) .checked-indicator { background-color: ${colorNeutralForegroundInverted}; margin-inline-start: calc(100% - 14px); } - :host([aria-checked='true']:hover) .checked-indicator { + :host(${checkedState}:hover) .checked-indicator { background: ${colorNeutralForegroundInvertedHover}; } - :host([aria-checked='true']:active) .checked-indicator { + :host(${checkedState}:active) .checked-indicator { background: ${colorNeutralForegroundInvertedPressed}; } :host(:hover) .checked-indicator { @@ -140,11 +106,11 @@ export const styles = css` :host(:active) .checked-indicator { background-color: ${colorNeutralForeground3Pressed}; } - :host([disabled]) .checked-indicator, + :host(:disabled) .checked-indicator, :host([readonly]) .checked-indicator { background: ${colorNeutralForegroundDisabled}; } - :host([aria-checked='true'][disabled]) .checked-indicator { + :host(${checkedState}:disabled) .checked-indicator { background: ${colorNeutralForegroundDisabled}; } @@ -155,12 +121,12 @@ export const styles = css` } `.withBehaviors( forcedColorsStylesheetBehavior(css` - .switch { + :host { border-color: InactiveBorder; } - :host([aria-checked='true']) .switch, - :host([aria-checked='true']:active) .switch, - :host([aria-checked='true']:hover) .switch { + :host(${checkedState}), + :host(${checkedState}:active), + :host(${checkedState}:hover) { background: Highlight; border-color: Highlight; } @@ -169,9 +135,9 @@ export const styles = css` :host(:active) .checked-indicator { background-color: ActiveCaption; } - :host([aria-checked='true']) .checked-indicator, - :host([aria-checked='true']:hover) .checked-indicator, - :host([aria-checked='true']:active) .checked-indicator { + :host(${checkedState}) .checked-indicator, + :host(${checkedState}:hover) .checked-indicator, + :host(${checkedState}:active) .checked-indicator { background-color: ButtonFace; } `), diff --git a/packages/web-components/src/switch/switch.template.ts b/packages/web-components/src/switch/switch.template.ts index 7dd2710e9a579..f804a29f6c527 100644 --- a/packages/web-components/src/switch/switch.template.ts +++ b/packages/web-components/src/switch/switch.template.ts @@ -1,27 +1,17 @@ -import { ElementViewTemplate, html, slotted } from '@microsoft/fast-element'; +import { ElementViewTemplate, html } from '@microsoft/fast-element'; import { staticallyCompose } from '../utils/index.js'; import { Switch, SwitchOptions } from './switch.js'; export function switchTemplate(options: SwitchOptions = {}): ElementViewTemplate { return html` `; } diff --git a/packages/web-components/src/switch/switch.ts b/packages/web-components/src/switch/switch.ts index 4a87be560bde7..b907cb3c8f9c0 100644 --- a/packages/web-components/src/switch/switch.ts +++ b/packages/web-components/src/switch/switch.ts @@ -1,80 +1,8 @@ -import { attr, observable } from '@microsoft/fast-element'; -import { keyEnter, keySpace } from '@microsoft/fast-web-utilities'; import type { StaticallyComposableHTML } from '../utils/index.js'; -import { SwitchLabelPosition } from './switch.options.js'; -import { FormAssociatedSwitch } from './switch.form-associated.js'; +import { BaseCheckbox } from '../checkbox/checkbox.js'; export type SwitchOptions = { switch?: StaticallyComposableHTML; }; -export class Switch extends FormAssociatedSwitch { - /** - * The label position of the switch - * - * @public - * @remarks - * HTML Attribute: labelposition - */ - @attr({ attribute: 'label-position' }) - public labelPosition: SwitchLabelPosition | undefined; - - /** - * 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' }) - public readOnly!: boolean; // Map to proxy element - protected readOnlyChanged(): void { - 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 - */ - public initialValue: string = 'on'; - - /** - * @internal - */ - @observable - public defaultSlottedNodes!: Node[]; - - public constructor() { - super(); - - this.proxy.setAttribute('type', 'checkbox'); - } - - /** - * @internal - */ - public keypressHandler = (e: KeyboardEvent) => { - if (this.readOnly) { - return; - } - - switch (e.key) { - case keyEnter: - case keySpace: - this.checked = !this.checked; - break; - } - }; - - /** - * @internal - */ - public clickHandler = (e: MouseEvent) => { - if (!this.disabled && !this.readOnly) { - this.checked = !this.checked; - } - }; -} +export class Switch extends BaseCheckbox {} From 22f2fcb4874441e1e5897bc2a6d342d534a6aff3 Mon Sep 17 00:00:00 2001 From: Chris Holt <13071055+chrisdholt@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:28:01 -0700 Subject: [PATCH 2/6] remove label from vr tests --- .../src/stories/switch/switch.stories.tsx | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/apps/vr-tests-web-components/src/stories/switch/switch.stories.tsx b/apps/vr-tests-web-components/src/stories/switch/switch.stories.tsx index a3605ba4bcdab..de1b215e203c0 100644 --- a/apps/vr-tests-web-components/src/stories/switch/switch.stories.tsx +++ b/apps/vr-tests-web-components/src/stories/switch/switch.stories.tsx @@ -59,24 +59,3 @@ export const DisabledChecked = () => `); export const DisabledCheckedDark = getStoryVariant(DisabledChecked, DARK_MODE); - -export const LabelBefore = () => - parse(` - Label before - `); - -export const LabelBeforeRTL = getStoryVariant(LabelBefore, RTL); - -export const LabelAbove = () => - parse(` - Label above - `); - -export const LabelAboveRTL = getStoryVariant(LabelAbove, RTL); - -export const LabelAfter = () => - parse(` - Label after - `); - -export const LabelAfterRTL = getStoryVariant(LabelAfter, RTL); From afad9549ee5901fa355b422ae43fe72e966a7c72 Mon Sep 17 00:00:00 2001 From: Chris Holt <13071055+chrisdholt@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:31:58 -0700 Subject: [PATCH 3/6] change files --- ...eb-components-08ae4a29-13c9-459b-8b03-c40e270288cc.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-web-components-08ae4a29-13c9-459b-8b03-c40e270288cc.json diff --git a/change/@fluentui-web-components-08ae4a29-13c9-459b-8b03-c40e270288cc.json b/change/@fluentui-web-components-08ae4a29-13c9-459b-8b03-c40e270288cc.json new file mode 100644 index 0000000000000..933f494ebd083 --- /dev/null +++ b/change/@fluentui-web-components-08ae4a29-13c9-459b-8b03-c40e270288cc.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: refactor Switch to use ElementInternals by extending BaseCheckbox", + "packageName": "@fluentui/web-components", + "email": "13071055+chrisdholt@users.noreply.github.com", + "dependentChangeType": "patch" +} From d8c80cb9c0a9eb21670691113fa758a291c7a392 Mon Sep 17 00:00:00 2001 From: Chris Holt <13071055+chrisdholt@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:38:17 -0700 Subject: [PATCH 4/6] fix focus-visible state --- packages/web-components/src/switch/switch.styles.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/web-components/src/switch/switch.styles.ts b/packages/web-components/src/switch/switch.styles.ts index e1006c990b2a8..68ee4c11b21c4 100644 --- a/packages/web-components/src/switch/switch.styles.ts +++ b/packages/web-components/src/switch/switch.styles.ts @@ -115,6 +115,10 @@ export const styles = css` } :host(:focus-visible) { + outline: none; + } + + :host(:not([slot='input']):focus-visible) { border-color: ${colorTransparentStroke}; outline: ${strokeWidthThick} solid ${colorTransparentStroke}; box-shadow: ${shadow4}, 0 0 0 2px ${colorStrokeFocus2}; From 786534fcad0fe83dc18df1e1edd4fc373599f89b Mon Sep 17 00:00:00 2001 From: Chris Holt <13071055+chrisdholt@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:44:47 -0700 Subject: [PATCH 5/6] test updates --- .../web-components/src/switch/switch.spec.ts | 548 +++++++----------- 1 file changed, 218 insertions(+), 330 deletions(-) diff --git a/packages/web-components/src/switch/switch.spec.ts b/packages/web-components/src/switch/switch.spec.ts index 41d648c70e00f..ceb41ca75e6a0 100644 --- a/packages/web-components/src/switch/switch.spec.ts +++ b/packages/web-components/src/switch/switch.spec.ts @@ -1,459 +1,347 @@ import { expect, test } from '@playwright/test'; -import type { Locator, Page } from '@playwright/test'; import { fixtureURL } from '../helpers.tests.js'; import type { Switch } from './switch.js'; test.describe('Switch', () => { - let page: Page; - let element: Locator; - let root: Locator; - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); - - element = page.locator('fluent-switch'); - - root = page.locator('#root'); - + test.beforeEach(async ({ page }) => { await page.goto(fixtureURL('components-switch--switch')); - }); - test.afterAll(async () => { - await page.close(); + await page.waitForFunction(() => customElements.whenDefined('fluent-switch')); }); - test('should have a role of `switch`', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should have a role of `checkbox`', async ({ page }) => { + const element = page.locator('fluent-switch'); - await expect(element).toHaveAttribute('role', 'switch'); + await page.setContent(/* html */ ` + + `); + + await expect(element).toHaveJSProperty('elementInternals.role', 'checkbox'); }); - test('should set the label value when passed a value attribute', async () => { - const testValue = 'My Test Value'; + test('should set the `ariaChecked` property to `false` when `checked` is not defined', async ({ page }) => { + const element = page.locator('fluent-switch'); - await root.evaluate((node, testValue) => { - node.innerHTML = /* html */ ` - - ${testValue} - - `; - }, testValue); + await page.setContent(/* html */ ` + + `); - const switchElement = page.locator('fluent-switch'); + await expect(element).not.toHaveAttribute('checked'); - expect(await switchElement.textContent()).toContain(testValue); - await expect(switchElement).toHaveAttribute('current-value', testValue); + await expect(element).toHaveJSProperty('elementInternals.ariaChecked', 'false'); }); - test('should set and retrieve the `label-position` property correctly', async () => { - await element.evaluate((node: Switch) => { - node.labelPosition = 'before'; - }); - - await expect(element).toHaveJSProperty('labelPosition', 'before'); + test('should set the `ariaChecked` property equal to the `checked` property', async ({ page }) => { + const element = page.locator('fluent-switch'); - await element.evaluate((node: Switch) => { - node.labelPosition = 'after'; - }); + await page.setContent(/* html */ ` + + `); - await expect(element).toHaveJSProperty('labelPosition', 'after'); + await expect(element).toHaveJSProperty('elementInternals.ariaChecked', 'true'); await element.evaluate((node: Switch) => { - node.labelPosition = 'above'; + node.checked = false; }); - await expect(element).toHaveJSProperty('labelPosition', 'above'); + await expect(element).toHaveJSProperty('elementInternals.ariaChecked', 'false'); }); - test('should reflect the correct value when supplied the labelPosition', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should NOT set a default `aria-required` value when `required` is not defined', async ({ page }) => { + const element = page.locator('fluent-switch'); - await expect(element).toHaveAttribute('tabindex', '0'); - }); + await page.setContent(/* html */ ` + + `); - test('should set a tabindex of 0 on the element', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + await expect(element).not.toHaveAttribute('required'); - await expect(element).toHaveAttribute('tabindex', '0'); + await expect(element).not.toHaveAttribute('aria-required'); }); - test('should set a default `aria-checked` value when `checked` is not defined', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should be focusable by default', async ({ page }) => { + const element = page.locator('fluent-switch'); - await expect(element).toHaveAttribute('aria-checked', 'false'); - }); + await page.setContent(/* html */ ` + + `); - test('should set a default `aria-disabled` value when `disabled` is not defined', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + await element.focus(); - await expect(element).toHaveAttribute('aria-disabled', 'false'); + await expect(element).toBeFocused(); }); - test('should NOT set a default `aria-readonly` value when `readonly` is not defined', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should NOT be focusable when the `disabled` attribute is set', async ({ page }) => { + const element = page.locator('fluent-switch'); + + await page.setContent(/* html */ ` + + `); + + await element.focus(); - const hasAriaReadonlyAttribute = await element.evaluate((node: Element) => node.hasAttribute('aria-readonly')); - expect(hasAriaReadonlyAttribute).toBe(false); + await expect(element).not.toBeFocused(); }); - test('should set the `aria-checked` attribute equal to the `checked` property', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should set the `ariaChecked` attribute to "mixed" when `indeterminate` property is true', async ({ page }) => { + const element = page.locator('fluent-switch'); + + await page.setContent(/* html */ ` + + `); await element.evaluate((node: Switch) => { - node.checked = true; + node.indeterminate = true; }); - await expect(element).toHaveAttribute('aria-checked', 'true'); + await expect(element).toHaveJSProperty('elementInternals.ariaChecked', 'mixed'); await element.evaluate((node: Switch) => { - node.checked = false; + node.indeterminate = false; }); - await expect(element).toHaveAttribute('aria-checked', 'false'); + await expect(element).toHaveJSProperty('elementInternals.ariaChecked', 'false'); }); - test('should set the `aria-readonly` attribute equal to the `readonly` value', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should set off `indeterminate` on `checked` change by user click', async ({ page }) => { + const element = page.locator('fluent-switch'); + + await page.setContent(/* html */ ` + + `); await element.evaluate((node: Switch) => { - node.readOnly = true; + node.indeterminate = true; }); - await expect(element).toHaveAttribute('aria-readonly', 'true'); + await expect(element).toHaveJSProperty('indeterminate', true); - await element.evaluate((node: Switch) => { - node.readOnly = false; - }); + await element.click(); - await expect(element).toHaveAttribute('aria-readonly', 'false'); + await expect(element).toHaveJSProperty('indeterminate', false); }); - test('should initialize to the initial value if no value property is set', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); - - const initialValue = await element.evaluate(node => node.initialValue); + test('should clear the `indeterminate` state when the `checked` property is true', async ({ page }) => { + const element = page.locator('fluent-switch'); - await expect(element).toHaveJSProperty('value', initialValue); - }); + await page.setContent(/* html */ ` + + `); - test('should add a class of `label` to the internal label when default slotted content exists', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; + await element.evaluate((node: Switch) => { + node.indeterminate = true; }); - const label = element.locator('.label'); - await element.evaluate(node => { - node.innerHTML = 'Label'; - }); + await expect(element).toHaveJSProperty('indeterminate', true); - await expect(label).toHaveClass(/label/); + await element.press(' '); - await expect(label).not.toHaveClass(/label__hidden/); + await expect(element).toHaveJSProperty('indeterminate', false); }); - test('should add classes of `label` and `label__hidden` to the internal label when default slotted content exists', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - Switch - `; - }); + test('should initialize to the initial value if no value property is set', async ({ page }) => { + const element = page.locator('fluent-switch'); - const label = element.locator('.label'); + await page.setContent(/* html */ ` + + `); - await element.evaluate(node => { - node.innerHTML = ''; - }); + await expect(element).toHaveJSProperty('value', 'on'); + }); - await expect(label).toHaveClass(/label/); + test('should initialize to the provided `value` attribute when set pre-connection', async ({ page }) => { + const element = page.locator('fluent-switch'); - await expect(label).toHaveClass(/label__hidden/); - }); + await page.setContent(/* html */ ` + + `); - test('should set the `aria-disabled` attribute equal to the `disabled` value', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + await expect(element).toHaveJSProperty('value', 'foo'); + }); - await expect(element).toHaveAttribute('aria-disabled', 'false'); + test('should initialize to the provided `value` attribute when set post-connection', async ({ page }) => { + const element = page.locator('fluent-switch'); - await element.evaluate((node: Switch) => { - node.disabled = true; - }); + await page.setContent(/* html */ ` + + `); - await expect(element).toHaveAttribute('aria-disabled', 'true'); + const expectedValue = 'foobar'; - await element.evaluate((node: Switch) => { - node.disabled = false; - }); + await element.evaluate((node: Switch, expectedValue) => { + node.setAttribute('value', expectedValue); + }, expectedValue); - await expect(element).toHaveAttribute('aria-disabled', 'false'); + await expect(element).toHaveJSProperty('value', expectedValue); }); - test('should NOT set a tabindex when disabled is `true`', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should initialize to the provided `value` property when set pre-connection', async ({ page }) => { + // const element = page.locator('fluent-switch'); - await expect(element).not.toHaveAttribute('tabindex', ''); + await page.setContent(/* html */ ` + + `); - await element.evaluate((node: Switch) => { - node.disabled = false; - }); + const expectedValue = 'foobar'; - await expect(element).toHaveAttribute('tabindex', '0'); - }); + const value = await page.evaluate(expectedValue => { + const node = document.createElement('fluent-switch') as Switch; - test('should initialize to the provided value attribute if set pre-connection', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + node.value = expectedValue; - await expect(element).toHaveJSProperty('value', 'foo'); + return Promise.resolve(node.value); + }, expectedValue); + + expect(value).toBe(expectedValue); }); - test('should initialize to the provided value attribute if set post-connection', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + test('should be invalid when unchecked', async ({ page }) => { + const element = page.locator('fluent-switch'); - await element.evaluate((node: Switch) => { - node.setAttribute('value', 'foo'); - }); + await page.setContent(/* html */ ` +
+ +
+ `); - await expect(element).toHaveJSProperty('value', 'foo'); + expect(await element.evaluate((node: Switch) => node.validity.valueMissing)).toBe(true); }); - test('should initialize to the provided value property if set pre-connection', async () => { - await root.evaluate(node => { - node.innerHTML = ''; + test('should be valid when checked', async ({ page }) => { + const element = page.locator('fluent-switch'); - const switchElement = document.createElement('fluent-switch') as Switch; - switchElement.value = 'foobar'; - node.appendChild(switchElement); - }); + await page.setContent(/* html */ ` +
+ checkbox +
+ `); - await expect(element).toHaveJSProperty('value', 'foobar'); - }); + await element.click(); - test('should emit an event when clicked', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; - }); + await expect(element).toHaveJSProperty('checked', true); - const [wasClicked] = await Promise.all([ - element.evaluate( - node => - new Promise(resolve => { - node.addEventListener('click', () => resolve(true)); - }), - ), - element.evaluate(node => { - node.dispatchEvent(new MouseEvent('click')); - }), - ]); - - expect(wasClicked).toBe(true); + expect(await element.evaluate((node: Switch) => node.validity.valueMissing)).toBe(false); }); - test('should fire an event when spacebar is invoked', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` + test('should set the `checked` property to false if the `checked` attribute is unset', async ({ page }) => { + const element = page.locator('fluent-switch'); + const form = page.locator('form'); + + await page.setContent(/* html */ ` +
- `; - }); +
+ `); - const [wasEmitted] = await Promise.all([ - element.evaluate( - node => - new Promise(resolve => { - node.addEventListener('keydown', () => resolve(true)); - }), - ), - element.evaluate(node => { - node.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); - }), - ]); - - expect(wasEmitted).toBe(true); - }); + await expect(element).toHaveJSProperty('checked', false); - test('should fire an event when enter is invoked', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` - - `; + await element.evaluate((node: Switch) => { + node.checked = true; }); - const [wasEmitted] = await Promise.all([ - element.evaluate( - node => - new Promise(resolve => { - node.addEventListener('keydown', () => resolve(true)); - }), - ), - element.evaluate(node => { - node.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - }), - ]); - - expect(wasEmitted).toBe(true); - }); + await expect(element).toHaveJSProperty('checked', true); - test('should be invalid when required and unchecked', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` -
- -
- `; + await form.evaluate((node: HTMLFormElement) => { + node.reset(); }); - expect(await element.evaluate(node => node.validity.valueMissing)).toBe(true); + await expect(element).toHaveJSProperty('checked', false); }); - test('should be valid when required and checked', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` -
- -
- `; - }); + test('should set its checked property to true if the checked attribute is set', async ({ page }) => { + const element = page.locator('fluent-switch'); + const form = page.locator('form'); - expect(await element.evaluate(node => node.validity.valueMissing)).toBe(false); - }); + await page.setContent(/* html */ ` +
+ +
+ `); + + await expect(element).toHaveJSProperty('checked', false); - test.describe("who's parent form has it's reset() method invoked", () => { - test('should set its checked property to false if the checked attribute is unset', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` -
- -
- `; - }); + await element.evaluate((node: Switch) => { + node.setAttribute('checked', ''); + }); - const form = page.locator('form'); + await expect(element).toHaveJSProperty('checked', true); - const hasCheckedAttributeInitially = await element.evaluate((node: Element) => node.hasAttribute('checked')); - expect(hasCheckedAttributeInitially).toBe(false); // Direct evaluation + await form.evaluate((node: HTMLFormElement) => { + node.reset(); + }); - await element.evaluate((node: Switch) => { - node.checked = true; - }); + await expect(element).toHaveJSProperty('checked', true); + }); - await expect(element).toHaveJSProperty('checked', 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 ({ + page, + }) => { + const element = page.locator('fluent-switch'); + const form = page.locator('form'); - await form.evaluate((node: HTMLFormElement) => { - node.reset(); - }); + await page.setContent(/* html */ ` +
+ +
+ `); - await expect(element).toHaveJSProperty('checked', false); + await element.evaluate((node: Switch) => { + node.checked = true; + node.removeAttribute('checked'); }); - test('should set its checked property to true if the checked attribute is set', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` -
- -
- `; - }); + await expect(element).toHaveJSProperty('checked', true); - const form = page.locator('form'); + await form.evaluate((node: HTMLFormElement) => { + node.reset(); + }); - const hasCheckedAttributeInitially = await element.evaluate((node: Element) => node.hasAttribute('checked')); - expect(hasCheckedAttributeInitially).toBe(true); + await expect(element).toHaveJSProperty('checked', false); - await element.evaluate((node: Switch) => { - node.checked = false; - }); + await element.evaluate((node: Switch) => { + node.setAttribute('checked', ''); + }); - await expect(element).toHaveJSProperty('checked', false); + expect(await element.evaluate((node: Switch) => node.value)).toBeTruthy(); + }); - await form.evaluate((node: HTMLFormElement) => { - node.reset(); - }); + test('should submit the value of the switch when checked', async ({ page }) => { + const element = page.locator('fluent-switch'); + const submitButton = page.locator('button'); - await expect(element).toHaveJSProperty('checked', true); - }); + await page.setContent(/* html */ ` +
+ + +
+ `); - test('should put the control into a clean state, where `checked` attribute modifications update the `checked` property prior to user or programmatic interaction', async () => { - await root.evaluate(node => { - node.innerHTML = /* html */ ` -
- -
- `; - }); + await element.click(); - const form = page.locator('form'); + await submitButton.click(); - await element.evaluate((node: Switch) => { - node.checked = true; - node.removeAttribute('checked'); - }); + expect(page.url()).toContain('?switch=foo'); + }); - await expect(element).toHaveJSProperty('checked', true); + test('should submit the values of multiple switches when checked', async ({ page }) => { + const switches = page.locator('fluent-switch'); + const element1 = switches.nth(0); + const element2 = switches.nth(1); + const submitButton = page.locator('button'); - await form.evaluate((node: HTMLFormElement) => { - node.reset(); - }); + await page.setContent(/* html */ ` +
+ + + +
+ `); - await expect(element).toHaveJSProperty('checked', false); + await element1.click(); + await element2.click(); - await element.evaluate((node: Switch) => { - node.setAttribute('checked', ''); - }); + await submitButton.click(); - await expect(element).toHaveJSProperty('checked', true); - }); + expect(page.url()).toContain('?switch=foo&switch=bar'); }); }); From b9f29591c1d18c33d5442c95e7dc7f878ffd22e7 Mon Sep 17 00:00:00 2001 From: Chris Holt <13071055+chrisdholt@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:12:15 -0700 Subject: [PATCH 6/6] remove indeterminate from stories, tests --- .../web-components/src/switch/switch.spec.ts | 56 ------------------- .../src/switch/switch.stories.ts | 1 - 2 files changed, 57 deletions(-) diff --git a/packages/web-components/src/switch/switch.spec.ts b/packages/web-components/src/switch/switch.spec.ts index ceb41ca75e6a0..d0bf836324ef0 100644 --- a/packages/web-components/src/switch/switch.spec.ts +++ b/packages/web-components/src/switch/switch.spec.ts @@ -83,62 +83,6 @@ test.describe('Switch', () => { await expect(element).not.toBeFocused(); }); - test('should set the `ariaChecked` attribute to "mixed" when `indeterminate` property is true', async ({ page }) => { - const element = page.locator('fluent-switch'); - - await page.setContent(/* html */ ` - - `); - - await element.evaluate((node: Switch) => { - node.indeterminate = true; - }); - - await expect(element).toHaveJSProperty('elementInternals.ariaChecked', 'mixed'); - - await element.evaluate((node: Switch) => { - node.indeterminate = false; - }); - - await expect(element).toHaveJSProperty('elementInternals.ariaChecked', 'false'); - }); - - test('should set off `indeterminate` on `checked` change by user click', async ({ page }) => { - const element = page.locator('fluent-switch'); - - await page.setContent(/* html */ ` - - `); - - await element.evaluate((node: Switch) => { - node.indeterminate = true; - }); - - await expect(element).toHaveJSProperty('indeterminate', true); - - await element.click(); - - await expect(element).toHaveJSProperty('indeterminate', false); - }); - - test('should clear the `indeterminate` state when the `checked` property is true', async ({ page }) => { - const element = page.locator('fluent-switch'); - - await page.setContent(/* html */ ` - - `); - - await element.evaluate((node: Switch) => { - node.indeterminate = true; - }); - - await expect(element).toHaveJSProperty('indeterminate', true); - - await element.press(' '); - - await expect(element).toHaveJSProperty('indeterminate', false); - }); - test('should initialize to the initial value if no value property is set', async ({ page }) => { const element = page.locator('fluent-switch'); diff --git a/packages/web-components/src/switch/switch.stories.ts b/packages/web-components/src/switch/switch.stories.ts index f52dfa1a736fe..2769ac7fdc7f0 100644 --- a/packages/web-components/src/switch/switch.stories.ts +++ b/packages/web-components/src/switch/switch.stories.ts @@ -13,7 +13,6 @@ const storyTemplate = html>` ?checked="${x => x.checked}" ?disabled="${x => x.disabled}" id="${x => x.id}" - :indeterminate="${x => x.indeterminate}" name="${x => x.name}" ?required="${x => x.required}" slot="${x => x.slot}"