Skip to content

fix(bulma-ui): announce polite notifications through a status region kept from mount - #835

Merged
allxsmith merged 16 commits into
mainfrom
fix/790-live-regions
Oct 1, 2026
Merged

allxsmith merged 16 commits into
mainfrom
fix/790-live-regions

Conversation

@allxsmith

@allxsmith allxsmith commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #790. Builds on #779's per-position stacks (#834, now merged), which change the same container.

A polite notification put its text in a role="status" span created in the same render as the text. A live region has to exist before its content changes to be announced reliably, and the container rendered nothing while empty. So a success or info notification could go unannounced.

  • A status region from mount. NotificationContainer now keeps one visually hidden role="status" region from mount, in the same portal as its stacks, and it stays put while stacks come and go.
  • How the text gets there. A polite notification's text is written into the region shortly after the notification appears and cleared a moment later. That way it's announced once, and a test that queries the text right after show() doesn't find it twice.
  • No double announcement. The notification's own message span no longer carries a role.
  • Danger and warning notifications are unchanged. They keep their own role="alert" and aria-live="assertive", because an assertive alert inserted together with its text is announced reliably.
  • Toast is out of scope. Toasts keep role="alert", so getByRole('alert') still finds a toast with the notification container mounted. A non-urgent toast pairs that role with aria-live="polite", which makes it a polite region arriving with its text, the shape [A11y] bulma-ui: polite programmatic notifications may not be announced, since their status region appears with its text #790 describes, so it may want the same treatment separately.
  • Review follow-ups. The region hides itself with inline styles that match the repo's extras-sr-only mixin, so it needs no stylesheet and adds no overflow. Written announcements keep their whole lifetime: a newer batch is added beside them, and a notification closing after its announcement was written doesn't cut the announcement short (one that closes before it's written still isn't announced). The text comes from a small reader that uses aria-label and alt, leaves out aria-hidden, hidden and inline display: none content, and breaks at block elements. The region sets aria-atomic="false" so a screen reader reads only what's added. The message TSDoc lists what the reader follows and what it doesn't.
  • Server rendering and hydration. The server still renders nothing. The region portals in on the render right after hydration, so the hydrating render still matches the server's.

The one thing an app's tests might notice: a polite notification's message no longer has role="status", so getByRole('status') finds the container's region instead. The Notification page's Accessibility section says so, and says to query the text or .notification.

The region lives in a small internal helper with its own tests, not exported. Notification's tests cover:

  • the region existing from mount;
  • which colors go through it, and danger and warning keeping their alert role;
  • announcing once, clearing, and the queue and positions;
  • getByRole('alert') finding a toast beside a notification container;
  • pre-mount, StrictMode, unmount, SSR and hydration.

React 18 was checked too.

pnpm all passes locally.

…ainer mounts

ToastContainer and NotificationContainer started from an empty list and only subscribed to
later updates, so anything shown before they mounted stayed hidden until the next show() call
brought it in alongside the new item. Calls at module scope or during an app's first render
showed nothing.

Each store now keeps the list a container should show, and the containers read it through
useSyncExternalStore, so a container that mounts late starts from what is already showing.
The server snapshot is an empty list: the server has no portal target and shares the module
across requests. The hydrating render reads that same empty list, so it matches the server
markup, and the client shows the real list right after. subscribe() keeps its signature and
behavior.

An item shown before mount keeps its order, its place in the queue and its dismissal. Its
auto-close timer starts when it appears, not when it was raised.

Drops the caveat from NotificationContainer's TSDoc, the Notification page's setup note and
the 2-to-3 migration guide.

Closes #782
…ntainer hydrates empty

The visible list started as its own empty array, a different one from the empty server
snapshot, so useSyncExternalStore saw a changed snapshot after hydration and rendered the
container again with nothing new to show. The same happened after the store emptied, since each
notify built a fresh empty array.

The visible list now starts as the server snapshot's array and goes back to it whenever it is
empty, so an empty store reads the same snapshot on the server and the client. It is still
replaced rather than mutated, and subscribers still get their own copy.

Refs #782
DialogContainer had the same shape as the toast and notification containers: it started with
no dialog and only subscribed to later changes. A dialog.confirm() or dialog.alert() made before
it mounted stayed hidden, and its promise stayed pending with nothing on screen to answer it.

The container now reads the open dialog through useSyncExternalStore, so one raised before it
mounts opens as soon as it does. The server snapshot is null: the server's copy of the store is
shared by every request, so the dialog it holds may belong to another page. The hydrating render
reads that same null, so it matches the server markup, and the dialog opens right after. An empty
store already reads null on both sides, so hydrating with no dialog renders once.

Refs #782
…re shown with

toast.show() and notification.show() both take a position option, and neither container
read it. ToastContainer passed its own position to every toast, and NotificationContainer
laid out every notification from its own prop, so an item shown with a position appeared
at the container's.

Each container now splits what it shows into one stack per position in use: an item's own
position when it has one, the container's otherwise. Stacks render across the top of the
screen, then across the bottom, and a position with nothing at it gets no stack. The stack
at the container's own position keeps one key whatever that position is, so changing the
container's position prop moves its items instead of remounting them.

An app that never passes a position renders the same markup as before. Queued items still
share one queue across positions, and each shows at its own position when its turn comes.
The store, subscribe() and the exported types are unchanged.

The position TSDoc on ToastProps and NotificationOptions no longer calls it ignored, the
Toast and Notification pages say how it combines with the container's position, and each
ProgrammaticAPI story gains a button that uses it.

Closes #779
…kept from mount

NotificationContainer rendered nothing while it had nothing to show, so a success, info or
uncolored notification's role="status" span arrived in the same render as its text. A polite live
region that arrives already holding its text often isn't announced, so those notifications could
go unannounced. Screen readers reliably announce a polite region whose content changes once it is
in the page.

NotificationContainer now keeps a visually hidden role="status" region in the page from the moment
it mounts, rendered with Bulma's is-sr-only class next to the stacks in the same portal. A moment
after a polite notification appears, the text its message renders is written into the region, and
it is cleared shortly after, or as soon as the notification goes, so the page doesn't keep a second
copy of the text.

- Nothing is written in the commit that mounts the region or the notification, and that includes
  notifications raised before the container mounted.
- Each notification is announced once. The region keeps its own state, so writing and clearing an
  announcement re-renders the region and not the notifications.
- The polite notification's message span drops its role, so it isn't announced twice.
- danger and warning keep their own role="alert", which is announced reliably even when it
  arrives with its text, and the container has no alert region of its own. That also keeps
  getByRole('alert') unambiguous when a toast is showing beside it.
- The server still renders nothing. The region portals in on the render after hydration, so a
  container that hydrates with nothing to show renders once more, to add it.

Toast is left as it is. Every toast carries role="alert", and an alert inserted with its text is
announced reliably, so toasts don't have the problem this fixes.

The Notification page's Accessibility section describes the region and says that
getByRole('status') now finds it rather than the notification, so a test should query the
message text or .notification instead.

Closes #790
# Conflicts:
#	bulma-ui/src/components/__tests__/Toast.ssr.test.tsx
#	bulma-ui/src/components/__tests__/Toast.test.tsx
#	bulma-ui/src/elements/Notification.tsx
#	bulma-ui/src/elements/__tests__/Notification.ssr.test.tsx
#	bulma-ui/src/elements/__tests__/Notification.test.tsx
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 20 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ca2f4cca-b56b-4ff6-920b-1e2e3bb555e3

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9e880 and 930e167.

📒 Files selected for processing (7)
  • bestax-mcp/data/components/Notification.json
  • bulma-ui/src/elements/Notification.tsx
  • bulma-ui/src/elements/__tests__/Notification.test.tsx
  • bulma-ui/src/helpers/__tests__/statusRegion.styles.test.tsx
  • bulma-ui/src/helpers/__tests__/statusRegion.test.tsx
  • bulma-ui/src/helpers/statusRegion.tsx
  • docs/docs/api/elements/notification.md

Walkthrough

Notifications with danger or warning colors continue to use assertive alerts. Other notifications now use a persistent, visually hidden status region that announces message text after a delay. The container waits for hydration before creating its portal.

Changes

Notification announcements

Layer / File(s) Summary
Status region scheduling
bulma-ui/src/helpers/statusRegion.tsx, bulma-ui/src/helpers/__tests__/statusRegion.test.tsx, bulma-ui/src/helpers/__tests__/statusRegion.styles.test.tsx
Adds a persistent polite status region. It batches nonempty descriptions, clears written announcements after their lifetime, and reads text from message elements, labels, and image alt text. Tests cover timing, text extraction, hidden styling, overlapping batches, StrictMode, and timer cleanup.
Notification integration
bulma-ui/src/elements/Notification.tsx, bulma-ui/src/elements/__tests__/Notification*.test.tsx, docs/docs/api/elements/notification.md, bestax-mcp/data/components/Notification.json
NotificationContainer registers message elements and supplies non-urgent descriptions to StatusRegion. Danger and warning notifications remain alerts. Tests cover announcement behavior, hydration, server rendering, and coexistence with toasts. Documentation describes the announcement behavior and testing guidance.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Low

