Skip to content

docs: types style guide and POC refactor of Badge types#6058

Closed
caseyisonit wants to merge 3 commits intomainfrom
caseyisonit/types-guide
Closed

docs: types style guide and POC refactor of Badge types#6058
caseyisonit wants to merge 3 commits intomainfrom
caseyisonit/types-guide

Conversation

@caseyisonit
Copy link
Contributor

@caseyisonit caseyisonit commented Mar 2, 2026

Description

Introduces a contributor style guide for *.types.ts files and demonstrates the recommended patterns by refactoring the Badge component's types as a proof of concept.

The style guide defines conventions for constant naming, type derivation, file structure, S1/S2 coexistence, and clean S1 removal. The Badge types file is updated to serve as the canonical reference implementation of these patterns.

Motivation and context

Component types files lacked consistent conventions for naming, organization, and multi-generation coexistence. Different components used different approaches for defining constants and types (e.g., _S2 suffixes on canonical names, S2 arrays spreading from S1, union types instead of derived types). This guide codifies a single pattern so all components follow the same structure, making future S1 removal straightforward.

Related issue(s)

  • SWC-1419

Key changes

Style guide (CONTRIBUTOR-DOCS/02_style-guide/03_component-types.md)

  • Defines naming conventions: uppercase SCREAMING_SNAKE_CASE constants, PascalCase types, component-prefixed names
  • Establishes the "unsuffixed = canonical" rule: only S1-only names get the S1 suffix
  • Documents file structure with section separators (Shared → S1-only → Canonical → Types)
  • Provides constant patterns for sizes, variants, static colors, and non-variant constants
  • Includes type derivation patterns using (typeof ARRAY)[number]
  • Documents S1 removal strategy with dependency direction rules
  • Lists anti-patterns with corrections

Badge types POC refactor (Badge.types.ts)

  • Renamed BADGE_VARIANTS_COLOR_S2BADGE_VARIANTS_COLOR (canonical, unsuffixed)
  • Renamed BADGE_VARIANTS_S2BADGE_VARIANTS (canonical, unsuffixed)
  • Renamed BadgeColorVariantS2BadgeColorVariant (canonical type)
  • Renamed BadgeVariantS2BadgeVariant (canonical type, no longer a union)
  • Added BADGE_VARIANTS_COLOR as the canonical color array; BADGE_VARIANTS_COLOR_S1 now satisfies readonly BadgeColorVariant[]
  • Reorganized file with section separators: Shared → S1-only → Canonical → Types
  • Updated as const satisfies to use readonly ElementSize[] for correctness

Sized mixin refactor (sized-mixin.ts)

  • Replaced ElementSizes record with ELEMENT_SIZES const array (follows as const pattern)
  • Added DEFAULT_ELEMENT_SIZES const array for the common ['s', 'm', 'l', 'xl'] set
  • Changed VALID_SIZES and validSizes parameter types to readonly ElementSize[]
  • Preserved backward compatibility via deprecated re-export in 1st-gen/tools/base/src/sizedMixin.ts

Consuming components updated

  • Badge.ts, badge.stories.ts, badge.test.ts — updated all imports to use canonical (unsuffixed) names

Research docs

  • Added SWC-1419_research.md — summarized research findings and recommendations
  • Added SWC-1419_research-full-patterns.md — comprehensive pattern analysis across the codebase

Storybook

  • Added Customization section to sidebar navigation in preview.ts

Screenshots (if appropriate)

N/A — no visual changes; types and documentation only.

Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published.
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Includes thoughtfully written changeset if changes suggested include patch, minor, or major features
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers
  • All VRTs are approved before the author can update Golden Hash

