Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Source/for_useOverlayZIndex/when_deciding_the_panel_z_index.ts
Original file line number Diff line number Diff line change
@@ -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;
});
});
48 changes: 40 additions & 8 deletions Source/useOverlayZIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});

Expand All @@ -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;
}
Loading