Skip to content

release: v4.2.0 into develop - #648

Merged
mathewtaylor merged 75 commits into
developfrom
v4.2.0
Nov 7, 2026
Merged

mathewtaylor merged 75 commits into
developfrom
v4.2.0

Conversation

@mathewtaylor

Copy link
Copy Markdown
Contributor

Summary

Brings the v4.2.0 release branch into develop so it can be released with release.sh (Primitives 4.2.0, Components 4.2.0 and the first BlazorBlueprint.PdfViewer, 1.0.0). The full list of changes is in the 2026-11-07 (v4.2.0) section of CHANGELOG.md.

Highlights:

  • New package: BlazorBlueprint.PdfViewer (BbPdfViewer moved out of Components)
  • Dialog position and enter/exit animations; non-modal Sheet; data-slot on modal overlays
  • DataGrid stacked mobile layout; Toast custom content; ColorPicker apply/cancel
  • Localization: the remaining hard-coded English text now goes through IBbLocalizer
  • About 35 fixes, including comma-decimal cultures, overlays on plain HTTP, FormField labels and ARIA, and Menubar/keyboard shortcuts
  • bUnit component tests, now run in CI

Issues resolved

develop isn't the default branch, so this PR's merge closes none of these. They close when the release reaches main.

Denny09310 and others added 30 commits September 23, 2026 09:19
A resize drag reports every column's measured width, so a control column with
nothing to measure reported zero - stored as the column's width and preferred
over the one it declares. table-columns.js now leaves such a column out of the
report, OnResizeCompleted refuses a width that is not a width, and the column
state treats one as unset where it is stored or restored. Non-pixel lengths
(20%, auto, clamp()) pass through untouched.
Runs axe (WCAG 2.0, 2.1 and 2.2 A/AA) on every component, primitive
and chart demo, plus a keyboard pass over the main interactive
components, against the Server demo in Chromium. Report mode is the
default and writes test-results/a11y/report.md; BB_A11Y_MODE=enforce
fails on findings.
…its in

Floating content is portalled to the body and given z-index 50 (9999 for
fixed positioning), whatever its trigger sits inside. A trigger inside a
raised container therefore opened its overlay underneath that container:
a dropdown in a toast (the toast stack is z-100) rendered behind the toast.

computePosition now returns the z-index that clears every raised ancestor
of the reference, and applyPosition uses it. Positions that come through
C# (PositionResult) carry no zIndex and keep the old values.

Unchanged for triggers in normal flow (still 50). A select in a popover in
a dialog now sits at 10000 over the popover's 9999 instead of tying with it.
A toast could only hold text. It can now hold any markup or components.

- ToastService.Show<TComponent>(parameters, toast) shows a component in a
  toast, so its content is written as an ordinary .razor component. It
  works from pages and from plain C# services, and mirrors
  DialogService.OpenAsync<TComponent>.
- IToastReference is cascaded to everything inside a toast; DismissAsync()
  lets a component close the toast it is shown in.
- ToastData.Icon, Content and Actions are RenderFragment slots, named after
  BbAlert's, for small inline content. Icon replaces the variant icon,
  Content renders below the title and description, Actions is a row below.
- A toast with Content or Actions stops auto-dismissing once the user
  presses or moves focus inside it (BbToast.OnHold). Hover alone can't keep
  it open: a menu opened from it renders outside the toast, so moving onto
  the menu reads as leaving and the toast was dismissed, menu and all.
- ToastService docs now say to keep it scoped; a singleton on Blazor Server
  would share toasts, and their fragments, between users.

Demo: a Custom Content section (a ReplyToast component) and an Inline
Content section (a dropdown in Actions), with code samples and API
reference entries.
feat(Toast): show components and custom content in toasts
test: add accessibility audit to the browser suite
- Rename ColumnWidth to DataGridColumnWidth and mark it
  [EditorBrowsable(Never)]. It is public only because Components shares it
  with Primitives, and consumers import this namespace for DataGridState, so
  a type of their own called ColumnWidth would have become ambiguous (CS0104).
- Parse widths with NumberStyles.Float instead of Any. Any accepts thousands
  separators, so "12,5px" (12.5 in a comma-decimal culture) read as 125 and
  "0,4px" slipped past the sub-pixel guard as 4.
- Test the Gantt's resize guard, which no test covered, and correct its doc
  comment: the script no longer reports every column.
fix(DataGrid): never store or render a zero column width
The vendored files were the modern, unminified build. It calls
Map.prototype.getOrInsertComputed with no polyfill, so documents failed to
load on anything older than Chrome/Edge 145, Firefox 144 or Safari 26.2
(iOS 18, Firefox ESR, pinned enterprise browsers). The legacy build carries
its own polyfills, and the minified files are 1.8 MB instead of 3.1 MB with
no references to source maps that weren't shipped.

