diff --git a/Source/for_useOverlayZIndex/when_deciding_the_panel_z_index.ts b/Source/for_useOverlayZIndex/when_deciding_the_panel_z_index.ts new file mode 100644 index 0000000..a60e9a2 --- /dev/null +++ b/Source/for_useOverlayZIndex/when_deciding_the_panel_z_index.ts @@ -0,0 +1,22 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { raisedOverlayZIndex } from '../useOverlayZIndex'; + +describe('when deciding the panel z-index', () => { + it('should raise a panel sitting below the floor', () => { + raisedOverlayZIndex('1001', 10000)!.should.equal('10000'); + }); + + it('should raise a panel with no z-index of its own', () => { + raisedOverlayZIndex('', 10000)!.should.equal('10000'); + }); + + it('should leave a panel sitting exactly on the floor alone', () => { + (raisedOverlayZIndex('10000', 10000) === undefined).should.be.true; + }); + + it('should never lower a panel PrimeReact stacked above the floor', () => { + (raisedOverlayZIndex('20102', 10000) === undefined).should.be.true; + }); +}); diff --git a/Source/useOverlayZIndex.ts b/Source/useOverlayZIndex.ts index c636de1..9298d29 100644 --- a/Source/useOverlayZIndex.ts +++ b/Source/useOverlayZIndex.ts @@ -4,19 +4,31 @@ import { useEffect } from 'react'; /** - * Hook to force a specific z-index on PrimeReact overlay components. - * This is a workaround for PrimeReact's automatic z-index calculation - * which can cause overlays to appear behind dialogs. + * Hook that keeps a PrimeReact overlay panel at or above a minimum z-index. + * + * PrimeReact computes overlay z-indexes relative to whatever is currently open — in an application that + * configures the PrimeReact z-index tiers this already stacks panels above dialogs correctly. This hook is + * the safety net for applications that do not: when PrimeReact's computed value is *below* the floor, the + * panel is raised to it. + * + * It is strictly raise-only. It must never lower a value PrimeReact computed, for two reasons: the computed + * value is what stacks the panel above an open dialog's mask, and PrimeReact's `ZIndexUtils.clear()` + * un-registers an overlay by reading its inline z-index back — overwriting it with a different number makes + * that lookup miss, leaks the registry entry, and every dialog opened afterwards escalates its mask's + * z-index further above the panels until dropdowns render behind the very dialogs that host them. * * @param className - The CSS class name to target (e.g., 'location-autocomplete-overlay') - * @param zIndex - The desired z-index value (default: 10000) + * @param minimumZIndex - The floor the panel must not sit below (default: 10000) */ -export function useOverlayZIndex(className: string, zIndex: number = 10000): void { +export function useOverlayZIndex(className: string, minimumZIndex: number = 10000): void { useEffect(() => { const observer = new MutationObserver(() => { const panel = document.querySelector(`.${className}`); - if (panel instanceof HTMLElement && panel.style.zIndex !== zIndex.toString()) { - panel.style.zIndex = zIndex.toString(); + if (!(panel instanceof HTMLElement)) return; + + const raised = raisedOverlayZIndex(panel.style.zIndex, minimumZIndex); + if (raised !== undefined) { + panel.style.zIndex = raised; } }); @@ -28,5 +40,25 @@ export function useOverlayZIndex(className: string, zIndex: number = 10000): voi }); return () => observer.disconnect(); - }, [className, zIndex]); + }, [className, minimumZIndex]); +} + +/** + * Decides whether an overlay panel's inline z-index must be raised to the floor. + * + * Raise-only by design: a value PrimeReact computed at or above the floor is left untouched, both because + * it is what stacks the panel above an open dialog and because PrimeReact reads it back to un-register the + * overlay when it closes. + * + * @param currentZIndex - The panel's current inline z-index value, possibly empty. + * @param minimumZIndex - The floor the panel must not sit below. + * @returns The value to assign, or undefined when the current value must be left alone. + */ +export function raisedOverlayZIndex(currentZIndex: string, minimumZIndex: number): string | undefined { + const current = Number.parseInt(currentZIndex, 10); + if (Number.isNaN(current) || current < minimumZIndex) { + return minimumZIndex.toString(); + } + + return undefined; }