Skip to content

Commit a215567

Browse files
authored
Code cleanup for expansion panel and accordion components (#1941)
* refactor(expansion-panel): Expansion panel cleanups Unify open state logic and event behavior * refactor(accordion): Accordion component cleanups * Abstracted keyboard behavior * Added slot controller to handle dynamic content changes * No more hanging promises in the internal API * Code style
1 parent 23e8af1 commit a215567

File tree

2 files changed

+167
-118
lines changed

2 files changed

+167
-118
lines changed

src/components/accordion/accordion.ts

Lines changed: 128 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { html, LitElement } from 'lit';
2-
import { property, queryAssignedElements } from 'lit/decorators.js';
3-
2+
import { property } from 'lit/decorators.js';
43
import {
54
addKeybindings,
65
altKey,
@@ -10,6 +9,7 @@ import {
109
homeKey,
1110
shiftKey,
1211
} from '../common/controllers/key-bindings.js';
12+
import { addSlotController, setSlots } from '../common/controllers/slot.js';
1313
import { registerComponent } from '../common/definitions/register.js';
1414
import { addSafeEventListener, first, last } from '../common/util.js';
1515
import IgcExpansionPanelComponent from '../expansion-panel/expansion-panel.js';
@@ -28,135 +28,189 @@ export default class IgcAccordionComponent extends LitElement {
2828
public static override styles = styles;
2929

3030
/* blazorSuppress */
31-
public static register() {
31+
public static register(): void {
3232
registerComponent(IgcAccordionComponent, IgcExpansionPanelComponent);
3333
}
3434

35-
@queryAssignedElements({
36-
selector: `${IgcExpansionPanelComponent.tagName}:not([disabled])`,
37-
})
38-
private enabledPanels!: Array<IgcExpansionPanelComponent>;
35+
//#region Internal state and properties
36+
37+
private _panels: IgcExpansionPanelComponent[] = [];
38+
39+
private readonly _slots = addSlotController(this, {
40+
slots: setSlots(),
41+
onChange: this._handleSlotChange,
42+
initial: true,
43+
});
44+
45+
private get _interactivePanels(): IgcExpansionPanelComponent[] {
46+
return this._panels.filter((panel) => !panel.disabled);
47+
}
48+
49+
//#endregion
50+
51+
//#region Public attributes and properties
3952

4053
/**
4154
* Allows only one panel to be expanded at a time.
4255
* @attr single-expand
56+
* @default false
4357
*/
44-
@property({ attribute: 'single-expand', reflect: true, type: Boolean })
58+
@property({ type: Boolean, reflect: true, attribute: 'single-expand' })
4559
public singleExpand = false;
4660

4761
/* blazorSuppress */
4862
/** Returns all of the accordions's direct igc-expansion-panel children. */
49-
@queryAssignedElements({ selector: IgcExpansionPanelComponent.tagName })
50-
public panels!: Array<IgcExpansionPanelComponent>;
63+
public get panels(): IgcExpansionPanelComponent[] {
64+
return Array.from(this._panels);
65+
}
66+
67+
//#endregion
5168

5269
constructor() {
5370
super();
5471

55-
addSafeEventListener(this, 'igcOpening' as any, this.handlePanelOpening);
72+
addSafeEventListener(this, 'igcOpening' as any, this._handlePanelOpening);
73+
74+
addKeybindings(this, { skip: this._skipKeybinding })
75+
.set(homeKey, this._navigateToFirst)
76+
.set(endKey, this._navigateToLast)
77+
.set(arrowUp, this._navigateToPrevious)
78+
.set(arrowDown, this._navigateToNext)
79+
.set([shiftKey, altKey, arrowDown], this._expandAll)
80+
.set([shiftKey, altKey, arrowUp], this._collapseAll);
81+
}
5682

57-
addKeybindings(this, { skip: this.skipKeybinding })
58-
.set(homeKey, () =>
59-
this.getPanelHeader(first(this.enabledPanels)).focus()
60-
)
61-
.set(endKey, () => this.getPanelHeader(last(this.enabledPanels)).focus())
62-
.set(arrowUp, this.navigatePrev)
63-
.set(arrowDown, this.navigateNext)
64-
.set([shiftKey, altKey, arrowDown], this.expandAll)
65-
.set([shiftKey, altKey, arrowUp], this.collapseAll);
83+
//#region Event handlers
84+
85+
private _handleSlotChange(): void {
86+
this._panels = this._slots.getAssignedElements('[default]', {
87+
selector: IgcExpansionPanelComponent.tagName,
88+
});
89+
}
90+
91+
private async _handlePanelOpening(event: Event): Promise<void> {
92+
const current = event.target as IgcExpansionPanelComponent;
93+
94+
if (!(this.singleExpand && this.panels.includes(current))) {
95+
return;
96+
}
97+
98+
await Promise.all(
99+
this._interactivePanels
100+
.filter((panel) => panel.open && panel !== current)
101+
.map((panel) => this._closePanel(panel))
102+
);
66103
}
67104

68-
private skipKeybinding(target: Element) {
105+
//#endregion
106+
107+
//#region Keyboard interaction handlers
108+
109+
private _skipKeybinding(target: Element): boolean {
69110
return !(
70-
target.matches(IgcExpansionPanelComponent.tagName) &&
71-
this.enabledPanels.includes(target as IgcExpansionPanelComponent)
111+
target instanceof IgcExpansionPanelComponent &&
112+
this._interactivePanels.includes(target)
72113
);
73114
}
74115

75-
private navigatePrev(event: KeyboardEvent) {
116+
private _navigateToFirst(): void {
117+
this._getPanelHeader(first(this._interactivePanels))?.focus();
118+
}
119+
120+
private _navigateToLast(): void {
121+
this._getPanelHeader(last(this._interactivePanels))?.focus();
122+
}
123+
124+
private _navigateToPrevious(event: KeyboardEvent): void {
76125
const current = event.target as IgcExpansionPanelComponent;
77-
const next = this.getNextPanel(current, -1);
126+
const next = this._getNextPanel(current, -1);
78127

79128
if (next !== current) {
80-
this.getPanelHeader(next).focus();
129+
this._getPanelHeader(next)?.focus();
81130
}
82131
}
83132

84-
private navigateNext(event: KeyboardEvent) {
133+
private _navigateToNext(event: KeyboardEvent): void {
85134
const current = event.target as IgcExpansionPanelComponent;
86-
const next = this.getNextPanel(current, 1);
135+
const next = this._getNextPanel(current, 1);
87136

88137
if (next !== current) {
89-
this.getPanelHeader(next).focus();
138+
this._getPanelHeader(next)?.focus();
90139
}
91140
}
92141

93-
private collapseAll() {
94-
for (const panel of this.enabledPanels) {
95-
this.closePanel(panel);
96-
}
142+
private async _collapseAll(): Promise<void> {
143+
await Promise.all(
144+
this._interactivePanels.map((panel) => this._closePanel(panel))
145+
);
97146
}
98147

99-
private expandAll(event: KeyboardEvent) {
148+
private async _expandAll(event: KeyboardEvent): Promise<void> {
100149
const current = event.target as IgcExpansionPanelComponent;
101150

102-
for (const panel of this.enabledPanels) {
103-
if (this.singleExpand) {
104-
current === panel ? this.openPanel(panel) : this.closePanel(panel);
105-
} else {
106-
this.openPanel(panel);
107-
}
151+
if (this.singleExpand) {
152+
const closePromises = this._interactivePanels
153+
.filter((panel) => panel.open && panel !== current)
154+
.map((panel) => this._closePanel(panel));
155+
156+
await Promise.all(closePromises);
157+
await this._openPanel(current);
158+
} else {
159+
await Promise.all(
160+
this._interactivePanels.map((panel) => this._openPanel(panel))
161+
);
108162
}
109163
}
110164

111-
private handlePanelOpening(event: Event) {
112-
const current = event.target as IgcExpansionPanelComponent;
165+
//#endregion
113166

114-
if (!(this.singleExpand && this.panels.includes(current))) {
115-
return;
116-
}
167+
//#region Internal API
117168

118-
for (const panel of this.enabledPanels) {
119-
if (panel.open && panel !== current) {
120-
this.closePanel(panel);
121-
}
122-
}
169+
private _getPanelHeader(
170+
panel: IgcExpansionPanelComponent
171+
): HTMLElement | undefined {
172+
// biome-ignore lint/complexity/useLiteralKeys: Direct property access instead of DOM query
173+
return panel['_headerRef'].value;
123174
}
124175

125-
private getNextPanel(panel: IgcExpansionPanelComponent, dir: 1 | -1 = 1) {
126-
const idx = this.enabledPanels.indexOf(panel);
127-
return this.enabledPanels[idx + dir] || panel;
128-
}
176+
private _getNextPanel(
177+
panel: IgcExpansionPanelComponent,
178+
dir: 1 | -1 = 1
179+
): IgcExpansionPanelComponent {
180+
const panels = this._interactivePanels;
181+
const idx = panels.indexOf(panel);
129182

130-
private getPanelHeader(panel: IgcExpansionPanelComponent) {
131-
return panel.renderRoot.querySelector('div[part="header"]') as HTMLElement;
183+
return panels[idx + dir] || panel;
132184
}
133185

134-
private async closePanel(panel: IgcExpansionPanelComponent) {
135-
if (
136-
!(
137-
panel.open &&
138-
panel.emitEvent('igcClosing', { cancelable: true, detail: panel })
139-
)
140-
) {
186+
private async _closePanel(p: IgcExpansionPanelComponent): Promise<void> {
187+
const args = { detail: p };
188+
189+
if (!(p.open && p.emitEvent('igcClosing', { cancelable: true, ...args }))) {
141190
return;
142191
}
143192

144-
await panel.hide();
145-
panel.emitEvent('igcClosed', { detail: panel });
193+
if (await p.hide()) {
194+
p.emitEvent('igcClosed', args);
195+
}
146196
}
147197

148-
private async openPanel(panel: IgcExpansionPanelComponent) {
149-
if (
150-
panel.open ||
151-
!panel.emitEvent('igcOpening', { cancelable: true, detail: panel })
152-
) {
198+
private async _openPanel(p: IgcExpansionPanelComponent): Promise<void> {
199+
const args = { detail: p };
200+
201+
if (p.open || !p.emitEvent('igcOpening', { cancelable: true, ...args })) {
153202
return;
154203
}
155204

156-
await panel.show();
157-
panel.emitEvent('igcOpened', { detail: panel });
205+
if (await p.show()) {
206+
p.emitEvent('igcOpened', args);
207+
}
158208
}
159209

210+
//#endregion
211+
212+
//#region Public API
213+
160214
/** Hides all of the child expansion panels' contents. */
161215
public async hideAll(): Promise<void> {
162216
await Promise.all(this.panels.map((panel) => panel.hide()));
@@ -167,6 +221,8 @@ export default class IgcAccordionComponent extends LitElement {
167221
await Promise.all(this.panels.map((panel) => panel.show()));
168222
}
169223

224+
//#endregion
225+
170226
protected override render() {
171227
return html`<slot></slot>`;
172228
}

0 commit comments

Comments
 (0)