PDF.js 6 decodes JPEG 2000, JBIG2 and CCITT images only through WebAssembly
modules, and none were shipped or located, so scanned documents rendered
blank or wrong. openjpeg.wasm, jbig2.wasm and qcms_bg.wasm now ship under
lib/pdfjs/wasm/ and every getDocument call passes wasmUrl, resolved from the
interop module's own URL like the worker. Their licences are added to the
third-party notices.

Files are byte-identical to pdfjs-dist 6.3.289 (npm integrity
sha512-ZHjSVpDa3D6izMq8...).
dispose() called pdf.destroy(), which PDF.js 6 no longer has; the TypeError
was swallowed, so no document or worker was ever freed. Every Url change,
LoadDataAsync call or disposal left a worker running with a parsed PDF in
it. Only the loading task frees them, so it is now kept and destroyed.

Loads also raced. load/loadData registered the document after awaiting it
without checking whether a newer load had started, and dispose() found
nothing to free while a load was in flight. A slow document could replace
a newer fast one, and a load running at disposal re-registered itself on
the removed canvas. Each canvas now has a generation: a new load or dispose
bumps it and destroys the task in flight, and an overtaken load destroys
its own document and returns { superseded: true }.

The C# side gets the same guarantee: every load takes a version and its
own CancellationTokenSource, passed to the interop call, so a late result
never wins ApplyState or raises OnDocumentLoaded. The explicit token also
replaces Blazor Server's 60-second interop timeout, which reported a large
document on a slow link as failed while it was still loading.

The page, zoom and fit functions return failures in `error` instead of
throwing, and a render only clears its own task, not a newer one's.
OnDocumentLoaded is raised outside the interop error handling, so an
exception from the consumer's handler reaches them instead of showing
"Unable to load".
A byte-loaded document downloaded as a 0-byte file: PDF.js transfers the
ArrayBuffer given to getDocument to its worker, which leaves the copy the
viewer kept for downloads empty.

download() now asks the document for its data (pdf.getData()) for both
URL- and byte-loaded documents. A URL-loaded document is no longer fetched
a second time, so an expiring signed URL or a server without CORS no
longer matters, and the window.open fallback for a failed fetch, which had
no noopener, is gone. It returns why a download did not happen instead of
throwing.
- Public methods wait for the module import. LoadDataAsync called from a
  consumer's own OnAfterRenderAsync(firstRender) ran before the viewer's
  import came back and silently did nothing, the first time per circuit.
  The import task is kept and awaited, and retried if it failed.
- Page, zoom, fit, download and the getters catch interop failures the
  way loading does and log them. They run from toolbar clicks, and an
  exception escaping an event handler ends a Blazor Server circuit: a
  corrupt page, or "Worker was destroyed" while a document was being
  replaced, did exactly that. A result from before a newer load, or one
  answered with no document open, is no longer applied, so it can't wipe
  the viewer or show "Unable to load" while a load is in flight.
- A failed load always shows the localized PdfViewer.LoadFailed message.
  PDF.js's own text was shown as-is: raw English, sometimes with the full
  URL and its signed query string. It now goes to the log through a
  source-generated PdfViewerLog, which also replaces Console.Error.
- Relative URLs resolve against document.baseURI. PDF.js resolves them
  against the page's location, so Url="docs/a.pdf" requested
  /components/docs/a.pdf on a page at /components/pdf-viewer.
…parameter

- Height=null is documented to fill the parent, but the root had no height
  of its own. It now takes h-full and the viewport min-h-0, so the canvas
  area gets what the toolbar leaves.
- ShouldRender ignored MinScale, MaxScale, DefaultScale and
  AdditionalAttributes, so changing the zoom bounds left the zoom buttons
  enabled or disabled as before. Attributes compare by content, so a
  parent re-render's new dictionary alone still doesn't redraw the viewer.
- Changing page scrolls back to the top instead of landing wherever the
  previous page was scrolled to.
- The page-input wrapper span no longer carries an aria-label; a label on
  a generic element is not announced, and the input has its own.
