From d678e8169f06188f16eb78a9679e21d081caf3e5 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 8 May 2026 14:08:10 -0400 Subject: [PATCH 1/4] feat(spinner): add pf-v6-spinner element Port pf-v5-spinner to pf-v6-spinner with v6 design tokens and API: - Migrate CSS tokens from --pf-v5-* to --pf-v6-* namespace - Update icon size tokens to --pf-t--global--icon--size--* (v6) - Add `xs` size variant (new in v6) - Add `inline` boolean attribute for font-size-inherited spinners - Add `accessible-label` attribute (abstracts aria-label via internals) - Set role=progressbar and aria-valuetext via InternalsController - Export `SpinnerSize` type union - Avoid sprouting default `size` attribute (CSS handles xl default) - Add demos: basic, size-variations, custom-size, inline-size - Add unit tests with a11ySnapshot assertions and size measurements - Update elements/package.json exports Closes #3037 Assisted-By: Claude Opus 4.6 --- elements/package.json | 1 + elements/pf-v6-spinner/demo/basic.html | 17 +++ elements/pf-v6-spinner/demo/custom-size.html | 17 +++ elements/pf-v6-spinner/demo/inline-size.html | 35 +++++ .../pf-v6-spinner/demo/size-variations.html | 24 ++++ elements/pf-v6-spinner/pf-v6-spinner.css | 105 ++++++++++++++ elements/pf-v6-spinner/pf-v6-spinner.ts | 69 +++++++++ .../pf-v6-spinner/test/pf-v6-spinner.spec.ts | 131 ++++++++++++++++++ 8 files changed, 399 insertions(+) create mode 100644 elements/pf-v6-spinner/demo/basic.html create mode 100644 elements/pf-v6-spinner/demo/custom-size.html create mode 100644 elements/pf-v6-spinner/demo/inline-size.html create mode 100644 elements/pf-v6-spinner/demo/size-variations.html create mode 100644 elements/pf-v6-spinner/pf-v6-spinner.css create mode 100644 elements/pf-v6-spinner/pf-v6-spinner.ts create mode 100644 elements/pf-v6-spinner/test/pf-v6-spinner.spec.ts diff --git a/elements/package.json b/elements/package.json index fe8dda2080..6e3341d464 100644 --- a/elements/package.json +++ b/elements/package.json @@ -49,6 +49,7 @@ "./pf-v5-progress/pf-v5-progress.js": "./pf-v5-progress/pf-v5-progress.js", "./pf-v5-search-input/pf-v5-search-input.js": "./pf-v5-search-input/pf-v5-search-input.js", "./pf-v5-spinner/pf-v5-spinner.js": "./pf-v5-spinner/pf-v5-spinner.js", + "./pf-v6-spinner/pf-v6-spinner.js": "./pf-v6-spinner/pf-v6-spinner.js", "./pf-v5-switch/pf-v5-switch.js": "./pf-v5-switch/pf-v5-switch.js", "./pf-v5-table/context.js": "./pf-v5-table/context.js", "./pf-v5-table/pf-v5-caption.js": "./pf-v5-table/pf-v5-caption.js", diff --git a/elements/pf-v6-spinner/demo/basic.html b/elements/pf-v6-spinner/demo/basic.html new file mode 100644 index 0000000000..38bfd69761 --- /dev/null +++ b/elements/pf-v6-spinner/demo/basic.html @@ -0,0 +1,17 @@ +--- +name: Basic +description: A basic spinner indicates that an action is in progress. +--- +
+ Loading... +
+ + + + diff --git a/elements/pf-v6-spinner/demo/custom-size.html b/elements/pf-v6-spinner/demo/custom-size.html new file mode 100644 index 0000000000..5c0b3f6d31 --- /dev/null +++ b/elements/pf-v6-spinner/demo/custom-size.html @@ -0,0 +1,17 @@ +--- +name: Custom size +description: A spinner can have a custom diameter using the `diameter` attribute. +--- +
+ Loading... +
+ + + + diff --git a/elements/pf-v6-spinner/demo/inline-size.html b/elements/pf-v6-spinner/demo/inline-size.html new file mode 100644 index 0000000000..ac12dc37a2 --- /dev/null +++ b/elements/pf-v6-spinner/demo/inline-size.html @@ -0,0 +1,35 @@ +--- +name: Inline size +description: An inline spinner inherits its font size, so its size will match the content around it. +--- +
+

+ Heading + Loading... +

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed hendrerit nisi in cursus maximus.

+

+ Second level + Loading... +

+

+ Curabitur accumsan turpis pharetra blandit. Quisque condimentum maximus mi, + Loading... + sit amet commodo arcu rutrum id. Proin pretium urna vel cursus venenatis. + Suspendisse potenti. +

