diff --git a/change/@fluentui-web-components-be76ef06-94f5-4803-a242-d3d953a616ab.json b/change/@fluentui-web-components-be76ef06-94f5-4803-a242-d3d953a616ab.json new file mode 100644 index 0000000000000..7f613100872f7 --- /dev/null +++ b/change/@fluentui-web-components-be76ef06-94f5-4803-a242-d3d953a616ab.json @@ -0,0 +1,6 @@ +{ + "type": "patch", + "comment": "fix: defer fallback custom-state attributes until after construction", + "packageName": "@fluentui/web-components", + "email": "16242628+sylvesterkaczmarek@users.noreply.github.com" +} diff --git a/packages/web-components/src/tablist/tablist.spec.ts b/packages/web-components/src/tablist/tablist.spec.ts index d40c2aece1aee..4d88240c7c6cd 100644 --- a/packages/web-components/src/tablist/tablist.spec.ts +++ b/packages/web-components/src/tablist/tablist.spec.ts @@ -31,6 +31,75 @@ test.describe('Tablist', () => { expect(hasError).toBe(false); }); + test.describe('custom state fallback', () => { + test.use({ ssr: false }); + + test.beforeEach(async ({ page, fastPage }) => { + await page.addInitScript(() => { + const supports = CSS.supports.bind(CSS); + CSS.supports = (property: string, value?: string): boolean => { + if (property === 'selector(:state(g))') { + return false; + } + return value === undefined ? supports(property) : supports(property, value); + }; + }); + await fastPage.goto(); + }); + + test('should construct without adding host attributes', async ({ page }) => { + const errors: Error[] = []; + page.on('pageerror', error => errors.push(error)); + + const result = await page.evaluate(tagName => { + const element = document.createElement(tagName); + return { + upgraded: element instanceof customElements.get(tagName)!, + attributes: element.getAttributeNames(), + }; + }, tagName); + + expect(result).toEqual({ upgraded: true, attributes: [] }); + expect(errors).toEqual([]); + }); + + test('should apply default states after construction', async ({ page }) => { + await page.evaluate(tagName => { + document.body.append(document.createElement(tagName)); + }, tagName); + + await expect(page.locator(tagName)).toHaveAttribute('state--horizontal'); + await expect(page.locator(tagName)).not.toHaveAttribute('state--vertical'); + await expect(page.locator(tagName)).not.toHaveAttribute('state--disabled'); + }); + + test('should retain the latest states when updated before connection', async ({ page }) => { + await page.evaluate(tagName => { + const element = document.createElement(tagName) as Tablist; + element.orientation = 'vertical'; + element.orientation = 'horizontal'; + element.orientation = 'vertical'; + element.disabled = true; + element.disabled = false; + element.disabled = true; + document.body.append(element); + }, tagName); + + const element = page.locator(tagName); + await expect(element).toHaveAttribute('state--vertical'); + await expect(element).not.toHaveAttribute('state--horizontal'); + await expect(element).toHaveAttribute('state--disabled'); + + await element.evaluate((node: Tablist) => { + node.disabled = false; + node.orientation = 'horizontal'; + }); + await expect(element).not.toHaveAttribute('state--disabled'); + await expect(element).not.toHaveAttribute('state--vertical'); + await expect(element).toHaveAttribute('state--horizontal'); + }); + }); + test('should have reflect disabled attribute on control', async ({ fastPage }) => { const { element } = fastPage; diff --git a/packages/web-components/src/utils/element-internals.ts b/packages/web-components/src/utils/element-internals.ts index b60b03c172bc4..f8f134a41fc31 100644 --- a/packages/web-components/src/utils/element-internals.ts +++ b/packages/web-components/src/utils/element-internals.ts @@ -1,3 +1,4 @@ +import { Updates } from '@microsoft/fast-element'; import { CustomStatesSetSupported } from './support.js'; /** @@ -54,7 +55,8 @@ export function toggleState( } if (!CustomStatesSetSupported) { - elementInternals.shadowRoot!.host.toggleAttribute(`state--${state}`, force); + // Attribute mutations must wait until a custom element's constructor has returned. + Updates.enqueue(() => elementInternals.shadowRoot!.host.toggleAttribute(`state--${state}`, force)); return; } if (force ?? !elementInternals.states.has(state)) {