docs(meter): 2nd-gen migration plan#6215
Conversation
- align meter API with React Spectrum S2 (`progress` -> `value`, add
`minValue`/`maxValue`, replace `side-label` with `label-position` enum,
expose `value-label` and `format-options`)
- normalize variant set to `{informative, positive, notice, negative}`
per Figma `S2 / Web (Desktop scale)` Meter frame
- migrate as an independent component; bar/track/fill styles live in
`meter.css` (no shared base with progress-bar / progress-circle)
- render plain `<label class="spectrum-FieldLabel ...">` matching
`spectrum-css` `spectrum-two` HTML output until `<swc-field-label>`
is migrated
- fix 1st-gen `role="meter progressbar"` to single `role="meter"`;
add missing `aria-valuemin`/`aria-valuemax`/`aria-valuetext`
- ship `static-color="black"` and a `help-text` slot net-new from S2
SWC-2007 (epic SWC-2005)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
📚 Branch Preview Links🔍 First Generation Visual Regression Test ResultsWhen a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:
Deployed to Azure Blob Storage: If the changes are expected, update the |
5t3ph
left a comment
There was a problem hiding this comment.
Let me know if you need clarification on any of these!
The most impactful is to re-consider having a shared base between meter and progress bar.
|
|
||
| - 1st-gen `<sp-meter>` extended progress-bar visuals via CSS `@import` of `spectrum-progress-bar.css` and `progress-bar-overrides.css`. In 2nd-gen, this is replaced with a self-contained `meter.css` that copies the relevant bar/track/fill rules from `spectrum-css` `spectrum-two` ([`components/progressbar/index.css`](https://github.com/adobe/spectrum-css/blob/spectrum-two/components/progressbar/index.css)) plus the meter-specific rules from [`components/meter/index.css`](https://github.com/adobe/spectrum-css/blob/spectrum-two/components/meter/index.css). | ||
| - The 1st-gen progress-bar package is not migrated yet; making meter wait on or share with it would couple two epics unnecessarily and slow SWC-2005. | ||
| - The components have different ARIA semantics (`role="meter"` vs `role="progressbar"`), different default behaviors (meter is determinate-only; progress-bar can be indeterminate), and different value semantics. A shared base would have to gate most of its API per subclass anyway. |
There was a problem hiding this comment.
I would challenge this as a reason to not have a shared base in core. The ARIA role would be on the rendering layer and not shared in core anyway. Styles would be fine to remain separate, at least for now.
Many of the exposed consumer attributes are the same, and the ones that are different can be reserved and added via the rendering layer. This is how we are approaching Button as well, for reference (you could have your agent look at that migration plan for comparison).
The lingering issue may be deciding how to name it in core - maybe progress-meter?
There was a problem hiding this comment.
Could this be done as a mixin or controller?
There was a problem hiding this comment.
Thanks for pushing on reuse — we’re aligned that role and styling living in SWC don’t rule out pulling out small shared pieces later (localized formatting, normalized fill fraction, label-from-slot) via helpers, a mixin, or a controller if it stays cheap to maintain.
I'd still gently suggest treating meter and progress-bar as separate semantic cores, not one shared base type, mainly because the parity numbers don’t look like Button vs action-button:
Properties (1st gen): 5 of 8 line up on both (progress, label, side-label, static-color, size). 3 of 8 are only on one (variant on meter; indeterminate and over-background on progress-bar), so cross-component duplication is already partial.
Slots: Same default label slot/hoisting pattern (1/1) in gen 1 — good candidate for helpers, less so for inheritance.
Role / ARIA: Only naming via label → aria-label lines up cleanly; role, aria-valuemin/max, aria-valuetext, and aria-valuenow when indeterminate don’t align with meter’s shape today — that mismatch grows once meter picks up value/min/max and progress-bar locks its own gen‑2 API.
Templates: Visibility rules for the label chrome and the percent/value row differ materially; shared bar markup is mechanical, not a shared semantic contract like Button’s family.
Button/action-button deliberately share one interactive primitive and a roadmap of sibling components on the same semantic contract. Here we have different roles/use cases, uneven property overlap, and different timelines — so I'd suggest keeping MeterBase meter-scoped, and optional shared utilities once progress-bar’s gen‑2 design is settled, rather than coupling this epic on a hypothetical *-base. Happy to revise if we later see a firmer convergence on API and semantics.
There was a problem hiding this comment.
In 1st gen they already don’t share a TypeScript base class — they’re separate components; only styling is shared
There was a problem hiding this comment.
Is there any difference in the APIs?
|
|
||
| Sourced from [`accessibility-migration-analysis.md`](./accessibility-migration-analysis.md) and the React Spectrum S2 Meter API: | ||
|
|
||
| - `role="meter"` set in `firstUpdated` if not already set. Fixed; not author-overridable. |
There was a problem hiding this comment.
This can be simplified as it should be part of the render template permanently.
There was a problem hiding this comment.
This can be simplified as it should be part of the render template permanently.
@5t3ph If the role is on an element in the shadow DOM we will not be able to use IDREFs like aria-labelledby, although eventually we will have a corss-root ARIA solution.
There was a problem hiding this comment.
Going with role + aria-value* on host (not on the shadow .swc-Meter wrapper). This keeps consumer IDREFs (aria-labelledby, aria-describedby) working — light-DOM IDs can't resolve into shadow DOM today. Cross-root ARIA (Reference Target) is not yet shipped in stable browsers, so host placement is the only safe option right now.
| - [ ] Static-color rules for both `staticWhite` and `staticBlack` modifiers | ||
| - [ ] `label-position="side"` layout rule (`.swc-Meter--sideLabel`) mirroring `spectrum-css` `spectrum-two` `.spectrum-ProgressBar--sideLabel` | ||
| - [ ] Help-text spacing rule (`--swc-meter-help-text-spacing`) and visibility gating when `slot="help-text"` is empty | ||
| - [ ] Hide the percent label cell when `static-color` is set (matches Figma `Static white` / `Static black` panels) |
There was a problem hiding this comment.
Reminder not to do this, remove this item.
|
|
||
| #### Naming and semantics | ||
|
|
||
| - [ ] Single `role="meter"` on host. No combined role string. |
There was a problem hiding this comment.
| - [ ] Single `role="meter"` on host. No combined role string. | |
| - [ ] Single `role="meter"` on `.swc-Meter` container. No combined role string. |
There was a problem hiding this comment.
disregard for now, pending aria discussion
There was a problem hiding this comment.
This is valid again since you are taking aria off the host, per other comment discussion.
| #### Naming and semantics | ||
|
|
||
| - [ ] Single `role="meter"` on host. No combined role string. | ||
| - [ ] `aria-valuemin=<minValue>`, `aria-valuemax=<maxValue>`, `aria-valuenow=<clamped value>`, `aria-valuetext=<formatted value>` on host |
There was a problem hiding this comment.
| - [ ] `aria-valuemin=<minValue>`, `aria-valuemax=<maxValue>`, `aria-valuenow=<clamped value>`, `aria-valuetext=<formatted value>` on host | |
| - [ ] `aria-valuemin=<minValue>`, `aria-valuemax=<maxValue>`, `aria-valuenow=<clamped value>`, `aria-valuetext=<formatted value>` on `.swc-Meter` container |
There was a problem hiding this comment.
disregard for now, pending aria discussion
There was a problem hiding this comment.
This is valid again since you are taking aria off the host, per other comment discussion.
|
|
||
| - [ ] Single `role="meter"` on host. No combined role string. | ||
| - [ ] `aria-valuemin=<minValue>`, `aria-valuemax=<maxValue>`, `aria-valuenow=<clamped value>`, `aria-valuetext=<formatted value>` on host | ||
| - [ ] Accessible name from `label`, default slot text, `aria-label`, or `aria-labelledby` |
There was a problem hiding this comment.
Update pending @nikkimk feedback on earlier comment
|
|
||
| All drafting-time questions are resolved. Resolutions: | ||
|
|
||
| - **Q1 (architecture)** — Closed. Independent `<swc-meter>` with no shared base. Bar styles live in `meter.css`. Reflected in [Architecture: core vs SWC split](#architecture-core-vs-swc-split) and [Migration sequencing and prerequisites](#migration-sequencing-and-prerequisites). |
There was a problem hiding this comment.
Noting that the shared base idea should be re-opened, see earlier comment.
There was a problem hiding this comment.
Interested in this outcome too! My first impression is 2 bases with the shared mixin sounds like a reasonable approach.
There was a problem hiding this comment.
Would love to chat about this as I'm currently assigned to progress bar!
| - **Progress bar** ([`1st-gen/packages/progress-bar`](../../../../1st-gen/packages/progress-bar/)) — independent migration on its own epic. No coupling to this work. | ||
| - **Progress circle** ([`2nd-gen/packages/swc/components/progress-circle`](../../../../2nd-gen/packages/swc/components/progress-circle/)) — already migrated. Used as the structural precedent for `aria-value*` plumbing, slot-as-label hoisting, locale-aware formatting, and the dev-mode accessible-name warning. Not extended. | ||
| - **Field label** — internal render dependency; not migrated. 2nd-gen `<swc-meter>` renders plain `<label class="spectrum-FieldLabel ...">` matching `spectrum-css` `spectrum-two` HTML output. No dependency on `<swc-field-label>`. | ||
| - **Help text** — 1st-gen `<sp-help-text>` package is not migrated. The `help-text` slot in `<swc-meter>` accepts any DOM, so consumers can place 1st-gen `<sp-help-text>` or any composed help-text element until `<swc-help-text>` exists. |
There was a problem hiding this comment.
Should this be a string attribute instead? I'm not sure we need a slot here...
There was a problem hiding this comment.
I could see folks trying to add like a link, or an icon/tooltip. But, maybe we're comfortable with those being a reason they might have to extend the component.
| | ------------- | -------------------- | ----- | | ||
| | (default) | Visible meter label | **Confirmed.** Slot text hoists into `label` property via `slotchange` + `getLabelFromSlot`. Single naming path. | | ||
| | `value-label` | Custom value content | **Confirmed.** Optional. Renders inside the percent label region. Used for richer value displays (e.g. `<strong>1</strong> of 4`). When the slot is empty and `value-label` attribute is unset, the auto-formatted value is rendered. | | ||
| | `help-text` | Optional help text | **Confirmed.** Renders inside the meter's S2 `helptext` region (per `spectrum-css` `spectrum-two` HTML output). Consumers compose `<sp-help-text>` (1st-gen, until `<swc-help-text>` exists) or any DOM. Wire to `aria-describedby` if the slot is non-empty. | |
There was a problem hiding this comment.
same comment as above - I think we can simplify this with a property.
| - [ ] `staticColor` set matches `spectrum-css` `spectrum-two` (`staticWhite`, `staticBlack`) and Figma `Static white` / `Static black` panels; React's `'auto'` is deferred (no S2 CSS support) | ||
| - [ ] `labelPosition` matches React Spectrum (`top` default, `side`); side-label coverage backed by `spectrum-css` `spectrum-two` `progressbar/index.css` `.spectrum-ProgressBar--sideLabel` | ||
| - [ ] `value-label` slot + attribute matches React Spectrum `valueLabel` | ||
| - [ ] `format-options` matches React Spectrum `formatOptions` (property only — no string serialization) |
There was a problem hiding this comment.
React forwards these options to Intl.NumberFormat. If we want this parity we probably shouldn’t try to flatten or remap everything the native API supports today (and may support in the future).
I think the cleanest option is to expose this as a JavaScript property only, I don't see a very scalable path trying to model it as attributes.
Also, we'll need to hook this up to our LanguageResolutionController (which now that I think about it, should actually be named LocaleResolutionController 😛
5t3ph
left a comment
There was a problem hiding this comment.
I would suggest making these updates, then asking the agent to run a consistency pass based on your staged changes before committing as I didn't mark every instance of where these changes impact the plan. You should see changes throughout the checklists as well.
|
|
||
| - **Progress bar** ([`1st-gen/packages/progress-bar`](../../../../1st-gen/packages/progress-bar/)) — independent migration on its own epic. No coupling to this work. | ||
| - **Progress circle** ([`2nd-gen/packages/swc/components/progress-circle`](../../../../2nd-gen/packages/swc/components/progress-circle/)) — already migrated. Used as the structural precedent for `aria-value*` plumbing, slot-as-label hoisting, locale-aware formatting, and the dev-mode accessible-name warning. Not extended. | ||
| - **Field label** — internal render dependency; not migrated. 2nd-gen `<swc-meter>` renders plain `<label class="swc-Meter-label">` / `<label class="swc-Meter-value">` (SWC-namespaced selectors per the [contributor docs selector patterns](../../../../CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md#selector-patterns)). No dependency on `<swc-field-label>`. |
There was a problem hiding this comment.
Just validated that use of role="meter" does not make it eligible to be attached to <label> - it would only work if we were going to use native meter.
Since you will be creating the meter with aria attributes to support custom styling, it is semantically meaningless to use <label> and instead a span could be used given the plan is to use aria-labelledby to associate it to the meter.
| - **Progress bar** ([`1st-gen/packages/progress-bar`](../../../../1st-gen/packages/progress-bar/)) — independent migration on its own epic. No coupling to this work. | ||
| - **Progress circle** ([`2nd-gen/packages/swc/components/progress-circle`](../../../../2nd-gen/packages/swc/components/progress-circle/)) — already migrated. Used as the structural precedent for `aria-value*` plumbing, slot-as-label hoisting, locale-aware formatting, and the dev-mode accessible-name warning. Not extended. | ||
| - **Field label** — internal render dependency; not migrated. 2nd-gen `<swc-meter>` renders plain `<label class="swc-Meter-label">` / `<label class="swc-Meter-value">` (SWC-namespaced selectors per the [contributor docs selector patterns](../../../../CONTRIBUTOR-DOCS/02_style-guide/01_css/01_component-css.md#selector-patterns)). No dependency on `<swc-field-label>`. | ||
| - **Help text** — exposed as a `help-text` string attribute on `<swc-meter>` (not a slot). Renders as plain text inside the internal `swc-Meter-helptext` region. Consumers needing rich/interactive help-text content compose externally and wire `aria-describedby` themselves. |
There was a problem hiding this comment.
In the current plan to move ARIA internally, consumers will not be able to successfully use aria-describedby for external help text due to cross-root ARIA.
| - `role="meter"` set on the host in `firstUpdated`. Always set (no "if not already set" conditional). Fixed; not author-overridable. | ||
| - `aria-valuemin=<minValue>`, `aria-valuemax=<maxValue>`, `aria-valuenow=<value>` (clamped), `aria-valuetext=<formatted value>` set on host. Update on every relevant property change and on locale change. | ||
| - **Placement rationale.** `role` + `aria-value*` live on the host (not on the shadow `.swc-Meter` wrapper) so consumer-supplied IDREFs bind correctly. Cross-root ARIA (Reference Target / cross-root ARIA spec) is not yet shipped in stable browsers. |
There was a problem hiding this comment.
These need updated as well with the decision to move aria internal. Essentially, nothing will be set on the host after all.
|
|
||
| | Slot | Content | Notes | | ||
| | ------------- | -------------------- | ----- | | ||
| | (default) | Primary visible meter label | **Confirmed.** Default slot is the **primary** visible label and primary a11y name source. Slot text hoists into `accessibleLabel` via `slotchange` + `getLabelFromSlot` only when `accessibleLabel` is unset. | |
There was a problem hiding this comment.
This reads to me as if you will be forcing the default slot text into aria-label which wouldn't be correct since it would be intended to be a visible label. Am I misinterpreting this?
| <label class="swc-Meter-label"> | ||
| <slot @slotchange=...>{accessibleLabel}</slot> | ||
| </label> | ||
| <label class="swc-Meter-value"> | ||
| {valueLabel ?? auto-formatted value} | ||
| </label> |
There was a problem hiding this comment.
Explained in earlier comment, but these should be simple <span> elements
| <div class="swc-Meter-helptext" id="<internal-id>" hidden=${!helpText}> | ||
| {helpText} | ||
| </div> |
There was a problem hiding this comment.
Rather than toggling the hidden attribute, you can conditionally prevent this entire block from showing if there isn't help text. True for the other labeling parts as well.
Description
Adds the Phase 1 (Preparation) migration plan for the
<sp-meter>→<swc-meter>2nd-gen migration atCONTRIBUTOR-DOCS/03_project-planning/03_components/meter/migration-plan.md. This is a planning deliverable only — no code, CSS, or component changes are included. The components README TOC is regenerated by the contributor-docs nav script.The plan codifies the public API, breaking changes, additive items, architecture (core vs SWC split), migration sequencing, and per-phase checklists for the remaining tickets in the Meter migration epic.
Key decisions documented:
<swc-meter>with the React Spectrum S2 Meter API and the supplied FigmaS2 / Web (Desktop scale)Meter frame.progress→value; addminValue/maxValue(defaults0/100); replaceside-labelboolean withlabel-positionenum ('top'default,'side'); exposevalue-labelandformat-options; normalize the variant set to{informative (default), positive, notice, negative}.<swc-progress-bar>or<swc-progress-circle>. Bar/track/fill styles are inlined inmeter.cssfromspectrum-cssspectrum-two(progressbar/index.css+meter/index.css).<label class="spectrum-FieldLabel ...">matchingspectrum-cssspectrum-twoHTML output until<swc-field-label>is migrated.role="meter progressbar"to a singlerole="meter"; add missingaria-valuemin,aria-valuemax, andaria-valuetext.static-color="black"and ahelp-textslot as net-new from S2.Motivation and context
This is the first deliverable in the Meter migration epic. The plan is the shared baseline that every later phase (
SWC-2008setup →SWC-2016review) consumes for naming, public API, behavioral semantics, and architecture decisions, per.claude/skills/migration-prep/references/migration-plan-contract.md. Aligning with React Spectrum and Figma now avoids a second, more disruptive migration later, and resolves the 1st-gen ARIA bug (combined role string + missing value attributes) before consumers depend on the 2nd-gen API.Related issue(s)
Screenshots (if appropriate)
N/A — planning document only; no rendered UI changes.
Author's checklist
meterpattern explicitly.SWC-2013.)Reviewer's checklist
patch,minor, ormajorfeaturesManual review test cases
Plan content review
CONTRIBUTOR-DOCS/03_project-planning/03_components/meter/migration-plan.mdon the PR branch.TL;DR,Changes overview,2nd-gen API decisions, andArchitecture: core vs SWC splitsections.S2 / Web (Desktop scale)Meter frame supplied during planning. Flag any divergence.Architecture call
Migration sequencing and prerequisitesandArchitecture: core vs SWC splitsections.ProgressBase) is acceptable given the upcoming<swc-progress-bar>migration on a separate epic.SWC-2008(Setup) starts.Doc structure and links
accessibility-migration-analysis.md,rendering-and-styling-migration-analysis.md,1st-gen/packages/meter/src/Meter.ts, and the CSS style guide pages.node update-nav.js(run fromCONTRIBUTOR-DOCS/01_contributor-guides/07_authoring-contributor-docs/) reports no new broken links introduced by this PR.Device review
Accessibility testing checklist
This PR ships a planning document, not a rendered component. The full accessibility testing checklist applies to
SWC-2010(Implement accessibility features) andSWC-2013(Review and complete test suites). Steps below verify that the plan itself is reviewable with assistive technology and that the documented ARIA recommendations are coherent.Keyboard — no focusable component is added by this PR; verify the rendered markdown is navigable.
Files changedview on GitHub and locatemigration-plan.md.2nd-gen API decisions); expect focus to move to the corresponding heading.Screen reader — verify the documented ARIA contract reads cleanly and the rendered doc structure is correct.
migration-plan.mdon GitHub.h1→h2→h3→h4. Expect a logical hierarchy with no skipped levels.Property,Type,Default,Attribute,Notes).2nd-gen API decisions → Accessibility semantics notessection. Confirm the documentedrole="meter",aria-valuemin/aria-valuemax/aria-valuenow/aria-valuetext, andaria-describedbywiring matches the WAI-ARIA 1.2 meter spec and the APG meter pattern.ProgressCircleBase.updated.