From a398aea827d4f5cb4827eedc21d867738f9c935b Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 8 May 2026 14:06:13 -0400 Subject: [PATCH 1/4] feat(badge): add pf-v6-badge element Port pf-v5-badge to pf-v6-badge with v6 design tokens, CSS, and API parity with PatternFly v6 React Badge component. Changes from v5: - Updated CSS custom properties to --pf-v6-c-badge--* namespace - Added border overlay via ::after pseudo-element (new in v6) - Added disabled state with pointer-events: none - Switched to CSS logical properties (padding-inline-start/end) - Simplified render: no wrapper span, slot for default content - Added `@cssprop` JSDoc for all public CSS custom properties - Added `@summary` and `@slot` JSDoc annotations - Exported BadgeState type union Demos match patternfly.org: read, unread, disabled, plus WC-specific threshold demo. Tests cover: instantiation, number/threshold display, state colors, disabled styling, accessibility tree, slot content. Closes #2982 Assisted-By: Claude Opus 4.6 --- elements/package.json | 1 + elements/pf-v6-badge/demo/disabled.html | 12 ++ elements/pf-v6-badge/demo/index.html | 12 ++ elements/pf-v6-badge/demo/read.html | 12 ++ elements/pf-v6-badge/demo/threshold.html | 10 ++ elements/pf-v6-badge/demo/unread.html | 12 ++ elements/pf-v6-badge/pf-v6-badge.css | 58 ++++++++ elements/pf-v6-badge/pf-v6-badge.ts | 72 +++++++++ elements/test/pf-v6-badge.spec.ts | 181 +++++++++++++++++++++++ 9 files changed, 370 insertions(+) create mode 100644 elements/pf-v6-badge/demo/disabled.html create mode 100644 elements/pf-v6-badge/demo/index.html create mode 100644 elements/pf-v6-badge/demo/read.html create mode 100644 elements/pf-v6-badge/demo/threshold.html create mode 100644 elements/pf-v6-badge/demo/unread.html create mode 100644 elements/pf-v6-badge/pf-v6-badge.css create mode 100644 elements/pf-v6-badge/pf-v6-badge.ts create mode 100644 elements/test/pf-v6-badge.spec.ts diff --git a/elements/package.json b/elements/package.json index fe8dda2080..11d9359e77 100644 --- a/elements/package.json +++ b/elements/package.json @@ -18,6 +18,7 @@ "./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", + "./pf-v6-badge/pf-v6-badge.js": "./pf-v6-badge/pf-v6-badge.js", "./pf-v5-banner/pf-v5-banner.js": "./pf-v5-banner/pf-v5-banner.js", "./pf-v5-button/pf-v5-button.js": "./pf-v5-button/pf-v5-button.js", "./pf-v5-card/pf-v5-card.js": "./pf-v5-card/pf-v5-card.js", diff --git a/elements/pf-v6-badge/demo/disabled.html b/elements/pf-v6-badge/demo/disabled.html new file mode 100644 index 0000000000..3e57ccf9e2 --- /dev/null +++ b/elements/pf-v6-badge/demo/disabled.html @@ -0,0 +1,12 @@ +--- +name: Disabled +description: Disabled badges indicate that the associated content or action is currently unavailable. +--- +7 +24 +240 +999+ + + diff --git a/elements/pf-v6-badge/demo/index.html b/elements/pf-v6-badge/demo/index.html new file mode 100644 index 0000000000..b0239ac479 --- /dev/null +++ b/elements/pf-v6-badge/demo/index.html @@ -0,0 +1,12 @@ +--- +name: Read +description: Read badges display a numeric value with read state styling, indicating the content has been viewed. +--- +7 +24 +240 +999+ + + diff --git a/elements/pf-v6-badge/demo/read.html b/elements/pf-v6-badge/demo/read.html new file mode 100644 index 0000000000..b0239ac479 --- /dev/null +++ b/elements/pf-v6-badge/demo/read.html @@ -0,0 +1,12 @@ +--- +name: Read +description: Read badges display a numeric value with read state styling, indicating the content has been viewed. +--- +7 +24 +240 +999+ + + diff --git a/elements/pf-v6-badge/demo/threshold.html b/elements/pf-v6-badge/demo/threshold.html new file mode 100644 index 0000000000..b27ea00fca --- /dev/null +++ b/elements/pf-v6-badge/demo/threshold.html @@ -0,0 +1,10 @@ +--- +name: Threshold +description: A threshold appends a "+" when the numeric value exceeds a set maximum. +--- +400 +900 + + diff --git a/elements/pf-v6-badge/demo/unread.html b/elements/pf-v6-badge/demo/unread.html new file mode 100644 index 0000000000..7e16b908e2 --- /dev/null +++ b/elements/pf-v6-badge/demo/unread.html @@ -0,0 +1,12 @@ +--- +name: Unread +description: Unread badges use a bold, branded style to indicate content that has not yet been viewed. +--- +7 +24 +240 +999+ + + diff --git a/elements/pf-v6-badge/pf-v6-badge.css b/elements/pf-v6-badge/pf-v6-badge.css new file mode 100644 index 0000000000..e158aedfcd --- /dev/null +++ b/elements/pf-v6-badge/pf-v6-badge.css @@ -0,0 +1,58 @@ +:host { + position: relative; + display: inline-block; + min-width: var(--pf-v6-c-badge--MinWidth, + var(--pf-t--global--spacer--xl, 2rem)); + padding-inline-start: var(--pf-v6-c-badge--PaddingInlineStart, + var(--pf-t--global--spacer--sm, 0.5rem)); + padding-inline-end: var(--pf-v6-c-badge--PaddingInlineEnd, + var(--pf-t--global--spacer--sm, 0.5rem)); + font-size: var(--pf-v6-c-badge--FontSize, + var(--pf-t--global--font--size--body--sm, 0.75rem)); + font-weight: var(--pf-v6-c-badge--FontWeight, + var(--pf-t--global--font--weight--body--bold, 700)); + color: var(--pf-v6-c-badge--Color, + var(--pf-t--global--text--color--nonstatus--on-gray--default, #151515)); + text-align: center; + white-space: nowrap; + background-color: var(--pf-v6-c-badge--BackgroundColor, + var(--pf-t--global--color--nonstatus--gray--default, #f0f0f0)); + border-radius: var(--pf-v6-c-badge--BorderRadius, + var(--pf-t--global--border--radius--pill, 180em)); +} + +:host::after { + position: absolute; + inset: 0; + pointer-events: none; + content: ""; + border: var(--pf-v6-c-badge--BorderWidth, + var(--pf-t--global--border--width--regular, 1px)) solid var(--pf-v6-c-badge--BorderColor, transparent); + border-radius: inherit; +} + +:host([state="read"]) { + --pf-v6-c-badge--Color: var(--pf-v6-c-badge--m-read--Color, + var(--pf-t--global--text--color--nonstatus--on-gray--default, #151515)); + --pf-v6-c-badge--BackgroundColor: var(--pf-v6-c-badge--m-read--BackgroundColor, + var(--pf-t--global--color--nonstatus--gray--default, #f0f0f0)); + --pf-v6-c-badge--BorderColor: var(--pf-v6-c-badge--m-read--BorderColor, + var(--pf-t--global--border--color--high-contrast, #151515)); +} + +:host([state="unread"]) { + --pf-v6-c-badge--Color: var(--pf-v6-c-badge--m-unread--Color, + var(--pf-t--global--text--color--on-brand--default, #fff)); + --pf-v6-c-badge--BackgroundColor: var(--pf-v6-c-badge--m-unread--BackgroundColor, + var(--pf-t--global--color--brand--default, #06c)); +} + +:host([disabled]) { + --pf-v6-c-badge--Color: var(--pf-v6-c-badge--m-disabled--Color, + var(--pf-t--global--text--color--on-disabled, #6a6e73)); + --pf-v6-c-badge--BackgroundColor: var(--pf-v6-c-badge--m-disabled--BackgroundColor, + var(--pf-t--global--background--color--disabled--default, #d2d2d2)); + --pf-v6-c-badge--BorderColor: var(--pf-v6-c-badge--m-disabled--BorderColor, + var(--pf-t--global--border--color--disabled, #d2d2d2)); + pointer-events: none; +} diff --git a/elements/pf-v6-badge/pf-v6-badge.ts b/elements/pf-v6-badge/pf-v6-badge.ts new file mode 100644 index 0000000000..2492456c14 --- /dev/null +++ b/elements/pf-v6-badge/pf-v6-badge.ts @@ -0,0 +1,72 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; + +import styles from './pf-v6-badge.css'; + +export type BadgeState = 'unread' | 'read'; + +/** + * A **badge** is used to annotate other information like a label or an object name. + * @summary Displays a numeric value as an annotation + * @slot - Badge content, typically a number or short text + * @cssprop {} --pf-v6-c-badge--Color - Text color of the badge + * @cssprop {} --pf-v6-c-badge--BackgroundColor - Background color of the badge + * @cssprop {} --pf-v6-c-badge--BorderColor - Border color of the badge + * @cssprop {} --pf-v6-c-badge--BorderWidth - Border width of the badge + * @cssprop {} --pf-v6-c-badge--BorderRadius - Border radius of the badge + * @cssprop {} --pf-v6-c-badge--MinWidth - Minimum width of the badge + * @cssprop {} --pf-v6-c-badge--PaddingInlineStart - Inline start padding + * @cssprop {} --pf-v6-c-badge--PaddingInlineEnd - Inline end padding + * @cssprop {} --pf-v6-c-badge--FontSize - Font size of the badge text + * @cssprop {} --pf-v6-c-badge--FontWeight - Font weight of the badge text + * @cssprop {} --pf-v6-c-badge--m-read--Color - Text color in read state + * @cssprop {} --pf-v6-c-badge--m-read--BackgroundColor - Background color in read state + * @cssprop {} --pf-v6-c-badge--m-read--BorderColor - Border color in read state + * @cssprop {} --pf-v6-c-badge--m-unread--Color - Text color in unread state + * @cssprop {} --pf-v6-c-badge--m-unread--BackgroundColor - Background color in unread state + * @cssprop {} --pf-v6-c-badge--m-disabled--Color - Text color when disabled + * @cssprop {} --pf-v6-c-badge--m-disabled--BackgroundColor - Background color when disabled + * @cssprop {} --pf-v6-c-badge--m-disabled--BorderColor - Border color when disabled + */ +@customElement('pf-v6-badge') +export class PfV6Badge extends LitElement { + static readonly styles = [styles]; + + /** + * Denotes the state-of-affairs this badge represents. + */ + @property({ reflect: true }) state?: BadgeState; + + /** + * Sets a numeric value for a badge. + * + * You can pair it with `threshold` attribute to add a `+` sign + * if the number exceeds the threshold value. + */ + @property({ reflect: true, type: Number }) number?: number; + + /** + * Sets a threshold for the numeric value and adds `+` sign if + * the numeric value exceeds the threshold value. + */ + @property({ reflect: true, type: Number }) threshold?: number; + + /** Disables the badge */ + @property({ type: Boolean, reflect: true }) disabled = false; + + override render(): TemplateResult<1> { + const { threshold, number } = this; + const displayText = + (threshold && number && (threshold < number)) ? `${threshold.toString()}+` + : (number != null) ? number.toString() + : ''; + return html`${!displayText ? html`` : displayText}`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-v6-badge': PfV6Badge; + } +} diff --git a/elements/test/pf-v6-badge.spec.ts b/elements/test/pf-v6-badge.spec.ts new file mode 100644 index 0000000000..ad2dee0e5b --- /dev/null +++ b/elements/test/pf-v6-badge.spec.ts @@ -0,0 +1,181 @@ +import { expect, html } 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 { getColor, hexToRgb } from '@patternfly/pfe-tools/test/hex-to-rgb.js'; +import { PfV6Badge } from '@patternfly/elements/pf-v6-badge/pf-v6-badge.js'; + +describe('', function() { + it('imperatively instantiates', function() { + expect(document.createElement('pf-v6-badge')).to.be.an.instanceof(PfV6Badge); + }); + + it('should upgrade', async function() { + const el = await createFixture(html`10`); + expect(el) + .to.be.an.instanceOf(customElements.get('pf-v6-badge')) + .and + .to.be.an.instanceOf(PfV6Badge); + }); + + describe('with number attribute', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + 100 + `); + await element.updateComplete; + }); + + it('should have the number property set', function() { + expect(element.number).to.equal(100); + }); + + it('should be visible in the accessibility tree', async function() { + const snapshot = await a11ySnapshot(); + expect(snapshot.children?.length).to.be.greaterThan(0); + const badgeNode = snapshot.children?.find( + (child: { name?: string }) => child.name?.includes('100') + ); + expect(badgeNode).to.exist; + }); + }); + + describe('with number exceeding threshold', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + 900 + `); + await element.updateComplete; + }); + + it('should display threshold with "+" in the accessibility tree', async function() { + const snapshot = await a11ySnapshot(); + const badgeNode = snapshot.children?.find( + (child: { name?: string }) => child.name?.includes('100+') + ); + expect(badgeNode).to.exist; + }); + }); + + describe('with number below threshold', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + 50 + `); + await element.updateComplete; + }); + + it('should display the number without "+" in the accessibility tree', async function() { + const snapshot = await a11ySnapshot(); + const badgeNode = snapshot.children?.find( + (child: { name?: string }) => + child.name?.includes('50') && !child.name?.includes('+') + ); + expect(badgeNode).to.exist; + }); + }); + + describe('without state attribute', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + 10 + `); + await element.updateComplete; + }); + + it('should display default background color', function() { + const [r, g, b] = getColor(element, 'background-color'); + expect([r, g, b]).to.deep.equal(hexToRgb('#f0f0f0')); + }); + }); + + describe('with state="read"', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + 10 + `); + await element.updateComplete; + }); + + it('should display read background color', function() { + const [r, g, b] = getColor(element, 'background-color'); + expect([r, g, b]).to.deep.equal(hexToRgb('#f0f0f0')); + }); + }); + + describe('with state="unread"', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + 10 + `); + await element.updateComplete; + }); + + it('should display unread background color', function() { + const [r, g, b] = getColor(element, 'background-color'); + expect([r, g, b]).to.deep.equal(hexToRgb('#0066cc')); + }); + + it('should display unread text color', function() { + const [r, g, b] = getColor(element, 'color'); + expect([r, g, b]).to.deep.equal(hexToRgb('#ffffff')); + }); + }); + + describe('with disabled attribute', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + 10 + `); + await element.updateComplete; + }); + + it('should display disabled background color', function() { + const [r, g, b] = getColor(element, 'background-color'); + expect([r, g, b]).to.deep.equal(hexToRgb('#d2d2d2')); + }); + + it('should have pointer-events: none', function() { + const styles = getComputedStyle(element); + expect(styles.pointerEvents).to.equal('none'); + }); + }); + + describe('accessibility', function() { + it('should contain text in the accessibility tree', async function() { + await createFixture(html` + 10 + `); + const snapshot = await a11ySnapshot(); + expect(snapshot.children?.length).to.be.greaterThan(0); + }); + }); + + describe('slot content', function() { + let element: PfV6Badge; + + beforeEach(async function() { + element = await createFixture(html` + Custom Text + `); + await element.updateComplete; + }); + + it('should display slotted text content when no number is set', function() { + expect(element.textContent?.trim()).to.equal('Custom Text'); + }); + }); +}); From 4b6f72826973cc640289af87ce60ee1d4f63db54 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 8 May 2026 15:28:56 -0400 Subject: [PATCH 2/4] fix(badge): apply review fixes - Remove reflect from number and threshold properties (no CSS selectors) - Fix threshold comparison: use <= instead of < so number=threshold shows + - Simplify render return type to TemplateResult - Add cem generate output path to config Assisted-By: Claude Opus 4.6 --- .config/cem.yaml | 1 + elements/pf-v6-badge/pf-v6-badge.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) 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/pf-v6-badge/pf-v6-badge.ts b/elements/pf-v6-badge/pf-v6-badge.ts index 2492456c14..50bcd55173 100644 --- a/elements/pf-v6-badge/pf-v6-badge.ts +++ b/elements/pf-v6-badge/pf-v6-badge.ts @@ -44,21 +44,21 @@ export class PfV6Badge extends LitElement { * You can pair it with `threshold` attribute to add a `+` sign * if the number exceeds the threshold value. */ - @property({ reflect: true, type: Number }) number?: number; + @property({ type: Number }) number?: number; /** * Sets a threshold for the numeric value and adds `+` sign if * the numeric value exceeds the threshold value. */ - @property({ reflect: true, type: Number }) threshold?: number; + @property({ type: Number }) threshold?: number; /** Disables the badge */ @property({ type: Boolean, reflect: true }) disabled = false; - override render(): TemplateResult<1> { + override render(): TemplateResult { const { threshold, number } = this; const displayText = - (threshold && number && (threshold < number)) ? `${threshold.toString()}+` + (threshold && number && (threshold <= number)) ? `${threshold.toString()}+` : (number != null) ? number.toString() : ''; return html`${!displayText ? html`` : displayText}`; From 4253b926e62542166f42e1c2ef718e5fe3687574 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 8 May 2026 15:49:23 -0400 Subject: [PATCH 3/4] test(badge): move test to element directory --- elements/pf-v6-badge/pf-v6-badge.ts | 2 +- elements/{ => pf-v6-badge}/test/pf-v6-badge.spec.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename elements/{ => pf-v6-badge}/test/pf-v6-badge.spec.ts (100%) diff --git a/elements/pf-v6-badge/pf-v6-badge.ts b/elements/pf-v6-badge/pf-v6-badge.ts index 50bcd55173..e22a11ee9c 100644 --- a/elements/pf-v6-badge/pf-v6-badge.ts +++ b/elements/pf-v6-badge/pf-v6-badge.ts @@ -31,7 +31,7 @@ export type BadgeState = 'unread' | 'read'; */ @customElement('pf-v6-badge') export class PfV6Badge extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; /** * Denotes the state-of-affairs this badge represents. diff --git a/elements/test/pf-v6-badge.spec.ts b/elements/pf-v6-badge/test/pf-v6-badge.spec.ts similarity index 100% rename from elements/test/pf-v6-badge.spec.ts rename to elements/pf-v6-badge/test/pf-v6-badge.spec.ts From 3a4af73fbece2c80b62ceb051aa70c9d5aa8a99b Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 8 May 2026 16:01:27 -0400 Subject: [PATCH 4/4] docs(badge): correct index demo to simplest --- elements/pf-v6-badge/demo/index.html | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/elements/pf-v6-badge/demo/index.html b/elements/pf-v6-badge/demo/index.html index b0239ac479..418d93581a 100644 --- a/elements/pf-v6-badge/demo/index.html +++ b/elements/pf-v6-badge/demo/index.html @@ -1,11 +1,8 @@ --- -name: Read -description: Read badges display a numeric value with read state styling, indicating the content has been viewed. +name: Basic +description: A basic badge displays a numeric value. --- -7 -24 -240 -999+ +7