Skip to content

Commit 1668a3c

Browse files
authored
fix(web-components): defer Accordion item setup for SSR/DSD hydration (microsoft#35954)
1 parent 71a0d35 commit 1668a3c

4 files changed

Lines changed: 85 additions & 47 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "feat: enhance Accordion component with improved item handling and lifecycle management",
4+
"packageName": "@fluentui/web-components",
5+
"email": "863023+radium-v@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/docs/web-components.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import { ViewTemplate } from '@microsoft/fast-element';
2222
export class Accordion extends FASTElement {
2323
// @internal (undocumented)
2424
protected accordionItems: Element[];
25+
// (undocumented)
26+
connectedCallback(): void;
2527
expandmode: AccordionExpandMode;
2628
// (undocumented)
27-
expandmodeChanged(prev: AccordionExpandMode, next: AccordionExpandMode): void;
29+
expandmodeChanged(prev: AccordionExpandMode | undefined, next: AccordionExpandMode): void;
2830
// @internal
2931
handleChange(source: any, propertyName: string): void;
3032
// @internal (undocumented)

packages/web-components/src/accordion-item/accordion-item.options.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { ValuesOf } from '../utils/index.js';
1+
import { isCustomElement, type ValuesOf } from '../utils/typings.js';
2+
import type { BaseAccordionItem } from './accordion-item.base.js';
23

34
/**
45
* An Accordion Item header font size can be small, medium, large, and extra-large
@@ -29,3 +30,18 @@ export const AccordionItemMarkerPosition = {
2930
* @public
3031
*/
3132
export type AccordionItemMarkerPosition = ValuesOf<typeof AccordionItemMarkerPosition>;
33+
34+
/**
35+
* Predicate function that determines if the element should be considered an accordion item element.
36+
*
37+
* @param element - The element to check.
38+
* @param tagName - The tag name to check against, defaults to '-accordion-item'.
39+
* @returns True if the element is an accordion item element, false otherwise.
40+
* @public
41+
*/
42+
export function isAccordionItem(
43+
element?: Node | null,
44+
tagName: string = '-accordion-item',
45+
): element is BaseAccordionItem {
46+
return isCustomElement(tagName)(element);
47+
}

packages/web-components/src/accordion/accordion.ts

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Observable } from '@microsoft/fast-element';
2-
import { attr, FASTElement, observable } from '@microsoft/fast-element';
1+
import { attr, FASTElement, Observable, observable, Updates } from '@microsoft/fast-element';
32
import { BaseAccordionItem } from '../accordion-item/accordion-item.base.js';
3+
import { waitForConnectedDescendants } from '../utils/request-idle-callback.js';
4+
import { isAccordionItem } from '../accordion-item/accordion-item.options.js';
45
import { AccordionExpandMode } from './accordion.options.js';
56

67
/**
@@ -24,8 +25,8 @@ export class Accordion extends FASTElement {
2425
* HTML attribute: expand-mode
2526
*/
2627
@attr({ attribute: 'expand-mode' })
27-
public expandmode: AccordionExpandMode = AccordionExpandMode.multi;
28-
public expandmodeChanged(prev: AccordionExpandMode, next: AccordionExpandMode) {
28+
public expandmode!: AccordionExpandMode;
29+
public expandmodeChanged(prev: AccordionExpandMode | undefined, next: AccordionExpandMode) {
2930
if (!this.$fastController.isConnected) {
3031
return;
3132
}
@@ -88,14 +89,13 @@ export class Accordion extends FASTElement {
8889
* @returns {void}
8990
*/
9091
private findExpandedItem(): BaseAccordionItem | Element | null {
91-
if (this.accordionItems.length === 0) {
92+
if (!this.accordionItems || this.accordionItems?.length === 0) {
9293
return null;
9394
}
9495

9596
return (
96-
this.accordionItems.find(
97-
(item: Element | BaseAccordionItem) => item instanceof BaseAccordionItem && item.expanded,
98-
) ?? this.accordionItems[0]
97+
this.accordionItems.find((item: Element | BaseAccordionItem) => isAccordionItem(item) && item.expanded) ??
98+
this.accordionItems[0]
9999
);
100100
}
101101

@@ -105,29 +105,31 @@ export class Accordion extends FASTElement {
105105
* @returns {void}
106106
*/
107107
private setItems = (): void => {
108-
if (this.slottedAccordionItems.length === 0) {
109-
return;
110-
}
108+
waitForConnectedDescendants(this, () => {
109+
if (this.slottedAccordionItems.length === 0) {
110+
return;
111+
}
111112

112-
// Get all existing children and remove event listeners
113-
const children: Element[] = Array.from(this.children);
114-
this.removeItemListeners(children);
113+
// Get all existing children and remove event listeners
114+
const children: Element[] = Array.from(this.children);
115+
this.removeItemListeners(children);
115116

116-
// Resubscribe to the `disabled` attribute of all children
117-
children.forEach((child: Element) => Observable.getNotifier(child).subscribe(this, 'disabled'));
117+
// Resubscribe to the `disabled` attribute of all children
118+
children.forEach((child: Element) => Observable.getNotifier(child).subscribe(this, 'disabled'));
118119

119-
// Add event listeners to each non-disabled AccordionItem
120-
this.accordionItems = children.filter(child => !child.hasAttribute('disabled'));
121-
this.accordionItems.forEach((item: Element, index: number) => {
122-
item.addEventListener('click', this.expandedChangedHandler);
123-
// Subscribe to the expanded attribute of the item
124-
Observable.getNotifier(item).subscribe(this, 'expanded');
125-
});
120+
// Add event listeners to each non-disabled AccordionItem
121+
this.accordionItems = children.filter(child => !child.hasAttribute('disabled'));
122+
this.accordionItems.forEach((item: Element, index: number) => {
123+
item.addEventListener('click', this.expandedChangedHandler);
124+
// Subscribe to the expanded attribute of the item
125+
Observable.getNotifier(item).subscribe(this, 'expanded');
126+
});
126127

127-
if (this.isSingleExpandMode()) {
128-
const expandedItem = this.findExpandedItem() as BaseAccordionItem;
129-
this.setSingleExpandMode(expandedItem);
130-
}
128+
if (this.isSingleExpandMode()) {
129+
const expandedItem = this.findExpandedItem() as BaseAccordionItem;
130+
this.setSingleExpandMode(expandedItem);
131+
}
132+
});
131133
};
132134

133135
/**
@@ -144,25 +146,27 @@ export class Accordion extends FASTElement {
144146
* @returns {void}
145147
*/
146148
private setSingleExpandMode(expandedItem: Element): void {
147-
if (this.accordionItems.length === 0) {
148-
return;
149-
}
150-
const currentItems = Array.from(this.accordionItems);
151-
this.activeItemIndex = currentItems.indexOf(expandedItem);
152-
153-
currentItems.forEach((item: Element, index: number) => {
154-
if (item instanceof BaseAccordionItem) {
155-
if (this.activeItemIndex === index) {
156-
item.expanded = true;
157-
item.expandbutton.setAttribute('aria-disabled', 'true');
158-
} else {
159-
item.expanded = false;
160-
161-
if (!item.hasAttribute('disabled')) {
162-
item.expandbutton.removeAttribute('aria-disabled');
149+
requestAnimationFrame(() => {
150+
if (this.accordionItems.length === 0) {
151+
return;
152+
}
153+
const currentItems = Array.from(this.accordionItems);
154+
this.activeItemIndex = currentItems.indexOf(expandedItem);
155+
156+
currentItems.forEach((item: Element, index: number) => {
157+
if (isAccordionItem(item)) {
158+
if (this.activeItemIndex === index) {
159+
item.expanded = true;
160+
item.expandbutton.setAttribute('aria-disabled', 'true');
161+
} else {
162+
item.expanded = false;
163+
164+
if (!item.hasAttribute('disabled')) {
165+
item.expandbutton.removeAttribute('aria-disabled');
166+
}
163167
}
164168
}
165-
}
169+
});
166170
});
167171
}
168172

@@ -186,7 +190,7 @@ export class Accordion extends FASTElement {
186190
private expandedChangedHandler: EventListener = (evt: Event): void => {
187191
const item = evt.target as HTMLElement;
188192

189-
if (item instanceof BaseAccordionItem) {
193+
if (isAccordionItem(item)) {
190194
if (!this.isSingleExpandMode()) {
191195
item.expanded = !item.expanded;
192196
// setSingleExpandMode sets activeItemIndex on its own
@@ -198,4 +202,13 @@ export class Accordion extends FASTElement {
198202
this.$emit('change');
199203
}
200204
};
205+
206+
connectedCallback(): void {
207+
super.connectedCallback();
208+
209+
requestAnimationFrame(() => {
210+
this.expandmode = this.expandmode || AccordionExpandMode.multi;
211+
this.setItems();
212+
});
213+
}
201214
}

0 commit comments

Comments
 (0)