Skip to content

Commit 2b09aba

Browse files
authored
fix(accordion, accordion-item): icon-position, icon-type, selection-mode and scale can now be set as props or attributes (#7191)
**Related Issue:** #6199, #6038, #6200 ## Summary This should resolve #6199, where you're able to set `accordionEl.setAttribute("icon-position", "start")`, but not `accordionEl.iconPosition = "start"`, happening because of a logical issue within `getElementProp` function. `getElementProp` is being refactored across child components as an outdated pattern in #6038, so this will also kick off that issue as well. Instead of the `accordion-item` looking up the parent for `iconPosition, iconType, selectionMode, or scale`, these get set by the `accordion` parent. The logic for setting these props thus moves to the parent, getting rid of the `getElementProp` altogether. The parent component gets a `mutation observer` to do this as well as `watchers` for when it needs to modify the children. With the `mutationObserver` added, there is no longer a need for the `calciteInternalAccordionItemRegister` event logic.
1 parent 743ed34 commit 2b09aba

File tree

3 files changed

+147
-92
lines changed

3 files changed

+147
-92
lines changed

packages/calcite-components/src/components/accordion-item/accordion-item.tsx

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,11 @@ import {
1414
connectConditionalSlotComponent,
1515
disconnectConditionalSlotComponent
1616
} from "../../utils/conditionalSlot";
17-
import {
18-
closestElementCrossShadowBoundary,
19-
getElementDir,
20-
getElementProp,
21-
getSlotted,
22-
toAriaBoolean
23-
} from "../../utils/dom";
17+
import { getElementDir, getSlotted, toAriaBoolean } from "../../utils/dom";
2418
import { CSS_UTILITY } from "../../utils/resources";
2519
import { SLOTS, CSS } from "./resources";
26-
import { FlipContext, Position, Scale } from "../interfaces";
27-
import { RegistryEntry, RequestedItem } from "./interfaces";
20+
import { FlipContext, Position, Scale, SelectionMode } from "../interfaces";
21+
import { RequestedItem } from "./interfaces";
2822

2923
/**
3024
* @slot - A slot for adding custom content, including nested `calcite-accordion-item`s.
@@ -69,6 +63,40 @@ export class AccordionItem implements ConditionalSlotComponent {
6963
/** Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`"rtl"`). */
7064
@Prop({ reflect: true }) iconFlipRtl: FlipContext;
7165

66+
/**
67+
* Specifies the placement of the icon in the header inherited from the `calcite-accordion`.
68+
*
69+
* @internal
70+
*/
71+
@Prop() iconPosition: Position;
72+
73+
/** Specifies the type of the icon in the header inherited from the `calcite-accordion`.
74+
*
75+
* @internal
76+
*/
77+
@Prop() iconType: "chevron" | "caret" | "plus-minus";
78+
79+
/**
80+
* The containing `accordion` element.
81+
*
82+
* @internal
83+
*/
84+
@Prop() accordionParent: HTMLCalciteAccordionElement;
85+
86+
/**
87+
* Specifies the `selectionMode` of the component inherited from the `calcite-accordion`.
88+
*
89+
* @internal
90+
*/
91+
@Prop() selectionMode: Extract<"single" | "single-persist" | "multiple", SelectionMode>;
92+
93+
/**
94+
* Specifies the size of the component inherited from the `calcite-accordion`.
95+
*
96+
* @internal
97+
*/
98+
@Prop() scale: Scale;
99+
72100
//--------------------------------------------------------------------------
73101
//
74102
// Events
@@ -85,34 +113,16 @@ export class AccordionItem implements ConditionalSlotComponent {
85113
*/
86114
@Event({ cancelable: false }) calciteInternalAccordionItemClose: EventEmitter<void>;
87115

88-
/**
89-
* @internal
90-
*/
91-
@Event({ cancelable: false }) calciteInternalAccordionItemRegister: EventEmitter<RegistryEntry>;
92-
93116
//--------------------------------------------------------------------------
94117
//
95118
// Lifecycle
96119
//
97120
//--------------------------------------------------------------------------
98121

99122
connectedCallback(): void {
100-
this.parent = this.el.parentElement as HTMLCalciteAccordionElement;
101-
this.iconType = getElementProp(this.el, "icon-type", "chevron");
102-
this.iconPosition = getElementProp(this.el, "icon-position", this.iconPosition);
103-
this.scale = getElementProp(this.el, "scale", this.scale);
104-
105123
connectConditionalSlotComponent(this);
106124
}
107125

108-
componentDidLoad(): void {
109-
this.itemPosition = this.getItemPosition();
110-
this.calciteInternalAccordionItemRegister.emit({
111-
parent: this.parent,
112-
position: this.itemPosition
113-
});
114-
}
115-
116126
disconnectedCallback(): void {
117127
disconnectConditionalSlotComponent(this);
118128
}
@@ -248,38 +258,19 @@ export class AccordionItem implements ConditionalSlotComponent {
248258
//
249259
//--------------------------------------------------------------------------
250260

251-
/** the containing accordion element */
252-
private parent: HTMLCalciteAccordionElement;
253-
254-
/** position within parent */
255-
private itemPosition: number;
256-
257261
/** the latest requested item */
258262
private requestedAccordionItem: HTMLCalciteAccordionItemElement;
259263

260-
/** what selection mode is the parent accordion in */
261-
private selectionMode: string;
262-
263-
/** what icon position does the parent accordion specify */
264-
private iconPosition: Position = "end";
265-
266-
/** what icon type does the parent accordion specify */
267-
private iconType: string;
268-
269264
/** handle clicks on item header */
270265
private itemHeaderClickHandler = (): void => this.emitRequestedItem();
271266

272-
/** Specifies the scale of the `accordion-item` controlled by the parent, defaults to m */
273-
scale: Scale = "m";
274-
275267
//--------------------------------------------------------------------------
276268
//
277269
// Private Methods
278270
//
279271
//--------------------------------------------------------------------------
280272

281273
private determineActiveItem(): void {
282-
this.selectionMode = getElementProp(this.el, "selection-mode", "multiple");
283274
switch (this.selectionMode) {
284275
case "multiple":
285276
if (this.el === this.requestedAccordionItem) {
@@ -302,14 +293,4 @@ export class AccordionItem implements ConditionalSlotComponent {
302293
requestedAccordionItem: this.el as HTMLCalciteAccordionItemElement
303294
});
304295
}
305-
306-
private getItemPosition(): number {
307-
const { el } = this;
308-
309-
const items = closestElementCrossShadowBoundary(el, "calcite-accordion")?.querySelectorAll(
310-
"calcite-accordion-item"
311-
);
312-
313-
return items ? Array.from(items).indexOf(el) : -1;
314-
}
315296
}

packages/calcite-components/src/components/accordion/accordion.e2e.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { newE2EPage } from "@stencil/core/testing";
2-
import { accessible, renders, hidden } from "../../tests/commonTests";
2+
import { accessible, defaults, hidden, reflects, renders } from "../../tests/commonTests";
33
import { html } from "../../../support/formatting";
44
import { CSS } from "../accordion-item/resources";
55

@@ -14,6 +14,14 @@ describe("calcite-accordion", () => {
1414
<calcite-accordion-item heading="Accordion Title 3" id="3">Accordion Item Content </calcite-accordion-item>
1515
`;
1616

17+
const accordionContentInheritablePropsNonDefault = html`
18+
<calcite-accordion-item heading="Accordion Title 1" id="1">
19+
<calcite-action></calcite-action>Accordion Item Content<calcite-action></calcite-action>
20+
</calcite-accordion-item>
21+
<calcite-accordion-item heading="Accordion Title 1" id="2">Accordion Item Content </calcite-accordion-item>
22+
<calcite-accordion-item heading="Accordion Title 3" id="3">Accordion Item Content </calcite-accordion-item>
23+
`;
24+
1725
describe("renders", () => {
1826
renders("calcite-accordion", { display: "block" });
1927
});
@@ -26,18 +34,70 @@ describe("calcite-accordion", () => {
2634
accessible(`<calcite-accordion>${accordionContent}</calcite-accordion>`);
2735
});
2836

29-
it("renders default props when none are provided", async () => {
37+
describe("defaults", () => {
38+
defaults("calcite-accordion", [
39+
{
40+
propertyName: "appearance",
41+
defaultValue: "solid"
42+
},
43+
{
44+
propertyName: "iconPosition",
45+
defaultValue: "end"
46+
},
47+
{
48+
propertyName: "scale",
49+
defaultValue: "m"
50+
},
51+
{
52+
propertyName: "selectionMode",
53+
defaultValue: "multiple"
54+
},
55+
{
56+
propertyName: "iconType",
57+
defaultValue: "chevron"
58+
}
59+
]);
60+
});
61+
62+
describe("reflects", () => {
63+
reflects("calcite-accordion", [
64+
{
65+
propertyName: "iconPosition",
66+
value: "start"
67+
},
68+
{
69+
propertyName: "iconPosition",
70+
value: "end"
71+
},
72+
{
73+
propertyName: "selectionMode",
74+
value: "single-persist"
75+
},
76+
{
77+
propertyName: "selectionMode",
78+
value: "single"
79+
},
80+
{
81+
propertyName: "selectionMode",
82+
value: "multiple"
83+
}
84+
]);
85+
});
86+
87+
it("inheritable props: `iconPosition`, `iconType`, `selectionMode`, and `scale` modified on the parent get passed into items", async () => {
3088
const page = await newE2EPage();
3189
await page.setContent(`
32-
<calcite-accordion>
33-
${accordionContent}
90+
<calcite-accordion icon-position="start", icon-type="plus-minus", selection-mode="single-persist" scale="l">
91+
${accordionContentInheritablePropsNonDefault}
3492
</calcite-accordion>`);
35-
const element = await page.find("calcite-accordion");
36-
expect(element).toEqualAttribute("appearance", "solid");
37-
expect(element).toEqualAttribute("icon-position", "end");
38-
expect(element).toEqualAttribute("scale", "m");
39-
expect(element).toEqualAttribute("selection-mode", "multiple");
40-
expect(element).toEqualAttribute("icon-type", "chevron");
93+
const accordionItems = await page.findAll("calcite-accordion-items");
94+
95+
accordionItems.forEach(async (item) => {
96+
expect(await item.getProperty("iconPosition")).toBe("start");
97+
expect(await item.getProperty("iconType")).toBe("plus-minus");
98+
expect(await item.getProperty("selectionMode")).toBe("single-persist");
99+
expect(await item.getProperty("scale")).toBe("l");
100+
});
41101
});
42102

43103
it("renders requested props when valid props are provided", async () => {

packages/calcite-components/src/components/accordion/accordion.tsx

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
import { Component, Element, Event, EventEmitter, h, Listen, Prop, VNode } from "@stencil/core";
1+
import {
2+
Component,
3+
Element,
4+
Event,
5+
EventEmitter,
6+
h,
7+
Listen,
8+
Prop,
9+
VNode,
10+
Watch
11+
} from "@stencil/core";
212
import { Appearance, Position, Scale, SelectionMode } from "../interfaces";
13+
import { createObserver } from "../../utils/observers";
314
import { RequestedItem } from "./interfaces";
415
/**
516
* @slot - A slot for adding `calcite-accordion-item`s. `calcite-accordion` cannot be nested, however `calcite-accordion-item`s can.
@@ -24,6 +35,9 @@ export class Accordion {
2435
//
2536
//--------------------------------------------------------------------------
2637

38+
/** Specifies the parent of the component. */
39+
@Prop({ reflect: true }) accordionParent: HTMLCalciteAccordionElement;
40+
2741
/** Specifies the appearance of the component. */
2842
@Prop({ reflect: true }) appearance: Extract<"solid" | "transparent", Appearance> = "solid";
2943

@@ -45,6 +59,14 @@ export class Accordion {
4559
SelectionMode
4660
> = "multiple";
4761

62+
@Watch("iconPosition")
63+
@Watch("iconType")
64+
@Watch("scale")
65+
@Watch("selectionMode")
66+
handlePropsChange(): void {
67+
this.updateAccordionItems();
68+
}
69+
4870
//--------------------------------------------------------------------------
4971
//
5072
// Events
@@ -62,11 +84,13 @@ export class Accordion {
6284
//
6385
//--------------------------------------------------------------------------
6486

65-
componentDidLoad(): void {
66-
if (!this.sorted) {
67-
this.items = this.sortItems(this.items);
68-
this.sorted = true;
69-
}
87+
connectedCallback(): void {
88+
this.mutationObserver?.observe(this.el, { childList: true });
89+
this.updateAccordionItems();
90+
}
91+
92+
disconnectedCallback(): void {
93+
this.mutationObserver?.disconnect();
7094
}
7195

7296
render(): VNode {
@@ -89,19 +113,6 @@ export class Accordion {
89113
//
90114
//--------------------------------------------------------------------------
91115

92-
@Listen("calciteInternalAccordionItemRegister")
93-
registerCalciteAccordionItem(event: CustomEvent): void {
94-
const item = {
95-
item: event.target as HTMLCalciteAccordionItemElement,
96-
parent: event.detail.parent as HTMLCalciteAccordionElement,
97-
position: event.detail.position as number
98-
};
99-
if (this.el === item.parent) {
100-
this.items.push(item);
101-
}
102-
event.stopPropagation();
103-
}
104-
105116
@Listen("calciteInternalAccordionItemSelect")
106117
updateActiveItemOnChange(event: CustomEvent): void {
107118
this.requestedAccordionItem = event.detail.requestedAccordionItem;
@@ -117,11 +128,7 @@ export class Accordion {
117128
//
118129
//--------------------------------------------------------------------------
119130

120-
/** created list of Accordion items */
121-
private items = [];
122-
123-
/** keep track of whether the items have been sorted so we don't re-sort */
124-
private sorted = false;
131+
mutationObserver = createObserver("mutation", () => this.updateAccordionItems());
125132

126133
/** keep track of the requested item for multi mode */
127134
private requestedAccordionItem: HTMLCalciteAccordionItemElement;
@@ -132,6 +139,13 @@ export class Accordion {
132139
//
133140
//--------------------------------------------------------------------------
134141

135-
private sortItems = (items: any[]): any[] =>
136-
items.sort((a, b) => a.position - b.position).map((a) => a.item);
142+
private updateAccordionItems(): void {
143+
this.el.querySelectorAll("calcite-accordion-item").forEach((item) => {
144+
item.iconPosition = this.iconPosition;
145+
item.iconType = this.iconType;
146+
item.selectionMode = this.selectionMode;
147+
item.scale = this.scale;
148+
item.accordionParent = this.el;
149+
});
150+
}
137151
}

0 commit comments

Comments
 (0)