mathewtaylor and others added 28 commits October 9, 2026 22:34
…ce-y-2 (#598) (#603)

The presets label was an inline span, so the wrapper's space-y-2 margin
never applied. Make it block-level. The hue and alpha slider wrappers have
a single child, so space-y-2 did nothing there; remove it.
…ds late on Server (#608) (#609)

overlay.close reaches the browser ahead of the render that writes
data-state="closed", up to a round trip ahead when the portal host is still
waiting on its previous batch. hidePosition read data-state to tell a reopen
from a close, saw "open", and skipped the hide, leaving the overlay on screen.

overlay.close now passes its own token check, so only a real reopen (which
takes a new token) cancels the hide. Direct hidePosition callers keep the
data-state check.
… (#606)

With UpdateTiming.Immediate or Debounced, BbInput, BbTextarea, BbInputGroupInput
and BbInputGroupTextarea raised ValueChanged a second time on blur with the
value typing had just raised, so a @bind-Value:after handler ran twice for one
edit. JsOnChange now raises only when the value differs from the current Value,
as BbInputField already does. A pending debounced edit is still flushed on blur.
…r first render (#607)

* fix(text-input): apply UpdateTiming and DebounceInterval changes after first render

The input handlers read the config captured at initialize, so updateConfig had no effect. They now read state.config.

* fix(text-input): drop pending input callbacks when updateConfig changes mode

A debounce timer or animation frame queued under the old mode would otherwise send a stale value after the switch.
… dark mode (#611)

* fix(inputs): use shadcn's input fill so theme --input colours show in dark mode

Input-style fields filled with bg-background; shadcn v4 uses bg-transparent
with dark:bg-input/30, so a tweakcn or shadcn theme's --input had no effect
on our fields in dark mode. Swap the fill on every field box, add shadow-xs,
add dark:hover:bg-input/50 on clickable triggers, and follow shadcn's outline
button variant. The outline's light fill uses not-dark:bg-background so an
add-on stylesheet that repeats .bb:bg-background (PdfViewer's) can't override
the dark fill.

Refs discussion #610.

* fix(inputs): keep Class background overrides on Outline and tint the editable DatePicker on hover

not-dark:bg-background survived cn when Class set a background (bb:bg-primary),
because the variants differ, so the light fill won. Use bg-background/100:
the same colour, still merged away by a Class background, and not repeated by
PdfViewer's stylesheet, so dark:bg-input/30 keeps winning in dark mode.

The editable DatePicker wrapper now gets dark:hover:bg-input/50 like the
other triggers.
* feat(Dialog): add Position option to Dialog and DialogOpenOptions (#612)

Dialog takes a Position parameter (DialogPosition: Center, TopCenter,
BottomCenter, CenterLeft, CenterRight, TopLeft, TopRight, BottomLeft,
BottomRight). DialogService dialogs take the same value through
DialogOpenOptions.Position. Center stays the default, so dialogs that
don't set it are unchanged.

Adds a demo section, code snippet and API Reference entries, and
updates the API surface snapshot.

* test(conventions): exempt DialogPositionClasses from logical-direction check

DialogPosition names physical sides (CenterLeft, TopRight, ...), the same
way ToastPosition does, so its placement classes stay physical.
* feat(Dialog): add enter and exit animations to Dialog and DialogOpenOptions (#613)

Dialog and DialogOpenOptions take EnterPreset, ExitPreset (MotionPreset?),
EnterKeyframes, ExitKeyframes and AnimationDuration. A closing dialog stays on
the page, with data-state="closed", until its exit animation ends. With none
set, dialogs open and close as before. Reduced motion skips the animations.

* fix(Dialog): keep exit lifetime and modal protection right (#613)

- An exit that ends at once now releases the dialog on both controlled and
  context closes.
- A superseded native exit no longer closes a reopened <dialog>.
- DialogService dialogs keep the focus trap, scroll lock and Escape handling
  until their exit ends; a stacked exiting dialog keeps focus until it leaves.
MobileLayout="Stacked" turns each row into a card below MobileBreakpoint
(Sm, Md or Lg). The grid writes each body cell's label as data-label, and
CSS under the root's data-mobile-stacked attribute does the switch, so no
JavaScript is needed. Columns take MobileLabel (defaults to the title; an
empty string shows none) and HideOnMobile. Pinned cells drop their sticky
offset, full-width rows keep their own layout, and the row groups get an
explicit role. A grid left at None renders exactly as before.

Co-authored-by: Mathew Taylor <mathew@mac-m5.local>
10.0.12 of Microsoft.AspNetCore.Components.WebAssembly.Server has two
high-severity advisories (GHSA-qjpx-jqm5-h2c4, GHSA-g27p-qf6j-c96c),
which fail the solution build with NU1903. Both are patched in 10.0.13.
Move every Components package in the demos to it together.

The src/ libraries reference only Components.Web, which neither
advisory covers, so their 10.0.12 floor stays.

Co-authored-by: Mathew Taylor <mathew@mac-m5.local>
…623)

Selecting a node redrew every node about five times (ten with the Items
API), and each draw sorted the node's siblings to find aria-posinset, so
a click cost grew with the square of the sibling count.

- TreeViewContext: keep each node's sibling index, refreshed once after
  removals, so GetPosInSet is O(1); IsFirstEnabledRoot no longer sorts.
- Primitive BbTreeItem: on a tree state change, re-render only when the
  node's own state (selected, expanded, checked, focus, position) changed.
- Primitive BbTreeView: skip a bound-value callback when the bound
  parameter already holds the value, so a selection no longer raises
  ExpandedValuesChanged and re-renders the page for nothing.
- Styled BbTreeView: pass OnNodeClick to the primitive only when the app
  listens for it.
- TreeItemNode and styled BbTreeItem: merge node classes once per state
  instead of per node per render.

aria-posinset, aria-setsize, focus and selection are unchanged.

Co-authored-by: Mathew Taylor <mathew@mac-m5.local>
…#624)

Navigating to a page with another layout disposes the layout's provider
and mounts a new one. The scoped ToastService keeps its toasts, but the
new provider only started timers on the next change, so toasts already
showing stayed until another toast was raised. Start their timers when
the provider initialises.

Co-authored-by: Mathew Taylor <mathew@mac-m5.local>
…ilwind CLI to v4.3.5 (#639)

10.0.12 and 10.0.13 carry GHSA-g27p-qf6j-c96c, which fails restore under TreatWarningsAsErrors. All Microsoft.AspNetCore.* references, including BlazorBlueprint.PdfViewer, now use 10.0.14. tools/install.sh and install.ps1 now default to Tailwind v4.3.5 (override with TAILWIND_VERSION).
…601)

Update the theme switcher radius options to use each radius value for the button border radius instead of a fixed rounded class. This makes the preview better reflect the selected corner style.
… (#619)

Co-authored-by: Mathew Taylor <mathew@mac-m5.local>
…itable elements (#634, #632) (#640)

fix(Menubar): use one roving tab stop (#634)
feat(KeyboardShortcuts): allow shortcuts in editable elements (#632)

Both by @ric-ros, resolved against #631, #633 and #635. BbMenubarItem also re-registers its shortcut when AllowShortcutInEditable changes.

Co-authored-by: ric-ros <ric.ros@icloud.com>
…ancel buttons (#630, #600) (#641)

fix(Toast): announce non-error toasts politely (#630)
feat(ColorPicker): add apply/cancel buttons (#600)

Resolved against v4.2.0's Toast changes and the #598 presets label fix. Adds ColorPickerActionButtonsTests and the ActionButtons API snapshot.

Co-authored-by: ric-ros <ric.ros@icloud.com>
Co-authored-by: garrenf <144177629+garrenf@users.noreply.github.com>
… on modal overlays (#626) (#642)

BbGauge sent the progress colour as progress.color, which ECharts ignores; it now sends progress.itemStyle.color. Sheet, Dialog, DialogService, AlertDialog and Drawer overlays carry a data-slot so they can be styled without relying on generated ids.
… and generate the key docs (#638) (#643)

Adds DefaultBbLocalizer.EnglishDefaults and generates the guide's key table from it (#638). Routes hard-coded literals and English parameter defaults through IBbLocalizer with 36 new keys (#637 parts a and b); affected parameters and DialogService option texts now default to null and fall back to the localized text.
BarcodeFormatException gains Error/Arguments (BarcodeFormatError); GanttBuilder throws GanttBuildException (GanttBuildError), an ArgumentException. BbBarcode, BbGantt, BbFormFieldInput and BbDynamicForm show these messages through IBbLocalizer; English output unchanged by default.
BbSheetContent gains ShowOverlay and CloseOnInteractOutside; Primitives BbSheetContent gains NonModal and CloseOnInteractOutside. Non-modal sheets render no overlay, are aria-modal="false", don't trap focus or lock scroll, and close on Escape or an outside pointer-down. Default sheets are unchanged.
CHANGELOG gets a 2026-11-07 (v4.2.0) section with upgrade notes, and the
Components and Primitives package notes are rewritten for v4.2.0.

Claude-Session: https://claude.ai/code/session_01K3XidR4yZdEDicFv6gUyoq

Co-authored-by: Claude <noreply@anthropic.com>
@mathewtaylor
mathewtaylor merged commit 51ba7b6 into develop Nov 7, 2026
2 checks passed
@mathewtaylor
mathewtaylor deleted the v4.2.0 branch November 7, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants