Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .config/cem.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
sourceControlRootUrl: https://github.com/patternfly/patternfly-elements/tree/main/
generate:
output: ./elements/custom-elements.json
files:
- ./elements/*/*.ts
- ./core/*/*.ts
Expand Down
1 change: 1 addition & 0 deletions elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions elements/pf-v6-badge/demo/disabled.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Disabled
description: Disabled badges indicate that the associated content or action is currently unavailable.
---
<pf-v6-badge state="read" disabled number="7">7</pf-v6-badge>
<pf-v6-badge state="read" disabled number="24">24</pf-v6-badge>
<pf-v6-badge state="unread" disabled number="240">240</pf-v6-badge>
<pf-v6-badge state="unread" disabled threshold="999" number="999">999+</pf-v6-badge>

<script type="module">
import '@patternfly/elements/pf-v6-badge/pf-v6-badge.js';
</script>
9 changes: 9 additions & 0 deletions elements/pf-v6-badge/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: Basic
description: A basic badge displays a numeric value.
---
<pf-v6-badge>7</pf-v6-badge>

<script type="module">
import '@patternfly/elements/pf-v6-badge/pf-v6-badge.js';
</script>
12 changes: 12 additions & 0 deletions elements/pf-v6-badge/demo/read.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Read
description: Read badges display a numeric value with read state styling, indicating the content has been viewed.
---
<pf-v6-badge state="read" number="7">7</pf-v6-badge>
<pf-v6-badge state="read" number="24">24</pf-v6-badge>
<pf-v6-badge state="read" number="240">240</pf-v6-badge>
<pf-v6-badge state="read" threshold="999" number="999">999+</pf-v6-badge>

<script type="module">
import '@patternfly/elements/pf-v6-badge/pf-v6-badge.js';
</script>
10 changes: 10 additions & 0 deletions elements/pf-v6-badge/demo/threshold.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: Threshold
description: A threshold appends a "+" when the numeric value exceeds a set maximum.
---
<pf-v6-badge state="read" number="400" threshold="500">400</pf-v6-badge>
<pf-v6-badge state="read" number="900" threshold="500">900</pf-v6-badge>

<script type="module">
import '@patternfly/elements/pf-v6-badge/pf-v6-badge.js';
</script>
12 changes: 12 additions & 0 deletions elements/pf-v6-badge/demo/unread.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Unread
description: Unread badges use a bold, branded style to indicate content that has not yet been viewed.
---
<pf-v6-badge state="unread" number="7">7</pf-v6-badge>
<pf-v6-badge state="unread" number="24">24</pf-v6-badge>
<pf-v6-badge state="unread" number="240">240</pf-v6-badge>
<pf-v6-badge state="unread" threshold="999" number="999">999+</pf-v6-badge>

<script type="module">
import '@patternfly/elements/pf-v6-badge/pf-v6-badge.js';
</script>
58 changes: 58 additions & 0 deletions elements/pf-v6-badge/pf-v6-badge.css
Original file line number Diff line number Diff line change
@@ -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;
}
72 changes: 72 additions & 0 deletions elements/pf-v6-badge/pf-v6-badge.ts
Original file line number Diff line number Diff line change
@@ -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 {<color>} --pf-v6-c-badge--Color - Text color of the badge
Comment thread
zeroedin marked this conversation as resolved.
Outdated
* @cssprop {<color>} --pf-v6-c-badge--BackgroundColor - Background color of the badge
* @cssprop {<color>} --pf-v6-c-badge--BorderColor - Border color of the badge
* @cssprop {<length>} --pf-v6-c-badge--BorderWidth - Border width of the badge
* @cssprop {<length>} --pf-v6-c-badge--BorderRadius - Border radius of the badge
* @cssprop {<length>} --pf-v6-c-badge--MinWidth - Minimum width of the badge
* @cssprop {<length>} --pf-v6-c-badge--PaddingInlineStart - Inline start padding
* @cssprop {<length>} --pf-v6-c-badge--PaddingInlineEnd - Inline end padding
* @cssprop {<length>} --pf-v6-c-badge--FontSize - Font size of the badge text
* @cssprop {<integer>} --pf-v6-c-badge--FontWeight - Font weight of the badge text
* @cssprop {<color>} --pf-v6-c-badge--m-read--Color - Text color in read state
* @cssprop {<color>} --pf-v6-c-badge--m-read--BackgroundColor - Background color in read state
* @cssprop {<color>} --pf-v6-c-badge--m-read--BorderColor - Border color in read state
* @cssprop {<color>} --pf-v6-c-badge--m-unread--Color - Text color in unread state
* @cssprop {<color>} --pf-v6-c-badge--m-unread--BackgroundColor - Background color in unread state
* @cssprop {<color>} --pf-v6-c-badge--m-disabled--Color - Text color when disabled
* @cssprop {<color>} --pf-v6-c-badge--m-disabled--BackgroundColor - Background color when disabled
* @cssprop {<color>} --pf-v6-c-badge--m-disabled--BorderColor - Border color when disabled
*/
@customElement('pf-v6-badge')
export class PfV6Badge extends LitElement {
static readonly styles: CSSStyleSheet[] = [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({ type: Number }) number?: number;

/**
* Sets a threshold for the numeric value and adds `+` sign if
* the numeric value exceeds the threshold value.
*/
@property({ type: Number }) threshold?: number;

/** Disables the badge */
@property({ type: Boolean, reflect: true }) disabled = false;

override render(): TemplateResult {
const { threshold, number } = this;
const displayText =
(threshold && number && (threshold <= number)) ? `${threshold.toString()}+`
: (number != null) ? number.toString()
: '';
return html`${!displayText ? html`<slot></slot>` : displayText}`;
}
}

declare global {
interface HTMLElementTagNameMap {
'pf-v6-badge': PfV6Badge;
}
}
181 changes: 181 additions & 0 deletions elements/pf-v6-badge/test/pf-v6-badge.spec.ts
Original file line number Diff line number Diff line change
@@ -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('<pf-v6-badge>', 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<PfV6Badge>(html`<pf-v6-badge>10</pf-v6-badge>`);
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<PfV6Badge>(html`
<pf-v6-badge number="100">100</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge number="900" threshold="100">900</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge number="50" threshold="100">50</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge number="10">10</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge state="read" number="10">10</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge state="unread" number="10">10</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge state="read" disabled number="10">10</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge state="read" number="10">10</pf-v6-badge>
`);
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<PfV6Badge>(html`
<pf-v6-badge state="read">Custom Text</pf-v6-badge>
`);
await element.updateComplete;
});

it('should display slotted text content when no number is set', function() {
expect(element.textContent?.trim()).to.equal('Custom Text');
});
});
});
Loading