diff --git a/2nd-gen/packages/core/components/illustrated-message/IllustratedMessage.base.ts b/2nd-gen/packages/core/components/illustrated-message/IllustratedMessage.base.ts index a016b53d3c8..0efc75f69a5 100644 --- a/2nd-gen/packages/core/components/illustrated-message/IllustratedMessage.base.ts +++ b/2nd-gen/packages/core/components/illustrated-message/IllustratedMessage.base.ts @@ -72,19 +72,6 @@ export abstract class IllustratedMessageBase extends SpectrumElement { // IMPLEMENTATION // ────────────────────── - protected override firstUpdated(changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - const headingSlot = this.shadowRoot?.querySelector( - 'slot[name="heading"]' - ); - if (headingSlot) { - headingSlot.addEventListener('slotchange', () => - this.warnInvalidHeadingSlot(headingSlot) - ); - this.warnInvalidHeadingSlot(headingSlot); - } - } - protected override updated(changedProperties: PropertyValues): void { super.updated(changedProperties); if (window.__swc?.DEBUG) { @@ -114,18 +101,26 @@ export abstract class IllustratedMessageBase extends SpectrumElement { } } - private warnInvalidHeadingSlot(headingSlot: HTMLSlotElement): void { - if (!window.__swc?.DEBUG) { - return; - } - for (const el of headingSlot.assignedElements()) { - if (!['H2', 'H3', 'H4', 'H5', 'H6'].includes(el.tagName)) { - window.__swc?.warn( - this, - `<${this.localName}> heading slot received a <${el.tagName.toLowerCase()}> element. Only

elements are allowed in the heading slot.`, - 'https://opensource.adobe.com/spectrum-web-components/components/illustrated-message/', - { issues: [`heading slot: <${el.tagName.toLowerCase()}>`] } - ); + /** + * Validates that the heading slot only contains `

`–`

` elements. + * Rendering subclasses must wire this to the heading slot's `slotchange` + * event (e.g. ``) + * for the validation warning to fire. + * + * @internal + */ + protected handleHeadingSlotChange(event: Event): void { + if (window.__swc?.DEBUG) { + const headingSlot = event.target as HTMLSlotElement; + for (const el of headingSlot.assignedElements()) { + if (!['H2', 'H3', 'H4', 'H5', 'H6'].includes(el.tagName)) { + window.__swc.warn( + this, + `<${this.localName}> heading slot received a <${el.tagName.toLowerCase()}> element. Only

elements are allowed in the heading slot.`, + 'https://opensource.adobe.com/spectrum-web-components/components/illustrated-message/', + { issues: [`heading slot: <${el.tagName.toLowerCase()}>`] } + ); + } } } } diff --git a/2nd-gen/packages/swc/components/illustrated-message/IllustratedMessage.ts b/2nd-gen/packages/swc/components/illustrated-message/IllustratedMessage.ts index b261f22628d..da76fd09428 100644 --- a/2nd-gen/packages/swc/components/illustrated-message/IllustratedMessage.ts +++ b/2nd-gen/packages/swc/components/illustrated-message/IllustratedMessage.ts @@ -51,7 +51,7 @@ export class IllustratedMessage extends IllustratedMessageBase {
- +
diff --git a/2nd-gen/packages/swc/components/illustrated-message/illustrated-message.css b/2nd-gen/packages/swc/components/illustrated-message/illustrated-message.css index 436fb3f9e3d..eb90e05a2db 100644 --- a/2nd-gen/packages/swc/components/illustrated-message/illustrated-message.css +++ b/2nd-gen/packages/swc/components/illustrated-message/illustrated-message.css @@ -10,8 +10,6 @@ * governing permissions and limitations under the License. */ -/* @todo SWC-1838 — replace with full S2 token-based styles */ - :host { display: block; } @@ -21,61 +19,133 @@ } .swc-IllustratedMessage { + --_swc-illustrated-message-illustration-to-content: var(--swc-illustrated-message-illustration-to-content, token("spacing-200")); + display: flex; flex-direction: column; + gap: var(--_swc-illustrated-message-illustration-to-content); align-items: center; - text-align: center; + max-inline-size: var(--swc-illustrated-message-max-inline-size, token("illustrated-message-vertical-maximum-width")); + margin-inline: auto; } .swc-IllustratedMessage-illustration { + --_swc-illustrated-message-illustration-size: var(--swc-illustrated-message-illustration-size, 96px); + display: flex; - align-items: center; + flex-shrink: 0; justify-content: center; + inline-size: var(--swc-illustrated-message-illustration-inline-size, var(--_swc-illustrated-message-illustration-size)); + block-size: var(--swc-illustrated-message-illustration-block-size, var(--_swc-illustrated-message-illustration-size)); + color: var(--swc-illustrated-message-illustration-color, token("neutral-content-color-default")); } .swc-IllustratedMessage-content { display: flex; flex-direction: column; - align-items: center; + gap: token("spacing-75"); + text-align: center; +} + +.swc-IllustratedMessage-description { + font-size: var(--swc-illustrated-message-description-font-size, token("illustrated-message-medium-body-font-size")); + font-weight: token("regular-font-weight"); + line-height: var(--swc-illustrated-message-description-line-height, token("body-line-height")); + color: token("body-color"); } -/* @todo SWC-1838 — heading typography (size, weight, line-height, color) */ +/* ── Size: small ─────────────────────────────────────────────────────────── */ -/* - * Reset native heading styles (font-size, font-weight, margin) so component tokens can take over. - * The :not([class]) guard leaves intentionally-classed headings untouched. - */ -::slotted(h2:not([class])), -::slotted(h3:not([class])), -::slotted(h4:not([class])), -::slotted(h5:not([class])), -::slotted(h6:not([class])) { - margin: 0; - font: inherit; +:host([size="s"]) { + --swc-illustrated-message-illustration-size: 96px; + --swc-illustrated-message-illustration-to-content: token("spacing-200"); + --swc-illustrated-message-heading-font-size: token("illustrated-message-small-title-font-size"); + --swc-illustrated-message-description-font-size: token("illustrated-message-small-body-font-size"); } -/* @todo SWC-1838 — description typography (size, weight, line-height, color) */ +/* ── Size: large ─────────────────────────────────────────────────────────── */ -/* ── Size ───────────────────────────────────────────────────────────────── */ +:host([size="l"]) { + --swc-illustrated-message-illustration-size: 160px; + --swc-illustrated-message-illustration-to-content: token("spacing-100"); + --swc-illustrated-message-heading-font-size: token("illustrated-message-large-title-font-size"); + --swc-illustrated-message-description-font-size: token("illustrated-message-large-body-font-size"); +} + +/* ── CJK language support ────────────────────────────────────────────────── */ -/* @todo SWC-1838 — replace with full S2 token-based size values */ +:host(:lang(ja)), +:host(:lang(ko)), +:host(:lang(zh)) { + --swc-illustrated-message-heading-font-size: token("illustrated-message-medium-cjk-title-font-size"); + --swc-illustrated-message-heading-line-height: token("cjk-line-height-100"); + --swc-illustrated-message-description-line-height: token("cjk-line-height-200"); +} -:host([size="s"]) .swc-IllustratedMessage-illustration { - /* @todo SWC-1838 */ +:host([size="s"]:lang(ja)), +:host([size="s"]:lang(ko)), +:host([size="s"]:lang(zh)) { + --swc-illustrated-message-heading-font-size: token("illustrated-message-small-cjk-title-font-size"); } -:host([size="l"]) .swc-IllustratedMessage-illustration { - /* @todo SWC-1838 */ +:host([size="l"]:lang(ja)), +:host([size="l"]:lang(ko)), +:host([size="l"]:lang(zh)) { + --swc-illustrated-message-heading-font-size: token("illustrated-message-large-cjk-title-font-size"); } -/* ── Orientation ─────────────────────────────────────────────────────────── */ +/* ── Orientation: horizontal ─────────────────────────────────────────────── */ -/* @todo SWC-1838 — replace with full S2 token-based orientation values */ +:host([orientation="horizontal"]) { + --swc-illustrated-message-max-inline-size: token("illustrated-message-horizontal-maximum-width"); +} :host([orientation="horizontal"]) .swc-IllustratedMessage { - /* @todo SWC-1838 */ + flex-direction: row; + align-items: center; } :host([orientation="horizontal"]) .swc-IllustratedMessage-content { - /* @todo SWC-1838 */ + align-items: flex-start; + text-align: start; +} + +/* ── Slotted content ─────────────────────────────────────────────────────── */ + +/* + * Heading styles live on the slot element itself. Slotted elements inherit + * from their assigned slot per the spec, so these properties flow into the + * heading without needing ::slotted() for each individual property. + */ +slot[name="heading"] { + font-size: var(--swc-illustrated-message-heading-font-size, token("illustrated-message-medium-title-font-size")); + font-style: token("title-sans-serif-font-style"); + font-weight: token("title-sans-serif-font-weight"); + line-height: var(--swc-illustrated-message-heading-line-height, token("title-line-height")); + color: token("heading-color"); +} + +/* + * Direct slotted headings to inherit from the slot element. + * The :not([class]) guard leaves intentionally-classed headings untouched. + * !important is required to win over any light DOM styles targeting + * the slotted heading (e.g. global h2 resets from the host document). + */ +::slotted([slot="heading"]:not([class])) { + margin: 0 !important; + font: inherit !important; + /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */ + color: inherit !important; +} + +/* + * Ensure slotted SVG illustrations fill the illustration container + * and use the component illustration color via currentcolor. + */ +::slotted(svg) { + display: block; + inline-size: 100%; + block-size: 100%; + fill: currentcolor; + stroke: currentcolor; } diff --git a/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts b/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts index 3d5e58c9b57..e600c869d41 100644 --- a/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts +++ b/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts @@ -15,7 +15,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import type { Meta, StoryObj as Story } from '@storybook/web-components'; import { getStorybookHelpers } from '@wc-toolkit/storybook-helpers'; -import '@adobe/spectrum-wc/illustrated-message'; +import { IllustratedMessage } from '@adobe/spectrum-wc/illustrated-message'; // ──────────────── // METADATA @@ -25,6 +25,18 @@ const { args, argTypes, template } = getStorybookHelpers( 'swc-illustrated-message' ); +argTypes.size = { + ...argTypes.size, + control: { type: 'select' }, + options: IllustratedMessage.VALID_SIZES, +}; + +argTypes.orientation = { + ...argTypes.orientation, + control: { type: 'select' }, + options: IllustratedMessage.VALID_ORIENTATIONS, +}; + /** * An illustrated message displays an illustration and a message, typically * used in empty states or error pages. @@ -58,16 +70,17 @@ export default { // HELPERS // ──────────────────── -const cloudIcon = ` - -`; +const cloudPath = ``; + +const cloudSvg = (a11yAttrs: string) => + `\n${cloudPath}\n`; // ──────────────────── // STORIES // ──────────────────── const defaultSlots = html` - ${unsafeHTML(cloudIcon)} + ${unsafeHTML(cloudSvg('aria-hidden="true"'))}

Illustrated message title

Illustrated message description. Give more information about what a user can @@ -76,15 +89,105 @@ const defaultSlots = html` `; export const Playground: Story = { + args: { + orientation: 'vertical', + }, render: (args) => template(args, defaultSlots), tags: ['autodocs', 'dev'], }; +// ────────────────────────── +// OVERVIEW STORY +// ────────────────────────── + export const Overview: Story = { render: (args) => template(args, defaultSlots), tags: ['overview'], }; +// ────────────────────────── +// OPTIONS STORIES +// ────────────────────────── + +/** + * Illustrated messages come in three sizes: + * + * - **Small (s)**: 96px illustration, compact spacing — for space-constrained contexts + * - **Medium (m)**: 96px illustration, standard spacing — the default + * - **Large (l)**: 160px illustration, reduced spacing — for prominent empty states + */ +export const Sizes: Story = { + render: (args) => html` + ${template( + { ...args, size: 's' }, + html` + ${unsafeHTML(cloudSvg('aria-hidden="true"'))} +

Small

+ Size s — 96px illustration + ` + )} + ${template( + { ...args, size: 'm' }, + html` + ${unsafeHTML(cloudSvg('aria-hidden="true"'))} +

Medium

+ Size m — 96px illustration (default) + ` + )} + ${template( + { ...args, size: 'l' }, + html` + ${unsafeHTML(cloudSvg('aria-hidden="true"'))} +

Large

+ Size l — 160px illustration + ` + )} + `, + tags: ['options'], + parameters: { + flexLayout: true, + 'section-order': 1, + }, +}; + +/** + * Illustrated messages support two layout orientations: + * + * - **Vertical** (default): illustration stacked above the heading and description, + * centered — use for full-page or centered empty states + * - **Horizontal**: illustration beside the heading and description in a row, + * left-aligned — use for inline or sidebar empty states + */ +export const Orientation: Story = { + render: (args) => html` + ${template( + { ...args, orientation: 'vertical' }, + html` + ${unsafeHTML(cloudSvg('aria-hidden="true"'))} +

Vertical (default)

+ Illustration stacked above the content. + ` + )} + ${template( + { ...args, orientation: 'horizontal' }, + html` + ${unsafeHTML(cloudSvg('aria-hidden="true"'))} +

Horizontal

+ Illustration beside the content. + ` + )} + `, + tags: ['options'], + parameters: { + styles: { display: 'flex', 'flex-direction': 'column', gap: '2rem' }, + 'section-order': 2, + }, +}; + +// ──────────────────────────────── +// ACCESSIBILITY STORIES +// ──────────────────────────────── + /** * SVGs slotted into the illustration slot should declare their accessibility * intent explicitly: @@ -99,7 +202,7 @@ export const IllustrationAccessibility: Story = { ${template( args, html` - ${unsafeHTML(cloudIcon)} + ${unsafeHTML(cloudSvg('aria-hidden="true"'))}

Illustrated message title

The icon above uses @@ -112,7 +215,9 @@ export const IllustrationAccessibility: Story = { ${template( args, html` - ${unsafeHTML(cloudIcon)} + ${unsafeHTML( + cloudSvg('role="img" aria-label="Cloud storage illustration"') + )}

Illustrated message title

The icon above uses