Skip to content

Commit e0b0ec2

Browse files
cdransfnikkimk5t3ph
authored
feat(avatar): migrate avatar component to S2 (#6113)
* chore(avatar): add 1st pass of migration plan * chore(avatar): iterate on component structure * chore(avatar): iterate on styles * chore(avatar): add stubs to storybook sidebar * chore(avatar): improve alignment w/react spectrum + design docs * chore(avatar): improved storybook controls * fix(avatar): lint violations + storybook improvements * fix(avatar): lint violations * chore(avatar): revise migration plan * chore(avatar): migrate api + typescript code * chore(avatar): implement a11y recommendations + aria-hidden on host element Per the accessibility migration analysis: - Set aria-hidden="true" on the host (<swc-avatar>) when alt="" so the entire shadow tree is reliably hidden from AT across JAWS/NVDA/VoiceOver - Consolidate the alt-change logic in updated() to sync aria-hidden and fire the missing-alt DEBUG warning in a single changes.has('alt') branch - Improve alt JSDoc to explain the host aria-hidden behavior and clarify that omitting alt only differs from decorative by the DEBUG warning Closes: accessibility checklist items in migration-plan.md * chore(avatar): migrate CSS to S2 style guide conventions * chore(avatar): add Storybook interaction tests and Playwright ARIA snapshot tests * chore(avatar): add Storybook documentation, args table defaults, and breaking changes note * chore(avatar): create consumer-facing migration guide * chore(avatar): fix test failures * fix(avatar): clean up todos and other minor issues * chore(avatar): adds disabled variant, tests and updates docs * Update 2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update CONTRIBUTOR-DOCS/03_project-planning/03_components/avatar/migration-plan.md Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update CONTRIBUTOR-DOCS/03_project-planning/03_components/avatar/migration-plan.md Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update CONTRIBUTOR-DOCS/03_project-planning/03_components/avatar/migration-plan.md Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/core/components/avatar/Avatar.base.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/core/components/avatar/Avatar.base.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/migration.md Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/migration.md Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/migration.md Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/migration.md Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/core/components/avatar/Avatar.base.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/core/components/avatar/Avatar.base.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/core/components/avatar/Avatar.base.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/core/components/avatar/Avatar.base.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * fix(avatar): additional refactoring to properly support decorative property * Update 2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/test/avatar.test.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * Update 2nd-gen/packages/swc/components/avatar/test/avatar.test.ts Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> * fix(avatar): remove duplicate test expectation * Update 2nd-gen/packages/swc/components/avatar/avatar.css Co-authored-by: Stephanie Eckles <seckles@adobe.com> * docs(avatar): remove linked variant content from accessibility analysis The Spectrum 2 spec dropped the linked avatar variant. Update the accessibility migration analysis to reflect this: remove all linked-variant ARIA/keyboard/tree-expectation sections, update property references from 1st-gen names (label/is-decorative) to 2nd-gen names (alt/decorative), and add the <a>-wrapper pattern for consumers who need a linked avatar. * chore(avatar): remove unnecessary avatar shim(s) * Update 2nd-gen/packages/swc/components/avatar/avatar.css Co-authored-by: Stephanie Eckles <seckles@adobe.com> * chore(avatar): remove unnecessary class declarations and clean up documentation * chore(avatar): rename stroke to avatar * chore(avatar): clean up storybook control descriptions * chore(avatar): make exposed css properties consistent with component property name * fix(avatar): remove method section in storybook --------- Co-authored-by: Nikki Massaro <5090492+nikkimk@users.noreply.github.com> Co-authored-by: Stephanie Eckles <seckles@adobe.com>
1 parent 51ee012 commit e0b0ec2

17 files changed

Lines changed: 1936 additions & 2 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
import { PropertyValues } from 'lit';
13+
import { property } from 'lit/decorators.js';
14+
15+
import { SpectrumElement } from '@spectrum-web-components/core/element/index.js';
16+
17+
import {
18+
AVATAR_DEFAULT_SIZE,
19+
AVATAR_VALID_SIZES,
20+
type AvatarSize,
21+
} from './Avatar.types.js';
22+
23+
/**
24+
* Base class for the avatar component.
25+
*
26+
* Provides the core API for displaying a circular profile image. Concrete
27+
* classes supply the stylesheet and render template.
28+
*/
29+
export abstract class AvatarBase extends SpectrumElement {
30+
// ─────────────────────────
31+
// STATIC
32+
// ─────────────────────────
33+
34+
/**
35+
* @internal
36+
*
37+
* The set of valid numeric size values for the avatar.
38+
*
39+
* This is an internal property not intended for consumer use, but used in
40+
* internal validation logic, stories, and tests to keep them in sync with
41+
* the canonical type definition in `Avatar.types.ts`.
42+
*/
43+
static readonly VALID_SIZES: readonly AvatarSize[] = AVATAR_VALID_SIZES;
44+
45+
// ──────────────────
46+
// CORE API
47+
// ──────────────────
48+
49+
/**
50+
* URL of the profile image to display.
51+
*/
52+
@property({ type: String })
53+
public src = '';
54+
55+
/**
56+
* Text description of the avatar image.
57+
*/
58+
@property({ type: String })
59+
public alt: string | undefined;
60+
61+
// ───────────────────
62+
// SIZE API
63+
// ───────────────────
64+
65+
/**
66+
* The size of the avatar. Invalid values fall back to the default (500).
67+
*/
68+
@property({ type: Number, reflect: true })
69+
public get size(): AvatarSize {
70+
return this._size;
71+
}
72+
73+
public set size(value: AvatarSize) {
74+
const validSize = (AVATAR_VALID_SIZES as readonly number[]).includes(
75+
Number(value)
76+
)
77+
? (Number(value) as AvatarSize)
78+
: AVATAR_DEFAULT_SIZE;
79+
80+
if (this._size === validSize) {
81+
return;
82+
}
83+
84+
const oldSize = this._size;
85+
this._size = validSize;
86+
this.requestUpdate('size', oldSize);
87+
}
88+
89+
private _size: AvatarSize = AVATAR_DEFAULT_SIZE;
90+
91+
// ───────────────────────
92+
// VISUAL API
93+
// ───────────────────────
94+
95+
/**
96+
* Renders a visual outline around the avatar image.
97+
*/
98+
@property({ type: Boolean, reflect: true })
99+
public outline = false;
100+
101+
/**
102+
* Renders the avatar at reduced opacity, indicating the entity is inactive or unavailable.
103+
*/
104+
@property({ type: Boolean, reflect: true })
105+
public disabled = false;
106+
107+
// ───────────────────────────
108+
// ACCESSIBILITY API
109+
// ───────────────────────────
110+
111+
/**
112+
* Marks the avatar as decorative, hiding it from assistive technology.
113+
*/
114+
@property({ type: Boolean, reflect: true })
115+
public decorative = false;
116+
117+
// ──────────────────────
118+
// IMPLEMENTATION
119+
// ──────────────────────
120+
121+
protected override firstUpdated(changes: PropertyValues): void {
122+
super.firstUpdated(changes);
123+
if (!this.hasAttribute('size')) {
124+
this.setAttribute('size', String(this.size));
125+
}
126+
this._syncAriaHidden();
127+
if (window.__swc?.DEBUG) {
128+
this._warnMissingAlt();
129+
}
130+
}
131+
132+
protected override updated(changes: PropertyValues): void {
133+
super.updated(changes);
134+
if (changes.has('decorative')) {
135+
this._syncAriaHidden();
136+
}
137+
if (changes.has('alt') && window.__swc?.DEBUG) {
138+
this._warnMissingAlt();
139+
}
140+
}
141+
142+
private _syncAriaHidden(): void {
143+
if (this.decorative) {
144+
this.setAttribute('aria-hidden', 'true');
145+
} else {
146+
this.removeAttribute('aria-hidden');
147+
}
148+
}
149+
150+
private _warnMissingAlt(): void {
151+
if (this.alt === undefined && !this.decorative) {
152+
window.__swc?.warn(
153+
this,
154+
`<${this.localName}> is missing an \`alt\` attribute. Provide a text description or pass \`alt=""\` and mark it as \`decorative\`.`,
155+
'https://opensource.adobe.com/spectrum-web-components/components/avatar/#accessibility',
156+
{
157+
type: 'accessibility',
158+
issues: [
159+
'Provide an `alt` attribute with meaningful alternative text, or',
160+
'Set `alt=""` and mark the image as `decorative` (hidden from screen readers).',
161+
],
162+
}
163+
);
164+
}
165+
}
166+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
/**
14+
* Valid numeric size values for the Avatar component.
15+
*
16+
* Sizes 50–700 match 1st-gen. Sizes 800–1500 are new in Spectrum 2.
17+
*/
18+
export const AVATAR_VALID_SIZES = [
19+
50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300,
20+
1400, 1500,
21+
] as const;
22+
23+
export type AvatarSize = (typeof AVATAR_VALID_SIZES)[number];
24+
25+
export const AVATAR_DEFAULT_SIZE = 500 as const satisfies AvatarSize;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
export * from './Avatar.base.js';
13+
export * from './Avatar.types.js';

2nd-gen/packages/core/element/spectrum-element.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function SpectrumMixin<T extends Constructor<ReactiveElement>>(
3333
* @internal
3434
*/
3535
public override shadowRoot!: ShadowRoot;
36+
/** @internal */
3637
public hasVisibleFocusInTree(): boolean {
3738
const getAncestors = (root: Document = document): HTMLElement[] => {
3839
let currentNode = root.activeElement as HTMLElement;

2nd-gen/packages/core/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"types": "./dist/components/asset/index.d.ts",
2424
"import": "./dist/components/asset/index.js"
2525
},
26+
"./components/avatar": {
27+
"types": "./dist/components/avatar/index.d.ts",
28+
"import": "./dist/components/avatar/index.js"
29+
},
2630
"./components/badge": {
2731
"types": "./dist/components/badge/index.d.ts",
2832
"import": "./dist/components/badge/index.js"
@@ -131,6 +135,9 @@
131135
"components/asset": [
132136
"dist/components/asset/index.d.ts"
133137
],
138+
"components/avatar": [
139+
"dist/components/avatar/index.d.ts"
140+
],
134141
"components/badge": [
135142
"dist/components/badge/index.d.ts"
136143
],

2nd-gen/packages/swc/.storybook/preview.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,11 @@ const preview = {
339339
'Asset',
340340
['Rendering and styling migration analysis'],
341341
'Avatar',
342-
['Rendering and styling migration analysis'],
342+
[
343+
'Accessibility migration analysis',
344+
'Migration plan',
345+
'Rendering and styling migration analysis',
346+
],
343347
'Badge',
344348
[
345349
'Accessibility migration analysis',
@@ -425,6 +429,14 @@ const preview = {
425429
},
426430
},
427431
},
432+
// Hide SpectrumElement infrastructure members from every component's API table.
433+
// These are internal properties that consumers should not configure directly.
434+
argTypes: {
435+
dir: { table: { disable: true } },
436+
VERSION: { table: { disable: true } },
437+
CORE_VERSION: { table: { disable: true } },
438+
hasVisibleFocusInTree: { table: { disable: true } },
439+
},
428440
tags: ['!autodocs', '!dev'], // We only want the playground stories to be visible in the docs and sidenav. Since a majority of our stories are tagged with '!autodocs' and '!dev', we set those tags globally. We can opt in to visibility by adding the 'autodocs' or 'dev' tags to individual stories.
429441
loaders: [FontLoader],
430442
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
import { CSSResultArray, html, TemplateResult } from 'lit';
13+
14+
import { AvatarBase } from '@spectrum-web-components/core/components/avatar';
15+
16+
import styles from './avatar.css';
17+
18+
/**
19+
* A static avatar component that displays a circular user profile image.
20+
*
21+
* Provide `alt` with a description of the person or entity depicted.
22+
* Pass `alt=""` to treat the image as decorative and hide it from assistive
23+
* technology.
24+
*
25+
* @element swc-avatar
26+
*
27+
* @example
28+
* <swc-avatar src="/path/to/image.jpg" alt="Jane Doe"></swc-avatar>
29+
*
30+
* @example
31+
* <swc-avatar src="/path/to/image.jpg" alt=""></swc-avatar>
32+
*
33+
* @example
34+
* <swc-avatar src="/path/to/image.jpg" alt="Jane Doe" outline></swc-avatar>
35+
*/
36+
export class Avatar extends AvatarBase {
37+
public static override get styles(): CSSResultArray {
38+
return [styles];
39+
}
40+
41+
protected override render(): TemplateResult {
42+
return html`
43+
<div class="swc-Avatar">
44+
<img class="swc-Avatar-image" src=${this.src} alt=${this.alt ?? ''} />
45+
</div>
46+
`;
47+
}
48+
}

0 commit comments

Comments
 (0)