Sequence Diagram(s)

sequenceDiagram
  participant NotificationContainer
  participant NotificationItem
  participant StatusRegion
  participant ScreenReader
  NotificationContainer->>NotificationItem: Render non-urgent notification
  NotificationItem->>NotificationContainer: Register message element
  NotificationContainer->>StatusRegion: Supply message descriptions
  StatusRegion->>StatusRegion: Write descriptions after 100 ms
  StatusRegion->>ScreenReader: Announce through polite status region
  StatusRegion->>StatusRegion: Clear announcements after 1,000 ms
Loading

Merge Risk: 🔵 Low · up to 0f9e8

Polite notifications are now announced through a shared hidden status region. If a message contains hidden content, that content can be read aloud even though it is not visible. This is a minor accessibility gap and the change is otherwise mergeable.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 0f9e8

The reviewed announcement path remains text-only, preserves urgent alerts, and has safeguards for stale timers and cleanup. No introduced security issue was established. Security coverage for all supporting changes remains incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The supported exposure change is confined to polite notification text in a mounted page. The reviewed path does not add a service, tenant, credential or data-store authority transition. Existing global-store behavior already allowed multiple mounted containers to render the same notification list.

Trust Boundaries and Controls

  • inferred — If caller-provided messages contain attacker-controlled text or accessible labels, those values can reach the polite region. They cross that helper boundary only as strings rendered as text, so the new sink does not establish an additional markup-execution path. Rendering caller-provided ReactNode content in the visible notification predates this change.

