`
-
- 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>`
+
+`).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 4677c871802a17..68ee4c11b21c4e 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,27 +106,31 @@ 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};
}
: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};
}
`.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 +139,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 7dd2710e9a5794..f804a29f6c527d 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`
x.keypressHandler(c.event as KeyboardEvent)}"
+ tabindex="${x => (!x.disabled ? 0 : void 0)}"
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
+ @input="${(x, c) => x.inputHandler(c.event as Event)}"
+ @keydown="${(x, c) => x.keydownHandler(c.event as KeyboardEvent)}"
+ @keyup="${(x, c) => x.keyupHandler(c.event as KeyboardEvent)}"
>
-
-
- ${staticallyCompose(options.switch)}
-
+ ${staticallyCompose(options.switch)}
`;
}
diff --git a/packages/web-components/src/switch/switch.ts b/packages/web-components/src/switch/switch.ts
index 4a87be560bde7b..b907cb3c8f9c05 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 {}