You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(bulma-ui): announce polite notifications through a status region kept from mount - #835
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.
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.
…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
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.
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.
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.
[ #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.
… 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.
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.
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.
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.
… 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.
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.
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.
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.
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.
> ✂ 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.
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.
# [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)
# [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))
# [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))
## [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))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.NotificationContainernow keeps one visually hiddenrole="status"region from mount, in the same portal as its stacks, and it stays put while stacks come and go.show()doesn't find it twice.role="alert"andaria-live="assertive", because an assertive alert inserted together with its text is announced reliably.role="alert", sogetByRole('alert')still finds a toast with the notification container mounted. A non-urgent toast pairs that role witharia-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.extras-sr-onlymixin, 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 usesaria-labelandalt, leaves outaria-hidden,hiddenand inlinedisplay: nonecontent, and breaks at block elements. The region setsaria-atomic="false"so a screen reader reads only what's added. ThemessageTSDoc lists what the reader follows and what it doesn't.The one thing an app's tests might notice: a polite notification's message no longer has
role="status", sogetByRole('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:
getByRole('alert')finding a toast beside a notification container;React 18 was checked too.
pnpm allpasses locally.