Resilience and Maintainability Implications

  • inferred — The helper assumes IDs are not reused and describes an ID only on appearance. Same-ID reuse or in-place text mutation could violate that generic assumption, but the observed producer generates a new monotonic ID for each notification and exposes no update-in-place operation. This does not establish an active security or ownership concern for the current integration.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed [ #790 ] is an active direct issue. NotificationContainer renders a persistent, visually hidden role="status" region with aria-live="polite". Non-urgent notifications write their message after a…
Out of Scope Changes check ✅ Passed The changes stay within [ #790 ]. The status-region helper, inline visually hidden styles, tests, Notification integration, and accessibility documentation all support reliable polite notification ann…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 6 files. (2 skipped: 2 …
Title check ✅ Passed The title clearly identifies the bulma-ui fix and the persistent status region used for polite notification announcements.
Description check ✅ Passed The description provides a detailed change summary, linked issue, affected package, implementation scope, testing coverage, documentation updates, and additional context. It does not reproduce every t…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://ac285c39.bestax.pages.dev

… a change remounts

ToastProps.position had lost the sentence saying it places a standalone <Toast>, the
component whose props table it feeds. It is back, next to the toast.show() wording.

The containers' position prop now carries TSDoc. Items shown without a position of their
own follow a change to it without remounting. One shown with its own position equal to the
old or new value moves between stacks, so it remounts and starts over as if it had just
been shown.

The Toast page and ToastContainer's summary no longer say the container keeps a stack at
each position. A toast from toast.show() portals into its own wrapper, so those stack
elements hold nothing unless the toast is inline. The Notification page keeps the clause,
since its stacks do hold the notifications.
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://6df874ea.bestax.pages.dev

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://db5a947b.bestax.pages.dev

Comment thread bulma-ui/src/helpers/statusRegion.tsx Outdated
Comment thread bulma-ui/src/helpers/statusRegion.tsx Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep review — 2 blocking · 4 advisory

# Severity Area Finding Location
1 🟡 Minor Robustness Region is hidden by is-sr-only, which lives in bulma/sass/helpers/visibility — a module the optimize skill and the Option C docs recipe both tell apps to omit; without it every polite notification paints a duplicate of its text on screen for ~1.1 s bulma-ui/src/helpers/statusRegion.tsx:160
2 🟡 Minor Correctness Neither announcement timer is keyed to its batch, so a clear that comes due in the scheduler gap before the superseding batch commits wipes it, and the notification is never announced bulma-ui/src/helpers/statusRegion.tsx:106-121
3 🔵 Advisory Robustness describe asserts twice (get(item.id)!.textContent!). Sound today — refs attach in the layout phase before StatusRegion's passive effect, and groupIntoPositionStacks renders every item — but it becomes a crash the moment the container renders a subset of items (a max-visible cap, say) bulma-ui/src/elements/Notification.tsx:519
4 🔵 Advisory Accessibility A polite notification whose duration is under announceDelay (100 ms), or one closed / closeAll'd inside that window, is never announced at all. Deliberate and pinned by a test, on the record because the notification ends up silent rather than late bulma-ui/src/helpers/statusRegion.tsx:123
5 🔵 Advisory API Toast is left on its per-toast role="alert". #790 asked for that call to be made here and it was, with a reason (an alert arrives with its text and is announced reliably; no container alert region keeps getByRole('alert') unambiguous) — recorded as the accepted divergence between the two containers bulma-ui/src/components/Toast.tsx:255
6 🔵 Advisory Robustness Two mounted NotificationContainers now mount two regions and write every announcement twice, and getByRole('status') throws. Pre-existing misuse — two containers already render every notification twice — but the region widens the blast radius from visual to audible bulma-ui/src/elements/Notification.tsx:548

Overall: The change is sound and the mechanism is the right one for the bug: the region is mounted from the container's first post-hydration commit, nothing is written in the commit that mounts it, the message span's role is dropped so there is no double announcement, and danger/warning keep the alert path that was already reliable. I ran the three touched suites (96 tests, all green) and typecheck:tests; coverage on statusRegion.tsx and Notification.tsx is 100/100/100/100, and the hydration test genuinely asserts ['mount', 'update'] with no console.error. The riskiest part is the batch state machine in useAnnouncements — look at finding 2 first, since an unkeyed timer silently re-creates the failure class this PR exists to close. Finding 1 is the one a human should decide: the region's only CSS dependency is missing from the library's own modular recipe, and the choice is between an inline style here or updating both modular surfaces in this PR.

Residual risk:

  • A polite notification going unannounced because the region arrived with its text — refuted. StatusRegion always renders empty in the commit that mounts it (useAnnouncements writes only from a passive effect, and batch.written starts false); Notification.test.tsx asserts toBeEmptyDOMElement() on the mount commit for the client, the hydration, the raised-before-mount and the StrictMode paths, and asserts the same node identity (toBe(status)) after the write.
  • Announced twice — refuted. The message span carries no role (verified by the outerHTML assertion at Notification.test.tsx:1082), announcedIdsRef gates on id, ids are monotonic (notification-${++notificationId}), and each announcement is its own keyed node so an unrelated re-render does not remutate it.
  • A stale timer dropping an announcement — open, posted as finding 2.
  • The region emptying re-announcing what is left — refuted. aria-relevant defaults to additions text, so removing an announcement node (a batch swap, an item closing, the clear) is not a relevant change; only the additions speak.
  • A short-lived notification never being announced — open, advisory row 4. Anything that leaves the screen inside the 100 ms wait is dropped by the onScreen filter rather than announced late. Tested as intended behaviour, so it is a design position, not a defect.
  • A fast stream starving the region — open, low realism. Each new item increments key and restarts the write timer, so notifications arriving more often than every 100 ms keep deferring the write; they accumulate in announcements while written is false, so nothing is lost, but nothing is spoken until the stream pauses.
  • Announcement truncated by the next batch — refuted as a loss. A batch arriving while the previous is written replaces it after as little as 100 ms, but the previous text had already mutated a mounted live region, which is the event screen readers queue on; removing the node afterwards does not unspeak it.
  • The region breaking getByRole('alert') for a toast beside it — refuted. The container mounts no alert region, and Notification.test.tsx renders both containers and asserts the single alert is the .toast.
  • Hydration mismatch from the always-on portal — refuted empirically. useIsHydrated returns the server snapshot during the hydrating render, the SSR suite asserts empty markup with and without notifications raised, and the client hydration test asserts container.innerHTML === '<main></main>', exactly one extra update render, and a clean console.error spy.

🏄 Nice clean set on this one, dude — the region paddles out before the wave instead of trying to catch it mid-break, and the tests actually hold it under. Just two little rocks in the shallows: a timer that forgot which wave it belongs to, and an sr-only class that isn't in the modular build's beach bag. Patch those and it's a smooth ride in. 🤙

…et and key its timers

The status region relied on Bulma's is-sr-only class, which lives in a helper module a modular
build leaves out unless the app's own helper props need it. Without it, every polite notification
painted a second copy of its text at the end of the page for as long as the announcement lasted.
The region now hides itself with inline styles (absolute, 1px, clipped, no wrapping, no margin,
padding or border), so it needs no stylesheet, and the docs bullet asking for Bulma's helpers is
gone.

The write and clear timers applied to whichever batch was current when they fired. A newer batch
can be set before the render that cancels the older batch's timers, so a clear coming due in that
gap emptied the newer batch before it was written, and its notification was never announced. Both
timers now touch only the batch they were started for. Tests reproduce the gap with flushSync and
fake timers, and each fails without its guard.

NotificationContainer's describe no longer asserts that a notification's message element is
registered. Without one, a plain-text message is announced as it is and anything else is skipped,
instead of throwing.

The container's TSDoc now says a polite notification that closes before its announcement is
written isn't announced at all.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 00:06
@allxsmith

Copy link
Copy Markdown
Owner Author

On the advisories:

  • 3 (describe asserts). Taken in 1963004. describe no longer asserts. Without a registered message element, a plain-text message is announced as it is and anything else is skipped, so a future container that renders fewer notifications than it announces can't throw here. The branch is unreachable today, so it carries an istanbul ignore if with the reason.
  • 4 (short-lived polite notifications). On the record. A polite notification that's gone before its announcement is written stays silent rather than being announced late, and the container's TSDoc now says so.
  • 5 (Toast keeps role="alert"). By design: an alert that arrives with its text is announced reliably, so Toast doesn't have the problem [A11y] bulma-ui: polite programmatic notifications may not be announced, since their status region appears with its text #790 is about. NotificationContainer has no alert region of its own, so getByRole('alert') still finds a toast shown beside it, and a test pins that.
  • 6 (two containers). On the record. Two containers already render every notification twice, so it stays a misuse; a single container at the app root is the documented setup.

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

1963004 hides the region with inline styles, keys both timers to their batch and drops the asserts in describe. Review the PR as it stands now.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Polite announcements can be dropped prematurely or derived incorrectly from rich React content.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds a persistent polite live region for programmatic notifications while retaining assertive alerts.

Changes:

  • Adds delayed, batched status announcements.
  • Integrates the region with notification rendering and hydration.
  • Expands tests and accessibility documentation.
File Description
docs/​docs/​api/​elements/​notification.md Documents announcement and testing behavior.
bulma-ui/​src/​helpers/​statusRegion.tsx Implements the persistent live region.
bulma-ui/​src/​helpers/​__tests__/​statusRegion.test.tsx Tests batching, timing, and lifecycle behavior.
bulma-ui/​src/​elements/​Notification.tsx Routes polite notifications through the region.
bulma-ui/​src/​elements/​__tests__/​Notification.test.tsx Tests announcements, queues, hydration, and integration.
bulma-ui/​src/​elements/​__tests__/​Notification.ssr.test.tsx Verifies empty server rendering.
bestax-mcp/​data/​components/​Notification.json Updates generated accessibility guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread bulma-ui/src/elements/Notification.tsx Outdated
Comment thread bulma-ui/src/helpers/statusRegion.tsx Outdated
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://4d8477da.bestax.pages.dev

Comment thread bulma-ui/src/helpers/statusRegion.tsx Outdated
… notification closes

Closing a notification removed its announcement from the status region at once, even if it had
only just been written, so the pattern the programmatic API shows (an indefinite notification,
closed, then the result) could give the first announcement a few milliseconds in the region and
leave it unspoken. Whether an item is on screen is now decided once, when its announcement is
due. One that went before then still isn't announced, and one that goes after keeps its
announcement for its whole lifetime, when the region holds the only copy of its text.

The text read from a message ran block-level elements together, so two paragraphs were
announced as one word. An element the browser displays as a block by default, and a line break,
now stands apart from the text around it, judged by tag since there's no layout to ask. Parts
with the hidden attribute or an inline display: none are left out, as aria-hidden parts already
were.

Tests reproduce each case and fail without the change. The message TSDoc says what the reader
follows and what it doesn't, the docs say an announcement can outlast its notification for a
moment, and the danger and warning wording now names their assertive live region. The styles
test drops a load path nothing resolved through.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 00:58
@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

e1cd732 adds block separators to the reader and moves the on-screen check to write time so a written announcement outlives its notification closing. Review the PR as it stands now.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://4327b5af.bestax.pages.dev

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Stateful messages can remain unannounced, and inline-hidden SVG content is read incorrectly.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Skip announcing messages that become non-empty after mount

bulma-ui/​src/​helpers/​statusRegion.tsx:127

The message text is snapshotted in the first effect after the item mounts, and the ID is then treated as handled forever. Since message accepts an arbitrary React node, a stateful message that initially renders nothing and fills in before announceDelay (for example, after an async result) is skipped here and is never announced, even though the notification visibly gains text. Keep the item pending until the write and derive its text then, or otherwise observe message changes until the first non-empty announcement is written.

Comment thread bulma-ui/src/helpers/statusRegion.tsx Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep review — 0 blocking · 4 advisory

# Severity Area Finding Location
1 🔵 Advisory Accessibility The region is now an append-style log (several announcements coexist) built on role="status" + explicit aria-atomic="false". role="log" is the role whose implicit aria-atomic="false" / aria-relevant="additions" match that shape. On an AT that under-honours aria-atomic="false" on a status region, adding announcement n+1 while n is still there re-reads n — a trade the previous "empty the region first" design did not have. jsdom cannot settle it either way; worth a manual NVDA/VoiceOver pass before release bulma-ui/src/helpers/statusRegion.tsx:344-350
2 🔵 Advisory Accessibility A polite notification that closes inside the 100 ms announceDelay — a duration under 100, a fast close()/closeAll(), or an earlier item whose write was pushed past its own close by a later arrival — is never announced at all. Deliberate, pinned by doesn't announce a notification that closes before its announcement is due, and stated in the container TSDoc; on the record because the outcome is silence rather than lateness bulma-ui/src/helpers/statusRegion.tsx:154-168
3 🔵 Advisory API For the maintainer. Two consumer-visible changes ship under fix(bulma-ui): a polite notification's message loses role="status", and its text now lingers in the region for the full 1 s after the notification closes, so queryByText(msg) right after close() can still match. Both are documented on the Notification page's Testing bullet — nothing to change in the diff, just worth knowing it will move some apps' assertions docs/docs/api/elements/notification.md:191
4 🔵 Advisory Accessibility spokenText is a deliberate approximation and says so, but two gaps are worth recording: aria-label is honoured on any element (a bare <span aria-label> has no role that names from author, so accname would ignore it — the test at statusRegion.test.tsx:364 pins the approximation), and <details> content is read whether open or closed. Neither silences a notification bulma-ui/src/helpers/statusRegion.tsx:287-303

Overall: Sound, and the two blocking findings from the previous round are genuinely fixed — I re-ran each and resolved both threads. Moving the on-screen decision into the write callback (statusRegion.tsx:154) is the right shape: the filter now runs once, when the announcement is due, so a notification that closes after its write keeps its whole lifetime while one that closes before is still skipped, and the render path returns written untouched. The block separators are tag-driven rather than layout-driven, which is the only option in jsdom and is the correct call. The riskiest part is not in the code but in the AT behaviour of row 1 — role="status" + aria-atomic="false" holding multiple siblings is the one thing no test here can verify; that is where a human should look first, ideally with a real screen reader on the show(indefinite) → close() → success() pattern the docs advertise.

Verified empirically: full bulma-ui suite 4500/4500 green; statusRegion.tsx at 100% statements/branches/functions/lines; typecheck:tests and eslint clean on every changed file; the styles test still compiles extras-sr-only with node_modules dropped from loadPaths, confirming that path resolved nothing.

Residual risk:

  • Region missing when the write lands (the literal #790 shape) — refuted. NotificationContainer returns the portal unconditionally once hydrated, so the region outlives every stack; writes the announcement into the region that was already there asserts node identity across the write and keeps its region in place while stacks come and go across closeAll().
  • An announcement cut short by a newer batch — refuted. Write and clear are both keyed to their batch and the clear is started by its own write, so it is timed from the write rather than from a render. The three with a timer from the batch before coming due cases reproduce the scheduler gap with flushSync + fake timers and pass.
  • An announcement cut short by its own notification closing — refuted. The render-time filter is gone; keeps an announcement for its whole lifetime after its notification closes and the reworked queue case (['Q1', 'Q2']) both pass.
  • Hiding depending on a stylesheet — refuted. Inline styles only; hides itself without any stylesheet asserts document.styleSheets is empty first, and the styles test compares the region against every declaration extras-sr-only makes, including the margin: -1px that keeps the always-mounted 1 px box from adding overflow.
  • Block-level text running together — refuted. Six parametrized shapes (divs, paragraphs, list items, br, heading + text, table cells) all read Upload complete 3 files, and the inline run still reads Uploaded.
  • Open: a stream arriving faster than every 100 ms never writes anything — each arrival bumps key and restarts the wait, so waiting grows and nothing is announced until the stream pauses. Pathological rate, no regression (the pre-e1cd732b code dropped the same items at render time), but it is the tail of row 2 rather than a separate mechanism.
  • Open: a message with nothing readable (an <img> with no alt, an icon-only message) yields the empty string, which if (text) skips, so that notification is silent. Narrowed a lot by the alt/aria-label support, not closed by it.

🏄 Third set on this wave and it finally barrels clean — the announcement rides out its whole lifetime even after the notification wipes out, and the paragraphs stopped mushing into one word. Only thing left is paddling out with a real screen reader to check that aria-atomic="false" log pattern holds up in the lineup. Good to go.

…ouncements

The announcement reader only checked an inline display: none on HTML elements, so an SVG hidden
that way, or any other element with an inline style, was still read out, although the TSDoc says
inline-hidden content is left out. It now reads the inline style of any element that has one,
and skips the check for an element that has none. The hidden-content test covers an SVG case,
which fails without the change, and a new test reads an element with no inline style at all.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 01:15
@allxsmith allxsmith added the needs-human-review Loop converged (or contested): awaiting owner review + manual merge label Oct 1, 2026
@allxsmith

Copy link
Copy Markdown
Owner Author

Converged: the last fresh review found nothing blocking and confirmed both round-4 fixes, and the one Copilot thread after it is fixed in 930e167. Advisories on the record for the maintainer: the region is an append-style role="status" with aria-atomic="false" (worth a real screen-reader pass; role="log" is the alternative shape), a polite notification closed within the announce delay stays silent, and two consumer-visible test changes ship under fix (the message loses role="status", and its text lingers in the region briefly after closing), both documented on the Testing bullet.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📸 Story screenshots at handoff — 930e167

6 stories (light + dark) from this PR's changed files under bulma-ui/src/ (workflow run · artifact story-screenshots-pr-835, 30-day retention).

elements-notification--default · Elements/Notification · Default
Light Dark
elements-notification--default light elements-notification--default dark
elements-notification--primary · Elements/Notification · Primary
Light Dark
elements-notification--primary light elements-notification--primary dark
elements-notification--primary-light · Elements/Notification · Primary Light
Light Dark
elements-notification--primary-light light elements-notification--primary-light dark
elements-notification--success · Elements/Notification · Success
Light Dark
elements-notification--success light elements-notification--success dark
elements-notification--success-light · Elements/Notification · Success Light
Light Dark
elements-notification--success-light light elements-notification--success-light dark
elements-notification--warning · Elements/Notification · Warning
Light Dark
elements-notification--warning light elements-notification--warning dark
> ✂ 13 more matched stories skipped by the cap: `elements-notification--warning-light`, `elements-notification--danger`, `elements-notification--danger-light`, `elements-notification--info`, `elements-notification--info-light`, `elements-notification--link`, `elements-notification--link-light`, `elements-notification--with-delete`, `elements-notification--with-margin`, `elements-notification--custom-content`, … — full set in the artifact.

github-actions Bot added a commit that referenced this pull request Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://1fe70b2b.bestax.pages.dev

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Repeated arrivals can indefinitely postpone pending announcements until notifications disappear.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Debounce can indefinitely postpone announcements and drop auto-dismissed items

bulma-ui/​src/​helpers/​statusRegion.tsx:133

Incrementing key for every arrival turns the write delay into a trailing-edge debounce: each render cancels the previous write timer. If notifications keep arriving less than 100 ms apart, the pending batch can be postponed indefinitely; meanwhile auto-dismissed items are removed by the on-screen filter and are never announced. Keep the existing timer when appending to a non-empty waiting batch (schedule a new timer only on the empty-to-non-empty transition), so every batch has a bounded announcement delay.

@allxsmith
allxsmith merged commit c93478d into main Oct 1, 2026
23 checks passed
@allxsmith
allxsmith deleted the fix/790-live-regions branch October 1, 2026 03:05
bestax-release-bot Bot pushed a commit that referenced this pull request Oct 1, 2026
# [5.19.0](https://github.com/allxsmith/bestax/compare/@allxsmith/bestax-bulma@5.18.2...@allxsmith/bestax-bulma@5.19.0) (2026-10-01)

### Bug Fixes

* **bulma-ui:** announce polite notifications through a status region kept from mount ([#835](#835)) ([c93478d](c93478d))
* **bulma-ui:** color the calendar from the color prop and read TimeInput's wheel variables ([2251e67](2251e67)), closes [#832](#832)
* **bulma-ui:** draw the time wheel focus ring where the wheel's mask shows it ([e1d8c9f](e1d8c9f)), closes [#832](#832)
* **bulma-ui:** give the selected calendar cell a ring that shows on its fill ([b3eecc6](b3eecc6))
* **bulma-ui:** keep button defaults through a spread, and type the buttons that had none ([#853](#853)) ([0810482](0810482))
* **bulma-ui:** keep the picker focus rings visible in every color ([612aa09](612aa09))
* **bulma-ui:** read clicks and focus inside a shadow root ([474f2cc](474f2cc))
* **bulma-ui:** restore a portaled Modal's focus to an opener in a shadow root ([c564bd3](c564bd3))

### Features

* **bulma-ui:** add Bulma's position, overflow, radius and aspect-ratio helper props ([#845](#845)) ([64fb91c](64fb91c))
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.19.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

bestax-release-bot Bot pushed a commit that referenced this pull request Oct 3, 2026
# [1.14.0](https://github.com/allxsmith/bestax/compare/bestax-mcp@1.13.0...bestax-mcp@1.14.0) (2026-10-03)

### Bug Fixes

* **bulma-ui:** announce polite notifications through a status region kept from mount ([#835](#835)) ([c93478d](c93478d))
* **bulma-ui:** answer the granularity review on native input and blur ([7a17486](7a17486))
* **bulma-ui:** color the calendar from the color prop and read TimeInput's wheel variables ([2251e67](2251e67)), closes [#832](#832)
* **bulma-ui:** draw the time wheel focus ring where the wheel's mask shows it ([e1d8c9f](e1d8c9f)), closes [#832](#832)
* **bulma-ui:** fall back when useFocusTrap's initialFocusRef can't take focus ([c191931](c191931))
* **bulma-ui:** give the selected calendar cell a ring that shows on its fill ([b3eecc6](b3eecc6))
* **bulma-ui:** hold Portal to looking a selector up on each render ([8fe02ce](8fe02ce))
* **bulma-ui:** judge image map areas by their image in useFocusTrap ([af7bc59](af7bc59))
* **bulma-ui:** keep button defaults through a spread, and type the buttons that had none ([#853](#853)) ([0810482](0810482))
* **bulma-ui:** keep button types and Avatar's accessible name through a spread ([#826](#826)) ([dc798d4](dc798d4))
* **bulma-ui:** keep the picker focus rings visible in every color ([612aa09](612aa09))
* **bulma-ui:** make useFocusTrap follow radio groups and initialFocusRef past the trap ([d6219d4](d6219d4))
* **bulma-ui:** place toasts and notifications at the position they were shown with ([#834](#834)) ([62feab9](62feab9))
* **bulma-ui:** read clicks and focus inside a shadow root ([474f2cc](474f2cc))
* **bulma-ui:** restore a portaled Modal's focus to an opener in a shadow root ([c564bd3](c564bd3))
* **bulma-ui:** settle Popover's deep review findings ([feadaf4](feadaf4))
* **bulma-ui:** stop Bulma's spinners under reduced motion in every stylesheet ([b234712](b234712)), closes [#770](#770)
* **bulma-ui:** stop TimeInput reopening on the focus its popover hands back ([1c87c15](1c87c15))

### Features

* **bestax-mcp:** publish to the official MCP Registry ([74efc7f](74efc7f))
* **bulma-ui:** add Bulma's position, overflow, radius and aspect-ratio helper props ([#845](#845)) ([64fb91c](64fb91c))
* **bulma-ui:** add Collapses, an accordion group for Collapse ([#849](#849)) ([a12c823](a12c823))
* **bulma-ui:** add Loader, Bulma's inline spinner ([c5e9eb0](c5e9eb0)), closes [#770](#770)
* **bulma-ui:** add month and year pickers to DateInput through granularity ([c4cfc2b](c4cfc2b)), closes [#773](#773)
* **bulma-ui:** add Popover ([2e17fac](2e17fac)), closes [#776](#776)
* **bulma-ui:** export ClientOnly and useIsHydrated ([2c0c1e6](2c0c1e6))
* **bulma-ui:** export Portal ([346d8c2](346d8c2))
* **bulma-ui:** export useFocusTrap, counting the stops the browser's Tab visits ([c304fef](c304fef)), closes [#772](#772)
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.14.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

bestax-release-bot Bot pushed a commit that referenced this pull request Oct 3, 2026
# [1.2.0](https://github.com/allxsmith/bestax/compare/@allxsmith/eslint-plugin-bestax@1.1.9...@allxsmith/eslint-plugin-bestax@1.2.0) (2026-10-03)

### Bug Fixes

* **bulma-ui:** announce polite notifications through a status region kept from mount ([#835](#835)) ([c93478d](c93478d))
* **bulma-ui:** answer the granularity review on native input and blur ([7a17486](7a17486))
* **bulma-ui:** color the calendar from the color prop and read TimeInput's wheel variables ([2251e67](2251e67)), closes [#832](#832)
* **bulma-ui:** draw the time wheel focus ring where the wheel's mask shows it ([e1d8c9f](e1d8c9f)), closes [#832](#832)
* **bulma-ui:** fall back when useFocusTrap's initialFocusRef can't take focus ([c191931](c191931))
* **bulma-ui:** give the selected calendar cell a ring that shows on its fill ([b3eecc6](b3eecc6))
* **bulma-ui:** hold Portal to looking a selector up on each render ([8fe02ce](8fe02ce))
* **bulma-ui:** judge image map areas by their image in useFocusTrap ([af7bc59](af7bc59))
* **bulma-ui:** keep button defaults through a spread, and type the buttons that had none ([#853](#853)) ([0810482](0810482))
* **bulma-ui:** keep button types and Avatar's accessible name through a spread ([#826](#826)) ([dc798d4](dc798d4))
* **bulma-ui:** keep the picker focus rings visible in every color ([612aa09](612aa09))
* **bulma-ui:** make useFocusTrap follow radio groups and initialFocusRef past the trap ([d6219d4](d6219d4))
* **bulma-ui:** place toasts and notifications at the position they were shown with ([#834](#834)) ([62feab9](62feab9))
* **bulma-ui:** read clicks and focus inside a shadow root ([474f2cc](474f2cc))
* **bulma-ui:** restore a portaled Modal's focus to an opener in a shadow root ([c564bd3](c564bd3))
* **bulma-ui:** settle Popover's deep review findings ([feadaf4](feadaf4))
* **bulma-ui:** stop Bulma's spinners under reduced motion in every stylesheet ([b234712](b234712)), closes [#770](#770)
* **bulma-ui:** stop TimeInput reopening on the focus its popover hands back ([1c87c15](1c87c15))
* **eslint-plugin:** judge Columns' gap by its own steps, and lengths as lengths ([4786c8d](4786c8d))

### Features

* **bestax-mcp:** publish to the official MCP Registry ([74efc7f](74efc7f))
* **bulma-ui:** add Bulma's gap helper props to every component ([#877](#877)) ([1bd4abc](1bd4abc))
* **bulma-ui:** add Bulma's position, overflow, radius and aspect-ratio helper props ([#845](#845)) ([64fb91c](64fb91c))
* **bulma-ui:** add Collapses, an accordion group for Collapse ([#849](#849)) ([a12c823](a12c823))
* **bulma-ui:** add Loader, Bulma's inline spinner ([c5e9eb0](c5e9eb0)), closes [#770](#770)
* **bulma-ui:** add month and year pickers to DateInput through granularity ([c4cfc2b](c4cfc2b)), closes [#773](#773)
* **bulma-ui:** add Popover ([2e17fac](2e17fac)), closes [#776](#776)
* **bulma-ui:** export ClientOnly and useIsHydrated ([2c0c1e6](2c0c1e6))
* **bulma-ui:** export Portal ([346d8c2](346d8c2))
* **bulma-ui:** export useFocusTrap, counting the stops the browser's Tab visits ([c304fef](c304fef)), closes [#772](#772)
* **bulma-ui:** make columnGap the column gap helper on Theme ([eb2c611](eb2c611)), closes [#694](#694)
* **eslint-plugin:** check the gap helper props ([b53a749](b53a749))
bestax-release-bot Bot pushed a commit that referenced this pull request Oct 7, 2026
# [2.25.0](https://github.com/allxsmith/bestax/compare/bestax-migrate@2.24.0...bestax-migrate@2.25.0) (2026-10-07)

### Bug Fixes

* **bulma-ui:** announce polite notifications through a status region kept from mount ([#835](#835)) ([c93478d](c93478d))
* **bulma-ui:** answer the granularity review on native input and blur ([7a17486](7a17486))
* **bulma-ui:** attach useFocusTrap to a container that mounts after it turns on ([17a944d](17a944d))
* **bulma-ui:** bring File and Taginput under the bare-Control Field rule ([cdaa6c2](cdaa6c2))
* **bulma-ui:** clear a picker's unused seed whenever its popover closes ([cbe86c5](cbe86c5))
* **bulma-ui:** color the calendar from the color prop and read TimeInput's wheel variables ([2251e67](2251e67)), closes [#832](#832)
* **bulma-ui:** commit nothing when a picker popover is dismissed ([0a46209](0a46209))
* **bulma-ui:** commit nothing when Enter confirms untouched picker text ([773e146](773e146))
* **bulma-ui:** commit nothing when focus leaves a picker untouched ([30b4849](30b4849))
* **bulma-ui:** draw the time wheel focus ring where the wheel's mask shows it ([e1d8c9f](e1d8c9f)), closes [#832](#832)
* **bulma-ui:** fall back when useFocusTrap's initialFocusRef can't take focus ([c191931](c191931))
* **bulma-ui:** fire the pickers' onOpen and onClose once under StrictMode ([#896](#896)) ([20b51ed](20b51ed))
* **bulma-ui:** give the selected calendar cell a ring that shows on its fill ([b3eecc6](b3eecc6))
* **bulma-ui:** hide a picker's launcher for a loading Control it sits in ([9a3888e](9a3888e))
* **bulma-ui:** hide a picker's launcher only for the spinner it draws ([7a28a0e](7a28a0e))
* **bulma-ui:** hold Portal to looking a selector up on each render ([8fe02ce](8fe02ce))
* **bulma-ui:** hold the year-1 floor under a max before it ([4f6eb18](4f6eb18))
* **bulma-ui:** judge image map areas by their image in useFocusTrap ([af7bc59](af7bc59))
* **bulma-ui:** keep a tab stop in the year list when the year falls outside it ([fe028c1](fe028c1))
* **bulma-ui:** keep an empty picker's seed while a portaled popover is open ([1e66528](1e66528))
* **bulma-ui:** keep an inline date picker's calendar on its released id ([c26da07](c26da07)), closes [#897](#897) [#897](#897)
* **bulma-ui:** keep button defaults through a spread, and type the buttons that had none ([#853](#853)) ([0810482](0810482))
* **bulma-ui:** keep button types and Avatar's accessible name through a spread ([#826](#826)) ([dc798d4](dc798d4))
* **bulma-ui:** keep the Field for horizontal and fieldClassName in a bare Control ([e4dc513](e4dc513))
* **bulma-ui:** keep the picker focus rings visible in every color ([612aa09](612aa09))
* **bulma-ui:** let focus follow the keys in an inline calendar, and skip disabled days for the tab stop ([#885](#885)) ([f55af27](f55af27))
* **bulma-ui:** let the picker bases give way to a loading Control too ([66e803a](66e803a))
* **bulma-ui:** let useFocusTrap go of a container that leaves while it is on ([8c56d7a](8c56d7a))
* **bulma-ui:** make auto leave room for an in-place picker panel's themed offset ([8c534bd](8c534bd)), closes [#904](#904)
* **bulma-ui:** make Theme colorMode switch the scheme under the prefixed builds ([#894](#894)) ([2bd6ec2](2bd6ec2))
* **bulma-ui:** make useFocusTrap follow radio groups and initialFocusRef past the trap ([d6219d4](d6219d4))
* **bulma-ui:** match a picker's opening focus only inside its calendar ([e3c22ed](e3c22ed))
* **bulma-ui:** place a portaled picker panel by its coordinates alone ([#886](#886)) ([040299b](040299b))
* **bulma-ui:** place toasts and notifications at the position they were shown with ([#834](#834)) ([62feab9](62feab9))
* **bulma-ui:** read clicks and focus inside a shadow root ([474f2cc](474f2cc))
* **bulma-ui:** read picker bounds and months in years 0 to 99 as written ([#890](#890)) ([02fb619](02fb619))
* **bulma-ui:** restore a portaled Modal's focus to an opener in a shadow root ([c564bd3](c564bd3))
* **bulma-ui:** return focus to the input when a date picker closes ([b741264](b741264))
* **bulma-ui:** settle Popover's deep review findings ([feadaf4](feadaf4))
* **bulma-ui:** start a date picker's focus on the focused cell ([#897](#897)) ([6a55714](6a55714))
* **bulma-ui:** stop Bulma's spinners under reduced motion in every stylesheet ([b234712](b234712)), closes [#770](#770)
* **bulma-ui:** stop form wrappers nesting a Field in a bare Control ([2c6d295](2c6d295))
* **bulma-ui:** stop the date pickers at year 1 ([853142a](853142a))
* **bulma-ui:** stop the indeterminate progress bar and skeleton pulse under reduced motion ([bcf5407](bcf5407))
* **bulma-ui:** stop the Rate pop and DateTimeInput scrim under reduced motion ([3a59129](3a59129))
* **bulma-ui:** stop TimeInput reopening on the focus its popover hands back ([1c87c15](1c87c15))
* **bulma-ui:** warn when a root Theme drops its className and helpers ([05c10e0](05c10e0))
* **eslint-plugin:** judge Columns' gap by its own steps, and lengths as lengths ([4786c8d](4786c8d))

### Features

* **bestax-mcp:** publish to the official MCP Registry ([74efc7f](74efc7f))
* **bestax-migrate:** convert the gap, position, overflow, radius and aspect-ratio helpers ([13dcf63](13dcf63))
* **bulma-ui:** add Bulma's gap helper props to every component ([#877](#877)) ([1bd4abc](1bd4abc))
* **bulma-ui:** add Bulma's position, overflow, radius and aspect-ratio helper props ([#845](#845)) ([64fb91c](64fb91c))
* **bulma-ui:** add Collapses, an accordion group for Collapse ([#849](#849)) ([a12c823](a12c823))
* **bulma-ui:** add Loader, Bulma's inline spinner ([c5e9eb0](c5e9eb0)), closes [#770](#770)
* **bulma-ui:** add month and year pickers to DateInput through granularity ([c4cfc2b](c4cfc2b)), closes [#773](#773)
* **bulma-ui:** add Popover ([2e17fac](2e17fac)), closes [#776](#776)
* **bulma-ui:** export ClientOnly and useIsHydrated ([2c0c1e6](2c0c1e6))
* **bulma-ui:** export Portal ([346d8c2](346d8c2))
* **bulma-ui:** export useFocusTrap, counting the stops the browser's Tab visits ([c304fef](c304fef)), closes [#772](#772)
* **bulma-ui:** make columnGap the column gap helper on Theme ([eb2c611](eb2c611)), closes [#694](#694)
* **eslint-plugin:** check the gap helper props ([b53a749](b53a749))
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.25.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

bestax-release-bot Bot pushed a commit that referenced this pull request Oct 8, 2026
## [4.2.13](https://github.com/allxsmith/bestax/compare/create-bestax@4.2.12...create-bestax@4.2.13) (2026-10-08)

### Bug Fixes

* **bestax-mcp:** give .image's ratios their reason in the lookup ([3ea4d0c](3ea4d0c))
* **bestax-mcp:** say an .image converts around elements written out, not an expression ([49bb27b](49bb27b))
* **bestax-migrate:** convert an .image around its children only when they're elements ([ff99907](ff99907))
* **bestax-migrate:** flag react-bulma-components Pagination's delta={0} rather than renaming it ([e339ff1](e339ff1))
* **bestax-migrate:** keep a .menu-list around a bestax Menu.List as markup ([2119c3f](2119c3f))
* **bestax-migrate:** keep a Modal part as markup inside an existing bestax Modal ([cfd5f15](cfd5f15))
* **bestax-migrate:** keep a named anchor as markup instead of saying its name does nothing ([74dc360](74dc360)), closes [#fragment](https://github.com/allxsmith/bestax/issues/fragment)
* **bestax-migrate:** keep a page link as markup when its li is a component's only child ([0e72683](0e72683))
* **bestax-migrate:** know a bestax part imported under its flat export ([7a325ee](7a325ee))
* **bestax-migrate:** leave a wrapper tag alone when none of its classes would become a prop ([2397b9d](2397b9d))
* **bestax-migrate:** leave a wrapper tag with no helper class alone, TODOs and all ([1167a29](1167a29))
* **bestax-migrate:** map react-bulma-components Pagination onto the props it now renders from ([6cd4b51](6cd4b51))
* **bestax-migrate:** rename rbx innerRef to ref on a plain Navbar.Item too ([bb6ace0](bb6ace0)), closes [#734](#734)
* **bestax-migrate:** say why .image's ratios stay classes ([2fbfbd5](2fbfbd5))
* **bestax-migrate:** write a File text with a backslash as a string ([212831e](212831e))
* **bestax-migrate:** write a tabIndex string as a number on every root ([546bf8a](546bf8a)), closes [#814](#814)
* **bestax-migrate:** write isFullwidth from react-bulma-components too ([01da047](01da047))
* **bestax-migrate:** write isFullwidth rather than its deprecated alias from bloomer ([c2a1d22](c2a1d22)), closes [#680](#680)
* **bulma-ui:** announce polite notifications through a status region kept from mount ([#835](#835)) ([c93478d](c93478d))
* **bulma-ui:** answer the granularity review on native input and blur ([7a17486](7a17486))
* **bulma-ui:** apply textColor helper class on Navbar Menu, Start, End, and Burger ([#792](#792)) ([feb83c8](feb83c8))
* **bulma-ui:** attach useFocusTrap to a container that mounts after it turns on ([17a944d](17a944d))
* **bulma-ui:** avoid empty class attribute on several components ([#793](#793)) ([7c5c316](7c5c316))
* **bulma-ui:** bring File and Taginput under the bare-Control Field rule ([cdaa6c2](cdaa6c2))
* **bulma-ui:** clear a picker's unused seed whenever its popover closes ([cbe86c5](cbe86c5))
* **bulma-ui:** close a picker's popover on Alt+ArrowUp from inside it ([b6da4ef](b6da4ef))
* **bulma-ui:** color the calendar from the color prop and read TimeInput's wheel variables ([2251e67](2251e67)), closes [#832](#832)
* **bulma-ui:** color TimeInput's wheels from the color prop ([#832](#832)) ([ad370c9](ad370c9))
* **bulma-ui:** commit nothing when a picker popover is dismissed ([0a46209](0a46209))
* **bulma-ui:** commit nothing when Enter confirms untouched picker text ([773e146](773e146))
* **bulma-ui:** commit nothing when focus leaves a picker untouched ([30b4849](30b4849))
* **bulma-ui:** document the checkbox, radio and switch color and size tuples ([bd39ff3](bd39ff3))
* **bulma-ui:** draw the time wheel focus ring where the wheel's mask shows it ([e1d8c9f](e1d8c9f)), closes [#832](#832)
* **bulma-ui:** drop the openOnFocus TSDoc reason that only holds for segments ([efde0d9](efde0d9))
* **bulma-ui:** Dropdown items withhold attributes by tag and answer Enter and Space ([#827](#827)) ([f6aeb2d](f6aeb2d))
* **bulma-ui:** fall back when useFocusTrap's initialFocusRef can't take focus ([c191931](c191931))
* **bulma-ui:** fire the pickers' onOpen and onClose once under StrictMode ([#896](#896)) ([20b51ed](20b51ed))
* **bulma-ui:** give the calendar's day grid rows of a week ([971a196](971a196)), closes [#774](#774)
* **bulma-ui:** give the selected calendar cell a ring that shows on its fill ([b3eecc6](b3eecc6))
* **bulma-ui:** hide a picker's launcher for a loading Control it sits in ([9a3888e](9a3888e))
* **bulma-ui:** hide a picker's launcher only for the spinner it draws ([7a28a0e](7a28a0e))
* **bulma-ui:** hold Portal to looking a selector up on each render ([8fe02ce](8fe02ce))
* **bulma-ui:** hold the year-1 floor under a max before it ([4f6eb18](4f6eb18))
* **bulma-ui:** judge image map areas by their image in useFocusTrap ([af7bc59](af7bc59))
* **bulma-ui:** keep a range's end while the start's digits are coming ([8d1f862](8d1f862))
* **bulma-ui:** keep a read-only date or time picker closed on ArrowDown ([ba74bf7](ba74bf7))
* **bulma-ui:** keep a tab stop in the year list when the year falls outside it ([fe028c1](fe028c1))
* **bulma-ui:** keep an empty picker's seed while a portaled popover is open ([1e66528](1e66528))
* **bulma-ui:** keep an inline date picker's calendar on its released id ([c26da07](c26da07)), closes [#897](#897) [#897](#897)
* **bulma-ui:** keep button defaults through a spread, and type the buttons that had none ([#853](#853)) ([0810482](0810482))
* **bulma-ui:** keep button types and Avatar's accessible name through a spread ([#826](#826)) ([dc798d4](dc798d4))
* **bulma-ui:** keep menuitemcheckbox and menuitemradio items in Dropdown's arrow-key order ([#786](#786)) ([064155a](064155a))
* **bulma-ui:** keep Numberinput's buttons in a row inside Field > Control ([4dff84c](4dff84c))
* **bulma-ui:** keep Numberinput's Field examples in a row and labelled ([c937c88](c937c88))
* **bulma-ui:** keep radiusless zeroing the radius inside a Theme ([07db6a3](07db6a3)), closes [#829](#829) [#694](#694)
* **bulma-ui:** keep the calendar behind DateTimeInput's time wheels out of reach ([c7f674c](c7f674c))
* **bulma-ui:** keep the clock's seconds out of a picked date and time ([0af2e71](0af2e71))
* **bulma-ui:** keep the Field for horizontal and fieldClassName in a bare Control ([e4dc513](e4dc513))
* **bulma-ui:** keep the picker focus rings visible in every color ([612aa09](612aa09))
* **bulma-ui:** keep the range band under the pointer in the calendar ([c1f2008](c1f2008))
* **bulma-ui:** key the Avatar link warning on the element and attributes ([e628eba](e628eba)), closes [#733](#733) [#733](#733)
* **bulma-ui:** key the Control-level warning by its advice ([24cda3d](24cda3d))
* **bulma-ui:** leave keys on a control inside a tab to that control ([4a9f7b6](4a9f7b6))
* **bulma-ui:** leave rel out of the Avatar link attribute warning ([b059476](b059476)), closes [#733](#733) [#733](#733)
* **bulma-ui:** let Autocomplete's clear button give way to a loading spinner ([5c11fb2](5c11fb2))
* **bulma-ui:** let Escape in a popover's year dropdown go back to the grid ([a8b1dbf](a8b1dbf)), closes [#901](#901)
* **bulma-ui:** let File's picked-files count be reworded, and document its name in the label ([e86545c](e86545c))
* **bulma-ui:** let focus follow the keys in an inline calendar, and skip disabled days for the tab stop ([#885](#885)) ([f55af27](f55af27))
* **bulma-ui:** let several isRoot Themes share the :root style element ([04f399f](04f399f)), closes [#736](#736)
* **bulma-ui:** let the arrow keys move through the calendar's year dropdown ([6abc329](6abc329)), closes [#901](#901)
* **bulma-ui:** let the picker bases give way to a loading Control too ([66e803a](66e803a))
* **bulma-ui:** let useFocusTrap go of a container that leaves while it is on ([8c56d7a](8c56d7a))
* **bulma-ui:** let warnOnce decide when the Avatar link warning fires ([1a5965c](1a5965c)), closes [#733](#733)
* **bulma-ui:** make ArrowUp raise a time wheel's value ([c27c310](c27c310))
* **bulma-ui:** make auto leave room for an in-place picker panel's themed offset ([8c534bd](8c534bd)), closes [#904](#904)
* **bulma-ui:** make Tabs keyboard operable with the WAI-ARIA tabs pattern ([fe0e7ef](fe0e7ef))
* **bulma-ui:** make Theme colorMode switch the scheme under the prefixed builds ([#894](#894)) ([2bd6ec2](2bd6ec2))
* **bulma-ui:** make Theme's radius the helper it is typed as ([36c73d3](36c73d3)), closes [#694](#694)
* **bulma-ui:** make useFocusTrap follow radio groups and initialFocusRef past the trap ([d6219d4](d6219d4))
* **bulma-ui:** match a picker's opening focus only inside its calendar ([e3c22ed](e3c22ed))
* **bulma-ui:** merge className into Navbar.Divider instead of overwriting it ([#791](#791)) ([fcc0d51](fcc0d51))
* **bulma-ui:** move focus into DateTimeInput's time wheels as they open ([adfa9db](adfa9db))
* **bulma-ui:** move the year dropdown's tab stop with the arrow keys ([f56daf5](f56daf5))
* **bulma-ui:** name a picker's default icon when its icon size or column moves ([6f6de39](6f6de39))
* **bulma-ui:** name custom elements among the targets that get Avatar's target and rel ([a962259](a962259)), closes [#733](#733)
* **bulma-ui:** name DateRangeInput's default icon in its Control-level warning ([15cbbb8](15cbbb8)), closes [#952](#952)
* **bulma-ui:** name DateRangeInput's default icon in its Control-level warning ([9cbe7ca](9cbe7ca)), closes [#952](#952)
* **bulma-ui:** name Pagination's page links, and read a count that's no number as its default ([0a1cf06](0a1cf06))
* **bulma-ui:** open DateRangeInput's popover on Alt+ArrowDown, as the other pickers ([c0e22ec](c0e22ec))
* **bulma-ui:** open the date and time popovers on Alt+ArrowDown ([6db22cc](6db22cc))
* **bulma-ui:** order the Tabs fallback stop by index and style disabled tabs ([eefd283](eefd283))
* **bulma-ui:** pick a DateInput day at midnight by key as by click ([92c27ca](92c27ca))
* **bulma-ui:** place a portaled picker panel by its coordinates alone ([#886](#886)) ([040299b](040299b))
* **bulma-ui:** place toasts and notifications at the position they were shown with ([#834](#834)) ([62feab9](62feab9))
* **bulma-ui:** prefix the is-current and is-disabled classes on Pagination's parts ([c9a5fae](c9a5fae))
* **bulma-ui:** reach Taginput's delete buttons and Autocomplete's clear button ([21a064b](21a064b))
* **bulma-ui:** read a Pagination current page that's no number as the first ([68e963b](68e963b))
* **bulma-ui:** read clicks and focus inside a shadow root ([474f2cc](474f2cc))
* **bulma-ui:** read picker bounds and months in years 0 to 99 as written ([#890](#890)) ([02fb619](02fb619))
* **bulma-ui:** render the four burger spans Bulma v1 positions ([#785](#785)) ([8e4e1e6](8e4e1e6))
* **bulma-ui:** restore a portaled Modal's focus to an opener in a shadow root ([c564bd3](c564bd3))
* **bulma-ui:** return focus to the input when a date picker closes ([b741264](b741264))
* **bulma-ui:** say a picked DateTimeInput day keeps milliseconds and meets the bounds ([3536467](3536467))
* **bulma-ui:** say Tab reaches DateRangeInput's end input only when typing ([d2d3d1a](d2d3d1a))
* **bulma-ui:** say where Numberinput goes bare and test bare={false} in a Field ([0461631](0461631))
* **bulma-ui:** send a press on DateRangeInput's field to the nearer input ([f06b2b0](f06b2b0))
* **bulma-ui:** settle Popover's deep review findings ([feadaf4](feadaf4))
* **bulma-ui:** show the picked file with File's hasName and add a keyboard focus ring ([7881db4](7881db4))
* **bulma-ui:** show toasts, notifications and dialogs raised before their container mounts ([#828](#828)) ([2ab4493](2ab4493))
* **bulma-ui:** skip the Avatar target warning on elements with their own target ([27b58f4](27b58f4)), closes [#733](#733)
* **bulma-ui:** start a date picker's focus on the focused cell ([#897](#897)) ([6a55714](6a55714))
* **bulma-ui:** stop Bulma's spinners under reduced motion in every stylesheet ([b234712](b234712)), closes [#770](#770)
* **bulma-ui:** stop form wrappers nesting a Field in a bare Control ([2c6d295](2c6d295))
* **bulma-ui:** stop shipping test-only exports, and document notification() ([#781](#781)) ([5f95398](5f95398))
* **bulma-ui:** stop the date pickers at year 1 ([853142a](853142a))
* **bulma-ui:** stop the indeterminate progress bar and skeleton pulse under reduced motion ([bcf5407](bcf5407))
* **bulma-ui:** stop the Rate pop and DateTimeInput scrim under reduced motion ([3a59129](3a59129))
* **bulma-ui:** stop TimeInput reopening on the focus its popover hands back ([1c87c15](1c87c15))
* **bulma-ui:** strip the Avatar link warning from production builds ([a243fa5](a243fa5)), closes [#733](#733)
* **bulma-ui:** warn about a dropped Avatar href only for an as the caller wrote ([c93b27c](c93b27c)), closes [#733](#733)
* **bulma-ui:** warn in development when Avatar drops an href on a non-link as ([c76e632](c76e632)), closes [#733](#733)
* **bulma-ui:** warn when a root Theme drops its className and helpers ([05c10e0](05c10e0))
* **bulma-ui:** warn when a wrapper inside a Control drops its Control-level props ([cbf434b](cbf434b)), closes [#921](#921)
* **bulma-ui:** warn when an inline picker drops its Control-level props ([f0494bd](f0494bd)), closes [#921](#921)
* **bulma-ui:** warn when Avatar drops a target or rel on a non-link as ([50c385e](50c385e)), closes [#733](#733) [#733](#733)
* **create-bestax:** check the react ranges fit the library's peers, not equal them ([8ae5a0c](8ae5a0c)), closes [#950](#950)
* **create-bestax:** import the icon CSS for every Bulma flavor, not only complete ([a712b91](a712b91)), closes [#946](#946)
* **create-bestax:** install react and react-dom so Yarn 1 reports no unmet peers ([9c9814c](9c9814c)), closes [#644](#644) [#950](#950)
* **create-bestax:** name the files the overwrite prompt removes, and stop on a file path ([39b6232](39b6232)), closes [#945](#945)
* **create-bestax:** never empty a non-empty folder under -y, and validate flags first ([917f195](917f195)), closes [#950](#950) [#945](#945) [#950](#950)
* **create-bestax:** warn when the icon CSS import has nowhere to go ([f7f0b64](f7f0b64)), closes [#946](#946) [#946](#946)
* **eslint-plugin:** judge Columns' gap by its own steps, and lengths as lengths ([4786c8d](4786c8d))
* **eslint-plugin:** report .file as a class the codemod converts ([72a32c9](72a32c9))
* **eslint-plugin:** report .image with the message for a class the codemod converts ([d27c19b](d27c19b))
* **eslint-plugin:** report .menu, .menu-label and .menu-list with the converts message ([8ffaccc](8ffaccc))
* **eslint-plugin:** report .select and .breadcrumb as converted ([a9d8082](a9d8082))
* **eslint-plugin:** report Bulma form markup as classes the codemod converts ([128e832](128e832))
* **eslint-plugin:** report Bulma skeletons as converted ([04ca4ee](04ca4ee))
* **eslint-plugin:** report Modal's parts with the message for a class the codemod converts ([87f3c08](87f3c08))
* **eslint-plugin:** report Pagination's converting parts with the converts message ([6b94f14](6b94f14))
* **eslint-plugin:** report the pagination links and ellipsis as classes the codemod converts ([54ba0b3](54ba0b3))
* **eslint-plugin:** report Theme's deprecated radius route accurately ([5605816](5605816)), closes [#694](#694)
* **eslint-plugin:** suggest radiusless for a near miss on Theme ([21aa9a7](21aa9a7)), closes [#694](#694)

### Features

* **bestax-mcp:** look up .image as Image, with the props it takes from its <img> ([3e63230](3e63230))
* **bestax-mcp:** look up .menu and its label and list as Menu's parts ([f9fe7cd](f9fe7cd))
* **bestax-mcp:** look up Card's title and footer items on the tags their as takes ([ba8193e](ba8193e))
* **bestax-mcp:** look up Modal's parts as components ([b71583a](b71583a))
* **bestax-mcp:** look up Pagination, its list and its previous and next links as components ([f1298df](f1298df))
* **bestax-mcp:** look up the bestax component and props for a Bulma class string ([4a4b246](4a4b246)), closes [#744](#744)
* **bestax-mcp:** publish to the official MCP Registry ([74efc7f](74efc7f))
* **bestax-mcp:** say a Bulma field wraps its children only when horizontal ([929bc82](929bc82))
* **bestax-mcp:** say an .image converts around anything but a bare <img> ([60e9219](60e9219))
* **bestax-mcp:** say Menu.List renders .menu-list only at the top level ([dcce74f](dcce74f))
* **bestax-mcp:** say when a component renders the children from a count ([b60c295](b60c295))
* **bestax-mcp:** say when a component renders the element inside it ([4ced25c](4ced25c))
* **bestax-mcp:** say when a component renders the li around its element, or its own text ([8fd3faf](8fd3faf))
* **bestax-mcp:** say when a component renders the whole .file tree from props ([976d8a3](976d8a3))
* **bestax-migrate:** convert .file inside a Field, building its tree from props ([724dff5](724dff5)), closes [#809](#809)
* **bestax-migrate:** convert .icon-text, building its icons from props ([#823](#823)) ([933868f](933868f))
* **bestax-migrate:** convert .menu, its labels and its top-level lists ([02f4db6](02f4db6))
* **bestax-migrate:** convert .select and .breadcrumb with the element inside them ([d76c569](d76c569))
* **bestax-migrate:** convert .tabs and .icon around their children ([#815](#815)) ([3967a80](3967a80))
* **bestax-migrate:** convert a menu list's items to Menu.Item ([#816](#816)) ([b385687](b385687))
* **bestax-migrate:** convert an .image and its <img> to Image ([d4f3673](d4f3673)), closes [#793](#793)
* **bestax-migrate:** convert an .image around anything but a bare <img> ([0a31c5b](0a31c5b)), closes [#801](#801)
* **bestax-migrate:** convert Bulma cards built from their parts ([#752](#752)) ([1b08e17](1b08e17))
* **bestax-migrate:** convert Bulma form fields, controls, inputs and textareas ([3da788d](3da788d)), closes [#743](#743)
* **bestax-migrate:** convert Bulma grids and cells ([#760](#760)) ([22adc7c](22adc7c)), closes [#743](#743)
* **bestax-migrate:** convert Bulma navbars ([#754](#754)) ([02fdd72](02fdd72))
* **bestax-migrate:** convert Bulma skeletons ([2574ace](2574ace))
* **bestax-migrate:** convert Card's title and footer items on the tags their as takes ([20bc0c6](20bc0c6)), closes [#794](#794)
* **bestax-migrate:** convert conditional classes in clsx and classnames calls ([8948cce](8948cce))
* **bestax-migrate:** convert Modal's parts element by element ([4d41ad2](4d41ad2)), closes [#805](#805)
* **bestax-migrate:** convert navbar dividers with classes, and navbar text colors ([#796](#796)) ([c97c7e9](c97c7e9))
* **bestax-migrate:** convert Pagination, its list and its previous and next links ([d75bc70](d75bc70)), closes [#807](#807)
* **bestax-migrate:** convert Pagination's page links and ellipsis in their li's place ([ae720fa](ae720fa)), closes [#809](#809)
* **bestax-migrate:** convert Panel and its heading, tabs and blocks ([#811](#811)) ([0694e54](0694e54))
* **bestax-migrate:** convert the gap, position, overflow, radius and aspect-ratio helpers ([13dcf63](13dcf63))
* **bestax-migrate:** fold .table-container and .fixed-grid into the component inside ([#761](#761)) ([72504f5](72504f5)), closes [#743](#743)
* **bulma-ui:** add Bulma's gap helper props to every component ([#877](#877)) ([1bd4abc](1bd4abc))
* **bulma-ui:** add Bulma's position, overflow, radius and aspect-ratio helper props ([#845](#845)) ([64fb91c](64fb91c))
* **bulma-ui:** add Collapses, an accordion group for Collapse ([#849](#849)) ([a12c823](a12c823))
* **bulma-ui:** add DateRangeInput ([50fcba7](50fcba7)), closes [#774](#774)
* **bulma-ui:** add Loader, Bulma's inline spinner ([c5e9eb0](c5e9eb0)), closes [#770](#770)
* **bulma-ui:** add month and year pickers to DateInput through granularity ([c4cfc2b](c4cfc2b)), closes [#773](#773)
* **bulma-ui:** add Popover ([2e17fac](2e17fac)), closes [#776](#776)
* **bulma-ui:** export ClientOnly and useIsHydrated ([2c0c1e6](2c0c1e6))
* **bulma-ui:** export Portal ([346d8c2](346d8c2))
* **bulma-ui:** export useFocusTrap, counting the stops the browser's Tab visits ([c304fef](c304fef)), closes [#772](#772)
* **bulma-ui:** let Card.Header.Title and Card.FooterItem render Bulma's own tags ([#794](#794)) ([47ecdb6](47ecdb6))
* **bulma-ui:** make columnGap the column gap helper on Theme ([eb2c611](eb2c611)), closes [#694](#694)
* **bulma-ui:** render Pagination from total, current and onPageChange ([f7ddf04](f7ddf04)), closes [#813](#813)
* **eslint-plugin:** add an opt-in rule for plain elements styled with a Bulma component class ([#750](#750)) ([0bb4d80](0bb4d80))
* **eslint-plugin:** check the gap helper props ([b53a749](b53a749))
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 4.2.13 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deep-review needs-human-review Loop converged (or contested): awaiting owner review + manual merge released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[A11y] bulma-ui: polite programmatic notifications may not be announced, since their status region appears with its text

2 participants