Manual review test cases

  • Verify Badge types follow the style guide patterns [@cdransf]

    1. Open 2nd-gen/packages/core/components/badge/Badge.types.ts
    2. Confirm section separators (Shared → S1-only → Canonical → Types) are present
    3. Confirm canonical names are unsuffixed (BADGE_VARIANTS, BadgeVariant) and only S1-only names carry the S1 suffix
  • Verify no regressions in Badge component [@cdransf]

    1. Run yarn test for the badge package
    2. Confirm all existing tests pass with the renamed imports
  • Verify sized mixin backward compatibility [@cdransf]

    1. Confirm 1st-gen/tools/base/src/sizedMixin.ts re-exports ElementSizes with a deprecation notice
    2. Confirm ELEMENT_SIZES and DEFAULT_ELEMENT_SIZES are exported from core/mixins/index.ts
  • Review style guide completeness [@cdransf]

    1. Open CONTRIBUTOR-DOCS/02_style-guide/03_component-types.md
    2. Verify naming conventions, file structure, constant patterns, type derivation, and anti-patterns sections are clear and actionable

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

@caseyisonit caseyisonit requested a review from a team as a code owner March 2, 2026 20:35
@changeset-bot
Copy link

changeset-bot bot commented Mar 2, 2026

⚠️ No Changeset found

Latest commit: ee94864

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

📚 Branch Preview Links

🔍 First Generation Visual Regression Test Results

When 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: pr-6058

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

@caseyisonit caseyisonit added the Status:Ready for review PR ready for review or re-review. label Mar 2, 2026
Copy link
Member

@cdransf cdransf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks awesome! ✨

@nikkimk nikkimk self-assigned this Mar 6, 2026
Copy link
Contributor

@nikkimk nikkimk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. Definitely lots of discussion points here.

Comment on lines +80 to +92
**Renaming existing merged prefixes:** If a component's types file in core already uses a merged prefix (e.g., `STATUSLIGHT_`), rename the constants to the underscore-separated form (`STATUS_LIGHT_`) in core. To avoid a breaking change in 1st-gen, re-export the old name as a deprecated alias from the 1st-gen package:

```typescript
// 1st-gen/packages/status-light/src/StatusLight.ts
import { STATUS_LIGHT_VARIANTS_S1 } from '@spectrum-web-components/core/components/status-light';

/**
* @deprecated Use `STATUS_LIGHT_VARIANTS_S1` instead.
*/
export const STATUSLIGHT_VARIANTS_S1 = STATUS_LIGHT_VARIANTS_S1;
```

This keeps the canonical name correct in core while giving 1st-gen consumers a migration path.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this.

Comment on lines +129 to +134
1. **Copyright header**
2. **Imports** (typically only `ElementSize` from core mixins)
3. **Shared constants** — sizes, canonical base arrays (semantic variants, color variants), non-variant constants
4. **S1-only constants** — subset arrays exclusive to 1st-gen, validated against canonical types via `satisfies`
5. **Canonical constants** — composed arrays that spread from shared bases (e.g., `BADGE_VARIANTS`)
6. **Types** — shared types, then S1-only types, then canonical types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I like that #2 and 6 have examples (ElementSize import and BADGE_VARIANTS constant). Can we add examples for #3, 4, and 6 as well?

Comment on lines +314 to +315
export type BadgeColorVariantS1 = (typeof BADGE_VARIANTS_COLOR_S1)[number]; // remove with 1st-gen
export type BadgeVariantS1 = (typeof BADGE_VARIANTS_S1)[number]; // remove with 1st-gen
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/question: Should the inline comment have a @todo?

Suggested change
export type BadgeColorVariantS1 = (typeof BADGE_VARIANTS_COLOR_S1)[number]; // remove with 1st-gen
export type BadgeVariantS1 = (typeof BADGE_VARIANTS_S1)[number]; // remove with 1st-gen
export type BadgeColorVariantS1 = (typeof BADGE_VARIANTS_COLOR_S1)[number]; // @todo remove with 1st-gen
export type BadgeVariantS1 = (typeof BADGE_VARIANTS_S1)[number]; // @todo remove with 1st-gen

Comment on lines +328 to +355
### Dependency direction

The dependency always flows toward S1:

```text
Shared constants (base arrays + canonical types)
←── Canonical composed arrays spread from shared
←── S1 arrays validate against shared types via satisfies
```

This means:

- **Canonical base arrays and types live in SHARED** — `BADGE_VARIANTS_COLOR` and `BadgeColorVariant` are defined before S1-ONLY
- **S1 arrays validate against canonical types** — `BADGE_VARIANTS_COLOR_S1` uses `satisfies readonly BadgeColorVariant[]`
- **Canonical composed arrays never reference S1 arrays** — `BADGE_VARIANTS` does not spread `BADGE_VARIANTS_S1`
- **S1 arrays may spread from shared arrays** — `BADGE_VARIANTS_S1` spreads `BADGE_VARIANTS_SEMANTIC` (shared)
- **Deleting S1 content never breaks shared or canonical content**

### Removing S1

When 1st-gen is retired, the removal process for each types file is:

1. Delete the `S1-ONLY` section (constants and types)
2. Delete the `S1-ONLY` section separator
3. Remove the `@todo` comment about S1 removal
4. Optionally, if a shared constant was only shared for S1's benefit and is identical to the canonical array, inline it

No renames. No changes to canonical type names. No breaking changes for 2nd-gen consumers.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this.


### Event patterns

Standard events use `bubbles: true` and `composed: true` to cross shadow DOM boundaries:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
Standard events use `bubbles: true` and `composed: true` to cross shadow DOM boundaries:
Standard events use `bubbles: true` to allow the event to traverse up the DOM and `composed: true` to cross shadow DOM boundaries:

5. **Help text controller** — convert `ManageHelpText` mixin to controller pattern
6. **Platform utilities** — port `platform.ts` detection functions to `core/utils/`
7. **Focus utilities** — port `getActiveElement()`, `firstFocusableIn()`, focusable selectors to `core/utils/`
8. **Random ID utility** — port `randomID()` to `core/utils/` for ARIA ID generation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how ARIA factors into this when IDREFs don't cross shadow roots, but auto-generated IDREFs for other purposes may be useful.


**Pattern**: Interactive/semantic components (Progress Circle, Divider) set `role` in `firstUpdated()` and manage dynamic ARIA attributes in `updated()`. Non-interactive display components (Badge, Status Light) don't set roles. Asset handles accessibility within its SVG templates.

**Inconsistency**: Divider always sets `role="separator"` unconditionally, ignoring any user-supplied role. Progress Circle checks for an existing role before setting it. Divider should follow Progress Circle's pattern to allow role overrides.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be opinionated on divider's role. Honestly we should be on progress circle's role as well. If the consumer is overriding the role of these components, then they should likely be using a different component.

1. **Legacy namespace carryover**: Asset still uses `spectrum-*` class names while the rest of 2nd-gen uses `swc-*`.
2. **Dead code in render**: Status Light generates size classes (`swc-StatusLight--sizeS`) that aren't referenced in CSS.
3. **Underdocumented advanced CSS behavior**: Progress Circle forced-colors uses a track variable name (`--swc-progress-circle-track-color`) that appears inconsistent with base variable usage (`--swc-progress-circle-track-border-color`).
4. **Unconditional role assignment**: Divider sets `role="separator"` unconditionally in `firstUpdated()`, overriding any user-supplied role.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should be the desired pattern, and conditional an anti-pattern. If consumers need a different role, we have a feature request process where we can determine if the role is appropriate for the component or if a new component is needed.

### Missing patterns (what should be added)

1. **Explicit forced-colors decision records** for Badge and Asset (validate browser defaults vs required overrides).
2. **Accessible name validation** for Badge (icon-only variants) and Status Light.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status light on its own does not have an accessible name. It is up to the consumer include text with it.

### Priority 5: Standardize accessibility patterns

1. **Add accessible name validation** to Badge and Status Light base classes.
2. **Use conditional role assignment** (check before set) everywhere, following Progress Circle's pattern.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I believe this should be unconditional.

@caseyisonit
Copy link
Contributor Author

Closing as this is cover in #6067 and all feedback comments have been addressed there

@caseyisonit caseyisonit closed this Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status:Ready for review PR ready for review or re-review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants