Skip to content

Commit

Permalink
feat(segmented-control-item): allow displaying and icon when items ar…
Browse files Browse the repository at this point in the history
…e 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](#5),
[PR](#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)
  • Loading branch information
jcfranco authored May 13, 2024
1 parent f62d909 commit 9fc610d
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -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",
};
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -35,29 +36,19 @@ describe("calcite-segmented-control-item", () => {
expect(value).toBe("test-value");
});

it("uses value as fallback label", async () => {
const page = await newE2EPage();
await page.setContent(
"<calcite-segmented-control-item value='test-value' checked></calcite-segmented-control-item>",
);

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(`
<calcite-segmented-control-item icon-start="car">Content</calcite-accordion-item>`);
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);
});

it("does not render icon if not requested", async () => {
const page = await newE2EPage();
await page.setContent(`
<calcite-segmented-control-item>Content</calcite-accordion-item>`);
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);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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 ? (
<calcite-icon
class={CSS.segmentedControlItemIcon}
class={{
[CSS.icon]: true,
[CSS.iconSolo]: solo,
}}
flipRtl={this.iconFlipRtl}
icon={this.iconStart}
key="icon-start"
icon={icon}
scale="s"
/>
) : null;
}

const iconEndEl = this.iconEnd ? (
<calcite-icon
class={CSS.segmentedControlItemIcon}
flipRtl={this.iconFlipRtl}
icon={this.iconEnd}
key="icon-end"
scale="s"
/>
) : null;
render(): VNode {
const { appearance, checked, layout, scale, value } = this;

return (
<Host aria-checked={toAriaBoolean(checked)} aria-label={value} role="radio">
<label
class={{
"label--scale-s": scale === "s",
"label--scale-m": scale === "m",
"label--scale-l": scale === "l",
"label--horizontal": layout === "horizontal",
"label--outline": appearance === "outline",
"label--outline-fill": appearance === "outline-fill",
[CSS.label]: true,
[CSS.labelScale(scale)]: true,
[CSS.labelHorizontal]: layout === "horizontal",
[CSS.labelOutline]: appearance === "outline",
[CSS.labelOutlineFill]: appearance === "outline-fill",
}}
>
{this.iconStart ? iconStartEl : null}
<slot>{value}</slot>
<slot name={SLOTS.input} />
{this.iconEnd ? iconEndEl : null}
{this.renderContent()}
</label>
</Host>
);
}

private renderContent(): VNode | VNode[] {
const { hasSlottedContent, iconEnd, iconStart } = this;
const effectiveIcon = iconStart || iconEnd;
const canRenderIconOnly = !hasSlottedContent && effectiveIcon;

if (canRenderIconOnly) {
return [this.renderIcon(effectiveIcon, true), <slot onSlotchange={this.handleSlotChange} />];
}

return [
this.renderIcon(iconStart),
<slot onSlotchange={this.handleSlotChange} />,
<slot name={SLOTS.input} />,
this.renderIcon(iconEnd),
];
}

//--------------------------------------------------------------------------
//
// Private Methods
//
//--------------------------------------------------------------------------

private handleSlotChange = (event: Event): void => {
this.hasSlottedContent = slotChangeHasContent(event);
};

//--------------------------------------------------------------------------
//
// Private Properties
Expand All @@ -129,6 +146,8 @@ export class SegmentedControlItem {

@Element() el: HTMLCalciteSegmentedControlItemElement;

@State() hasSlottedContent = false;

//--------------------------------------------------------------------------
//
// Events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,29 @@ export const validationMessage_TestOnly = (): string => html`
</calcite-segmented-control>
</div>
`;

export const iconOnly = (): string => html`
<h1>small</h1>
<calcite-segmented-control scale="s">
<calcite-segmented-control-item icon-start="banana" value="react" checked></calcite-segmented-control-item>
<calcite-segmented-control-item icon-start="gear" value="ember"></calcite-segmented-control-item>
<calcite-segmented-control-item icon-start="3d-glasses" value="angular"></calcite-segmented-control-item>
<calcite-segmented-control-item icon-start="effects" value="vue"></calcite-segmented-control-item>
</calcite-segmented-control>
<h1>medium</h1>
<calcite-segmented-control scale="m">
<calcite-segmented-control-item icon-end="banana" value="react" checked></calcite-segmented-control-item>
<calcite-segmented-control-item icon-end="gear" value="ember"></calcite-segmented-control-item>
<calcite-segmented-control-item icon-end="3d-glasses" value="angular"></calcite-segmented-control-item>
<calcite-segmented-control-item icon-end="effects" value="vue"></calcite-segmented-control-item>
</calcite-segmented-control>
<h1>medium</h1>
<calcite-segmented-control scale="l">
<calcite-segmented-control-item icon-end="banana" value="react" checked></calcite-segmented-control-item>
<calcite-segmented-control-item icon-end="gear" value="ember"></calcite-segmented-control-item>
<calcite-segmented-control-item icon-end="3d-glasses" value="angular"></calcite-segmented-control-item>
<calcite-segmented-control-item icon-end="effects" value="vue"></calcite-segmented-control-item>
</calcite-segmented-control>
`;

0 comments on commit 9fc610d

Please sign in to comment.