diff --git a/change/@fluentui-web-components-7bba75e2-576b-4e9f-821b-9501f016a3a6.json b/change/@fluentui-web-components-7bba75e2-576b-4e9f-821b-9501f016a3a6.json new file mode 100644 index 00000000000000..a0d68c1d2a909b --- /dev/null +++ b/change/@fluentui-web-components-7bba75e2-576b-4e9f-821b-9501f016a3a6.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "add swapStates function for attribute-driven internal states", + "packageName": "@fluentui/web-components", + "email": "863023+radium-v@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index e1de583574f961..6406a392a2b80c 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -55,9 +55,9 @@ export class AccordionItem extends BaseAccordionItem { block: boolean; blockChanged(prev: boolean, next: boolean): void; markerPosition?: AccordionItemMarkerPosition; - markerPositionChanged(prev: AccordionItemMarkerPosition, next: AccordionItemMarkerPosition): void; + markerPositionChanged(prev: AccordionItemMarkerPosition | undefined, next: AccordionItemMarkerPosition | undefined): void; size?: AccordionItemSize; - sizeChanged(prev: AccordionItemSize, next: AccordionItemSize): void; + sizeChanged(prev: AccordionItemSize | undefined, next: AccordionItemSize | undefined): void; } // @internal @@ -583,7 +583,7 @@ export class BaseDivider extends FASTElement { elementInternals: ElementInternals; orientation?: DividerOrientation; // @internal - orientationChanged(previous: string | null, next: string | null): void; + orientationChanged(previous: DividerRole | undefined, next: DividerRole | undefined): void; role: DividerRole; // @internal roleChanged(previous: string | null, next: string | null): void; @@ -3268,7 +3268,7 @@ export class Slider extends FASTElement implements SliderConfiguration { mode: SliderMode; orientation?: Orientation; // (undocumented) - protected orientationChanged(prev: string | undefined, next: string | undefined): void; + protected orientationChanged(prev: Orientation | undefined, next: Orientation | undefined): void; // @internal (undocumented) position: string; reportValidity(): boolean; @@ -3279,7 +3279,7 @@ export class Slider extends FASTElement implements SliderConfiguration { setValidity(flags?: Partial, message?: string, anchor?: HTMLElement): void; size?: SliderSize; // (undocumented) - protected sizeChanged(prev: string, next: string): void; + protected sizeChanged(prev: SliderSize | undefined, next: SliderSize | undefined): void; step: string; // (undocumented) protected stepChanged(): void; diff --git a/packages/web-components/src/accordion-item/accordion-item.ts b/packages/web-components/src/accordion-item/accordion-item.ts index 23ae62eb2e4c75..00fc017c445943 100644 --- a/packages/web-components/src/accordion-item/accordion-item.ts +++ b/packages/web-components/src/accordion-item/accordion-item.ts @@ -4,8 +4,8 @@ import type { StaticallyComposableHTML } from '../utils/index.js'; import { StartEnd } from '../patterns/index.js'; import type { StartEndOptions } from '../patterns/index.js'; import { applyMixins } from '../utils/apply-mixins.js'; -import { toggleState } from '../utils/element-internals.js'; -import type { AccordionItemMarkerPosition, AccordionItemSize } from './accordion-item.options.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; +import { AccordionItemMarkerPosition, AccordionItemSize } from './accordion-item.options.js'; /** * Accordion Item configuration options @@ -128,13 +128,8 @@ export class AccordionItem extends BaseAccordionItem { * @param prev - previous value * @param next - next value */ - public sizeChanged(prev: AccordionItemSize, next: AccordionItemSize): void { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + public sizeChanged(prev: AccordionItemSize | undefined, next: AccordionItemSize | undefined) { + swapStates(this.elementInternals, prev, next, AccordionItemSize); } /** @@ -152,13 +147,11 @@ export class AccordionItem extends BaseAccordionItem { * @param prev - previous value * @param next - next value */ - public markerPositionChanged(prev: AccordionItemMarkerPosition, next: AccordionItemMarkerPosition): void { - if (prev) { - toggleState(this.elementInternals, `align-${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `align-${next}`, true); - } + public markerPositionChanged( + prev: AccordionItemMarkerPosition | undefined, + next: AccordionItemMarkerPosition | undefined, + ) { + swapStates(this.elementInternals, prev, next, AccordionItemMarkerPosition, 'align-'); } /** diff --git a/packages/web-components/src/anchor-button/anchor-button.ts b/packages/web-components/src/anchor-button/anchor-button.ts index 2a3515bf98886b..bfbf29186cbbfc 100644 --- a/packages/web-components/src/anchor-button/anchor-button.ts +++ b/packages/web-components/src/anchor-button/anchor-button.ts @@ -1,14 +1,14 @@ import { attr, FASTElement, Observable } from '@microsoft/fast-element'; import { keyEnter } from '@microsoft/fast-web-utilities'; -import { StartEnd } from '../patterns/index.js'; import type { StartEndOptions } from '../patterns/index.js'; +import { StartEnd } from '../patterns/index.js'; import { applyMixins } from '../utils/apply-mixins.js'; -import { toggleState } from '../utils/element-internals.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; import { AnchorAttributes, - type AnchorButtonAppearance, - type AnchorButtonShape, - type AnchorButtonSize, + AnchorButtonAppearance, + AnchorButtonShape, + AnchorButtonSize, type AnchorTarget, } from './anchor-button.options.js'; @@ -265,12 +265,7 @@ export class AnchorButton extends BaseAnchor { * @param next - the next state */ public appearanceChanged(prev: AnchorButtonAppearance | undefined, next: AnchorButtonAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, AnchorButtonAppearance); } /** @@ -289,12 +284,7 @@ export class AnchorButton extends BaseAnchor { * @param next - the next state */ public shapeChanged(prev: AnchorButtonShape | undefined, next: AnchorButtonShape | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, AnchorButtonShape); } /** @@ -313,12 +303,7 @@ export class AnchorButton extends BaseAnchor { * @param next - the next state */ public sizeChanged(prev: AnchorButtonSize | undefined, next: AnchorButtonSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, AnchorButtonSize); } /** diff --git a/packages/web-components/src/avatar/avatar.ts b/packages/web-components/src/avatar/avatar.ts index 790bdb8d5b4a80..861385cffabcd9 100644 --- a/packages/web-components/src/avatar/avatar.ts +++ b/packages/web-components/src/avatar/avatar.ts @@ -1,6 +1,6 @@ import { attr, FASTElement, nullableNumberConverter, Observable } from '@microsoft/fast-element'; import { getInitials } from '../utils/get-initials.js'; -import { toggleState } from '../utils/element-internals.js'; +import { swapStates } from '../utils/element-internals.js'; import { type AvatarActive, type AvatarAppearance, @@ -130,7 +130,7 @@ export class Avatar extends BaseAvatar { /** * Holds the current color state */ - private currentColor: string | undefined; + private currentColor: AvatarColor | undefined; /** * Handles changes to observable properties @@ -177,8 +177,6 @@ export class Avatar extends BaseAvatar { const colorful: boolean = this.color === AvatarColor.colorful; const prev = this.currentColor; - toggleState(this.elementInternals, `${prev}`, false); - this.currentColor = colorful && this.colorId ? this.colorId @@ -186,7 +184,7 @@ export class Avatar extends BaseAvatar { ? (Avatar.colors[getHashCode(this.name ?? '') % Avatar.colors.length] as AvatarColor) : this.color ?? AvatarColor.neutral; - toggleState(this.elementInternals, `${this.currentColor}`, true); + swapStates(this.elementInternals, prev, this.currentColor); } /** diff --git a/packages/web-components/src/badge/badge.ts b/packages/web-components/src/badge/badge.ts index df200a65d7e487..07b4e39b1e8cbc 100644 --- a/packages/web-components/src/badge/badge.ts +++ b/packages/web-components/src/badge/badge.ts @@ -1,9 +1,8 @@ import { attr, FASTElement } from '@microsoft/fast-element'; -// TODO: Remove with https://github.com/microsoft/fast/pull/6797 import { applyMixins } from '../utils/apply-mixins.js'; import { StartEnd } from '../patterns/index.js'; -import { toggleState } from '../utils/element-internals.js'; -import { BadgeAppearance, BadgeColor, type BadgeShape, type BadgeSize } from './badge.options.js'; +import { swapStates } from '../utils/element-internals.js'; +import { BadgeAppearance, BadgeColor, BadgeShape, BadgeSize } from './badge.options.js'; /** * The base class used for constructing a fluent-badge custom element @@ -33,12 +32,7 @@ export class Badge extends FASTElement { * @param next - the next state */ public appearanceChanged(prev: BadgeAppearance | undefined, next: BadgeAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, BadgeAppearance); } /** @@ -57,12 +51,7 @@ export class Badge extends FASTElement { * @param next - the next state */ public colorChanged(prev: BadgeColor | undefined, next: BadgeColor | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, BadgeColor); } /** @@ -81,12 +70,7 @@ export class Badge extends FASTElement { * @param next - the next state */ public shapeChanged(prev: BadgeShape | undefined, next: BadgeShape | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, BadgeShape); } /** @@ -105,12 +89,7 @@ export class Badge extends FASTElement { * @param next - the next state */ public sizeChanged(prev: BadgeSize | undefined, next: BadgeSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, BadgeSize); } } diff --git a/packages/web-components/src/button/button.ts b/packages/web-components/src/button/button.ts index bff33807e42ad1..90a41e14c560fb 100644 --- a/packages/web-components/src/button/button.ts +++ b/packages/web-components/src/button/button.ts @@ -2,9 +2,8 @@ import { attr, FASTElement, nullableNumberConverter, observable } from '@microso import { keyEnter, keySpace } from '@microsoft/fast-web-utilities'; import { StartEnd } from '../patterns/index.js'; import { applyMixins } from '../utils/apply-mixins.js'; -import { toggleState } from '../utils/element-internals.js'; -import type { ButtonAppearance, ButtonFormTarget, ButtonShape, ButtonSize } from './button.options.js'; -import { ButtonType } from './button.options.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; +import { ButtonAppearance, ButtonFormTarget, ButtonShape, ButtonSize, ButtonType } from './button.options.js'; /** * A Button Custom HTML Element. @@ -447,12 +446,7 @@ export class Button extends BaseButton { * @param next - the next state */ public appearanceChanged(prev: ButtonAppearance | undefined, next: ButtonAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, ButtonAppearance); } /** @@ -471,12 +465,7 @@ export class Button extends BaseButton { * @param next - the next state */ public shapeChanged(prev: ButtonShape | undefined, next: ButtonShape | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, ButtonShape); } /** @@ -495,12 +484,7 @@ export class Button extends BaseButton { * @param next - the next state */ public sizeChanged(prev: ButtonSize | undefined, next: ButtonSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, ButtonSize); } /** diff --git a/packages/web-components/src/checkbox/checkbox.ts b/packages/web-components/src/checkbox/checkbox.ts index 9e6707e588eaba..5fc61d47f4cb79 100644 --- a/packages/web-components/src/checkbox/checkbox.ts +++ b/packages/web-components/src/checkbox/checkbox.ts @@ -1,6 +1,6 @@ import { attr, FASTElement, Observable, observable } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { CheckboxShape, CheckboxSize } from './checkbox.options.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; +import { CheckboxShape, CheckboxSize } from './checkbox.options.js'; /** * The base class for a component with a toggleable checked state. @@ -515,12 +515,7 @@ export class Checkbox extends BaseCheckbox { * @internal */ protected shapeChanged(prev: CheckboxShape | undefined, next: CheckboxShape | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, CheckboxShape); } /** @@ -541,12 +536,7 @@ export class Checkbox extends BaseCheckbox { * @internal */ protected sizeChanged(prev: CheckboxSize | undefined, next: CheckboxSize | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, CheckboxSize); } constructor() { diff --git a/packages/web-components/src/counter-badge/counter-badge.spec.ts b/packages/web-components/src/counter-badge/counter-badge.spec.ts index dada9b854fd54d..d6a2ce3ac141ab 100644 --- a/packages/web-components/src/counter-badge/counter-badge.spec.ts +++ b/packages/web-components/src/counter-badge/counter-badge.spec.ts @@ -174,7 +174,7 @@ test.describe('CounterBadge component', () => { await expect(element).toHaveJSProperty('dot', false); }); - for (const shape in CounterBadgeShape) { + for (const shape of Object.values(CounterBadgeShape)) { test(`should set the \`shape\` property to "${shape}" when the attribute is set to "${shape}"`, async ({ page, }) => { @@ -192,7 +192,7 @@ test.describe('CounterBadge component', () => { }); } - for (const color in CounterBadgeColor) { + for (const color of Object.values(CounterBadgeColor)) { test(`should set the \`color\` property to "${color}" when the attribute is set to "${color}"`, async ({ page, }) => { @@ -210,7 +210,7 @@ test.describe('CounterBadge component', () => { }); } - for (const size in CounterBadgeSize) { + for (const size of Object.values(CounterBadgeSize)) { test(`should set the \`size\` property to "${size}" when the attribute is set to "${size}"`, async ({ page }) => { const element = page.locator('fluent-counter-badge'); @@ -226,7 +226,7 @@ test.describe('CounterBadge component', () => { }); } - for (const appearance in CounterBadgeAppearance) { + for (const appearance of Object.values(CounterBadgeAppearance)) { test(`should set the \`appearance\` property to "${appearance}" when the attribute is set to "${appearance}"`, async ({ page, }) => { diff --git a/packages/web-components/src/counter-badge/counter-badge.ts b/packages/web-components/src/counter-badge/counter-badge.ts index fd418237cdce7f..f3a6ff4fada783 100644 --- a/packages/web-components/src/counter-badge/counter-badge.ts +++ b/packages/web-components/src/counter-badge/counter-badge.ts @@ -1,9 +1,8 @@ import { attr, FASTElement, nullableNumberConverter } from '@microsoft/fast-element'; -// TODO: Remove with https://github.com/microsoft/fast/pull/6797 import { applyMixins } from '../utils/apply-mixins.js'; import { StartEnd } from '../patterns/index.js'; -import { toggleState } from '../utils/element-internals.js'; -import type { +import { swapStates, toggleState } from '../utils/element-internals.js'; +import { CounterBadgeAppearance, CounterBadgeColor, CounterBadgeShape, @@ -38,12 +37,7 @@ export class CounterBadge extends FASTElement { * @param next - the next state */ public appearanceChanged(prev: CounterBadgeAppearance | undefined, next: CounterBadgeAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, CounterBadgeAppearance); } /** @@ -62,12 +56,7 @@ export class CounterBadge extends FASTElement { * @param next - the next state */ public colorChanged(prev: CounterBadgeColor | undefined, next: CounterBadgeColor | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, CounterBadgeColor); } /** @@ -86,12 +75,7 @@ export class CounterBadge extends FASTElement { * @param next - the next state */ public shapeChanged(prev: CounterBadgeShape | undefined, next: CounterBadgeShape | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, CounterBadgeShape); } /** @@ -110,12 +94,7 @@ export class CounterBadge extends FASTElement { * @param next - the next state */ public sizeChanged(prev: CounterBadgeSize | undefined, next: CounterBadgeSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, CounterBadgeSize); } /** diff --git a/packages/web-components/src/divider/divider.ts b/packages/web-components/src/divider/divider.ts index 499b59d145fce0..3a5dde8a9235f7 100644 --- a/packages/web-components/src/divider/divider.ts +++ b/packages/web-components/src/divider/divider.ts @@ -1,5 +1,5 @@ import { attr, FASTElement } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; import { DividerAlignContent, DividerAppearance, DividerOrientation, DividerRole } from './divider.options.js'; /** @@ -70,16 +70,10 @@ export class BaseDivider extends FASTElement { * @param next - the current orientation value * @internal */ - public orientationChanged(previous: string | null, next: string | null): void { - this.elementInternals.ariaOrientation = this.role !== DividerRole.presentation ? next : null; + public orientationChanged(previous: DividerRole | undefined, next: DividerRole | undefined): void { + this.elementInternals.ariaOrientation = this.role !== DividerRole.presentation ? next ?? null : null; - if (previous) { - toggleState(this.elementInternals, `${previous}`, false); - } - - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, previous, next, DividerOrientation); } } @@ -104,12 +98,7 @@ export class Divider extends BaseDivider { * @param next - the next state */ public alignContentChanged(prev: DividerAlignContent | undefined, next: DividerAlignContent | undefined) { - if (prev) { - toggleState(this.elementInternals, `align-${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `align-${next}`, true); - } + swapStates(this.elementInternals, prev, next, DividerAlignContent, 'align-'); } /** @@ -126,12 +115,7 @@ export class Divider extends BaseDivider { * @param next - the next state */ public appearanceChanged(prev: DividerAppearance | undefined, next: DividerAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, DividerAppearance); } /** diff --git a/packages/web-components/src/image/image.ts b/packages/web-components/src/image/image.ts index c8032f0c4566b3..deebde2647559e 100644 --- a/packages/web-components/src/image/image.ts +++ b/packages/web-components/src/image/image.ts @@ -1,6 +1,6 @@ import { attr, FASTElement } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { ImageFit, ImageShape } from './image.options.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; +import { ImageFit, ImageShape } from './image.options.js'; /** * The base class used for constucting a fluent image custom element @@ -87,12 +87,7 @@ export class Image extends FASTElement { * @param next - the next state */ public fitChanged(prev: ImageFit | undefined, next: ImageFit | undefined) { - if (prev) { - toggleState(this.elementInternals, `fit-${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `fit-${next}`, true); - } + swapStates(this.elementInternals, prev, next, ImageFit, 'fit-'); } /** @@ -111,11 +106,6 @@ export class Image extends FASTElement { * @param next - the next state */ public shapeChanged(prev: ImageShape | undefined, next: ImageShape | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, ImageShape); } } diff --git a/packages/web-components/src/label/label.ts b/packages/web-components/src/label/label.ts index 2259bf236cb7c0..a1276ad783ab37 100644 --- a/packages/web-components/src/label/label.ts +++ b/packages/web-components/src/label/label.ts @@ -1,6 +1,6 @@ import { attr, FASTElement } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { LabelSize, LabelWeight } from './label.options.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; +import { LabelSize, LabelWeight } from './label.options.js'; /** * The base class used for constructing a fluent-label custom element @@ -30,12 +30,7 @@ export class Label extends FASTElement { * @param next - the next state */ public sizeChanged(prev: LabelSize | undefined, next: LabelSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, LabelSize); } /** @@ -54,12 +49,7 @@ export class Label extends FASTElement { * @param next - the next state */ public weightChanged(prev: LabelWeight | undefined, next: LabelWeight | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, LabelWeight); } /** diff --git a/packages/web-components/src/link/link.ts b/packages/web-components/src/link/link.ts index 875f32e0906c41..a7203b5d09e544 100644 --- a/packages/web-components/src/link/link.ts +++ b/packages/web-components/src/link/link.ts @@ -1,7 +1,7 @@ import { attr } from '@microsoft/fast-element'; import { BaseAnchor } from '../anchor-button/anchor-button.js'; -import { toggleState } from '../utils/element-internals.js'; -import type { LinkAppearance } from './link.options.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; +import { LinkAppearance } from './link.options.js'; /** * An Anchor Custom HTML Element. @@ -30,12 +30,7 @@ export class Link extends BaseAnchor { * @param next - the next state */ public appearanceChanged(prev: LinkAppearance | undefined, next: LinkAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, LinkAppearance); } /** diff --git a/packages/web-components/src/menu-list/menu-list.spec.ts b/packages/web-components/src/menu-list/menu-list.spec.ts index 67bf0de03c0c53..a0081bd1901f1f 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -126,7 +126,7 @@ test.describe('Menu', () => { await expect(firstMenuItem).toBeFocused(); }); - for (const role in MenuItemRole) { + for (const role of Object.values(MenuItemRole)) { test(`should accept elements as focusable child with "${role}" role`, async ({ page }) => { await page.setContent(/* html */ ` diff --git a/packages/web-components/src/message-bar/message-bar.ts b/packages/web-components/src/message-bar/message-bar.ts index 31e066299506e2..a712661309fb84 100644 --- a/packages/web-components/src/message-bar/message-bar.ts +++ b/packages/web-components/src/message-bar/message-bar.ts @@ -1,6 +1,6 @@ import { attr, FASTElement } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { MessageBarIntent, MessageBarLayout, MessageBarShape } from './message-bar.options.js'; +import { swapStates } from '../utils/element-internals.js'; +import { MessageBarIntent, MessageBarLayout, MessageBarShape } from './message-bar.options.js'; /** * A Message Bar Custom HTML Element. @@ -39,12 +39,7 @@ export class MessageBar extends FASTElement { * @param next - the next state */ public shapeChanged(prev: MessageBarShape | undefined, next: MessageBarShape | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, MessageBarShape); } /** @@ -63,12 +58,7 @@ export class MessageBar extends FASTElement { * @param next - the next state */ public layoutChanged(prev: MessageBarLayout | undefined, next: MessageBarLayout | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, MessageBarLayout); } /** @@ -87,12 +77,7 @@ export class MessageBar extends FASTElement { * @param next - the next state */ public intentChanged(prev: MessageBarIntent | undefined, next: MessageBarIntent | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, MessageBarIntent); } /** diff --git a/packages/web-components/src/progress-bar/progress-bar.ts b/packages/web-components/src/progress-bar/progress-bar.ts index 9b1fb66a8f322f..20cc4d297140e9 100644 --- a/packages/web-components/src/progress-bar/progress-bar.ts +++ b/packages/web-components/src/progress-bar/progress-bar.ts @@ -1,6 +1,6 @@ import { attr, FASTElement, nullableNumberConverter, volatile } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { ProgressBarShape, ProgressBarThickness, ProgressBarValidationState } from './progress-bar.options.js'; +import { swapStates } from '../utils/element-internals.js'; +import { ProgressBarShape, ProgressBarThickness, ProgressBarValidationState } from './progress-bar.options.js'; /** * A Progress HTML Element. @@ -33,12 +33,7 @@ export class BaseProgressBar extends FASTElement { prev: ProgressBarValidationState | undefined, next: ProgressBarValidationState | undefined, ) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, ProgressBarValidationState); } /** @@ -138,12 +133,7 @@ export class ProgressBar extends BaseProgressBar { * @param next - the next state */ public thicknessChanged(prev: ProgressBarThickness | undefined, next: ProgressBarThickness | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, ProgressBarThickness); } /** @@ -160,11 +150,6 @@ export class ProgressBar extends BaseProgressBar { * @param next - the next state */ public shapeChanged(prev: ProgressBarShape | undefined, next: ProgressBarShape | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, ProgressBarShape); } } diff --git a/packages/web-components/src/rating-display/rating-display.ts b/packages/web-components/src/rating-display/rating-display.ts index cb28dc8f536825..d9098e9faa4303 100644 --- a/packages/web-components/src/rating-display/rating-display.ts +++ b/packages/web-components/src/rating-display/rating-display.ts @@ -1,6 +1,6 @@ import { attr, FASTElement, nullableNumberConverter, observable } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { RatingDisplayColor, RatingDisplaySize } from './rating-display.options.js'; +import { swapStates } from '../utils/element-internals.js'; +import { RatingDisplayColor, RatingDisplaySize } from './rating-display.options.js'; /** * The base class used for constructing a fluent-rating-display custom element @@ -171,8 +171,7 @@ export class RatingDisplay extends BaseRatingDisplay { * @param next - The next state */ public colorChanged(prev: RatingDisplayColor | undefined, next: RatingDisplayColor | undefined): void { - if (prev) toggleState(this.elementInternals, prev, false); - if (next) toggleState(this.elementInternals, next, true); + swapStates(this.elementInternals, prev, next, RatingDisplayColor); } /** @@ -192,9 +191,8 @@ export class RatingDisplay extends BaseRatingDisplay { * @param prev - The previous state * @param next - The next state */ - public sizeChanged(prev: RatingDisplaySize | undefined, next: RatingDisplaySize | undefined): void { - if (prev) toggleState(this.elementInternals, prev, false); - if (next) toggleState(this.elementInternals, next, true); + public sizeChanged(prev: RatingDisplaySize | undefined, next: RatingDisplaySize | undefined) { + swapStates(this.elementInternals, prev, next, RatingDisplaySize); } /** diff --git a/packages/web-components/src/slider/slider.ts b/packages/web-components/src/slider/slider.ts index fd7ad37a091666..7544f3700bb823 100644 --- a/packages/web-components/src/slider/slider.ts +++ b/packages/web-components/src/slider/slider.ts @@ -13,7 +13,7 @@ import { } from '@microsoft/fast-web-utilities'; import { numberLikeStringConverter } from '../utils/converters.js'; import { getDirection } from '../utils/direction.js'; -import { toggleState } from '../utils/element-internals.js'; +import { swapStates } from '../utils/element-internals.js'; import { type SliderConfiguration, SliderMode, SliderOrientation, SliderSize } from './slider.options.js'; import { convertPixelToPercent } from './slider-utilities.js'; @@ -60,13 +60,8 @@ export class Slider extends FASTElement implements SliderConfiguration { */ @attr public size?: SliderSize; - protected sizeChanged(prev: string, next: string): void { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + protected sizeChanged(prev: SliderSize | undefined, next: SliderSize | undefined) { + swapStates(this.elementInternals, prev, next, SliderSize); } public handleChange(_: any, propertyName: string): void { @@ -501,15 +496,10 @@ export class Slider extends FASTElement implements SliderConfiguration { */ @attr public orientation?: Orientation; - protected orientationChanged(prev: string | undefined, next: string | undefined): void { + protected orientationChanged(prev: Orientation | undefined, next: Orientation | undefined) { this.elementInternals.ariaOrientation = next ?? Orientation.horizontal; - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, Orientation); if (this.$fastController.isConnected) { this.setSliderPosition(this.direction); diff --git a/packages/web-components/src/spinner/spinner.spec.ts b/packages/web-components/src/spinner/spinner.spec.ts index a509677369d4c6..8e84fef20c3498 100644 --- a/packages/web-components/src/spinner/spinner.spec.ts +++ b/packages/web-components/src/spinner/spinner.spec.ts @@ -10,7 +10,7 @@ test.describe('Spinner', () => { await page.waitForFunction(() => customElements.whenDefined('fluent-spinner')); }); - for (const thisAppearance in SpinnerAppearance) { + for (const thisAppearance of Object.values(SpinnerAppearance)) { test(`should set and retrieve the \`appearance\` property correctly to ${thisAppearance}`, async ({ page }) => { const element = page.locator('fluent-spinner'); @@ -21,7 +21,7 @@ test.describe('Spinner', () => { await expect(element).toHaveJSProperty('appearance', thisAppearance); await test.step('should add a custom state matching the `appearance` attribute when provided', async () => { - for (const appearance in SpinnerAppearance) { + for (const appearance of Object.values(SpinnerAppearance)) { // eslint-disable-next-line playwright/no-conditional-in-test if (appearance === thisAppearance) { await expect(element).toHaveCustomState(appearance); @@ -33,7 +33,7 @@ test.describe('Spinner', () => { }); } - for (const thisSize in SpinnerSize) { + for (const thisSize of Object.values(SpinnerSize)) { test(`should set and retrieve the \`size\` property correctly to ${thisSize}`, async ({ page }) => { const element = page.locator('fluent-spinner'); @@ -44,7 +44,7 @@ test.describe('Spinner', () => { await expect(element).toHaveJSProperty('size', thisSize); await test.step('should add a custom state matching the `appearance` attribute when provided', async () => { - for (const size in SpinnerSize) { + for (const size of Object.values(SpinnerSize)) { // eslint-disable-next-line playwright/no-conditional-in-test if (size === thisSize) { await expect(element).toHaveCustomState(size); diff --git a/packages/web-components/src/spinner/spinner.ts b/packages/web-components/src/spinner/spinner.ts index 856609aa8e7ced..0c941f194b89ad 100644 --- a/packages/web-components/src/spinner/spinner.ts +++ b/packages/web-components/src/spinner/spinner.ts @@ -1,6 +1,6 @@ import { attr, FASTElement } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { SpinnerAppearance, SpinnerSize } from './spinner.options.js'; +import { swapStates } from '../utils/element-internals.js'; +import { SpinnerAppearance, SpinnerSize } from './spinner.options.js'; /** * The base class used for constructing a fluent-spinner custom element @@ -43,12 +43,7 @@ export class Spinner extends BaseSpinner { * @param next - the next state */ public sizeChanged(prev: SpinnerSize | undefined, next: SpinnerSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, SpinnerSize); } /** @@ -66,11 +61,6 @@ export class Spinner extends BaseSpinner { * @param next - the next state */ public appearanceChanged(prev: SpinnerAppearance | undefined, next: SpinnerAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, SpinnerAppearance); } } diff --git a/packages/web-components/src/tablist/tablist.spec.ts b/packages/web-components/src/tablist/tablist.spec.ts index c5acaddae169ac..a2c96795c3d3c1 100644 --- a/packages/web-components/src/tablist/tablist.spec.ts +++ b/packages/web-components/src/tablist/tablist.spec.ts @@ -226,7 +226,7 @@ test.describe('Tablist', () => { }); }); - for (const appearance in TablistAppearance) { + for (const appearance of Object.values(TablistAppearance)) { test(`should set appearance to \`${appearance}\``, async ({ page }) => { const element = page.locator('fluent-tablist'); @@ -244,7 +244,7 @@ test.describe('Tablist', () => { }); } - for (const size in TablistSize) { + for (const size of Object.values(TablistSize)) { test(`should set size to \`${size}\``, async ({ page }) => { const element = page.locator('fluent-tablist'); diff --git a/packages/web-components/src/tablist/tablist.ts b/packages/web-components/src/tablist/tablist.ts index 2c30f5624b8d36..11442eae41933f 100644 --- a/packages/web-components/src/tablist/tablist.ts +++ b/packages/web-components/src/tablist/tablist.ts @@ -10,10 +10,10 @@ import { wrapInBounds, } from '@microsoft/fast-web-utilities'; import { getDirection } from '../utils/index.js'; -import { toggleState } from '../utils/element-internals.js'; +import { swapStates, toggleState } from '../utils/element-internals.js'; import { isFocusableElement } from '../utils/focusable-element.js'; import type { Tab } from '../tab/tab.js'; -import { TablistAppearance, TablistOrientation, type TablistSize } from './tablist.options.js'; +import { TablistAppearance, TablistOrientation, TablistSize } from './tablist.options.js'; type TabData = Omit; @@ -62,12 +62,7 @@ export class BaseTablist extends FASTElement { protected orientationChanged(prev: TablistOrientation, next: TablistOrientation): void { this.elementInternals.ariaOrientation = next ?? TablistOrientation.horizontal; - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, TablistOrientation); if (this.$fastController.isConnected) { this.setTabs(); @@ -308,12 +303,7 @@ export class Tablist extends BaseTablist { * @internal */ protected appearanceChanged(prev: TablistAppearance, next: TablistAppearance): void { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, TablistAppearance); } /** @@ -328,12 +318,7 @@ export class Tablist extends BaseTablist { * @internal */ protected sizeChanged(prev: TablistSize, next: TablistSize): void { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, TablistSize); } /** diff --git a/packages/web-components/src/tabs/tabs.spec.ts b/packages/web-components/src/tabs/tabs.spec.ts index 2ebfdb55fbad9f..1028637618e3ac 100644 --- a/packages/web-components/src/tabs/tabs.spec.ts +++ b/packages/web-components/src/tabs/tabs.spec.ts @@ -384,7 +384,7 @@ test.describe('Tabs', () => { }); }); - for (const appearance in TabsAppearance) { + for (const appearance of Object.values(TabsAppearance)) { test(`should set appearance to \`${appearance}\``, async ({ page }) => { const element = page.locator('fluent-tabs'); @@ -396,7 +396,7 @@ test.describe('Tabs', () => { }); } - for (const size in TabsSize) { + for (const size of Object.values(TabsSize)) { test(`should set size to \`${size}\``, async ({ page }) => { const element = page.locator('fluent-tabs'); diff --git a/packages/web-components/src/text-input/text-input.ts b/packages/web-components/src/text-input/text-input.ts index 67ef92d8fe7d0a..0d844c5e3d52e5 100644 --- a/packages/web-components/src/text-input/text-input.ts +++ b/packages/web-components/src/text-input/text-input.ts @@ -1,9 +1,13 @@ import { attr, FASTElement, nullableNumberConverter, Observable, observable } from '@microsoft/fast-element'; import { StartEnd } from '../patterns/start-end.js'; import { applyMixins } from '../utils/apply-mixins.js'; -import { toggleState } from '../utils/element-internals.js'; -import type { TextInputControlSize } from './text-input.options.js'; -import { ImplicitSubmissionBlockingTypes, TextInputAppearance, TextInputType } from './text-input.options.js'; +import { swapStates } from '../utils/element-internals.js'; +import { + ImplicitSubmissionBlockingTypes, + TextInputAppearance, + TextInputControlSize, + TextInputType, +} from './text-input.options.js'; /** * A Text Input Custom HTML Element. @@ -618,12 +622,7 @@ export class TextInput extends BaseTextInput { * @param next - the next state */ public appearanceChanged(prev: TextInputAppearance | undefined, next: TextInputAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, TextInputAppearance); } /** @@ -642,12 +641,7 @@ export class TextInput extends BaseTextInput { * @param next - the next state */ public controlSizeChanged(prev: TextInputControlSize | undefined, next: TextInputControlSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, TextInputControlSize); } } diff --git a/packages/web-components/src/text/text.spec.ts b/packages/web-components/src/text/text.spec.ts index 52a1dc3c313cf9..288a7b89392bdf 100644 --- a/packages/web-components/src/text/text.spec.ts +++ b/packages/web-components/src/text/text.spec.ts @@ -38,7 +38,7 @@ test.describe('Text Component', () => { }); } - for (const value in Object.values(TextSize)) { + for (const value of Object.values(TextSize)) { test(`should set and reflect the size attribute to \`${value}\` when provided`, async ({ page }) => { const element = page.locator('fluent-text'); @@ -54,7 +54,7 @@ test.describe('Text Component', () => { }); } - for (const value in TextWeight) { + for (const value of Object.values(TextWeight)) { test(`should set and reflect the weight attribute to the \`${value}\` when provided`, async ({ page }) => { const element = page.locator('fluent-text'); @@ -70,7 +70,7 @@ test.describe('Text Component', () => { }); } - for (const value in TextAlign) { + for (const value of Object.values(TextAlign)) { test(`should set and reflect the align attribute to \`${value}\` when provided`, async ({ page }) => { const element = page.locator('fluent-text'); @@ -86,7 +86,7 @@ test.describe('Text Component', () => { }); } - for (const value in TextFont) { + for (const value of Object.values(TextFont)) { test(`should set and reflect the font attribute to \`${value}\` when provided`, async ({ page }) => { const element = page.locator('fluent-text'); diff --git a/packages/web-components/src/text/text.ts b/packages/web-components/src/text/text.ts index deb24213fd08f5..4204e8716d05e9 100644 --- a/packages/web-components/src/text/text.ts +++ b/packages/web-components/src/text/text.ts @@ -1,6 +1,6 @@ import { attr, FASTElement, Observable } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; -import type { TextAlign, TextFont, TextSize, TextWeight } from './text.options.js'; +import { hasMatchingState, swapStates, toggleState } from '../utils/element-internals.js'; +import { TextAlign, TextFont, TextSize, TextWeight } from './text.options.js'; /** * The base class used for constructing a fluent-text custom element @@ -93,12 +93,7 @@ export class Text extends FASTElement { * @param next - the next state */ public sizeChanged(prev: TextSize | undefined, next: TextSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `size-${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `size-${next}`, true); - } + swapStates(this.elementInternals, prev, next, TextSize, 'size-'); } /** @@ -117,12 +112,7 @@ export class Text extends FASTElement { * @param next - the next state */ public fontChanged(prev: TextFont | undefined, next: TextFont | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, TextFont); } /** @@ -141,12 +131,7 @@ export class Text extends FASTElement { * @param next - the next state */ public weightChanged(prev: TextWeight | undefined, next: TextWeight | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, TextWeight); } /** @@ -165,12 +150,7 @@ export class Text extends FASTElement { * @param next - the next state */ public alignChanged(prev: TextAlign | undefined, next: TextAlign | undefined) { - if (prev) { - toggleState(this.elementInternals, prev, false); - } - if (next) { - toggleState(this.elementInternals, next, true); - } + swapStates(this.elementInternals, prev, next, TextAlign); } public connectedCallback(): void { diff --git a/packages/web-components/src/textarea/textarea.ts b/packages/web-components/src/textarea/textarea.ts index f574f3c3f52b23..3cc73450d84cc6 100644 --- a/packages/web-components/src/textarea/textarea.ts +++ b/packages/web-components/src/textarea/textarea.ts @@ -1,6 +1,6 @@ import { attr, FASTElement, nullableNumberConverter, observable, Observable } from '@microsoft/fast-element'; -import { toggleState } from '../utils/element-internals.js'; import type { Label } from '../label/label.js'; +import { hasMatchingState, swapStates, toggleState } from '../utils/element-internals.js'; import { TextAreaAppearance, TextAreaAppearancesForDisplayShadow, @@ -266,20 +266,12 @@ export class BaseTextArea extends FASTElement { @attr({ mode: 'fromView' }) public resize: TextAreaResize = TextAreaResize.none; protected resizeChanged(prev: TextAreaResize | undefined, next: TextAreaResize | undefined): void { - if (prev) { - toggleState(this.elementInternals, `resize-${prev}`, false); - } - - if (next) { - toggleState(this.elementInternals, `resize-${next}`, true); - } + swapStates(this.elementInternals, prev, next, TextAreaResize, 'resize-'); toggleState( this.elementInternals, - `resize`, - ([TextAreaResize.both, TextAreaResize.horizontal, TextAreaResize.vertical] as Partial).includes( - this.resize, - ), + 'resize', + hasMatchingState(TextAreaResize, next) && next !== TextAreaResize.none, ); } @@ -661,14 +653,12 @@ export class TextArea extends BaseTextArea { @attr({ mode: 'fromView' }) public appearance: TextAreaAppearance = TextAreaAppearance.outline; protected appearanceChanged(prev: TextAreaAppearance | undefined, next: TextAreaAppearance | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } + toggleState(this.elementInternals, prev, false); - if (!next || !Array.from(Object.values(TextAreaAppearance)).includes(next)) { - toggleState(this.elementInternals, TextAreaAppearance.outline, true); + if (hasMatchingState(TextAreaAppearance, next)) { + toggleState(this.elementInternals, next, true); } else { - toggleState(this.elementInternals, `${next}`, true); + this.appearance = TextAreaAppearance.outline; } } @@ -695,12 +685,7 @@ export class TextArea extends BaseTextArea { @attr public size?: TextAreaSize; protected sizeChanged(prev: TextAreaSize | undefined, next: TextAreaSize | undefined) { - if (prev) { - toggleState(this.elementInternals, `${prev}`, false); - } - if (next) { - toggleState(this.elementInternals, `${next}`, true); - } + swapStates(this.elementInternals, prev, next, TextAreaSize); } /** diff --git a/packages/web-components/src/utils/element-internals.ts b/packages/web-components/src/utils/element-internals.ts index 26cb6517697d38..a9ccc3e1c8d357 100644 --- a/packages/web-components/src/utils/element-internals.ts +++ b/packages/web-components/src/utils/element-internals.ts @@ -46,7 +46,11 @@ export function stateSelector(state: S): StateSelector { * @param force - force the state to be toggled on or off * @internal */ -export function toggleState(elementInternals: ElementInternals, state: string, force?: boolean): void { +export function toggleState(elementInternals: ElementInternals, state: string | undefined, force?: boolean): void { + if (!state) { + return; + } + if (!CustomStatesSetSupported) { elementInternals.shadowRoot!.host.toggleAttribute(`state--${state}`, force); return; @@ -60,3 +64,57 @@ export function toggleState(elementInternals: ElementInternals, state: string, f // @ts-expect-error - Baseline 2024 elementInternals.states.delete(state); } + +/** + * A weak map to store the valid states for attributes. + * @internal + */ +const matchingStateMap = new WeakMap, Set>(); + +/** + * Check if a given attribute value is a valid state. Attribute values are often kebab-cased, so this function converts + * the kebab-cased `state` to camelCase and checks if it exists in as a key in the `States` object. + * + * @param States - the object containing valid states for the attribute + * @param state - the state to check + * @returns true if the state is in the States object + * @internal + */ +export function hasMatchingState(States: Record | undefined, state: string | undefined): boolean { + if (!States || !state) { + return false; + } + + if (matchingStateMap.has(States)) { + return matchingStateMap.get(States)!.has(state); + } + + const stateSet = new Set(Object.values(States)); + matchingStateMap.set(States, stateSet); + return stateSet.has(state); +} + +/** + * Swap an old state for a new state. + * + * @param elementInternals - the `ElementInternals` instance for the component + * @param prev - the previous state to remove + * @param next - the new state to add + * @param States - the object containing valid states for the attribute + * @param prefix - an optional prefix to add to the state + * + * @internal + */ +export function swapStates( + elementInternals: ElementInternals, + prev: string | undefined = '', + next: string | undefined = '', + States?: Record, + prefix: string = '', +): void { + toggleState(elementInternals, `${prefix}${prev}`, false); + + if (!States || hasMatchingState(States, next)) { + toggleState(elementInternals, `${prefix}${next}`, true); + } +}