From 9fc610df63e1d8c78cde1affda2ecc942573e8cd Mon Sep 17 00:00:00 2001 From: JC Franco Date: Mon, 13 May 2024 11:37:58 -0700 Subject: [PATCH] feat(segmented-control-item): allow displaying and icon when items are empty with a start/end icon (#9300) *Related Issue:** #6413 ## Summary This updates `segmented-control-item` to display a centered icon when specified and the item is empty. **Note:** this removes using `value` as a fallback label as non-breaking for the following reasons: * this behavior is [intentional](https://github.com/Esri/calcite-design-system/blob/main/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.e2e.ts#L38-L46), but there is no explicit spec for it in the [original issue](https://github.com/Esri/calcite-design-system/issues/5), [PR](https://github.com/Esri/calcite-design-system/pull/72) nor [documentation](https://developers.arcgis.com/calcite-design-system/components/segmented-control/) * it is inconsistent with how other components expect text to be provided * it [breaks if there's any whitespace](https://codepen.io/jcfranco/pen/XWwWGEy?editors=1000) * the current behavior will lead to label that might not be user-friendly in most cases (e.g., casing, localization) --- .../segmented-control-item/resources.ts | 10 ++- .../segmented-control-item.e2e.ts | 15 +--- .../segmented-control-item.scss | 38 +++++---- .../segmented-control-item.tsx | 77 ++++++++++++------- .../segmented-control.stories.ts | 26 +++++++ 5 files changed, 109 insertions(+), 57 deletions(-) diff --git a/packages/calcite-components/src/components/segmented-control-item/resources.ts b/packages/calcite-components/src/components/segmented-control-item/resources.ts index 7c4dfb8a978..ee5ef83894b 100644 --- a/packages/calcite-components/src/components/segmented-control-item/resources.ts +++ b/packages/calcite-components/src/components/segmented-control-item/resources.ts @@ -1,7 +1,15 @@ +import { Scale } from "../interfaces"; + export const SLOTS = { input: "input", }; export const CSS = { - segmentedControlItemIcon: "segmented-control-item-icon", + label: "label", + labelScale: (scale: Scale) => `label--scale-${scale}` as const, + labelHorizontal: "label--horizontal", + labelOutline: "label--outline", + labelOutlineFill: "label--outline-fill", + icon: "icon", + iconSolo: "icon--solo", }; diff --git a/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.e2e.ts b/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.e2e.ts index a280103baff..704e861001c 100644 --- a/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.e2e.ts +++ b/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.e2e.ts @@ -1,5 +1,6 @@ import { newE2EPage } from "@stencil/core/testing"; import { renders, hidden } from "../../tests/commonTests"; +import { CSS } from "./resources"; describe("calcite-segmented-control-item", () => { describe("renders", () => { @@ -35,21 +36,11 @@ describe("calcite-segmented-control-item", () => { expect(value).toBe("test-value"); }); - it("uses value as fallback label", async () => { - const page = await newE2EPage(); - await page.setContent( - "", - ); - - const label = await page.find("calcite-segmented-control-item >>> label"); - expect(label).toEqualText("test-value"); - }); - it("renders icon at start if requested", async () => { const page = await newE2EPage(); await page.setContent(` Content`); - const icon = await page.find("calcite-segmented-control-item >>> .segmented-control-item-icon"); + const icon = await page.find(`calcite-segmented-control-item >>> .${CSS.icon}`); expect(icon).not.toBe(null); }); @@ -57,7 +48,7 @@ describe("calcite-segmented-control-item", () => { const page = await newE2EPage(); await page.setContent(` Content`); - const icon = await page.find("calcite-segmented-control-item >>> .segmented-control-item-icon"); + const icon = await page.find(`calcite-segmented-control-item >>> .${CSS.icon}`); expect(icon).toBe(null); }); diff --git a/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.scss b/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.scss index f96fca806be..3f666f4130d 100644 --- a/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.scss +++ b/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.scss @@ -84,40 +84,48 @@ :host([checked]) .label--outline-fill { @apply outline-none; } - :host([checked]) label:not([class~="label--outline"]) .segmented-control-item-icon { + :host([checked]) label:not([class~="label--outline"]) .icon { color: highlightText; } } -// icon -.segmented-control-item-icon { +.icon { @apply relative m-0 inline-flex; line-height: inherit; + + margin-inline-start: var(--calcite-internal-segmented-control-icon-margin-start); + margin-inline-end: var(--calcite-internal-segmented-control-icon-margin-end); +} + +:host([icon-start]) .label--scale-s { + --calcite-internal-segmented-control-icon-margin-end: theme("margin.2"); } -:host([icon-start]) .label--scale-s .segmented-control-item-icon { - margin-inline-end: theme("margin.2"); +:host([icon-end]) .label--scale-s { + --calcite-internal-segmented-control-icon-margin-start: theme("margin.2"); } -:host([icon-end]) .label--scale-s .segmented-control-item-icon { - margin-inline-start: theme("margin.2"); +:host([icon-start]) .label--scale-m { + --calcite-internal-segmented-control-icon-margin-end: theme("margin.3"); } -:host([icon-start]) .label--scale-m .segmented-control-item-icon { - margin-inline-end: theme("margin.3"); +:host([icon-end]) .label--scale-m { + --calcite-internal-segmented-control-icon-margin-start: theme("margin.3"); } -:host([icon-end]) .label--scale-m .segmented-control-item-icon { - margin-inline-start: theme("margin.3"); +:host([icon-start]) .label--scale-l { + --calcite-internal-segmented-control-icon-margin-end: theme("margin.4"); } -:host([icon-start]) .label--scale-l .segmented-control-item-icon { - margin-inline-end: theme("margin.4"); +:host([icon-end]) .label--scale-l { + --calcite-internal-segmented-control-icon-margin-start: theme("margin.4"); } -:host([icon-end]) .label--scale-l .segmented-control-item-icon { - margin-inline-start: theme("margin.4"); + +.label .icon--solo { + --calcite-internal-segmented-control-icon-margin-start: 0; + --calcite-internal-segmented-control-icon-margin-end: 0; } @include base-component(); diff --git a/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.tsx b/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.tsx index f03bf8183a2..91281352cd8 100644 --- a/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.tsx +++ b/packages/calcite-components/src/components/segmented-control-item/segmented-control-item.tsx @@ -6,10 +6,11 @@ import { h, Host, Prop, + State, VNode, Watch, } from "@stencil/core"; -import { toAriaBoolean } from "../../utils/dom"; +import { slotChangeHasContent, toAriaBoolean } from "../../utils/dom"; import { Appearance, Layout, Scale } from "../interfaces"; import { CSS, SLOTS } from "./resources"; @@ -47,8 +48,7 @@ export class SegmentedControlItem { * The component's value. */ // eslint-disable-next-line @stencil-community/strict-mutable -- updated by form module - @Prop({ mutable: true }) - value: any | null; + @Prop({ mutable: true }) value: any | null; /** * Specifies the appearance style of the component inherited from parent `calcite-segmented-control`, defaults to `solid`. @@ -77,50 +77,67 @@ export class SegmentedControlItem { // //-------------------------------------------------------------------------- - render(): VNode { - const { appearance, checked, layout, scale, value } = this; - - const iconStartEl = this.iconStart ? ( + private renderIcon(icon: string, solo: boolean = false): VNode { + return icon ? ( ) : null; + } - const iconEndEl = this.iconEnd ? ( - - ) : null; + render(): VNode { + const { appearance, checked, layout, scale, value } = this; return ( ); } + private renderContent(): VNode | VNode[] { + const { hasSlottedContent, iconEnd, iconStart } = this; + const effectiveIcon = iconStart || iconEnd; + const canRenderIconOnly = !hasSlottedContent && effectiveIcon; + + if (canRenderIconOnly) { + return [this.renderIcon(effectiveIcon, true), ]; + } + + return [ + this.renderIcon(iconStart), + , + , + this.renderIcon(iconEnd), + ]; + } + + //-------------------------------------------------------------------------- + // + // Private Methods + // + //-------------------------------------------------------------------------- + + private handleSlotChange = (event: Event): void => { + this.hasSlottedContent = slotChangeHasContent(event); + }; + //-------------------------------------------------------------------------- // // Private Properties @@ -129,6 +146,8 @@ export class SegmentedControlItem { @Element() el: HTMLCalciteSegmentedControlItemElement; + @State() hasSlottedContent = false; + //-------------------------------------------------------------------------- // // Events diff --git a/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts b/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts index 1a6ab0a60e0..5b8cf7cff93 100644 --- a/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts +++ b/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts @@ -137,3 +137,29 @@ export const validationMessage_TestOnly = (): string => html` `; + +export const iconOnly = (): string => html` +

small

+ + + + + + + +

medium

+ + + + + + + +

medium

+ + + + + + +`;