Skip to content

refactor(window-group): reactive stacking + deferred layout (fixes #23, #24)#25

Merged
gfazioli merged 3 commits into
masterfrom
refactor-window-group-state
Apr 21, 2026
Merged

refactor(window-group): reactive stacking + deferred layout (fixes #23, #24)#25
gfazioli merged 3 commits into
masterfrom
refactor-window-group-state

Conversation

@gfazioli
Copy link
Copy Markdown
Owner

@gfazioli gfazioli commented Apr 21, 2026

Summary

Unified refactor that addresses both open issues by replacing the imperative ref-based stacking/registry model with a reactive one.

  • Fixes Z-index escalation: unbounded counter exceeds modals and menus #23 (z-index unbounded escalation): the 'increment' counter now honors a maxZIndex cap and wraps back to initialZIndex when exceeded; new 'normalize' strategy derives z-indexes from a reactive stackOrder so values always stay compact (initialZIndex .. initialZIndex + N - 1). Stand-alone Window also gains initialZIndex/maxZIndex props.
  • Fixes Layout presets don't work with dynamically rendered windows (.map) #24 (layout presets broken on dynamic .map() windows): applyLayout now defers when the registry is empty or the container has not been measured, and flushes once both become ready via a requestAnimationFrame.

New public API

  • Window.GroupinitialZIndex, maxZIndex, zIndexStrategy ('increment' | 'normalize')
  • Window (stand-alone) — initialZIndex, maxZIndex
  • WindowGroupContextValue — adds read-only stackOrder: string[]

All additive — the default zIndexStrategy='increment' with no maxZIndex preserves the previous behavior.

Behavioral changes (documented in Upgrade guide)

  • Stand-alone Window instances still share a module-level z-index counter per scope (portal vs container), but the counter now wraps to initialZIndex when it would exceed maxZIndex. Before, the counter grew unbounded.
  • Window.Group stacking is state-driven instead of ref-driven. Externally observable behavior is unchanged unless you read intermediate z-index values via internals.

Test plan

  • 148/148 jest tests pass, including 9 new tests covering normalize/increment strategies, maxZIndex wrap, stackOrder mutations, dynamic .map() registration, deferred layout, and stand-alone z-index props
  • yarn test (syncpack + oxfmt + typecheck + lint + jest) green
  • yarn build + yarn docgen clean
  • yarn docs:build clean
  • Browser verification via Claude in Chrome:
    • Dynamic Windows demo: added 2 windows at runtime → tile layout applied correctly on 4 windows in 2×2 grid
    • Z-Index Management demo (normalize): Alpha=100, Beta=101, Gamma=102 → click Alpha → Alpha=102, Beta=100, Gamma=101 (compact)
    • Z-Index Management demo (increment + maxZIndex=105): 10 alternating clicks, all z-indexes stay in [100..105] (wrap works)
    • No regressions on existing Window.Group / Multiple Windows demos
    • Browser console clean (only an unrelated Next.js HMR warning)
  • Manual review by @gfazioli

…layout

Resolves the root-cause shared by #23 (unbounded z-index escalation) and #24
(layout presets silently no-op on dynamically rendered windows) by replacing
the imperative ref-based stacking model with a reactive one and deferring
layout application until the registry and container are ready.

- WindowGroup: introduce stackOrder state, derive normalized z-index from it
- WindowGroup: bound the increment counter via maxZIndex wrap to initialZIndex
- WindowGroup: defer applyLayout when registry or container dims are not ready;
  flushed once both become available
- Expose initialZIndex, maxZIndex, zIndexStrategy on WindowGroup and Window
- Expose stackOrder on WindowGroupContextValue for introspection
- Docs: new "Dynamic Windows" and "Z-index Management" sections with demos
- Upgrade guide: v3.1 behavioral notes and migration snippets
Copilot AI review requested due to automatic review settings April 21, 2026 11:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors Window.Group stacking/layout logic to be reactive and adds bounded z-index behavior to address long-running z-index escalation and dynamic .map() registration/layout timing issues.

Changes:

  • Introduces zIndexStrategy (increment/normalize) and stackOrder for Window.Group, plus initialZIndex/maxZIndex options.
  • Adds initialZIndex/maxZIndex to stand-alone Window z-index handling (with wrap-around behavior).
  • Defers applyLayout until registry and container measurements are ready; updates docs/demos/tests accordingly.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
package/src/hooks/use-window-state.ts Adds stand-alone initialZIndex/maxZIndex logic and a test reset helper for module counters
package/src/hooks/use-mantine-window.ts Wires new z-index props through to window state hook
package/src/WindowGroup.tsx Implements reactive stacking (stackOrder), new z-index strategies, and deferred applyLayout flushing
package/src/WindowGroup.context.ts Adds ZIndexStrategy type and exposes stackOrder on the group context value
package/src/Window.tsx Adds public props for stand-alone z-index configuration
package/src/Window.test.tsx Adds tests for group z-index strategies, deferred layout, and stand-alone z-index props
docs/migrations.mdx Documents v3.1 behavioral changes and new APIs
docs/docs.mdx Adds docs sections for Dynamic Windows and Z-index Management
docs/demos/index.ts Exports new demos
docs/demos/Window.demo.zIndexStrategy.tsx New demo showcasing z-index strategies
docs/demos/Window.demo.dynamicWindows.tsx New demo showcasing dynamic .map() windows and layout presets

Comment thread package/src/WindowGroup.tsx
Comment thread docs/demos/Window.demo.dynamicWindows.tsx
Comment thread package/src/Window.test.tsx Outdated
Comment thread package/src/hooks/use-window-state.ts Outdated
Comment thread package/src/WindowGroup.tsx
Comment thread package/src/WindowGroup.tsx Outdated
- Stand-alone z-index: seed the module counter up to `initialZIndex` before
  incrementing so `bringToFront` never moves a window backward; also raise
  `maxZIndex` to at least `initialZIndex` to avoid a degenerate wrap range.
  Reveals a real bug where `<Window initialZIndex=500 />` would snap to 201
  on first click.
- `registerWindow`: move the `incrementMap.has(id)` check inside a functional
  setState so unmount+remount in the same commit (StrictMode / .map() churn)
  always consults the latest map instead of a stale closure snapshot.
- `applyLayout`: defer only when the registry is empty; when the registry is
  populated but nothing is visible, treat as a no-op (pre-v3.1 behavior)
  instead of parking the layout in `pendingLayoutRef` where visibility
  changes can't trigger a flush.
- `maxZIndex` JSDoc: clarify that the prop is ignored under `'normalize'`,
  matching the current implementation.
- Dynamic Windows demo: derive the next id from `prev.length` inside the
  state updater and stop double-prefixing ids with `win_` (was producing
  `win_win_c` in state).
- Tests: tighten stand-alone z-index coverage to verify `initialZIndex`
  seeding and exact wrap-to-initialZIndex; add a regression test for
  `applyLayout` on a populated-but-all-hidden registry.
- Update Yarn to 4.14.1 and adjust yarnPath
- Upgrade Mantine core, hooks, and code-highlight to 9.0.2
- Bump Next, React, and React-DOM to the latest patch releases
- Update Storybook, oxlint, postcss, rollup, and vite to newer patch versions
- Keep package.json and docs/package.json in sync with new versions
@gfazioli gfazioli merged commit d664d26 into master Apr 21, 2026
1 check passed
@gfazioli gfazioli deleted the refactor-window-group-state branch April 21, 2026 15:29
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.

Layout presets don't work with dynamically rendered windows (.map) Z-index escalation: unbounded counter exceeds modals and menus

2 participants