diff --git a/first-gen/packages/status-light/src/StatusLight.ts b/first-gen/packages/status-light/src/StatusLight.ts index 5ebc0bd4659..98f184fb070 100644 --- a/first-gen/packages/status-light/src/StatusLight.ts +++ b/first-gen/packages/status-light/src/StatusLight.ts @@ -13,17 +13,100 @@ import { CSSResultArray, html, + PropertyValues, TemplateResult, } from '@spectrum-web-components/base'; -import { StatusLightBase } from '@swc/core/components/status-light'; + +import { property } from '@spectrum-web-components/base/src/decorators.js'; + +import { + STATUSLIGHT_VARIANTS_COLOR_S1, + STATUSLIGHT_VARIANTS_S1, + STATUSLIGHT_VARIANTS_SEMANTIC_S1, + StatusLightBase, + type StatusLightVariantS1, +} from '@swc/core/components/status-light'; + import statusLightStyles from './status-light.css.js'; +/** + * @deprecated The `STATUSLIGHT_VARIANTS` export is deprecated and will be removed + * in a future release. If needed, you can access the internal + * `StatusLight.VARIANTS` property from the constructor. + */ +export const STATUSLIGHT_VARIANTS = STATUSLIGHT_VARIANTS_S1; + +/** + * @deprecated The `StatusLightVariant` type export is deprecated and will be removed + * in a future release. If needed, you can infer this type from the `StatusLight` + * prototype as follows: `typeof StatusLight.prototype.variant` + */ +export type StatusLightVariant = StatusLightVariantS1; + /** * @element sp-status-light * * @slot - text label of the Status Light */ export class StatusLight extends StatusLightBase { + // ──────────────────── + // API OVERRIDES + // ──────────────────── + + /** + * @internal + */ + static override readonly VARIANTS_COLOR = STATUSLIGHT_VARIANTS_COLOR_S1; + + /** + * @internal + */ + static override readonly VARIANTS_SEMANTIC = + STATUSLIGHT_VARIANTS_SEMANTIC_S1; + + /** + * @internal + */ + static override readonly VARIANTS = STATUSLIGHT_VARIANTS_S1; + + /** + * The variant of the status light. + */ + @property({ type: String, reflect: true }) + public override variant: StatusLightVariantS1 = 'info'; + + // ─────────────────────── + // API ADDITIONS + // ─────────────────────── + + /** + * @deprecated The `disabled` property is is deprecated and will be removed + * in a future release. + * + * A status light in a disabled state shows that a status exists, but is not available in that circumstance. This can be used to maintain layout continuity and communicate that a status may become available later. + * + */ + @property({ type: Boolean, reflect: true }) + public disabled = false; + + // ────────────────────── + // IMPLEMENTATION + // ────────────────────── + + protected override updated(changes: PropertyValues): void { + super.updated(changes); + if (changes.has('disabled')) { + if (this.disabled) { + this.setAttribute('aria-disabled', 'true'); + } else { + this.removeAttribute('aria-disabled'); + } + } + } + // ────────────────────────────── + // RENDERING & STYLING + // ────────────────────────────── + public static override get styles(): CSSResultArray { return [statusLightStyles]; } diff --git a/first-gen/packages/status-light/test/status-light.test.ts b/first-gen/packages/status-light/test/status-light.test.ts index 97daa85285d..0f031a713fd 100644 --- a/first-gen/packages/status-light/test/status-light.test.ts +++ b/first-gen/packages/status-light/test/status-light.test.ts @@ -12,6 +12,7 @@ import '@spectrum-web-components/status-light/sp-status-light.js'; import { StatusLight } from '@spectrum-web-components/status-light'; import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; +import { spy } from 'sinon'; describe('Status Light', () => { it('loads correctly', async () => { @@ -41,4 +42,41 @@ describe('Status Light', () => { expect(el.hasAttribute('aria-disabled')).to.be.true; expect(el.getAttribute('aria-disabled')).to.equal('true'); }); + + describe('dev mode warnings', () => { + let warningMessage: typeof window.__swc.warn; + + beforeEach(() => { + // Create __swc if it doesn't exist + window.__swc = window.__swc || { warn: () => {} }; + // Store original warn function + warningMessage = window.__swc.warn; + // Reset issued warnings to avoid dedupe interference + window.__swc.issuedWarnings = new Set(); + // Enable debug guard + window.__swc.DEBUG = true; + }); + + afterEach(() => { + // Restore original warn function + window.__swc.warn = warningMessage; + }); + + it('warns when unsupported variant is used (brown)', async () => { + const warnSpy = spy(); + window.__swc.warn = warnSpy as unknown as typeof window.__swc.warn; + + const el = await fixture(html` + + `); + + await elementUpdated(el); + + expect(warnSpy.called).to.be.true; + expect(warnSpy.firstCall.args[0]).to.equal(el); + expect(warnSpy.firstCall.args[1]).to.equal( + `<${el.localName}> element expects the "variant" attribute to be one of the following:` + ); + }); + }); }); diff --git a/second-gen/packages/core/components/status-light/StatusLight.base.ts b/second-gen/packages/core/components/status-light/StatusLight.base.ts index e699a45fd9c..820ee53a56e 100644 --- a/second-gen/packages/core/components/status-light/StatusLight.base.ts +++ b/second-gen/packages/core/components/status-light/StatusLight.base.ts @@ -9,52 +9,108 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ - import { PropertyValues } from 'lit'; import { property } from 'lit/decorators.js'; import { SizedMixin, SpectrumElement } from '@swc/core/shared/base'; +import { type StatusLightVariant } from './StatusLight.types'; + /** - * @element sp-status-light + * A status light is a great way to convey semantic meaning and the condition of an entity, such as statuses and categories. It provides visual indicators through colored dots accompanied by descriptive text. * - * @slot - text label of the Status Light + * @slot - The text label of the status light. */ export abstract class StatusLightBase extends SizedMixin(SpectrumElement, { noDefaultSize: true, }) { + // ───────────────────────── + // API TO OVERRIDE + // ───────────────────────── + + /** + * @internal + * + * A readonly array of the valid color variants for the status light. + * + * This is an actual internal property, intended not for customer use + * but for use in internal validation logic, stories, tests, etc. + * + * Because S1 and S2 support different color variants, the value of this + * property must be set in each subclass. + */ + static readonly VARIANTS_COLOR: readonly string[]; + /** - * A status light in a disabled state shows that a status exists, but is not available in that circumstance. This can be used to maintain layout continuity and communicate that a status may become available later. + * @internal + * + * A readonly array of the valid semantic variants for the status light. + * + * This is an actual internal property, intended not for customer use + * but for use in internal validation logic, stories, tests, etc. + * + * Because S1 and S2 support different semantic variants, the value of this + * property must be set in each subclass. */ - @property({ type: Boolean, reflect: true }) - public disabled = false; + static readonly VARIANTS_SEMANTIC: readonly string[]; /** - * The visual variant to apply to this status light. + * @internal + * + * A readonly array of all valid variants for the status light. + * + * This is an actual internal property, intended not for customer use + * but for use in internal validation logic, stories, tests, etc. + * + * Because S1 and S2 support different variants, the value of this + * property must be set in each subclass. */ - @property({ reflect: true }) - public variant: - | 'negative' - | 'notice' - | 'positive' - | 'info' - | 'neutral' - | 'yellow' - | 'fuchsia' - | 'indigo' - | 'seafoam' - | 'chartreuse' - | 'magenta' - | 'celery' - | 'purple' = 'info'; + static readonly VARIANTS: readonly string[]; + + /** + * @internal + * + * The variant of the status light. + * + * This is a public property, but its valid values vary between S1 and S2, + * so the property (and its docs) need to be redefined in each subclass. + * + * The type declared here is a union of the valid values for S1 and S2, + * and should be narrowed in each subclass. + */ + @property({ type: String, reflect: true }) + public variant: StatusLightVariant = 'info'; + + // ────────────────────── + // IMPLEMENTATION + // ────────────────────── protected override updated(changes: PropertyValues): void { super.updated(changes); - if (changes.has('disabled')) { - if (this.disabled) { - this.setAttribute('aria-disabled', 'true'); - } else { - this.removeAttribute('aria-disabled'); + if (window.__swc?.DEBUG) { + const constructor = this.constructor as typeof StatusLightBase; + if (!constructor.VARIANTS.includes(this.variant)) { + window.__swc.warn( + this, + `<${this.localName}> element expects the "variant" attribute to be one of the following:`, + 'https://opensource.adobe.com/spectrum-web-components/components/status-light/#variants', + { + issues: [...constructor.VARIANTS], + } + ); + } + // Check disabled property if it exists (S1 only) + if (this.hasAttribute('disabled') && !('disabled' in this)) { + window.__swc.warn( + this, + `<${this.localName}> element does not support the disabled state.`, + 'https://opensource.adobe.com/spectrum-web-components/components/status-light/#states', + { + issues: [ + 'disabled is not a supported property in Spectrum 2', + ], + } + ); } } } diff --git a/second-gen/packages/core/components/status-light/StatusLight.types.ts b/second-gen/packages/core/components/status-light/StatusLight.types.ts new file mode 100644 index 00000000000..170247a15fe --- /dev/null +++ b/second-gen/packages/core/components/status-light/StatusLight.types.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* + * @todo The S1 types can be removed once we are no longer maintaining 1st-gen. + */ + +export const STATUSLIGHT_VARIANTS_SEMANTIC = [ + 'neutral', + 'info', + 'positive', + 'negative', + 'notice', +] as const; + +export const STATUSLIGHT_VARIANTS_SEMANTIC_S1 = [ + ...STATUSLIGHT_VARIANTS_SEMANTIC, + 'accent', +] as const; + +export const STATUSLIGHT_VARIANTS_SEMANTIC_S2 = [ + ...STATUSLIGHT_VARIANTS_SEMANTIC, +] as const; + +export const STATUSLIGHT_VARIANTS_COLOR_S1 = [ + 'fuchsia', + 'indigo', + 'magenta', + 'purple', + 'seafoam', + 'yellow', + 'chartreuse', + 'celery', + 'cyan', +] as const; + +export const STATUSLIGHT_VARIANTS_COLOR_S2 = [ + ...STATUSLIGHT_VARIANTS_COLOR_S1, + 'pink', + 'turquoise', + 'brown', + 'cinnamon', + 'silver', +] as const; + +export const STATUSLIGHT_VARIANTS_S1 = [ + ...STATUSLIGHT_VARIANTS_SEMANTIC_S1, + ...STATUSLIGHT_VARIANTS_COLOR_S1, +] as const; + +export const STATUSLIGHT_VARIANTS_S2 = [ + ...STATUSLIGHT_VARIANTS_SEMANTIC_S2, + ...STATUSLIGHT_VARIANTS_COLOR_S2, +] as const; + +export type StatusLightVariantS1 = (typeof STATUSLIGHT_VARIANTS_S1)[number]; +export type StatusLightVariantS2 = (typeof STATUSLIGHT_VARIANTS_S2)[number]; +export type StatusLightVariant = StatusLightVariantS1 | StatusLightVariantS2; diff --git a/second-gen/packages/core/components/status-light/index.ts b/second-gen/packages/core/components/status-light/index.ts index c76f68cde75..18c05012178 100644 --- a/second-gen/packages/core/components/status-light/index.ts +++ b/second-gen/packages/core/components/status-light/index.ts @@ -10,3 +10,4 @@ * governing permissions and limitations under the License. */ export * from './StatusLight.base'; +export * from './StatusLight.types'; diff --git a/second-gen/packages/swc/components/status-light/StatusLight.ts b/second-gen/packages/swc/components/status-light/StatusLight.ts index ef51cc59a2e..5d9bcbfca04 100644 --- a/second-gen/packages/swc/components/status-light/StatusLight.ts +++ b/second-gen/packages/swc/components/status-light/StatusLight.ts @@ -11,22 +11,78 @@ */ import { CSSResultArray, html, TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; -import { StatusLightBase } from '@swc/core/components/status-light'; +import { + STATUSLIGHT_VARIANTS_COLOR_S2, + STATUSLIGHT_VARIANTS_S2, + STATUSLIGHT_VARIANTS_SEMANTIC_S2, + StatusLightBase, + type StatusLightVariantS2 as StatusLightVariant, +} from '@swc/core/components/status-light'; -import statusLightStyles from './status-light.css'; +import styles from './status-light.css'; /** - * @element sp-status-light + * A status light is a great way to convey semantic meaning and the condition of an entity, such as statuses and categories. It provides visual indicators through colored dots accompanied by descriptive text. * - * @slot - text label of the Status Light + * @element swc-status-light + * + * @example + * Approved + * + * @example + * Supported in Edge */ export class StatusLight extends StatusLightBase { + // ──────────────────── + // API OVERRIDES + // ──────────────────── + + /** + * @internal + */ + static override readonly VARIANTS_COLOR = STATUSLIGHT_VARIANTS_COLOR_S2; + + /** + * @internal + */ + static override readonly VARIANTS_SEMANTIC = + STATUSLIGHT_VARIANTS_SEMANTIC_S2; + + /** + * @internal + */ + static override readonly VARIANTS = STATUSLIGHT_VARIANTS_S2; + + /** + * Changes the color of the status dot. The variant list includes both semantic and non-semantic options. + */ + @property({ type: String, reflect: true }) + public override variant: StatusLightVariant = 'info'; + + // ────────────────────────────── + // RENDERING & STYLING + // ────────────────────────────── + public static override get styles(): CSSResultArray { - return [statusLightStyles]; + return [styles]; } protected override render(): TemplateResult { - return html` `; + return html` +
+ +
+ `; } } diff --git a/second-gen/packages/swc/components/status-light/status-light.css b/second-gen/packages/swc/components/status-light/status-light.css index c392881bdc4..e4ad20d2a3c 100644 --- a/second-gen/packages/swc/components/status-light/status-light.css +++ b/second-gen/packages/swc/components/status-light/status-light.css @@ -9,16 +9,28 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +.spectrum-StatusLight { + /* Static tokens */ + --spectrum-statuslight-corner-radius: var(--spectrum-corner-radius-full); + --spectrum-statuslight-border-width: var(--spectrum-border-width-100); -:host([dir]), -:host { + /* Size */ --spectrum-statuslight-height: var(--spectrum-component-height-100); --spectrum-statuslight-dot-size: var( --spectrum-status-light-dot-size-medium ); + --spectrum-statuslight-line-height: var(--spectrum-line-height-100); + --spectrum-statuslight-line-height-cjk: var(--spectrum-cjk-line-height-100); + + /* Font */ + --spectrum-statuslight-font-family: var(--spectrum-sans-font-family-stack); + --spectrum-statuslight-font-weight: var(--spectrum-regular-font-weight); + --spectrum-statuslight-font-style: var(--spectrum-default-font-style); --spectrum-statuslight-font-size: var(--spectrum-font-size-100); + + /* Space */ --spectrum-statuslight-spacing-dot-to-label: var( - --spectrum-text-to-visual-100 + --spectrum-status-light-text-to-visual-100 ); --spectrum-statuslight-spacing-top-to-dot: var( --spectrum-status-light-top-to-dot-medium @@ -29,86 +41,17 @@ --spectrum-statuslight-spacing-bottom-to-label: var( --spectrum-component-bottom-to-text-100 ); -} -:host([size='s']) { - --spectrum-statuslight-height: var(--spectrum-component-height-75); - --spectrum-statuslight-dot-size: var( - --spectrum-status-light-dot-size-small - ); - --spectrum-statuslight-font-size: var(--spectrum-font-size-75); - --spectrum-statuslight-spacing-dot-to-label: var( - --spectrum-text-to-visual-75 - ); - --spectrum-statuslight-spacing-top-to-dot: var( - --spectrum-status-light-top-to-dot-small - ); - --spectrum-statuslight-spacing-top-to-label: var( - --spectrum-component-top-to-text-75 - ); - --spectrum-statuslight-spacing-bottom-to-label: var( - --spectrum-component-bottom-to-text-75 - ); -} - -:host([size='l']) { - --spectrum-statuslight-height: var(--spectrum-component-height-200); - --spectrum-statuslight-dot-size: var( - --spectrum-status-light-dot-size-large - ); - --spectrum-statuslight-font-size: var(--spectrum-font-size-200); - --spectrum-statuslight-spacing-dot-to-label: var( - --spectrum-text-to-visual-200 - ); - --spectrum-statuslight-spacing-top-to-dot: var( - --spectrum-status-light-top-to-dot-large - ); - --spectrum-statuslight-spacing-top-to-label: var( - --spectrum-component-top-to-text-200 - ); - --spectrum-statuslight-spacing-bottom-to-label: var( - --spectrum-component-bottom-to-text-200 - ); -} - -:host([size='xl']) { - --spectrum-statuslight-height: var(--spectrum-component-height-300); - --spectrum-statuslight-dot-size: var( - --spectrum-status-light-dot-size-extra-large - ); - --spectrum-statuslight-font-size: var(--spectrum-font-size-300); - --spectrum-statuslight-spacing-dot-to-label: var( - --spectrum-text-to-visual-300 - ); - --spectrum-statuslight-spacing-top-to-dot: var( - --spectrum-status-light-top-to-dot-extra-large - ); - --spectrum-statuslight-spacing-top-to-label: var( - --spectrum-component-top-to-text-300 - ); - --spectrum-statuslight-spacing-bottom-to-label: var( - --spectrum-component-bottom-to-text-300 - ); -} - -:host([dir]) { - --spectrum-statuslight-corner-radius: 50%; - --spectrum-statuslight-font-weight: 400; - --spectrum-statuslight-border-width: var(--spectrum-border-width-100); - --spectrum-statuslight-line-height: var(--spectrum-line-height-100); - --spectrum-statuslight-line-height-cjk: var(--spectrum-cjk-line-height-100); + /* Color */ --spectrum-statuslight-content-color-default: var( --spectrum-neutral-content-color-default ); --spectrum-statuslight-subdued-content-color-default: var( - --spectrum-neutral-subdued-content-color-default + --spectrum-gray-600 ); --spectrum-statuslight-semantic-neutral-color: var( --spectrum-neutral-visual-color ); - --spectrum-statuslight-semantic-accent-color: var( - --spectrum-accent-visual-color - ); --spectrum-statuslight-semantic-negative-color: var( --spectrum-negative-visual-color ); @@ -121,15 +64,8 @@ --spectrum-statuslight-semantic-positive-color: var( --spectrum-positive-visual-color ); - --spectrum-statuslight-nonsemantic-gray-color: var( - --spectrum-gray-visual-color - ); - --spectrum-statuslight-nonsemantic-red-color: var( - --spectrum-red-visual-color - ); - --spectrum-statuslight-nonsemantic-orange-color: var( - --spectrum-orange-visual-color - ); + + /* Non-Semantic Colors */ --spectrum-statuslight-nonsemantic-yellow-color: var( --spectrum-yellow-visual-color ); @@ -139,18 +75,12 @@ --spectrum-statuslight-nonsemantic-celery-color: var( --spectrum-celery-visual-color ); - --spectrum-statuslight-nonsemantic-green-color: var( - --spectrum-green-visual-color - ); --spectrum-statuslight-nonsemantic-seafoam-color: var( --spectrum-seafoam-visual-color ); --spectrum-statuslight-nonsemantic-cyan-color: var( --spectrum-cyan-visual-color ); - --spectrum-statuslight-nonsemantic-blue-color: var( - --spectrum-blue-visual-color - ); --spectrum-statuslight-nonsemantic-indigo-color: var( --spectrum-indigo-visual-color ); @@ -163,255 +93,222 @@ --spectrum-statuslight-nonsemantic-magenta-color: var( --spectrum-magenta-visual-color ); - - min-block-size: var( - --mod-statuslight-height, - var(--spectrum-statuslight-height) + --spectrum-statuslight-nonsemantic-pink-color: var( + --spectrum-pink-visual-color ); - box-sizing: border-box; - font-size: var( - --mod-statuslight-font-size, - var(--spectrum-statuslight-font-size) - ); - font-weight: 400; - font-weight: var( - --mod-statuslight-font-weight, - var(--spectrum-statuslight-font-weight) - ); - line-height: var( - --mod-statuslight-line-height, - var(--spectrum-statuslight-line-height) - ); - color: var( - --highcontrast-statuslight-content-color-default, - var( - --mod-statuslight-content-color-default, - var(--spectrum-statuslight-content-color-default) - ) + --spectrum-statuslight-nonsemantic-turquoise-color: var( + --spectrum-turquoise-visual-color ); - flex-direction: row; - align-items: flex-start; - padding-block-start: var( - --mod-statuslight-spacing-top-to-label, - var(--spectrum-statuslight-spacing-top-to-label) + --spectrum-statuslight-nonsemantic-cinnamon-color: var( + --spectrum-cinnamon-visual-color ); - padding-block-end: var( - --mod-statuslight-spacing-bottom-to-label, - var(--spectrum-statuslight-spacing-bottom-to-label) + --spectrum-statuslight-nonsemantic-brown-color: var( + --spectrum-brown-visual-color + ); + --spectrum-statuslight-nonsemantic-silver-color: var( + --spectrum-silver-visual-color ); - padding-inline: 0; - display: flex; } -:host(:lang(ja)), -:host(:lang(ko)), -:host(:lang(zh)) { - line-height: var( - --mod-statuslight-line-height-cjk, - var(--spectrum-statuslight-line-height-cjk) +.spectrum-StatusLight--sizeS { + --spectrum-statuslight-height: var(--spectrum-component-height-75); + --spectrum-statuslight-dot-size: var( + --spectrum-status-light-dot-size-small + ); + --spectrum-statuslight-font-size: var(--spectrum-font-size-75); + --spectrum-statuslight-spacing-dot-to-label: var( + --spectrum-status-light-text-to-visual-75 + ); + --spectrum-statuslight-spacing-top-to-dot: var( + --spectrum-status-light-top-to-dot-small + ); + --spectrum-statuslight-spacing-top-to-label: var( + --spectrum-component-top-to-text-75 + ); + --spectrum-statuslight-spacing-bottom-to-label: var( + --spectrum-component-bottom-to-text-75 ); } -:host:before { - --spectrum-statuslight-spacing-computed-top-to-dot: calc( - var( - --mod-statuslight-spacing-top-to-dot, - var(--spectrum-statuslight-spacing-top-to-dot) - ) - - var( - --mod-statuslight-spacing-top-to-label, - var(--spectrum-statuslight-spacing-top-to-label) - ) - ); - - content: ''; - inline-size: var( - --mod-statuslight-dot-size, - var(--spectrum-statuslight-dot-size) - ); - block-size: var( - --mod-statuslight-dot-size, - var(--spectrum-statuslight-dot-size) - ); - border-radius: var( - --mod-statuslight-corner-radius, - var(--spectrum-statuslight-corner-radius) - ); - flex-grow: 0; - flex-shrink: 0; - margin-block-start: var(--spectrum-statuslight-spacing-computed-top-to-dot); - margin-inline-end: var( - --mod-statuslight-spacing-dot-to-label, - var(--spectrum-statuslight-spacing-dot-to-label) - ); - display: inline-block; +.spectrum-StatusLight--sizeL { + --spectrum-statuslight-height: var(--spectrum-component-height-200); + --spectrum-statuslight-dot-size: var( + --spectrum-status-light-dot-size-large + ); + --spectrum-statuslight-font-size: var(--spectrum-font-size-200); + --spectrum-statuslight-spacing-dot-to-label: var( + --spectrum-status-light-text-to-visual-200 + ); + --spectrum-statuslight-spacing-top-to-dot: var( + --spectrum-status-light-top-to-dot-large + ); + --spectrum-statuslight-spacing-top-to-label: var( + --spectrum-component-top-to-text-200 + ); + --spectrum-statuslight-spacing-bottom-to-label: var( + --spectrum-component-bottom-to-text-200 + ); } -:host([variant='neutral']) { - color: var( - --highcontrast-statuslight-subdued-content-color-default, - var( - --mod-statuslight-subdued-content-color-default, - var(--spectrum-statuslight-subdued-content-color-default) - ) +.spectrum-StatusLight--sizeXL { + --spectrum-statuslight-height: var(--spectrum-component-height-300); + --spectrum-statuslight-dot-size: var( + --spectrum-status-light-dot-size-extra-large + ); + --spectrum-statuslight-font-size: var(--spectrum-font-size-300); + --spectrum-statuslight-spacing-dot-to-label: var( + --spectrum-status-light-text-to-visual-300 + ); + --spectrum-statuslight-spacing-top-to-dot: var( + --spectrum-status-light-top-to-dot-extra-large + ); + --spectrum-statuslight-spacing-top-to-label: var( + --spectrum-component-top-to-text-300 + ); + --spectrum-statuslight-spacing-bottom-to-label: var( + --spectrum-component-bottom-to-text-300 ); - font-style: italic; } -:host([variant='neutral']):before { - background-color: var( - --mod-statuslight-semantic-neutral-color, - var(--spectrum-statuslight-semantic-neutral-color) - ); +:host { + display: flex; } -.spectrum-StatusLight--accent:before { - background-color: var( - --mod-statuslight-semantic-accent-color, - var(--spectrum-statuslight-semantic-accent-color) - ); +.spectrum-StatusLight { + display: flex; + flex-direction: row; + align-items: flex-start; + min-block-size: var(--spectrum-statuslight-height); + padding-block-start: var(--spectrum-statuslight-spacing-top-to-label); + padding-block-end: var(--spectrum-statuslight-spacing-bottom-to-label); + padding-inline: 0; + box-sizing: border-box; + font-size: var(--spectrum-statuslight-font-size); + font-weight: var(--spectrum-statuslight-font-weight); + font-family: var(--spectrum-statuslight-font-family); + font-style: var(--spectrum-statuslight-font-style); + line-height: var(--spectrum-statuslight-line-height); + color: var(--highcontrast-statuslight-content-color-default, var(--spectrum-statuslight-content-color-default)); + + &:lang(ja), + &:lang(zh), + &:lang(ko) { + line-height: var(--spectrum-statuslight-line-height-cjk); + } + + /* Dot */ + &:before { + --spectrum-statuslight-spacing-computed-top-to-dot: calc( + var(--spectrum-statuslight-spacing-top-to-dot) + - + var(--spectrum-statuslight-spacing-top-to-label) + ); + + content: ''; + flex-grow: 0; + flex-shrink: 0; + display: inline-block; + inline-size: var(--spectrum-statuslight-dot-size); + block-size: var(--spectrum-statuslight-dot-size); + border-radius: var(--spectrum-statuslight-corner-radius); + margin-block-start: var(--spectrum-statuslight-spacing-computed-top-to-dot); + margin-inline-end: var(--spectrum-statuslight-spacing-dot-to-label); + } } -:host([variant='info']):before { - background-color: var( - --mod-statuslight-semantic-info-color, - var(--spectrum-statuslight-semantic-info-color) - ); +/* Semantic Colors */ +.spectrum-StatusLight--neutral { + color: var(--highcontrast-statuslight-subdued-content-color-default, var(--spectrum-statuslight-subdued-content-color-default)); + + &:before { + background-color: var(--spectrum-statuslight-semantic-neutral-color); + } } -:host([variant='negative']):before { - background-color: var( - --mod-statuslight-semantic-negative-color, - var(--spectrum-statuslight-semantic-negative-color) - ); +.spectrum-StatusLight--info:before { + background-color: var(--spectrum-statuslight-semantic-info-color); } -:host([variant='notice']):before { - background-color: var( - --mod-statuslight-semantic-notice-color, - var(--spectrum-statuslight-semantic-notice-color) - ); +.spectrum-StatusLight--negative:before { + background-color: var(--spectrum-statuslight-semantic-negative-color); } -:host([variant='positive']):before { - background-color: var( - --mod-statuslight-semantic-positive-color, - var(--spectrum-statuslight-semantic-positive-color) - ); +.spectrum-StatusLight--notice:before { + background-color: var(--spectrum-statuslight-semantic-notice-color); } -.spectrum-StatusLight--gray:before { - background-color: var( - --mod-statuslight-nonsemantic-gray-color, - var(--spectrum-statuslight-nonsemantic-gray-color) - ); +.spectrum-StatusLight--positive:before { + background-color: var(--spectrum-statuslight-semantic-positive-color); } -.spectrum-StatusLight--red:before { - background-color: var( - --mod-statuslight-nonsemantic-red-color, - var(--spectrum-statuslight-nonsemantic-red-color) - ); +/* Non-Semantic Colors */ +.spectrum-StatusLight--yellow:before { + background-color: var(--spectrum-statuslight-nonsemantic-yellow-color); } -.spectrum-StatusLight--orange:before { - background-color: var( - --mod-statuslight-nonsemantic-orange-color, - var(--spectrum-statuslight-nonsemantic-orange-color) - ); +.spectrum-StatusLight--chartreuse:before { + background-color: var(--spectrum-statuslight-nonsemantic-chartreuse-color); } -:host([variant='yellow']):before { - background-color: var( - --mod-statuslight-nonsemantic-yellow-color, - var(--spectrum-statuslight-nonsemantic-yellow-color) - ); +.spectrum-StatusLight--celery:before { + background-color: var(--spectrum-statuslight-nonsemantic-celery-color); } -:host([variant='chartreuse']):before { - background-color: var( - --mod-statuslight-nonsemantic-chartreuse-color, - var(--spectrum-statuslight-nonsemantic-chartreuse-color) - ); +.spectrum-StatusLight--seafoam:before { + background-color: var(--spectrum-statuslight-nonsemantic-seafoam-color); } -:host([variant='celery']):before { - background-color: var( - --mod-statuslight-nonsemantic-celery-color, - var(--spectrum-statuslight-nonsemantic-celery-color) - ); +.spectrum-StatusLight--cyan:before { + background-color: var(--spectrum-statuslight-nonsemantic-cyan-color); } -.spectrum-StatusLight--green:before { - background-color: var( - --mod-statuslight-nonsemantic-green-color, - var(--spectrum-statuslight-nonsemantic-green-color) - ); +.spectrum-StatusLight--indigo:before { + background-color: var(--spectrum-statuslight-nonsemantic-indigo-color); } -:host([variant='seafoam']):before { - background-color: var( - --mod-statuslight-nonsemantic-seafoam-color, - var(--spectrum-statuslight-nonsemantic-seafoam-color) - ); +.spectrum-StatusLight--purple:before { + background-color: var(--spectrum-statuslight-nonsemantic-purple-color); } -.spectrum-StatusLight--cyan:before { - background-color: var( - --mod-statuslight-nonsemantic-cyan-color, - var(--spectrum-statuslight-nonsemantic-cyan-color) - ); +.spectrum-StatusLight--fuchsia:before { + background-color: var(--spectrum-statuslight-nonsemantic-fuchsia-color); } -.spectrum-StatusLight--blue:before { - background-color: var( - --mod-statuslight-nonsemantic-blue-color, - var(--spectrum-statuslight-nonsemantic-blue-color) - ); +.spectrum-StatusLight--magenta:before { + background-color: var(--spectrum-statuslight-nonsemantic-magenta-color); } -:host([variant='indigo']):before { - background-color: var( - --mod-statuslight-nonsemantic-indigo-color, - var(--spectrum-statuslight-nonsemantic-indigo-color) - ); +.spectrum-StatusLight--pink:before { + background-color: var(--spectrum-statuslight-nonsemantic-pink-color); } -:host([variant='purple']):before { - background-color: var( - --mod-statuslight-nonsemantic-purple-color, - var(--spectrum-statuslight-nonsemantic-purple-color) - ); +.spectrum-StatusLight--turquoise:before { + background-color: var(--spectrum-statuslight-nonsemantic-turquoise-color); } -:host([variant='fuchsia']):before { - background-color: var( - --mod-statuslight-nonsemantic-fuchsia-color, - var(--spectrum-statuslight-nonsemantic-fuchsia-color) - ); +.spectrum-StatusLight--cinnamon:before { + background-color: var(--spectrum-statuslight-nonsemantic-cinnamon-color); } -:host([variant='magenta']):before { - background-color: var( - --mod-statuslight-nonsemantic-magenta-color, - var(--spectrum-statuslight-nonsemantic-magenta-color) - ); +.spectrum-StatusLight--brown:before { + background-color: var(--spectrum-statuslight-nonsemantic-brown-color); +} + +.spectrum-StatusLight--silver:before { + background-color: var(--spectrum-statuslight-nonsemantic-silver-color); } @media (forced-colors: active) { - :host([dir]) { + .spectrum-StatusLight { --highcontrast-statuslight-content-color-default: CanvasText; --highcontrast-statuslight-subdued-content-color-default: CanvasText; forced-color-adjust: none; - } - :host:before { - forced-color-adjust: none; - border: var( - --mod-statuslight-border-width, - var(--spectrum-statuslight-border-width) - ) - solid ButtonText; + /* Dot */ + &:before { + forced-color-adjust: none; + border: var(--spectrum-statuslight-border-width) solid ButtonText; + } } } diff --git a/second-gen/packages/swc/components/status-light/stories/status-light.stories.ts b/second-gen/packages/swc/components/status-light/stories/status-light.stories.ts index fcde78d68ce..958690ec558 100644 --- a/second-gen/packages/swc/components/status-light/stories/status-light.stories.ts +++ b/second-gen/packages/swc/components/status-light/stories/status-light.stories.ts @@ -10,88 +10,187 @@ * governing permissions and limitations under the License. */ import { html, TemplateResult } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; +import type { Meta, StoryObj as Story } from '@storybook/web-components'; +import { getStorybookHelpers } from '@wc-toolkit/storybook-helpers'; + +import { StatusLight } from '@swc/components/status-light'; import '@swc/components/status-light'; -export default { - component: 'swc-status-light', +// ──────────────── +// METADATA +// ──────────────── + +const { args, argTypes, template } = getStorybookHelpers('swc-status-light'); + +argTypes.variant = { + ...argTypes.variant, + control: { type: 'select' }, + options: StatusLight.VARIANTS, +}; + +/* + * @todo This is properly configuring the Select, but the control doesn't + * seem to work; need to investigate. + * + * We may have to explicitly bind the args to the component (particularly + * helpful for the size property) so the Storybook controls work as expected. + * + * i.e. render: (args) => + html`${args['default-slot']}`, + */ +// argTypes.size = { +// ...argTypes.size, +// control: { type: 'select' }, +// options: StatusLight.VALID_SIZES, +// }; + +args['default-slot'] = 'Status light'; +args.size = 'm'; + +const meta: Meta = { title: 'Status light', + component: 'swc-status-light', + argTypes, + parameters: {}, + args, + render: (args) => template(args), + tags: ['migrated'], }; -export const s = (): TemplateResult => html` - positive - negative - notice - info - neutral - yellow - fuchsia - indigo - seafoam - chartreuse - magenta - celery - purple -`; - -export const m = (): TemplateResult => html` - positive - negative - notice - info - neutral - yellow - fuchsia - indigo - seafoam - chartreuse - magenta - celery - purple -`; - -export const l = (): TemplateResult => html` - positive - negative - notice - info - neutral - yellow - fuchsia - indigo - seafoam - chartreuse - magenta - celery - purple -`; - -export const XL = (): TemplateResult => html` - positive - negative - notice - info - neutral - yellow - fuchsia - indigo - seafoam - chartreuse - magenta - celery - purple -`; +export default meta; -export const disabledTrue = (): TemplateResult => html` - positive -`; +// ─────────────── +// STORIES +// ─────────────── -disabledTrue.storyName = 'disabled: true'; +type StatusLightVariant = typeof StatusLight.prototype.variant; +type StatusLightSize = typeof StatusLight.prototype.size; + +/** + * Status lights should always include a label with text that clearly communicates the kind of status being shown. Color + * alone is not enough to communicate the status. Do not change the text color to match the dot. + */ +export const Default: Story = {}; + +/** When the text is too long for the horizontal space available, it wraps to form another line. */ +export const TextWrapping: Story = { + render: () => + html` + This is a very long status light label that wraps when it reaches + its max inline size + `, + tags: ['!dev'], +}; +TextWrapping.storyName = 'Text wrapping'; + +/** + * When status lights have a semantic meaning, they use semantic colors. Use these variants for the following statuses: + * - Informative (active, in use, live, published) + * - Neutral (archived, deleted, paused, draft, not started, ended) + * - Positive (approved, complete, success, new, purchased, licensed) + * - Notice (needs approval, pending, scheduled, syncing, indexing, processing) + * - Negative (error, alert, rejected, failed) + * + * Semantic status lights should never be used for color coding categories or labels, and vice versa. + */ +export const SemanticVariants: Story = { + render: () => + CONTAINER( + StatusLight.VARIANTS_SEMANTIC.map( + (variant: StatusLightVariant) => html` + ${capitalize(variant)} + ` + ) + ), + tags: ['!dev'], +}; +SemanticVariants.storyName = 'Semantic variants'; + +/** + * When status lights are used to color code categories and labels that are commonly found in data visualization, + * they use label colors. The ideal usage for these is when there are 8 or fewer categories or labels being color coded. + */ +export const NonsemanticVariants: Story = { + render: () => + CONTAINER( + StatusLight.VARIANTS_COLOR.map( + (variant: StatusLightVariant) => html` + ${capitalize(variant)} + ` + ) + ), + tags: ['!dev'], +}; +NonsemanticVariants.storyName = 'Non-semantic variants'; + +/** + * Status lights come in four different sizes: small, medium, large, and extra-large. The medium size is the + * default and most frequently used option. Use the other sizes sparingly; they should be used to create a + * hierarchy of importance within the page. + */ +export const Sizes: Story = { + render: () => + CONTAINER( + StatusLight.VALID_SIZES.map( + (size: StatusLightSize) => html` + ${sizeMap(size)} + ` + ) + ), + tags: ['!dev'], +}; + +// ──────────────────────── +// HELPER FUNCTIONS +// ──────────────────────── + +/* @todo Pull this up into a utility function for all components to leverage */ +function capitalize(str?: string): string { + if (typeof str !== 'string') { + return ''; + } + return str.charAt(0).toUpperCase() + str.slice(1); +} + +/* @todo Pull this up into a utility function for more components to leverage. Are all sizes accounted for? */ +function sizeMap(str?: StatusLightSize): string { + const sizeLabels = { + labels: { + xxs: 'Extra-extra-small', + xs: 'Extra-small', + s: 'Small', + m: 'Medium', + l: 'Large', + xl: 'Extra-large', + xxl: 'Extra-extra-large', + }, + }; + + return str ? sizeLabels.labels[str] : ''; +} + +/* @todo Pull this up into a decorator for all stories to leverage */ +function CONTAINER(content: TemplateResult<1>[]): TemplateResult { + return html`
+ ${content} +
`; +} diff --git a/second-gen/packages/swc/components/status-light/test/status-light.test.ts b/second-gen/packages/swc/components/status-light/test/status-light.test.ts new file mode 100644 index 00000000000..56ccab8d4a7 --- /dev/null +++ b/second-gen/packages/swc/components/status-light/test/status-light.test.ts @@ -0,0 +1,264 @@ +/** + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { html } from 'lit'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import type { StatusLight } from '@swc/components/status-light'; + +import '@swc/components/status-light'; + +import { fixture } from '../../../utils/test-utils.js'; + +describe('swc-status-light', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + // ────────────────────────────────────────────────────────────── + // TEST: Defaults + // ────────────────────────────────────────────────────────────── + + describe('defaults', () => { + test('should render with shadow root', async () => { + const statusLight = await fixture(html` + Test Status + `); + + expect(statusLight.shadowRoot).toBeTruthy(); + expect( + statusLight.shadowRoot?.querySelector('.spectrum-StatusLight') + ).toBeTruthy(); + }); + + test('should have correct default property values', async () => { + const statusLight = await fixture( + html`` + ); + + expect(statusLight.variant).toBe('info'); + expect(statusLight.size).toBe('m'); + }); + }); + + // ────────────────────────────────────────────────────────────── + // TEST: Properties / Attributes + // ────────────────────────────────────────────────────────────── + + describe('properties and attributes', () => { + test('should reflect variant property to attribute', async () => { + const statusLight = await fixture( + html`` + ); + + statusLight.variant = 'positive'; + await statusLight.updateComplete; + + expect(statusLight.getAttribute('variant')).toBe('positive'); + expect( + statusLight.shadowRoot?.querySelector( + '.spectrum-StatusLight--positive' + ) + ).toBeTruthy(); + }); + + test('should set variant via attribute', async () => { + const statusLight = await fixture(html` + + `); + + expect(statusLight.variant).toBe('negative'); + expect( + statusLight.shadowRoot?.querySelector( + '.spectrum-StatusLight--negative' + ) + ).toBeTruthy(); + }); + + test('should handle semantic variants', async () => { + const variants = [ + 'neutral', + 'info', + 'positive', + 'negative', + 'notice', + ] as const; + + for (const variant of variants) { + const statusLight = await fixture(html` + + `); + + expect(statusLight.variant).toBe(variant); + expect( + statusLight.shadowRoot?.querySelector( + `.spectrum-StatusLight--${variant}` + ) + ).toBeTruthy(); + } + }); + + test('should handle color variants', async () => { + const colorVariants = [ + 'fuchsia', + 'indigo', + 'magenta', + 'purple', + 'seafoam', + 'yellow', + 'chartreuse', + 'celery', + 'cyan', + 'pink', + 'turquoise', + 'brown', + 'cinnamon', + 'silver', + ] as const; + + for (const variant of colorVariants) { + const statusLight = await fixture(html` + + `); + + expect(statusLight.variant).toBe(variant); + expect( + statusLight.shadowRoot?.querySelector( + `.spectrum-StatusLight--${variant}` + ) + ).toBeTruthy(); + } + }); + + test('should handle size property', async () => { + const statusLight = await fixture( + html`` + ); + + expect(statusLight.size).toBe('m'); + + statusLight.size = 's'; + await statusLight.updateComplete; + + expect(statusLight.getAttribute('size')).toBe('s'); + expect( + statusLight.shadowRoot?.querySelector( + '.spectrum-StatusLight--sizeS' + ) + ).toBeTruthy(); + }); + }); + + // ────────────────────────────────────────────────────────────── + // TEST: Slots + // ────────────────────────────────────────────────────────────── + + describe('slots', () => { + test('should render default slot content', async () => { + const statusLight = await fixture(html` + Status Label + `); + + expect(statusLight.textContent).toBe('Status Label'); + }); + }); + + // ────────────────────────────────────────────────────────────── + // TEST: Events + // ────────────────────────────────────────────────────────────── + + describe.skip('events', () => { + // StatusLight component does not dispatch custom events + }); + + // ────────────────────────────────────────────────────────────── + // TEST: Accessibility + // ────────────────────────────────────────────────────────────── + // @TODO: Add accessibility tests with axe-core / playwright + describe('accessibility', () => { + test('should be accessible to screen readers', async () => { + const statusLight = await fixture(html` + Approved + `); + + const statusLightElement = statusLight.shadowRoot?.querySelector( + '.spectrum-StatusLight' + ); + expect(statusLightElement).toBeTruthy(); + expect(statusLight.textContent).toBe('Approved'); + }); + }); + + // ────────────────────────────────────────────────────────────── + // TEST: Dev Mode Warnings + // ────────────────────────────────────────────────────────────── + + describe('dev mode warnings', () => { + let originalWarn: typeof window.__swc.warn; + let originalDebug: boolean; + + beforeEach(() => { + // Create __swc if it doesn't exist + window.__swc = window.__swc || { warn: () => {} }; + // Store original warn function and debug state + originalWarn = window.__swc.warn; + originalDebug = window.__swc.DEBUG ?? false; + // Reset issued warnings to avoid dedupe interference + window.__swc.issuedWarnings = new Set(); + // Enable debug guard + window.__swc.DEBUG = true; + }); + + afterEach(() => { + // Restore original warn function and debug state + window.__swc.warn = originalWarn; + window.__swc.DEBUG = originalDebug; + }); + + test('should warn when unsupported variant is used', async () => { + const warnSpy = vi.fn(); + window.__swc.warn = warnSpy as unknown as typeof window.__swc.warn; + + const statusLight = await fixture(html` + + `); + + await statusLight.updateComplete; + + expect(warnSpy).toHaveBeenCalled(); + expect(warnSpy.mock.calls[0][0]).toBe(statusLight); + expect(warnSpy.mock.calls[0][1]).toBe( + `<${statusLight.localName}> element expects the "variant" attribute to be one of the following:` + ); + }); + + test('should warn when disabled attribute is used', async () => { + const warnSpy = vi.fn(); + window.__swc.warn = warnSpy as unknown as typeof window.__swc.warn; + + const statusLight = await fixture(html` + + `); + + await statusLight.updateComplete; + + expect(warnSpy).toHaveBeenCalled(); + expect(warnSpy.mock.calls[0][0]).toBe(statusLight); + expect(warnSpy.mock.calls[0][1]).toContain( + 'does not support the disabled state' + ); + }); + }); +});