+ + Sometimes you need small text + Loading... + +
+ + + + diff --git a/elements/pf-v6-spinner/demo/size-variations.html b/elements/pf-v6-spinner/demo/size-variations.html new file mode 100644 index 0000000000..16c5cd04af --- /dev/null +++ b/elements/pf-v6-spinner/demo/size-variations.html @@ -0,0 +1,24 @@ +--- +name: Size variations +description: Spinners can be sized using the `size` attribute with values `xs`, `sm`, `md`, `lg`, and `xl`. +--- +
+ Loading... + Loading... + Loading... + Loading... + Loading... +
+ + + + diff --git a/elements/pf-v6-spinner/pf-v6-spinner.css b/elements/pf-v6-spinner/pf-v6-spinner.css new file mode 100644 index 0000000000..8fb49f22cb --- /dev/null +++ b/elements/pf-v6-spinner/pf-v6-spinner.css @@ -0,0 +1,105 @@ +:host { + display: inline-block; + width: min-content; + min-height: 0; + aspect-ratio: 1 / 1; +} + +[hidden] { + display: none !important; +} + +svg { + overflow: hidden; + width: var(--pf-v6-c-spinner--Width, + var(--pf-v6-c-spinner--diameter, + var(--pf-t--global--icon--size--2xl, 3.5rem))); + height: var(--pf-v6-c-spinner--Height, + var(--pf-v6-c-spinner--diameter, + var(--pf-t--global--icon--size--2xl, 3.5rem))); + animation: + rotate + calc(var(--pf-v6-c-spinner--AnimationDuration, 1.4s) * 2) + var(--pf-v6-c-spinner--AnimationTimingFunction, linear) infinite; +} + +circle { + width: 100%; + height: 100%; + transform-origin: 50% 50%; + stroke-linecap: round; + stroke-dasharray: 283; + stroke-dashoffset: 280; + stroke: var(--pf-v6-c-spinner--Color, + var(--pf-t--global--icon--color--brand--default, #0066cc)); + stroke-width: var(--pf-v6-c-spinner--StrokeWidth, 10); + animation: + dash + var(--pf-v6-c-spinner--AnimationDuration, 1.4s) + var(--pf-v6-c-spinner__path--AnimationTimingFunction, ease-in-out) infinite; +} + +:host([size="xs"]) svg { + --pf-v6-c-spinner--diameter: var(--pf-v6-c-spinner--m-xs--diameter, + var(--pf-t--global--icon--size--sm, 0.75rem)); + + & circle { + stroke-width: var(--pf-v6-c-spinner--StrokeWidth, 15); + } +} + +:host([size="sm"]) svg { + --pf-v6-c-spinner--diameter: var(--pf-v6-c-spinner--m-sm--diameter, + var(--pf-t--global--icon--size--md, 0.875rem)); +} + +:host([size="md"]) svg { + --pf-v6-c-spinner--diameter: var(--pf-v6-c-spinner--m-md--diameter, + var(--pf-t--global--icon--size--lg, 1rem)); +} + +:host([size="lg"]) svg { + --pf-v6-c-spinner--diameter: var(--pf-v6-c-spinner--m-lg--diameter, + var(--pf-t--global--icon--size--xl, 1.5rem)); +} + +:host([size="xl"]) svg { + --pf-v6-c-spinner--diameter: var(--pf-v6-c-spinner--m-xl--diameter, + var(--pf-t--global--icon--size--2xl, 3.5rem)); +} + +:host([inline]) svg { + --pf-v6-c-spinner--diameter: var(--pf-v6-c-spinner--m-inline--diameter, 1em); +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes dash { + 0% { + stroke-dashoffset: 280; + transform: rotate(0); + } + + 15% { + stroke-width: calc(var(--pf-v6-c-spinner__path--StrokeWidth, + var(--pf-v6-c-spinner--StrokeWidth, 10)) - 4); + } + + 40% { + stroke-dasharray: 220; + stroke-dashoffset: 150; + } + + 100% { + stroke-dashoffset: 280; + transform: rotate(720deg); + } +} diff --git a/elements/pf-v6-spinner/pf-v6-spinner.ts b/elements/pf-v6-spinner/pf-v6-spinner.ts new file mode 100644 index 0000000000..a5c4b5d24c --- /dev/null +++ b/elements/pf-v6-spinner/pf-v6-spinner.ts @@ -0,0 +1,69 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { property } from 'lit/decorators/property.js'; + +import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js'; + +import styles from './pf-v6-spinner.css'; + +export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +/** + * A **spinner** is an animated visual that indicates when a quick action is + * in progress. For actions that may take a long time, use a progress bar instead. + * @summary Indicates that an action is in progress. + * @cssprop {} [--pf-v6-c-spinner--diameter] - Custom diameter of the spinner + * @cssprop {} [--pf-v6-c-spinner--Color] - Color of the spinner stroke + * @cssprop {