From 7117c6b3dfb1bb5b64b71b2ae0754357a8be8f89 Mon Sep 17 00:00:00 2001 From: Eliza Khachatryan Date: Mon, 17 Jun 2024 11:03:32 -0700 Subject: [PATCH] feat(block): add `icon start/end` properties (deprecate `icon` slot and `status`), add `actions-end` slot (deprecate `control`), add `content-start` (#9535) **Related Issue:** #4932 ## Summary - Add `icon-start` and `icon-end` properties. - Deprecate `status`. - Deprecate `icon` in favor of `icon-start`. - Add `actions-end` slot and deprecate `control` slot. - Add `content-start`. --- .../calcite-components/src/components.d.ts | 26 +++++ .../src/components/block/block.scss | 56 ++++++++-- .../src/components/block/block.stories.ts | 35 ++++++ .../src/components/block/block.tsx | 102 ++++++++++++++++-- .../src/components/block/resources.ts | 9 +- .../calcite-components/src/demos/block.html | 28 ++++- 6 files changed, 234 insertions(+), 22 deletions(-) diff --git a/packages/calcite-components/src/components.d.ts b/packages/calcite-components/src/components.d.ts index 7d61f308a65..a958e56b6e4 100644 --- a/packages/calcite-components/src/components.d.ts +++ b/packages/calcite-components/src/components.d.ts @@ -610,6 +610,18 @@ export namespace Components { * Specifies the heading level of the component's `heading` for proper document structure, without affecting visual styling. */ "headingLevel": HeadingLevel; + /** + * Specifies an icon to display at the end of the component. + */ + "iconEnd": string; + /** + * Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`"rtl"`). + */ + "iconFlipRtl": FlipContext; + /** + * Specifies an icon to display at the start of the component. + */ + "iconStart": string; /** * When `true`, a busy indicator is displayed. */ @@ -636,6 +648,7 @@ export namespace Components { "setFocus": () => Promise; /** * Displays a status-related indicator icon. + * @deprecated Use `icon-start` instead. */ "status": Status; } @@ -8337,6 +8350,18 @@ declare namespace LocalJSX { * Specifies the heading level of the component's `heading` for proper document structure, without affecting visual styling. */ "headingLevel"?: HeadingLevel; + /** + * Specifies an icon to display at the end of the component. + */ + "iconEnd"?: string; + /** + * Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`"rtl"`). + */ + "iconFlipRtl"?: FlipContext; + /** + * Specifies an icon to display at the start of the component. + */ + "iconStart"?: string; /** * When `true`, a busy indicator is displayed. */ @@ -8380,6 +8405,7 @@ declare namespace LocalJSX { "overlayPositioning"?: OverlayPositioning; /** * Displays a status-related indicator icon. + * @deprecated Use `icon-start` instead. */ "status"?: Status; } diff --git a/packages/calcite-components/src/components/block/block.scss b/packages/calcite-components/src/components/block/block.scss index 4de07e9741d..24451975682 100644 --- a/packages/calcite-components/src/components/block/block.scss +++ b/packages/calcite-components/src/components/block/block.scss @@ -20,7 +20,8 @@ @import "../../assets/styles/header"; .header { - @apply justify-start p-0; + @apply justify-start; + padding: var(--calcite-spacing-md); } .header, @@ -31,11 +32,33 @@ .header-container { @apply grid items-stretch; grid-template: auto / auto 1fr auto auto; - grid-template-areas: "handle header control menu"; - grid-column: header-start / menu-end; + grid-template-areas: "handle header control menu actions-end"; + grid-column: header-start / actions-end; grid-row: 1 / 2; } +.content-start, +.icon, +.icon--start, +.icon--end { + margin-inline-end: var(--calcite-spacing-md); +} + +.icon { + & calcite-loader { + margin-inline-end: var(--calcite-spacing-xxxs); + } +} + +.icon--start, +.icon--end { + @apply text-color-3; +} + +.actions-end { + grid-area: actions-end; +} + .toggle { @apply font-sans focus-base @@ -47,9 +70,10 @@ justify-between border-none p-0; - text-align: initial; + text-align: initial; background-color: transparent; + &:hover { @apply bg-foreground-2; } @@ -69,7 +93,6 @@ calcite-handle { .title { @apply m-0; - padding: theme("spacing.3"); } .header .title .heading { @@ -93,9 +116,7 @@ calcite-handle { } .icon { - display: flex; - margin-inline-start: theme("spacing.3"); - margin-inline-end: 0px; + @apply flex; } .status-icon.valid { @@ -118,16 +139,21 @@ calcite-handle { } } +.icon-end-container { + @apply flex items-center; + + margin-inline-start: auto; +} + .toggle-icon { @apply text-color-3 transition-color - my-3 self-center justify-self-end duration-150 ease-in-out; - margin-inline-end: theme("spacing.3"); - margin-inline-start: auto; + + margin-inline-end: var(--calcite-spacing-md); } .toggle:hover .toggle-icon { @@ -144,6 +170,10 @@ calcite-handle { padding-inline: var(--calcite-block-padding, var(--calcite-spacing-md)); } +.content-start { + @apply text-color-3 flex items-center; +} + .control-container { @apply m-0 flex; grid-area: control; @@ -153,6 +183,10 @@ calcite-action-menu { grid-area: menu; } +.actions-end { + @apply flex items-stretch; +} + :host([open]) { @apply my-2; diff --git a/packages/calcite-components/src/components/block/block.stories.ts b/packages/calcite-components/src/components/block/block.stories.ts index 2d7fb1993a2..b7c00bd6217 100644 --- a/packages/calcite-components/src/components/block/block.stories.ts +++ b/packages/calcite-components/src/components/block/block.stories.ts @@ -271,3 +271,38 @@ export const icons_TestOnly = (): string => html` `; + +export const iconStartEnd = (): string => html` +

content-start and actions-end

+ + + + + + +

loading and actions-end

+ + + + +`; diff --git a/packages/calcite-components/src/components/block/block.tsx b/packages/calcite-components/src/components/block/block.tsx index 73e9fc34a89..b888435d14f 100644 --- a/packages/calcite-components/src/components/block/block.tsx +++ b/packages/calcite-components/src/components/block/block.tsx @@ -16,7 +16,12 @@ import { connectConditionalSlotComponent, disconnectConditionalSlotComponent, } from "../../utils/conditionalSlot"; -import { focusFirstTabbable, getSlotted, toAriaBoolean } from "../../utils/dom"; +import { + focusFirstTabbable, + getSlotted, + toAriaBoolean, + slotChangeHasAssignedElement, +} from "../../utils/dom"; import { connectInteractive, disconnectInteractive, @@ -33,7 +38,7 @@ import { updateMessages, } from "../../utils/t9n"; import { Heading, HeadingLevel } from "../functional/Heading"; -import { Status } from "../interfaces"; +import { Status, Position } from "../interfaces"; import { componentFocusable, LoadableComponent, @@ -42,13 +47,16 @@ import { } from "../../utils/loadable"; import { onToggleOpenCloseComponent, OpenCloseComponent } from "../../utils/openCloseComponent"; import { OverlayPositioning } from "../../utils/floating-ui"; +import { FlipContext } from "../interfaces"; import { CSS, ICONS, IDS, SLOTS } from "./resources"; import { BlockMessages } from "./assets/block/t9n"; /** * @slot - A slot for adding custom content. - * @slot icon - A slot for adding a leading header icon with `calcite-icon`. - * @slot control - A slot for adding a single HTML input element in a header. + * @slot actions-end - A slot for adding actionable `calcite-action` elements after the content of the component. It is recommended to use two or fewer actions. + * @slot icon - [Deprecated] A slot for adding a leading header icon with `calcite-icon`. Use `icon-start` instead. + * @slot content-start - A slot for adding non-actionable elements before content of the component. + * @slot control - [Deprecated] A slot for adding a single HTML input element in a header. Use `actions-end` instead. * @slot header-menu-actions - A slot for adding an overflow menu with `calcite-action`s inside a dropdown menu. */ @Component({ @@ -97,6 +105,15 @@ export class Block */ @Prop({ reflect: true }) headingLevel: HeadingLevel; + /** Specifies an icon to display at the end of the component. */ + @Prop({ reflect: true }) iconEnd: string; + + /** Displays the `iconStart` and/or `iconEnd` as 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; + /** * When `true`, a busy indicator is displayed. */ @@ -114,6 +131,8 @@ export class Block /** * Displays a status-related indicator icon. + * + * @deprecated Use `icon-start` instead. */ @Prop({ reflect: true }) status: Status; @@ -191,6 +210,8 @@ export class Block @Element() el: HTMLCalciteBlockElement; + @State() defaultMessages: BlockMessages; + @State() effectiveLocale: string; @Watch("effectiveLocale") @@ -198,7 +219,9 @@ export class Block updateMessages(this, this.effectiveLocale); } - @State() defaultMessages: BlockMessages; + @State() hasContentStart = false; + + @State() hasEndActions = false; openTransitionProp = "opacity"; @@ -281,6 +304,14 @@ export class Block this.transitionEl = el; }; + private actionsEndSlotChangeHandler = (event: Event): void => { + this.hasEndActions = slotChangeHasAssignedElement(event); + }; + + private handleContentStartSlotChange = (event: Event): void => { + this.hasContentStart = slotChangeHasAssignedElement(event); + }; + // -------------------------------------------------------------------------- // // Render Methods @@ -294,7 +325,7 @@ export class Block return [loading ? : null, defaultSlot]; } - renderIcon(): VNode[] { + private renderLoaderStatusIcon(): VNode[] { const { loading, messages, status } = this; const hasSlottedIcon = !!getSlotted(this.el, SLOTS.icon); @@ -322,6 +353,23 @@ export class Block ) : null; } + private renderActionsEnd(): VNode { + return ( +
+ +
+ ); + } + + private renderContentStart(): VNode { + const { hasContentStart } = this; + return ( + + ); + } + renderTitle(): VNode { const { heading, headingLevel, description } = this; return heading || description ? ( @@ -334,6 +382,33 @@ export class Block ) : null; } + private renderIcon(position: Extract<"start" | "end", Position>): VNode { + const { iconFlipRtl } = this; + + const flipRtl = + iconFlipRtl === "both" || position === "start" + ? iconFlipRtl === "start" + : iconFlipRtl === "end"; + + const iconValue = position === "start" ? this.iconStart : this.iconEnd; + const iconClass = position === "start" ? CSS.iconStart : CSS.iconEnd; + + if (!iconValue) { + return undefined; + } + + /** Icon scale is not variable as the component does not have a scale property */ + return ( + + ); + } + render(): VNode { const { collapsible, el, loading, open, heading, messages } = this; @@ -341,7 +416,9 @@ export class Block const headerContent = (
- {this.renderIcon()} + {this.renderIcon("start")} + {this.renderContentStart()} + {this.renderLoaderStatusIcon()} {this.renderTitle()}
); @@ -364,8 +441,16 @@ export class Block title={toggleLabel} > {headerContent} -