diff --git a/.config/cem.yaml b/.config/cem.yaml
index febc90fcec..86f5f070a6 100644
--- a/.config/cem.yaml
+++ b/.config/cem.yaml
@@ -1,5 +1,6 @@
sourceControlRootUrl: https://github.com/patternfly/patternfly-elements/tree/main/
generate:
+ output: ./elements/custom-elements.json
files:
- ./elements/*/*.ts
- ./core/*/*.ts
diff --git a/elements/package.json b/elements/package.json
index fe8dda2080..dc67ca9ec7 100644
--- a/elements/package.json
+++ b/elements/package.json
@@ -15,6 +15,7 @@
"./pf-v5-accordion/pf-v5-accordion.js": "./pf-v5-accordion/pf-v5-accordion.js",
"./pf-v5-alert/pf-v5-alert.js": "./pf-v5-alert/pf-v5-alert.js",
"./pf-v5-avatar/pf-v5-avatar.js": "./pf-v5-avatar/pf-v5-avatar.js",
+ "./pf-v6-avatar/pf-v6-avatar.js": "./pf-v6-avatar/pf-v6-avatar.js",
"./pf-v5-back-to-top/pf-v5-back-to-top.js": "./pf-v5-back-to-top/pf-v5-back-to-top.js",
"./pf-v5-background-image/pf-v5-background-image.js": "./pf-v5-background-image/pf-v5-background-image.js",
"./pf-v5-badge/pf-v5-badge.js": "./pf-v5-badge/pf-v5-badge.js",
diff --git a/elements/pf-v6-avatar/demo/avatarImg.svg b/elements/pf-v6-avatar/demo/avatarImg.svg
new file mode 100644
index 0000000000..73726f9bc5
--- /dev/null
+++ b/elements/pf-v6-avatar/demo/avatarImg.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/elements/pf-v6-avatar/demo/basic.html b/elements/pf-v6-avatar/demo/basic.html
new file mode 100644
index 0000000000..4ca2a156ec
--- /dev/null
+++ b/elements/pf-v6-avatar/demo/basic.html
@@ -0,0 +1,10 @@
+---
+name: Basic
+description: A basic avatar displays a user image.
+---
+
+
+
diff --git a/elements/pf-v6-avatar/demo/bordered.html b/elements/pf-v6-avatar/demo/bordered.html
new file mode 100644
index 0000000000..f5ab4113cb
--- /dev/null
+++ b/elements/pf-v6-avatar/demo/bordered.html
@@ -0,0 +1,19 @@
+---
+name: Bordered
+description: A bordered avatar has a thin border around the image.
+---
+
+
+
+
+
diff --git a/elements/pf-v6-avatar/demo/index.html b/elements/pf-v6-avatar/demo/index.html
new file mode 100644
index 0000000000..d146cbb04f
--- /dev/null
+++ b/elements/pf-v6-avatar/demo/index.html
@@ -0,0 +1,9 @@
+---
+name: Basic
+description: A basic avatar displays a user image.
+---
+
+
+
diff --git a/elements/pf-v6-avatar/demo/size-variations.html b/elements/pf-v6-avatar/demo/size-variations.html
new file mode 100644
index 0000000000..33467cde0b
--- /dev/null
+++ b/elements/pf-v6-avatar/demo/size-variations.html
@@ -0,0 +1,63 @@
+---
+name: Size variations
+description: Avatars can be displayed in four sizes.
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/elements/pf-v6-avatar/pf-v6-avatar.css b/elements/pf-v6-avatar/pf-v6-avatar.css
new file mode 100644
index 0000000000..aeb19ce41b
--- /dev/null
+++ b/elements/pf-v6-avatar/pf-v6-avatar.css
@@ -0,0 +1,54 @@
+:host {
+ --_width: var(--pf-v6-c-avatar--Width, 2.25rem);
+ --_height: var(--pf-v6-c-avatar--Height, 2.25rem);
+ --_border-radius: var(--pf-v6-c-avatar--BorderRadius, var(--pf-t--global--border--radius--pill, 30em));
+ --_border-color: var(--pf-v6-c-avatar--BorderColor, transparent);
+ --_border-width: var(--pf-v6-c-avatar--BorderWidth, 0);
+ --_placeholder-bg: var(--pf-t--global--background--color--200, light-dark(var(--pf-t--color--gray--10, #f2f2f2), var(--pf-t--color--gray--80, #292929)));
+ --_placeholder-fg: var(--pf-t--global--icon--color--subtle, light-dark(var(--pf-t--color--gray--50, #707070), var(--pf-t--color--gray--40, #a3a3a3)));
+
+ display: inline-block;
+ width: var(--_width);
+ height: var(--_height);
+ border-radius: var(--_border-radius);
+}
+
+:host([hidden]),
+[hidden] {
+ display: none !important;
+}
+
+:host([bordered]) {
+ --_border-color: var(--pf-v6-c-avatar--m-bordered--BorderColor, var(--pf-t--global--border--color--default, #d2d2d2));
+ --_border-width: var(--pf-v6-c-avatar--m-bordered--BorderWidth, var(--pf-t--global--border--width--box--default, 1px));
+}
+
+:host([size="sm"]) {
+ --_width: var(--pf-v6-c-avatar--m-sm--Width, 1.5rem);
+ --_height: var(--pf-v6-c-avatar--m-sm--Height, 1.5rem);
+}
+
+:host([size="md"]) {
+ --_width: var(--pf-v6-c-avatar--m-md--Width, 2.25rem);
+ --_height: var(--pf-v6-c-avatar--m-md--Height, 2.25rem);
+}
+
+:host([size="lg"]) {
+ --_width: var(--pf-v6-c-avatar--m-lg--Width, 4.5rem);
+ --_height: var(--pf-v6-c-avatar--m-lg--Height, 4.5rem);
+}
+
+:host([size="xl"]) {
+ --_width: var(--pf-v6-c-avatar--m-xl--Width, 8rem);
+ --_height: var(--pf-v6-c-avatar--m-xl--Height, 8rem);
+}
+
+svg,
+img {
+ display: block;
+ object-fit: cover;
+ width: var(--_width);
+ height: var(--_height);
+ border-radius: var(--_border-radius);
+ border: var(--_border-width) solid var(--_border-color);
+}
diff --git a/elements/pf-v6-avatar/pf-v6-avatar.ts b/elements/pf-v6-avatar/pf-v6-avatar.ts
new file mode 100644
index 0000000000..a79e28f922
--- /dev/null
+++ b/elements/pf-v6-avatar/pf-v6-avatar.ts
@@ -0,0 +1,80 @@
+import { LitElement, html, type TemplateResult } from 'lit';
+import { property } from 'lit/decorators/property.js';
+import { customElement } from 'lit/decorators/custom-element.js';
+
+import style from './pf-v6-avatar.css';
+
+/** Size variants for the avatar. */
+export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
+
+export class PfV6AvatarLoadEvent extends Event {
+ constructor(public originalEvent: Event) {
+ super('load', { bubbles: true });
+ }
+}
+
+/**
+ * An **avatar** is a visual used to represent a user. It may contain an image
+ * or a placeholder graphic.
+ * @summary Displays a user's avatar image
+ * @fires {PfV6AvatarLoadEvent} load - when the avatar image loads
+ * @cssprop {} --pf-v6-c-avatar--Width - Width of the avatar
+ * @cssprop {} --pf-v6-c-avatar--Height - Height of the avatar
+ * @cssprop {} --pf-v6-c-avatar--BorderRadius - Border radius of the avatar
+ * @cssprop {} --pf-v6-c-avatar--BorderColor - Border color of the avatar
+ * @cssprop {} --pf-v6-c-avatar--BorderWidth - Border width of the avatar
+ * @cssprop {} --pf-v6-c-avatar--m-sm--Width - Width when size is `sm`
+ * @cssprop {} --pf-v6-c-avatar--m-sm--Height - Height when size is `sm`
+ * @cssprop {} --pf-v6-c-avatar--m-md--Width - Width when size is `md`
+ * @cssprop {} --pf-v6-c-avatar--m-md--Height - Height when size is `md`
+ * @cssprop {} --pf-v6-c-avatar--m-lg--Width - Width when size is `lg`
+ * @cssprop {} --pf-v6-c-avatar--m-lg--Height - Height when size is `lg`
+ * @cssprop {} --pf-v6-c-avatar--m-xl--Width - Width when size is `xl`
+ * @cssprop {} --pf-v6-c-avatar--m-xl--Height - Height when size is `xl`
+ * @cssprop {} --pf-v6-c-avatar--m-bordered--BorderColor - Border color when bordered
+ * @cssprop {} --pf-v6-c-avatar--m-bordered--BorderWidth - Border width when bordered
+ */
+@customElement('pf-v6-avatar')
+export class PfV6Avatar extends LitElement {
+ static readonly styles: CSSStyleSheet[] = [style];
+
+ /** The URL to the user's custom avatar image. */
+ @property() src?: string;
+
+ /** The alt text for the avatar image. */
+ @property({ reflect: true }) alt?: string;
+
+ /** Size of the avatar */
+ @property({ reflect: true }) size?: AvatarSize;
+
+ /** Whether to display a border around the avatar */
+ @property({ type: Boolean, reflect: true }) bordered = false;
+
+ override render(): TemplateResult {
+ return this.src != null ? html`
+
+ ` : html`
+
+ `;
+ }
+
+ #onLoad(event: Event) {
+ this.dispatchEvent(new PfV6AvatarLoadEvent(event));
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-v6-avatar': PfV6Avatar;
+ }
+}
diff --git a/elements/pf-v6-avatar/test/pf-v6-avatar.spec.ts b/elements/pf-v6-avatar/test/pf-v6-avatar.spec.ts
new file mode 100644
index 0000000000..ecd29d6988
--- /dev/null
+++ b/elements/pf-v6-avatar/test/pf-v6-avatar.spec.ts
@@ -0,0 +1,148 @@
+import { html, expect, oneEvent, nextFrame } from '@open-wc/testing';
+import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js';
+import { a11ySnapshot } from '@patternfly/pfe-tools/test/a11y-snapshot.js';
+import { PfV6Avatar, PfV6AvatarLoadEvent } from '@patternfly/elements/pf-v6-avatar/pf-v6-avatar.js';
+
+describe('', function() {
+ it('imperatively instantiates', function() {
+ expect(document.createElement('pf-v6-avatar')).to.be.an.instanceof(PfV6Avatar);
+ });
+
+ it('should upgrade', async function() {
+ const el = await createFixture(html``);
+ expect(el, 'pf-v6-avatar should be an instance of PfV6Avatar')
+ .to.be.an.instanceOf(customElements.get('pf-v6-avatar'))
+ .and
+ .to.be.an.instanceOf(PfV6Avatar);
+ });
+
+ describe('without src attr', function() {
+ let element: PfV6Avatar;
+ beforeEach(async function() {
+ element = await createFixture(html``);
+ await nextFrame();
+ });
+
+ it('displays a placeholder', function() {
+ const { offsetWidth } = element;
+ expect(offsetWidth).to.be.greaterThan(0);
+ });
+
+ it('has the default size', function() {
+ expect(element.offsetWidth).to.equal(36);
+ expect(element.offsetHeight).to.equal(36);
+ });
+
+ it('hides the placeholder from the accessibility tree', async function() {
+ const snapshot = await a11ySnapshot();
+ expect(snapshot?.children?.find(
+ (child: { role: string }) => child.role === 'img'
+ )).to.not.be.ok;
+ });
+ });
+
+ describe('with a src attr', function() {
+ let element: PfV6Avatar;
+ let loaded: string | undefined;
+ const datauri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAB0UlEQVR4Xu3UAQ0AAAyDsM+/6QspcwAh2zXawGj64K8A8AgKoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7gALADeD4PUAB4AZw/B6gAHADOH4PUAC4ARy/BygA3ACO3wMUAG4Ax+8BCgA3gOP3AAWAG8Dxe4ACwA3g+D1AAeAGcPweoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7gALADeD4PUAB4AZw/B6gAHADOH4PUAC4ARy/BygA3ACO3wMUAG4Ax+8BCgA3gOP3AAWAG8Dxe4ACwA3g+D1AAeAGcPweoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7ADyAB6SPAIFm19U7AAAAAElFTkSuQmCC';
+ const onLoad = (e: PfV6AvatarLoadEvent) => {
+ const paths = e.originalEvent.composedPath() as HTMLImageElement[];
+ loaded = paths.find(x => x.localName === 'img')?.src;
+ };
+ beforeEach(async function() {
+ element = await createFixture(html``);
+ setTimeout(() => element.src = datauri);
+ await oneEvent(element, 'load');
+ });
+
+ it('loads the image', function() {
+ expect(loaded).to.equal(datauri);
+ });
+
+ it('fires a PfV6AvatarLoadEvent', function() {
+ expect(loaded).to.be.ok;
+ });
+ });
+
+ describe('with alt attr', function() {
+ let element: PfV6Avatar;
+ beforeEach(async function() {
+ element = await createFixture(html`
+
+ `);
+ await oneEvent(element, 'load');
+ });
+
+ it('passes alt text to the image', async function() {
+ const snapshot = await a11ySnapshot();
+ expect(snapshot?.children?.find(
+ (child: { name: string }) => child.name === 'User avatar'
+ )).to.be.ok;
+ });
+ });
+
+ describe('with size="md"', function() {
+ let element: PfV6Avatar;
+ beforeEach(async function() {
+ element = await createFixture(html``);
+ await nextFrame();
+ });
+
+ it('renders at the medium size', function() {
+ expect(element.offsetWidth).to.equal(36);
+ expect(element.offsetHeight).to.equal(36);
+ });
+ });
+
+ describe('with size="sm"', function() {
+ let element: PfV6Avatar;
+ beforeEach(async function() {
+ element = await createFixture(html``);
+ await nextFrame();
+ });
+
+ it('renders at the small size', function() {
+ expect(element.offsetWidth).to.equal(24);
+ expect(element.offsetHeight).to.equal(24);
+ });
+ });
+
+ describe('with size="lg"', function() {
+ let element: PfV6Avatar;
+ beforeEach(async function() {
+ element = await createFixture(html``);
+ await nextFrame();
+ });
+
+ it('renders at the large size', function() {
+ expect(element.offsetWidth).to.equal(72);
+ expect(element.offsetHeight).to.equal(72);
+ });
+ });
+
+ describe('with size="xl"', function() {
+ let element: PfV6Avatar;
+ beforeEach(async function() {
+ element = await createFixture(html``);
+ await nextFrame();
+ });
+
+ it('renders at the extra large size', function() {
+ expect(element.offsetWidth).to.equal(128);
+ expect(element.offsetHeight).to.equal(128);
+ });
+ });
+
+ describe('with bordered', function() {
+ let element: PfV6Avatar;
+ beforeEach(async function() {
+ element = await createFixture(html``);
+ await nextFrame();
+ });
+
+ it('renders with a visible border', function() {
+ expect(element.offsetWidth).to.be.greaterThan(36);
+ });
+ });
+});