diff --git a/2nd-gen/packages/core/components/asset/Asset.base.ts b/2nd-gen/packages/core/components/asset/Asset.base.ts
index 97ae5602408..73909cd2bd5 100644
--- a/2nd-gen/packages/core/components/asset/Asset.base.ts
+++ b/2nd-gen/packages/core/components/asset/Asset.base.ts
@@ -44,7 +44,7 @@ export abstract class AssetBase extends SpectrumElement {
/**
* Accessible label for the asset’s file or folder variant.
*/
- @property()
+ @property({ type: String })
public label = '';
// ──────────────────────
diff --git a/2nd-gen/packages/core/components/icon/Icon.base.ts b/2nd-gen/packages/core/components/icon/Icon.base.ts
index 6df8ea9919f..b026273300b 100644
--- a/2nd-gen/packages/core/components/icon/Icon.base.ts
+++ b/2nd-gen/packages/core/components/icon/Icon.base.ts
@@ -40,7 +40,7 @@ export abstract class IconBase extends SizedMixin(SpectrumElement, {
/**
* Accessible label for the icon.
*/
- @property()
+ @property({ type: String })
public label = '';
@queryAssignedElements({ flatten: true })
diff --git a/2nd-gen/packages/core/components/progress-circle/ProgressCircle.base.ts b/2nd-gen/packages/core/components/progress-circle/ProgressCircle.base.ts
index a87df672664..e1042bc0d23 100644
--- a/2nd-gen/packages/core/components/progress-circle/ProgressCircle.base.ts
+++ b/2nd-gen/packages/core/components/progress-circle/ProgressCircle.base.ts
@@ -9,6 +9,7 @@
* 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';
@@ -24,6 +25,14 @@ import {
ProgressCircleStaticColor,
} from './ProgressCircle.types.js';
+/**
+ * @todo SWC-1891 Extract shared progress logic (ARIA, label, clamping, formatting,
+ * indeterminate derivation) into a `ProgressBase` mixin or abstract class in
+ * `core/components/progress/` so that both `ProgressCircleBase` and a future
+ * `ProgressBarBase` can extend it. Also add `formatOptions` support for
+ * progress-bar's custom value labels (e.g. "3 of 10", "45 MB / 100 MB").
+ */
+
/**
* A progress circle component that visually represents the completion progress of a task.
* Can be used in both determinate (with specific progress value) and indeterminate (loading) states.
@@ -75,20 +84,15 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
// SHARED API
// ──────────────────
- /**
- * @todo Revisit the default API for `indeterminate` and `progress`. SWC-1891
- *
- * Whether the progress circle shows indeterminate progress (loading state).
- *
- * When true, displays an animated loading indicator instead of a specific progress value.
- */
- @property({ type: Boolean, reflect: true })
- public indeterminate = false;
-
/**
* Accessible label for the progress circle.
*
* Used to provide context about what is loading or progressing.
+ * When no accessible name is provided (no label, aria-label, or
+ * aria-labelledby), a default "Loading" label is applied.
+ *
+ * @todo Localize the default "Loading" fallback via LanguageResolutionController
+ * once a runtime i18n system for static strings is available.
*/
@property({ type: String })
public label = '';
@@ -96,11 +100,15 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
/**
* Progress value from 0 to 100.
*
- * Only relevant when indeterminate is false. Values outside that range or
- * non-finite numbers are clamped to 0–100 (non-finite becomes 0).
+ * When `null` (indeterminate), the component shows a loading animation.
+ * Setting a number switches to determinate mode. Removing the `progress`
+ * attribute or setting this property to `null` returns to indeterminate.
+ * Values outside 0–100 or non-finite numbers are clamped (non-finite becomes 0).
+ *
+ * Reflected to the `progress` attribute when set; the attribute is omitted when indeterminate.
*/
- @property({ type: Number })
- public progress = 0;
+ @property({ type: Number, reflect: true })
+ public progress: number | null = null;
private languageResolver = new LanguageResolutionController(this);
@@ -108,6 +116,12 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
// IMPLEMENTATION
// ──────────────────────
+ /**
+ * @todo Localize via LanguageResolutionController once a runtime i18n
+ * system for static strings is available.
+ */
+ private static readonly DEFAULT_LABEL = 'Loading';
+
/** True when light DOM has element nodes or non-whitespace text (no default slot). */
private static hasMeaningfulLightDomChildren(host: HTMLElement): boolean {
for (const node of host.childNodes) {
@@ -121,6 +135,28 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
return false;
}
+ private hasAccessibleName(): boolean {
+ return Boolean(
+ this.label ||
+ this.getAttribute('aria-label') ||
+ this.getAttribute('aria-labelledby')
+ );
+ }
+
+ private static clampProgress(value: number): number {
+ if (!Number.isFinite(value)) {
+ return 0;
+ }
+ return Math.min(100, Math.max(0, value));
+ }
+
+ private formatProgress(): string {
+ return new Intl.NumberFormat(this.languageResolver.language, {
+ style: 'percent',
+ unitDisplay: 'narrow',
+ }).format((this.progress ?? 0) / 100);
+ }
+
private warnDeprecatedLightDomChildren(): void {
if (!window.__swc?.DEBUG) {
return;
@@ -136,15 +172,27 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
);
}
- private static clampProgress(value: number): number {
- if (!Number.isFinite(value)) {
- return 0;
+ private warnMissingAccessibleName(): void {
+ if (!window.__swc?.DEBUG) {
+ return;
}
- return Math.min(100, Math.max(0, value));
+ window.__swc?.warn(
+ this,
+ `<${this.localName}> requires an accessible name. A default label of "${ProgressCircleBase.DEFAULT_LABEL}" has been applied, but a more specific label should be provided via:`,
+ 'https://opensource.adobe.com/spectrum-web-components/second-gen/?path=/docs/components-progress-circle--docs',
+ {
+ type: 'accessibility',
+ issues: [
+ 'value supplied to the "label" attribute, which will be displayed visually as part of the element, or',
+ 'value supplied to the "aria-label" attribute, which will only be provided to screen readers, or',
+ 'an element ID reference supplied to the "aria-labelledby" attribute, which will be provided by screen readers and will need to be managed manually by the parent application.',
+ ],
+ }
+ );
}
protected override willUpdate(changes: PropertyValues): void {
- if (changes.has('progress')) {
+ if (changes.has('progress') && this.progress !== null) {
const clamped = ProgressCircleBase.clampProgress(this.progress);
if (clamped !== this.progress) {
this.progress = clamped;
@@ -155,40 +203,30 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
protected override firstUpdated(changes: PropertyValues): void {
super.firstUpdated(changes);
- if (!this.hasAttribute('role')) {
- this.setAttribute('role', 'progressbar');
- }
- }
-
- private formatProgress(): string {
- return new Intl.NumberFormat(this.languageResolver.language, {
- style: 'percent',
- unitDisplay: 'narrow',
- }).format(this.progress / 100);
+ this.setAttribute('role', 'progressbar');
}
protected override updated(changes: PropertyValues): void {
super.updated(changes);
- if (changes.has('indeterminate')) {
- if (this.indeterminate) {
- this.removeAttribute('aria-valuemin');
- this.removeAttribute('aria-valuemax');
- this.removeAttribute('aria-valuenow');
- this.removeAttribute('aria-valuetext');
- } else {
+
+ if (changes.has('progress')) {
+ if (this.progress !== null && this.progress >= 0) {
this.setAttribute('aria-valuemin', '0');
this.setAttribute('aria-valuemax', '100');
this.setAttribute('aria-valuenow', String(this.progress));
this.setAttribute('aria-valuetext', this.formatProgress());
+ } else {
+ this.removeAttribute('aria-valuemin');
+ this.removeAttribute('aria-valuemax');
+ this.removeAttribute('aria-valuenow');
+ this.removeAttribute('aria-valuetext');
}
}
- if (!this.indeterminate && changes.has('progress')) {
- this.setAttribute('aria-valuenow', String(this.progress));
- this.setAttribute('aria-valuetext', this.formatProgress());
- }
- if (!this.indeterminate && changes.has(languageResolverUpdatedSymbol)) {
+
+ if (this.progress !== null && changes.has(languageResolverUpdatedSymbol)) {
this.setAttribute('aria-valuetext', this.formatProgress());
}
+
if (changes.has('label')) {
if (this.label.length) {
this.setAttribute('aria-label', this.label);
@@ -197,31 +235,14 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
}
}
- const hasAccessibleName = (): boolean => {
- return Boolean(
- this.label ||
- this.getAttribute('aria-label') ||
- this.getAttribute('aria-labelledby')
- );
- };
+ // Apply default accessible name fallback after handling explicit label changes.
+ if (changes.has('label') && !this.hasAccessibleName()) {
+ this.setAttribute('aria-label', ProgressCircleBase.DEFAULT_LABEL);
+ this.warnMissingAccessibleName();
+ }
if (window.__swc?.DEBUG) {
this.warnDeprecatedLightDomChildren();
- if (!hasAccessibleName() && this.getAttribute('role') === 'progressbar') {
- window.__swc?.warn(
- this,
- `<${this.localName}> elements need one of the following to be accessible:`,
- 'https://opensource.adobe.com/spectrum-web-components/second-gen/?path=/docs/components-progress-circle--docs',
- {
- type: 'accessibility',
- issues: [
- 'value supplied to the "label" attribute, which will be displayed visually as part of the element, or',
- 'value supplied to the "aria-label" attribute, which will only be provided to screen readers, or',
- 'an element ID reference supplied to the "aria-labelledby" attribute, which will be provided by screen readers and will need to be managed manually by the parent application.',
- ],
- }
- );
- }
}
}
}
diff --git a/2nd-gen/packages/core/components/progress-circle/ProgressCircle.types.ts b/2nd-gen/packages/core/components/progress-circle/ProgressCircle.types.ts
index 03caa52603e..b506300e500 100644
--- a/2nd-gen/packages/core/components/progress-circle/ProgressCircle.types.ts
+++ b/2nd-gen/packages/core/components/progress-circle/ProgressCircle.types.ts
@@ -16,8 +16,9 @@ export const PROGRESS_CIRCLE_VALID_SIZES = [
's',
'm',
'l',
-] as const satisfies ElementSize[];
+] as const satisfies readonly ElementSize[];
export const PROGRESS_CIRCLE_STATIC_COLORS = ['white', 'black'] as const;
export type ProgressCircleStaticColor =
(typeof PROGRESS_CIRCLE_STATIC_COLORS)[number];
+export type ProgressCircleSize = (typeof PROGRESS_CIRCLE_VALID_SIZES)[number];
diff --git a/2nd-gen/packages/core/components/status-light/StatusLight.base.ts b/2nd-gen/packages/core/components/status-light/StatusLight.base.ts
index 06be7399e1a..d4b21589ec0 100644
--- a/2nd-gen/packages/core/components/status-light/StatusLight.base.ts
+++ b/2nd-gen/packages/core/components/status-light/StatusLight.base.ts
@@ -16,7 +16,7 @@ import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'
import { SizedMixin } from '@spectrum-web-components/core/mixins/index.js';
import {
- STATUSLIGHT_VALID_SIZES,
+ STATUS_LIGHT_VALID_SIZES,
type StatusLightVariant,
} from './StatusLight.types.js';
@@ -27,7 +27,7 @@ import {
* @attribute {ElementSize} size - The size of the status light.
*/
export abstract class StatusLightBase extends SizedMixin(SpectrumElement, {
- validSizes: STATUSLIGHT_VALID_SIZES,
+ validSizes: STATUS_LIGHT_VALID_SIZES,
noDefaultSize: true,
}) {
// ─────────────────────────
diff --git a/2nd-gen/packages/core/components/status-light/StatusLight.types.ts b/2nd-gen/packages/core/components/status-light/StatusLight.types.ts
index f41ebd4882a..820d58fa66a 100644
--- a/2nd-gen/packages/core/components/status-light/StatusLight.types.ts
+++ b/2nd-gen/packages/core/components/status-light/StatusLight.types.ts
@@ -10,20 +10,16 @@
* governing permissions and limitations under the License.
*/
-/**
- * @todo Rename STATUSLIGHT_ prefix to STATUS_LIGHT_ to align with type prefix
- * naming convention (use underscore separators for multi-word names).
- */
import type { ElementSize } from '@spectrum-web-components/core/mixins/index.js';
-export const STATUSLIGHT_VALID_SIZES = [
+export const STATUS_LIGHT_VALID_SIZES = [
's',
'm',
'l',
'xl',
] as const satisfies readonly ElementSize[];
-export const STATUSLIGHT_VARIANTS_SEMANTIC = [
+export const STATUS_LIGHT_VARIANTS_SEMANTIC = [
'neutral',
'info',
'positive',
@@ -31,7 +27,7 @@ export const STATUSLIGHT_VARIANTS_SEMANTIC = [
'notice',
] as const;
-export const STATUSLIGHT_VARIANTS_COLOR = [
+export const STATUS_LIGHT_VARIANTS_COLOR = [
'fuchsia',
'indigo',
'magenta',
@@ -48,15 +44,15 @@ export const STATUSLIGHT_VARIANTS_COLOR = [
'silver',
] as const;
-export const STATUSLIGHT_VARIANTS = [
- ...STATUSLIGHT_VARIANTS_SEMANTIC,
- ...STATUSLIGHT_VARIANTS_COLOR,
+export const STATUS_LIGHT_VARIANTS = [
+ ...STATUS_LIGHT_VARIANTS_SEMANTIC,
+ ...STATUS_LIGHT_VARIANTS_COLOR,
] as const;
export type StatusLightSemanticVariant =
- (typeof STATUSLIGHT_VARIANTS_SEMANTIC)[number];
+ (typeof STATUS_LIGHT_VARIANTS_SEMANTIC)[number];
export type StatusLightColorVariant =
- (typeof STATUSLIGHT_VARIANTS_COLOR)[number];
+ (typeof STATUS_LIGHT_VARIANTS_COLOR)[number];
-export type StatusLightVariant = (typeof STATUSLIGHT_VARIANTS)[number];
+export type StatusLightVariant = (typeof STATUS_LIGHT_VARIANTS)[number];
diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts
index f26f4005752..6932715d3ed 100644
--- a/2nd-gen/packages/swc/.storybook/preview.ts
+++ b/2nd-gen/packages/swc/.storybook/preview.ts
@@ -179,7 +179,7 @@ const preview = {
},
source: {
excludeDecorators: true,
- type: 'auto',
+ type: 'dynamic',
language: 'html',
transform: async (source: string) => {
try {
diff --git a/2nd-gen/packages/swc/components/avatar/Avatar.ts b/2nd-gen/packages/swc/components/avatar/Avatar.ts
index fbb81d61f67..ffc6dd2ca87 100644
--- a/2nd-gen/packages/swc/components/avatar/Avatar.ts
+++ b/2nd-gen/packages/swc/components/avatar/Avatar.ts
@@ -36,6 +36,10 @@ import styles from './avatar.css';
*
*/
export class Avatar extends AvatarBase {
+ // ──────────────────────────────
+ // RENDERING & STYLING
+ // ──────────────────────────────
+
public static override get styles(): CSSResultArray {
return [styles];
}
diff --git a/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts b/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts
index 04b8677b5d7..5050aea58bb 100644
--- a/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts
+++ b/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts
@@ -73,9 +73,9 @@ export default {
excludeStories: ['meta'],
} as Meta;
-// ────────────────
-// STORIES
-// ────────────────
+// ────────────────────
+// HELPERS
+// ────────────────────
const PLACEHOLDER_SRC = 'https://picsum.photos/id/64/500/500';
@@ -86,18 +86,18 @@ const PLACEHOLDER_SRC = 'https://picsum.photos/id/64/500/500';
// alt ?? '' guards against undefined produced by Storybook controls when
// the user clears the alt field. Explicit stories use typed args that are always defined.
export const Playground: Story = {
- render: ({ src, alt, size, outline, disabled }) => html`
+ render: (args) => html`
`,
@@ -112,13 +112,11 @@ export const Playground: Story = {
};
// ──────────────────────────
-// OVERVIEW STORY
+// OVERVIEW STORIES
// ──────────────────────────
export const Overview: Story = {
- render: ({ src, alt, size }) => html`
-
- `,
+ render: (args) => template({ ...args }),
tags: ['overview'],
args: {
src: PLACEHOLDER_SRC,
@@ -143,9 +141,14 @@ export const Overview: Story = {
* - `size`: Numeric size token (50–1500). Defaults to `500` (40 px).
*/
export const Anatomy: Story = {
- render: () => html`
-
+ render: (args) => html`
+
`,
+ args: {
+ src: PLACEHOLDER_SRC,
+ alt: 'Jane Doe',
+ size: '500',
+ },
tags: ['anatomy'],
};
@@ -160,17 +163,20 @@ export const Anatomy: Story = {
* The default size is `500` (40 px).
*/
export const Sizes: Story = {
- render: () => html`
+ render: (args) => html`
${AVATAR_VALID_SIZES.map(
(size) => html`
`
)}
`,
+ args: {
+ src: PLACEHOLDER_SRC,
+ },
parameters: {
flexLayout: 'row-wrap',
'section-order': 1,
@@ -186,15 +192,19 @@ export const Sizes: Story = {
* (e.g., their name appears next to the avatar).
*/
export const Decorative: Story = {
- render: () => html`
+ render: (args) => html`
Jane Doe
`,
+ args: {
+ src: PLACEHOLDER_SRC,
+ size: '500',
+ },
parameters: { 'section-order': 2 },
tags: ['options'],
};
@@ -211,15 +221,24 @@ export const Decorative: Story = {
* migrated to 2nd-gen.
*/
export const InActionButton: Story = {
- render: () => html`
+ render: (args) => html`
`,
+ args: {
+ src: PLACEHOLDER_SRC,
+ alt: 'Jane Doe',
+ size: '100',
+ },
tags: ['behaviors'],
};
@@ -232,24 +251,28 @@ export const InActionButton: Story = {
* defaults to `true` to visually separate stacked avatars.
*/
export const Outline: Story = {
- render: () => html`
+ render: (args) => html`
`,
+ args: {
+ src: PLACEHOLDER_SRC,
+ alt: 'Jane Doe',
+ },
parameters: { 'section-order': 3 },
tags: ['options'],
};
@@ -261,14 +284,19 @@ export const Outline: Story = {
* accessibility tree — `disabled` is purely visual.
*/
export const Disabled: Story = {
- render: () => html`
+ render: (args) => html`
`,
+ args: {
+ src: PLACEHOLDER_SRC,
+ alt: 'Jane Doe',
+ size: '500',
+ },
parameters: { 'section-order': 4 },
tags: ['options'],
};
@@ -294,9 +322,13 @@ export const Disabled: Story = {
* - Keep alt text short and descriptive: prefer `"Jane Doe"` over `"Profile photo of Jane Doe"`
*/
export const Accessibility: Story = {
- render: () => html`
-
-
+ render: (args) => html`
+
+
`,
+ args: {
+ src: PLACEHOLDER_SRC,
+ size: '500',
+ },
tags: ['a11y'],
};
diff --git a/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts b/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts
index d61043b27c4..7759f86684d 100644
--- a/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts
+++ b/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts
@@ -438,6 +438,7 @@ export const TextWrapping: Story = {
`,
tags: ['behaviors'],
};
+
// ────────────────────────────────
// ACCESSIBILITY STORIES
// ────────────────────────────────
diff --git a/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts b/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts
index f0a804bd572..b7583da4abe 100644
--- a/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts
+++ b/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts
@@ -53,6 +53,7 @@ const meta: Meta = {
subtitle: `Internal icon renderer for shared SVG templates.`,
},
},
+ tags: ['migrated'],
};
export default meta;
diff --git a/2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts b/2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts
index 94248de07ac..7f99fae7a62 100644
--- a/2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts
+++ b/2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts
@@ -13,6 +13,7 @@
import { CSSResultArray, html, TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
+import { ifDefined } from 'lit/directives/if-defined.js';
import {
PROGRESS_CIRCLE_STATIC_COLORS,
@@ -25,15 +26,15 @@ import styles from './progress-circle.css';
/**
* Progress circles show the progression of a system operation such as downloading, uploading, processing, etc. in a visual way.
- * They can represent determinate (with a specific progress value) or indeterminate (loading) progress.
+ *
+ * They can represent determinate (with a specific progress value) or indeterminate (loading) progress. If no `progress` value is given, the progress circle is indeterminate.
*
* @element swc-progress-circle
* @status preview
* @since 0.0.1
*
* @property {string} staticColor - Reflected as the `static-color` attribute. Static color variant for use on different backgrounds.
- * @property {number} progress - Progress value between 0 and 100.
- * @property {boolean} indeterminate - Indeterminate state for loading.
+ * @property {number | null} progress - Progress between 0 and 100, reflected as the `progress` attribute when set. When `null` (indeterminate), the attribute is omitted.
* @property {string} size - Size of the component.
* @property {string} label - Label for the component.
*
@@ -41,9 +42,7 @@ import styles from './progress-circle.css';
*
*
* @example
- *
- *
- * Light DOM children are not projected into the shadow tree. Use the `label` attribute or property, or `aria-label` / `aria-labelledby` on the host, for an accessible name.
+ *
*/
export class ProgressCircle extends ProgressCircleBase {
// ────────────────────
@@ -73,6 +72,27 @@ export class ProgressCircle extends ProgressCircleBase {
return [styles];
}
+ /**
+ * Compute the SVG stroke-dashoffset for the fill circle.
+ *
+ * - **Indeterminate** (`progress` is `null`): returns `undefined` so CSS
+ * animation keyframes fully control the offset.
+ * - **0%**: returns 98 instead of 100. A dashoffset of 100 hides the fill
+ * entirely, which fails WCAG 1.4.11 non-text contrast (the track alone
+ * may not meet 3:1 against the background). The 2-unit fill keeps the
+ * graphical element perceivable. `aria-valuenow` stays at 0.
+ * - **1–100%**: returns `100 - progress`.
+ */
+ private computeDashOffset(): number | undefined {
+ if (this.progress === null) {
+ return undefined;
+ }
+ if (this.progress === 0) {
+ return 98;
+ }
+ return 100 - this.progress;
+ }
+
protected override render(): TemplateResult {
const strokeWidth = this.size === 's' ? 2 : this.size === 'l' ? 6 : 4;
// SVG strokes are centered, so subtract half the stroke width from the radius to create an inner stroke.
@@ -82,14 +102,12 @@ export class ProgressCircle extends ProgressCircleBase {
-