diff --git a/src/components/accordion-item/accordion-item.tsx b/src/components/accordion-item/accordion-item.tsx index ccaaddbb2e9..e21ee0016d7 100644 --- a/src/components/accordion-item/accordion-item.tsx +++ b/src/components/accordion-item/accordion-item.tsx @@ -7,7 +7,8 @@ import { Host, Listen, Prop, - VNode + VNode, + Watch } from "@stencil/core"; import { getElementDir, getElementProp, getSlotted, toAriaBoolean } from "../../utils/dom"; import { @@ -17,7 +18,7 @@ import { } from "../../utils/conditionalSlot"; import { CSS_UTILITY } from "../../utils/resources"; import { SLOTS, CSS } from "./resources"; -import { FlipContext, Position } from "../interfaces"; +import { FlipContext, Position, Scale } from "../interfaces"; import { RegistryEntry, RequestedItem } from "./interfaces"; /** @@ -43,14 +44,17 @@ export class AccordionItem implements ConditionalSlotComponent { // //-------------------------------------------------------------------------- + /** Specifies a description for the component. */ + @Prop() description: string; + /** When `true`, the component is expanded. */ @Prop({ reflect: true, mutable: true }) expanded = false; /** Specifies heading text for the component. */ @Prop() heading: string; - /** Specifies a description for the component. */ - @Prop() description: string; + /** When `true`, the icon will be flipped when the element direction is right-to-left (`"rtl"`). */ + @Prop({ reflect: true }) iconFlipRtl: FlipContext; /** Specifies an icon to display at the start of the component. */ @Prop({ reflect: true }) iconStart: string; @@ -58,8 +62,17 @@ export class AccordionItem implements ConditionalSlotComponent { /** Specifies an icon to display at the end of the component. */ @Prop({ reflect: true }) iconEnd: string; - /** When `true`, the icon will be flipped when the element direction is right-to-left (`"rtl"`). */ - @Prop({ reflect: true }) iconFlipRtl: FlipContext; + /** + * Specifies the size of the component inherited from the `accordion`. + * + * @internal + */ + @Prop({ reflect: true }) scale: Scale = "m"; + + @Watch("scale") + onScaleChange(): void { + this.internalIconScale = this.scale === "l" ? "m" : "s"; + } //-------------------------------------------------------------------------- // @@ -142,16 +155,15 @@ export class AccordionItem implements ConditionalSlotComponent { flipRtl={iconFlipRtl === "both" || iconFlipRtl === "start"} icon={this.iconStart} key="icon-start" - scale="s" + scale={this.internalIconScale} /> ) : null; const iconEndEl = this.iconEnd ? ( ) : null; const { description } = this; @@ -191,7 +203,7 @@ export class AccordionItem implements ConditionalSlotComponent { ? "minus" : "plus" } - scale="s" + scale={this.internalIconScale} /> {this.renderActionsEnd()} @@ -258,6 +270,9 @@ export class AccordionItem implements ConditionalSlotComponent { /** what icon type does the parent accordion specify */ private iconType: string; + /** size of the component to be inherited from the `accordion` */ + private internalIconScale: Scale; + /** handle clicks on item header */ private itemHeaderClickHandler = (): void => this.emitRequestedItem(); //-------------------------------------------------------------------------- diff --git a/src/components/accordion-item/resources.ts b/src/components/accordion-item/resources.ts index a5dff5f1bd2..7121fc15c83 100644 --- a/src/components/accordion-item/resources.ts +++ b/src/components/accordion-item/resources.ts @@ -4,17 +4,17 @@ export const SLOTS = { }; export const CSS = { - icon: "icon", - header: "header", - headerContent: "header-content", actionsStart: "actions-start", actionsEnd: "actions-end", - headerText: "header-text", - heading: "heading", + content: "content", description: "description", expandIcon: "expand-icon", - content: "content", + header: "header", + headerContainer: "header-container", + headerContent: "header-content", + headerText: "header-text", + heading: "heading", + icon: "icon", iconStart: "icon--start", - iconEnd: "icon--end", - headerContainer: "header-container" + iconEnd: "icon--end" }; diff --git a/src/components/accordion/accordion.e2e.ts b/src/components/accordion/accordion.e2e.ts index fa653a72533..c304af0ba46 100644 --- a/src/components/accordion/accordion.e2e.ts +++ b/src/components/accordion/accordion.e2e.ts @@ -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(` - - Accordion Item Content - - Accordion Item Content - - Accordion Item Content - - `); - 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; + const scale = { l: "l", m: "m", s: "s" }; + const accordionItemContent = html` + + `; + + beforeEach(async () => { + page = await newE2EPage(); + await page.setContent(html` ${accordionItemContent} `); + 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); + }); + + 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); + }); }); it("renders expanded item based on attribute in dom", async () => { diff --git a/src/components/accordion/accordion.tsx b/src/components/accordion/accordion.tsx index 6e9bb00a59b..dffb72d3d37 100644 --- a/src/components/accordion/accordion.tsx +++ b/src/components/accordion/accordion.tsx @@ -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,6 +82,11 @@ 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); @@ -73,6 +94,10 @@ export class Accordion { } } + 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( + this.el.querySelectorAll("calcite-accordion-item") as any + ) as HTMLCalciteAccordionItemElement[] + ).forEach((accordionItem) => (accordionItem.scale = this.scale)); + }; }