-
Notifications
You must be signed in to change notification settings - Fork 89
refactor(accordion, accordion-item): getElementProp on child is factored out and parent gets a mutation observer to update children
#6055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3b18af1
2a59f04
c0a37e7
d01eb60
c598df4
7f49d70
75cda06
43eae42
47001c5
c2d904e
ddf6c34
4fbf745
2d5b3e2
abf837e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { newE2EPage } from "@stencil/core/testing"; | ||
| import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing"; | ||
| import { accessible, renders, hidden } from "../../tests/commonTests"; | ||
| import { html } from "../../../support/formatting"; | ||
| import { CSS } from "../accordion-item/resources"; | ||
|
|
@@ -47,23 +47,58 @@ describe("calcite-accordion", () => { | |
| expect(element).toEqualAttribute("icon-type", "caret"); | ||
| }); | ||
|
|
||
| it("renders icon if requested", async () => { | ||
| const page = await newE2EPage(); | ||
| await page.setContent(` | ||
| <calcite-accordion appearance="minimal" icon-position="start" scale="l" selection-mode="single-persist" icon-type="caret"> | ||
| <calcite-accordion-item heading="Accordion Title 1" icon-start="car" id="1">Accordion Item Content | ||
| </calcite-accordion-item> | ||
| <calcite-accordion-item heading="Accordion Title 1" id="2" expanded>Accordion Item Content | ||
| </calcite-accordion-item> | ||
| <calcite-accordion-item heading="Accordion Title 3" icon-start="car" id="3">Accordion Item Content | ||
| </calcite-accordion-item> | ||
| </calcite-accordion>`); | ||
| const icon1 = await page.find(`calcite-accordion-item[id='1'] >>> .${CSS.iconStart}`); | ||
| const icon2 = await page.find(`calcite-accordion-item[id='2'] >>> .${CSS.iconStart}`); | ||
| const icon3 = await page.find(`calcite-accordion-item[id='3'] >>> .${CSS.iconStart}`); | ||
| expect(icon1).not.toBe(null); | ||
| expect(icon2).toBe(null); | ||
| expect(icon3).not.toBe(null); | ||
| describe("icon behavior", () => { | ||
| let page: E2EPage; | ||
|
Elijbet marked this conversation as resolved.
|
||
| const scale = { l: "l", m: "m", s: "s" }; | ||
| const accordionItemContent = html`<calcite-accordion-item icon-start="car" id="1"></calcite-accordion-item> | ||
| <calcite-accordion-item id="2"></calcite-accordion-item> | ||
| <calcite-accordion-item icon-start="car" id="3"></calcite-accordion-item>`; | ||
|
|
||
| beforeEach(async () => { | ||
| page = await newE2EPage(); | ||
| await page.setContent(html`<calcite-accordion scale="l"> ${accordionItemContent} </calcite-accordion>`); | ||
| await page.waitForChanges(); | ||
| }); | ||
|
|
||
| it("renders icon if requested", async () => { | ||
| const icon1: E2EElement = await page.find(`calcite-accordion-item[id='1'] >>> .${CSS.iconStart}`); | ||
| const icon2: E2EElement = await page.find(`calcite-accordion-item[id='2'] >>> .${CSS.iconStart}`); | ||
| const icon3: E2EElement = await page.find(`calcite-accordion-item[id='3'] >>> .${CSS.iconStart}`); | ||
| expect(icon1).not.toBe(null); | ||
| expect(icon2).toBe(null); | ||
| expect(icon3).not.toBe(null); | ||
| }); | ||
|
|
||
| it("renders m scale icon for l scale accordion-item", async () => { | ||
| const item1: E2EElement = await page.find(`calcite-accordion-item[id='1']`); | ||
| expect(await item1.getProperty("scale")).toBe(scale.l); | ||
| const icon1: E2EElement = await page.find(`calcite-accordion-item[id='1'] >>> .${CSS.iconStart}`); | ||
| expect(await icon1.getProperty("scale")).toBe(scale.m); | ||
| }); | ||
|
|
||
| it("renders corresponding scale on accordion-item when parent scale changes, icon scale not affected", async () => { | ||
| const accordion: E2EElement = await page.find("calcite-accordion"); | ||
| await accordion.setProperty("scale", scale.s); | ||
| await page.waitForChanges(); | ||
|
|
||
| const item1: E2EElement = await page.find(`calcite-accordion-item[id='1']`); | ||
| expect(await item1.getProperty("scale")).toEqual(scale.s); | ||
|
|
||
| const icon1: E2EElement = await page.find(`calcite-accordion-item[id='1'] >>> .${CSS.iconStart}`); | ||
| expect(await icon1.getProperty("scale")).toEqual(scale.m); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we instead expect this icon be a scale
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably add this to tomorrow's Pattern Discussion: Parent <> Child Property Relationships. From what I understand while we expect a parent to update children where these 2 have a parent/child relationship (as in accordion and its item), element and its slotted icon don't have that relationship and are expected to have independent scales. But in this particular case, it does feel a bit odd. Erik also brought up that we should not override an item with the parent if a user sets it individually to a different scale, although I'm not sure what the use case for that would be.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not sure what the specific use case would be either, but I still think that if the |
||
| }); | ||
|
|
||
| it("renders expected scale icon on child when scale is set on child level (no parent override)", async () => { | ||
| const accordion: E2EElement = await page.find("calcite-accordion"); | ||
| expect(await icon1.getProperty("scale")).toEqual(scale.l); | ||
|
|
||
| const item1: E2EElement = await page.find(`calcite-accordion-item[id='1']`); | ||
| await item1.setProperty("scale", scale.m); | ||
| await page.waitForChanges(); | ||
|
|
||
| expect(await accordion.getProperty("scale")).toEqual(scale.l); | ||
| expect(await item1.getProperty("scale")).toEqual(scale.m); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't we want the scale of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems logical that if calcite-accordion-item is an internal property we shouldn't be letting users set it to a different scale, but based on other comments looks like this is also part of tomorrow's Child Property Relationships discussion.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree it seems odd to allow this, but if you think about it, this pattern breaks component autonomy. When a component defines its own
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, although if the property is internal and is set by / managed by the parent maybe I wouldn't expect to be able to set this at all, as its undocumented. Though to follow through on that the scale probably shouldn't have the default to "m" until it receives the scale from
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This same principle applies to object inheritance in programming. If I have Object A that defines a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right - but its not a public property (or, is proposed to be an undocumented, internal property) - so the test case of it being set in that way should not be valid?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For sure. I think with a component like
Agreed. I think if we mark these properties as internal, and call out in our documentation that modifying internal properties is not supported, then this pattern can be adopted for certain parent/child component systems. |
||
| }); | ||
| }); | ||
|
|
||
| it("renders expanded item based on attribute in dom", async () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,17 @@ | ||
| import { Component, Element, Event, EventEmitter, h, Listen, Prop, VNode } from "@stencil/core"; | ||
| import { | ||
| Component, | ||
| Element, | ||
| Event, | ||
| EventEmitter, | ||
| h, | ||
| Listen, | ||
| Prop, | ||
| VNode, | ||
| Watch | ||
| } from "@stencil/core"; | ||
| import { RequestedItem } from "./interfaces"; | ||
| import { Appearance, Position, Scale } from "../interfaces"; | ||
| import { createObserver } from "../../utils/observers"; | ||
| import { SelectionMode } from "../interfaces"; | ||
| /** | ||
| * @slot - A slot for adding `calcite-accordion-item`s. `calcite-accordion` cannot be nested, however `calcite-accordion-item`s can. | ||
|
|
@@ -40,6 +51,11 @@ export class Accordion { | |
| /** Specifies the size of the component. */ | ||
| @Prop({ reflect: true }) scale: Scale = "m"; | ||
|
|
||
| @Watch("scale") | ||
| onScaleChange(): void { | ||
| this.passPropsToAccordionItems(); | ||
| } | ||
|
|
||
| /** | ||
| * Specifies the selection mode - `"multiple"` (allow any number of open items), `"single"` (allow one open item), | ||
| * or `"single-persist"` (allow and require one open item). | ||
|
|
@@ -66,13 +82,22 @@ export class Accordion { | |
| // | ||
| //-------------------------------------------------------------------------- | ||
|
|
||
| connectedCallback(): void { | ||
| this.passPropsToAccordionItems(); | ||
| this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); | ||
| } | ||
|
|
||
| componentDidLoad(): void { | ||
| if (!this.sorted) { | ||
| this.items = this.sortItems(this.items); | ||
| this.sorted = true; | ||
| } | ||
| } | ||
|
|
||
| disconnectedCallback(): void { | ||
| this.mutationObserver?.disconnect(); | ||
| } | ||
|
|
||
| render(): VNode { | ||
| const transparent = this.appearance === "transparent"; | ||
| const minimal = this.appearance === "minimal"; | ||
|
|
@@ -126,11 +151,13 @@ export class Accordion { | |
| /** created list of Accordion items */ | ||
| private items = []; | ||
|
|
||
| /** keep track of the requested item for multi mode */ | ||
| private requestedAccordionItem: HTMLCalciteAccordionItemElement; | ||
|
|
||
| /** keep track of whether the items have been sorted so we don't re-sort */ | ||
| private sorted = false; | ||
|
|
||
| /** keep track of the requested item for multi mode */ | ||
| private requestedAccordionItem: HTMLCalciteAccordionItemElement; | ||
| mutationObserver = createObserver("mutation", () => this.passPropsToAccordionItems()); | ||
|
|
||
| //-------------------------------------------------------------------------- | ||
| // | ||
|
|
@@ -140,4 +167,12 @@ export class Accordion { | |
|
|
||
| private sortItems = (items: any[]): any[] => | ||
| items.sort((a, b) => a.position - b.position).map((a) => a.item); | ||
|
|
||
| private passPropsToAccordionItems = (): void => { | ||
| ( | ||
| Array.from( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use the existing and Seems to work reliably.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes sense as long as
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah good point the internal registration events are kind of an outdated pattern and wouldn't respond - slot change observer that responds to addition / removal of children and updates |
||
| this.el.querySelectorAll("calcite-accordion-item") as any | ||
| ) as HTMLCalciteAccordionItemElement[] | ||
| ).forEach((accordionItem) => (accordionItem.scale = this.scale)); | ||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.