Skip to content

feat(pds-modal): add disableTopLayer to opt out of the browser top layer - #802

Merged
QuintonJason merged 8 commits into
mainfrom
feat/pds-modal-disable-top-layer
Aug 27, 2026
Merged

feat(pds-modal): add disableTopLayer to opt out of the browser top layer#802
QuintonJason merged 8 commits into
mainfrom
feat/pds-modal-disable-top-layer

Conversation

@QuintonJason

@QuintonJason QuintonJason commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Description

pds-modal opens via the native dialog.showModal(), which promotes the dialog to the browser top layer. The top layer always paints above every normal-flow element regardless of z-index, so any overlay a consumer mounts elsewhere in the DOM (typically appended to document.body) renders behind an open pds-modal and is unreachable.

This is a recurring, cross-team issue — commerce file picker / media-library card / Adobe Express "Edit image", TinyMCE "Formats" dropdown, and now Clubs' image uploader (its own z-index modal sits behind the Pine edit modal). Patching z-index can't beat the top layer, moving nodes into the modal via a MutationObserver was abandoned (kajabi-products#51160, closed), and the only reliable workaround has been falling back to the legacy Sage modal.

This adds the escape hatch phillip proposed: a prop to render the modal outside the top layer.

What changed: new boolean prop disableTopLayer (default false):

  • Default (false): unchanged — dialog.showModal() (top layer, page inert). No behavior change for existing consumers.
  • true: dialog.show() — a non-modal dialog that stays in the normal stacking context at --pine-z-index-modal, so consumer overlays with a higher z-index can display above it.

The dimming backdrop still renders (it's the <dialog> element styled via .pds-modal__backdrop, not the ::backdrop pseudo) and outside-click dismiss still works — this mode is for letting a higher-z-index overlay appear above the modal, not for making the modal non-blocking. What changes in this mode: the page is not made inert, aria-modal is false, and the Tab focus trap is disabled so controls in an overlay stacked above the modal stay reachable by keyboard/AT. Escape closes the modal while focus is inside it, but is left to an overlay above the modal when that overlay owns focus — so a keyboard user can dismiss the overlay without losing the modal underneath it. The prop is read when the modal opens (documented as open-time-only; changing it mid-open is unsupported). Caveats (documented): the dialog now participates in normal stacking (a page element with a higher stacking context can overlap it), and don't mix a disable-top-layer modal with a default top-layer modal in one stack (dismiss targeting is z-index-based).

Fixes DSS-240 (https://linear.app/kajabi/issue/DSS-240). Consumer follow-up: adopt disable-top-layer on the Clubs post edit modal (kajabi-products#56131).

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

  • unit tests
  • e2e tests
  • accessibility tests
  • tested manually
  • other: verified end-to-end in a downstream app (kajabi-products, production build) — the previously-trapped picker displays above the modal with disable-top-layer.

Spec (stubbed modalRef): default → showModal(), disableTopLayershow(), aria-modal reflection. E2e (real Chromium — the only place the top-layer contract and focus/Escape behavior are observable): default is :modal true, disableTopLayer is :modal false + open + aria-modal="false", a higher-z-index body overlay paints above the non-modal dialog, and Escape is left to an overlay that owns focus (modal stays open) but closes the modal when focus is not held outside it. Full @pine-ds/core suite green (3018 tests). Storybook DisableTopLayer story + MDX example demonstrate a focusable, self-cleaning document.body overlay stacking above the modal.

Reviewed via pine-run-gauntlet (code + security + design/a11y) plus Cursor Bugbot + @pixelflips. Findings addressed: Tab focus trap disabled in non-top-layer mode; Escape left to an overlay that owns focus; aria-modal/prop documented as open-time-only; MDX heading blank-line parse fix; demo overlay now self-removes (click / Escape / modal close) so it can't orphan over the docs page.

Test Configuration:

  • Pine versions: @pine-ds/core (main)
  • OS: macOS
  • Browsers: Chromium

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes
  • Design has QA'ed and approved this PR

Note

Medium Risk
Changes modal open semantics, focus trapping, and Escape handling for opt-in consumers; default top-layer behavior is preserved but a11y/stacking caveats apply when the flag is set.

Overview
Adds disableTopLayer (default false) on pds-modal so consumers can open the dialog with dialog.show() instead of showModal(), keeping it in normal stacking so body-mounted overlays (file pickers, editor menus) can paint above it via z-index.

When enabled, the modal sets aria-modal="false", skips Tab focus trapping, and only handles Escape while focus stays inside the modal (overlays above can keep Escape). Default behavior is unchanged. Docs, Storybook, generated types, and unit/e2e tests cover the new mode.

Reviewed by Cursor Bugbot for commit c16b5ac. Bugbot is set up for automated code reviews on this repo. Configure here.

@QuintonJason
QuintonJason requested a review from a team as a code owner August 27, 2026 20:49
@netlify

netlify Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploy Preview for pine-design-system ready!

Name Link
🔨 Latest commit c16b5ac
🔍 Latest deploy log https://app.netlify.com/projects/pine-design-system/deploys/6a90ad2e755e5800086c8610
😎 Deploy Preview https://deploy-preview-802--pine-design-system.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the package: core Changes have been made to the Core package label Aug 27, 2026
@QuintonJason

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread libs/core/src/components/pds-modal/pds-modal.tsx
@QuintonJason QuintonJason added the ran-gauntlet Multi-agent review gauntlet has been run on this branch label Aug 27, 2026

@pixelflips pixelflips left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed through a3c820d. Four high-confidence findings left inline on the relevant lines: two in the component, two in the new docs section. The aria-modal prose fix in a3c820d resolves the earlier inaccuracy in the Technical Notes.

Lower-confidence items (no e2e coverage for the real dialog.show() path, the isInnermostModal() z-index comparison across mixed top-layer/non-modal modals, and the unrelated pds-link turbo* drift in components.d.ts) are not raised inline.

Comment thread libs/core/src/components/pds-modal/pds-modal.tsx
Comment thread libs/core/src/components/pds-modal/pds-modal.tsx
Comment thread libs/core/src/components/pds-modal/docs/pds-modal.mdx
Comment thread libs/core/src/components/pds-modal/docs/pds-modal.mdx Outdated
@QuintonJason
QuintonJason force-pushed the feat/pds-modal-disable-top-layer branch from a3c820d to 83d2b3a Compare August 27, 2026 21:18
@QuintonJason QuintonJason self-assigned this Aug 27, 2026

@pixelflips pixelflips left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for addressing the feedback. LGTM! 👍🏼

@QuintonJason

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c16b5ac. Configure here.

@QuintonJason
QuintonJason merged commit ac921ca into main Aug 27, 2026
21 checks passed
@QuintonJason
QuintonJason deleted the feat/pds-modal-disable-top-layer branch August 27, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core Changes have been made to the Core package ran-gauntlet Multi-agent review gauntlet has been run on this branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants