Web components : Add usa-alert component#271
Conversation
Move "sass" from dependencies to devDependencies in package.json to reflect it's a build-time tool. Update UsaBanner to use Lit's boolean attribute binding (?hidden) driven by isOpen and remove the manual DOM attribute toggle in toggle(). Correct spacing token "05" from .025rem to .25rem in tokens/dimension/spacing.json.
Introduce a new UsaAlert web component (LitElement) with status variants (info, warning, error, success, emergency), slim, no-icon, and closeable options. Ship styles, Storybook stories, and unit tests; dispatches a close event and uses role=alert/status for accessibility. Also register/export the component, add a Vite build entry with size limit, and update custom-elements.json and a changeset.
|
| statuses.forEach((status) => { | ||
| it(`renders ${status} variant`, async () => { | ||
| document.body.innerHTML = `<usa-alert status="${status}"><p>Test</p></usa-alert>`; | ||
| await getAlert().updateComplete; |
There was a problem hiding this comment.
Since you have helpers at the top of this file, you might create a helper for waiting for the update cycle to complete, something like:
async function updateComplete(): Promise<void> {
await getAlert().updateComplete;
}
This would allow you to easily decouple the test from proprietary lit syntax, if that were to be needed in the future. I think you could even make this framework agnostic with:
async function updateComplete(): Promise<void> {
await new Promise<void>((resolve) => requestAnimationFrame(resolve));
}
And then where you call await getAlert().updateComplete; in the tests, replace it with await updateComplete().
There was a problem hiding this comment.
Updated: extracted an updateComplete() helper
| this._visible = true; | ||
| } | ||
|
|
||
| private get _role(): string { |
There was a problem hiding this comment.
Can you move to get #role(): string { … } for true privacy? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements
Same for this._visible --> this.#visible
There was a problem hiding this comment.
Updated: moved _role to get #role() for true privacy.
For _visible: this one needs to stay as _visible because it's registered in static properties with { state: true }. Lit's reactive property system needs to intercept get/set on the field to trigger re-renders, and true private fields (#) are invisible to that mechanism. The _ prefix convention is Lit's standard pattern for internal reactive state.
| /** Slim */ | ||
| .usa-alert--slim .usa-alert__body { | ||
| padding: 0.5rem var(--usa-alert-padding-x); | ||
| padding-left: calc(var(--usa-alert-padding-x) + 1.5rem + 0.5rem); |
There was a problem hiding this comment.
Good point, updated! The two values represent icon-size + gap semantically, but pre-computed is simpler since they're both constants.
| aria-label="${this.closeLabel}" | ||
| @click="${this._handleClose}" | ||
| > | ||
| <span class="usa-alert__close-icon" aria-hidden="true"></span> |
There was a problem hiding this comment.
You might verify whether aria-hidden="true" is necessary on an empty span.
There was a problem hiding this comment.
Good point, updated!
| --usa-alert-emergency-icon: #fff; | ||
|
|
||
| display: block; | ||
| } |
There was a problem hiding this comment.
Should all of the below be within the :host { … }?
There was a problem hiding this comment.
I don't think so, the styles below :host target shadow DOM internals (.usa-alert, .usa-alert__body, .usa-alert__close, etc.) rather than the host element itself. In shadow DOM CSS, these selectors work at the top level of the stylesheet since they're already scoped to this component's shadow root. Only the custom property declarations and display: block belong in :host because they define the host element's own behavior and its theming API surface.
There was a problem hiding this comment.
Ah right you are. I was thinking there was a risk of it leaking out without nesting it within host, but as you say it all gets scoped into a particular component's shadow-root > adopted-style-sheets. And nesting under host actually bloats each selector by prefixing host on it. Good to know, thanks!
| static properties = { | ||
| status: { type: String, reflect: true }, | ||
| slim: { type: Boolean, reflect: true }, | ||
| noIcon: { type: Boolean, attribute: "no-icon", reflect: true }, |
There was a problem hiding this comment.
Is there a combo of noIcon and each of the statuses, or is the noIcon state actually one of the statuses? For example, are the alerts without icons always in info status? If so, I would consider creating a new status that is the no icon appearance, and ditch the noIcon property. If you do need the combination of any status with or without an icon, what you have works.
There was a problem hiding this comment.
It's a combo, USWDS core documents no-icon as an independent modifier applicable to any status.
| this.slim = false; | ||
| this.noIcon = false; | ||
| this.closeable = false; | ||
| this.closeLabel = "Close alert"; |
There was a problem hiding this comment.
Should this integrate msg(…) https://lit.dev/docs/localization/overview/ ?
There was a problem hiding this comment.
Good thought! The existing usa-banner also doesn't use @lit/localize, it uses a manual translations object with typed strings. Adding @lit/localize as a dependency for a single default string feels premature at this stage, but I'm open to it if the team wants to establish that pattern now. Worth noting that close-label is already a configurable attribute, so consumers can pass in whatever localized string they need from their own i18n system. Happy to follow whatever direction is decided for the library's localization strategy.
Introduce an async updateComplete() helper that awaits getAlert().updateComplete in src/components/usa-alert/usa-alert.spec.ts, and replace repeated await getAlert().updateComplete calls with await updateComplete() to reduce duplication and improve test readability/maintainability.
Update usa-alert.css to replace calc(var(--usa-alert-padding-x) + 1.5rem + 0.5rem) with calc(var(--usa-alert-padding-x) + 2rem) for .usa-alert--slim .usa-alert__body. This simplifies the expression by combining the two fixed offsets into a single value, keeping the computed spacing unchanged and improving readability.
Switch the private role getter from `_role` to the ECMAScript private accessor `#role` and update the template reference accordingly. Also remove the `aria-hidden="true"` attribute from the close-icon span so the icon is not explicitly hidden; the close button still uses an `aria-label` for its accessible name.
|
Localization-strategy-wise, let's ping @ethangardner and/or @heymatthenry to chat with me to determine a call on that. May take us a minute to reply, but we'll put it on the list. Thanks for this work, both of you! |
Summary
New alert component. Added usa-alert with support for info, warning, error, success, and emergency statuses, including slim, no-icon, and closeable variants.
Related issue
N/A
Preview link
N/A
Problem statement
The library currently has one real component (usa-banner). The alert pattern is one of the most commonly used USWDS components and its absence blocks adoption. Additionally, the architecture needed validation from a second non-trivial component to confirm that patterns around slots, CSS custom properties, variant attributes, and event dispatch work correctly.
Solution
Ported the VADS va-alert (Stencil) component to Lit as usa-alert, using USWDS core's alert pattern as the visual/behavioral reference. Key design decisions:
Major changes
Testing and review