@porsche-design-system/components-react 4.2.0-rc.4 → 4.2.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/cjs/lib/components/flyout.wrapper.cjs +3 -3
  3. package/esm/lib/components/flyout.wrapper.d.ts +11 -1
  4. package/esm/lib/components/flyout.wrapper.mjs +3 -3
  5. package/package.json +2 -2
  6. package/ssr/cjs/components/dist/styles/esm/styles-entry.cjs +85 -41
  7. package/ssr/cjs/components/dist/utils/esm/utils-entry.cjs +139 -21
  8. package/ssr/cjs/components-react/projects/react-ssr-wrapper/src/lib/components/flyout.wrapper.cjs +4 -4
  9. package/ssr/cjs/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/banner.cjs +6 -0
  10. package/ssr/cjs/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/flyout.cjs +7 -1
  11. package/ssr/cjs/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/modal.cjs +6 -0
  12. package/ssr/cjs/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/sheet.cjs +6 -0
  13. package/ssr/esm/components/dist/styles/esm/styles-entry.mjs +85 -41
  14. package/ssr/esm/components/dist/utils/esm/utils-entry.mjs +136 -22
  15. package/ssr/esm/components-react/projects/react-ssr-wrapper/src/lib/components/flyout.wrapper.mjs +4 -4
  16. package/ssr/esm/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/banner.mjs +7 -1
  17. package/ssr/esm/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/flyout.mjs +8 -2
  18. package/ssr/esm/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/modal.mjs +7 -1
  19. package/ssr/esm/components-react/projects/react-ssr-wrapper/src/lib/dsr-components/sheet.mjs +7 -1
  20. package/ssr/esm/lib/components/flyout.wrapper.d.ts +11 -1
  21. package/ssr/esm/lib/dsr-components/banner.d.ts +1 -0
  22. package/ssr/esm/lib/dsr-components/flyout.d.ts +1 -0
  23. package/ssr/esm/lib/dsr-components/modal.d.ts +1 -0
  24. package/ssr/esm/lib/dsr-components/sheet.d.ts +1 -0
@@ -25,12 +25,18 @@ class DSRFlyout extends react.Component {
25
25
  hasHeader;
26
26
  hasFooter;
27
27
  hasSubFooter;
28
+ topLayer = utilsEntry.createTopLayerController({
29
+ getElement: () => this.props.dialog,
30
+ isShown: () => !!this.props.dialog?.open,
31
+ show: () => utilsEntry.showDialog(this.props.dialog, this.props.scroller),
32
+ hide: () => this.props.dialog?.close(),
33
+ });
28
34
  render() {
29
35
  const { children, namedSlotChildren, otherChildren } = splitChildren.splitChildren(this.props.children);
30
36
  const hasHeader = namedSlotChildren.filter(({ props: { slot } }) => slot === 'header').length > 0;
31
37
  const hasFooter = namedSlotChildren.filter(({ props: { slot } }) => slot === 'footer').length > 0;
32
38
  const hasSubFooter = namedSlotChildren.filter(({ props: { slot } }) => slot === 'sub-footer').length > 0;
33
- const style = minifyCss.minifyCss(stylesEntry.getFlyoutCss(this.props.open, this.props.background, this.props.backdrop, this.props.position, hasHeader, hasFooter, hasSubFooter, this.props.footerBehavior));
39
+ const style = minifyCss.minifyCss(stylesEntry.getFlyoutCss(this.props.open, this.props.background, this.props.backdrop, this.props.position, hasHeader, hasFooter, hasSubFooter, this.props.footerBehavior, this.props.fullscreen));
34
40
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("template", { shadowroot: "open", shadowrootmode: "open", children: [jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: style } }), jsxRuntime.jsx(dialogBase.DialogBase, { host: null, dismissable: true, containerClass: "flyout", header: hasHeader ? jsxRuntime.jsx("slot", { name: "header" }) : undefined, footer: hasFooter ? jsxRuntime.jsx("slot", { name: "footer" }) : undefined, subFooter: hasSubFooter ? jsxRuntime.jsx("slot", { name: "sub-footer" }) : undefined, ariaAttributes: utilsEntry.parseAndGetAriaAttributes({
35
41
  'aria-modal': true,
36
42
  ...{ 'aria-label': hasHeader ? namedSlotChildren.find(({ props: { slot } }) => slot === 'header')?.props.children : 'Flyout' },
@@ -22,6 +22,12 @@ class DSRModal extends react.Component {
22
22
  footer;
23
23
  hasHeader;
24
24
  hasFooter;
25
+ topLayer = utilsEntry.createTopLayerController({
26
+ getElement: () => this.props.dialog,
27
+ isShown: () => !!this.props.dialog?.open,
28
+ show: () => utilsEntry.showDialog(this.props.dialog, this.props.scroller),
29
+ hide: () => this.props.dialog?.close(),
30
+ });
25
31
  render() {
26
32
  const { children, namedSlotChildren, otherChildren } = splitChildren.splitChildren(this.props.children);
27
33
  const hasHeader = namedSlotChildren.filter(({ props: { slot } }) => slot === 'header').length > 0;
@@ -19,6 +19,12 @@ class DSRSheet extends react.Component {
19
19
  dialog;
20
20
  scroller;
21
21
  hasHeader;
22
+ topLayer = utilsEntry.createTopLayerController({
23
+ getElement: () => this.props.dialog,
24
+ isShown: () => !!this.props.dialog?.open,
25
+ show: () => utilsEntry.showDialog(this.props.dialog, this.props.scroller),
26
+ hide: () => this.props.dialog?.close(),
27
+ });
22
28
  render() {
23
29
  const { children, namedSlotChildren, otherChildren } = splitChildren.splitChildren(this.props.children);
24
30
  const hasHeader = namedSlotChildren.filter(({ props: { slot } }) => slot === 'header').length > 0;
@@ -4130,12 +4130,30 @@ const OPTION_LIST_SAFE_ZONE = 6;
4130
4130
 
4131
4131
  const getCDNBaseURL = () => global.PORSCHE_DESIGN_SYSTEM_CDN_URL + "/porsche-design-system";
4132
4132
 
4133
- const prefix = `[Porsche Design System v${"4.2.0-rc.4"}]` // this part isn't covered by unit tests
4133
+ const prefix = `[Porsche Design System v${"4.2.0-rc.5"}]` // this part isn't covered by unit tests
4134
4134
  ;
4135
4135
  const consoleError = (...messages) => {
4136
4136
  console.error(prefix, ...messages);
4137
4137
  };
4138
4138
 
4139
+ // Single source of truth for the two CSS feature queries behind the "keep on #top-layer during fade-out" capability,
4140
+ // shared by the JS detection (`supportsOverlayTransition`) and the CSS `@supports` wrapper
4141
+ // (`overlayTransitionSupportsQuery`) so the two can never drift.
4142
+ const overlayFeature = 'overlay: auto';
4143
+ const allowDiscreteFeature = 'transition-behavior: allow-discrete';
4144
+ /**
4145
+ * Wraps JSS styles in `@supports (overlay: auto) and (transition-behavior: allow-discrete)`, kept in sync with
4146
+ * `supportsOverlayTransition()`. Use it for styles that should only apply where the dialog/popover can stay on the
4147
+ * `#top-layer` during its fade-out (Chromium); other browsers defer the native hide instead (see
4148
+ * `createTopLayerController`).
4149
+ *
4150
+ * @param {JssStyle} style - The styles to apply only when the `overlay` transition is supported.
4151
+ * @returns {JssStyle} The `@supports`-wrapped styles.
4152
+ */
4153
+ const overlayTransitionSupportsQuery = (style) => {
4154
+ return { [`@supports (${overlayFeature}) and (${allowDiscreteFeature})`]: style };
4155
+ };
4156
+
4139
4157
  const headerSlot = 'header';
4140
4158
  const anchorSlot = 'anchor';
4141
4159
 
@@ -4683,6 +4701,7 @@ const getComponentCss$18 = (isOpen, position, state, hasDismissButton, hasHeadin
4683
4701
  })),
4684
4702
  left: '50vw',
4685
4703
  width: `min(calc(100vw - 2 * ${ref(cssVarInsetX, gridExtendedOffsetBase)}),${ref(cssVarMaxWidth, '100ch')})`,
4704
+ overlay: 'none',
4686
4705
  '&:popover-open': {
4687
4706
  overlay: 'auto',
4688
4707
  },
@@ -4697,10 +4716,10 @@ const getComponentCss$18 = (isOpen, position, state, hasDismissButton, hasHeadin
4697
4716
  transform: 'translate3d(-50%,0,0)',
4698
4717
  }),
4699
4718
  transition,
4700
- // during transition the element will be removed from top-layer immediately, resulting in other elements laying over (as of Mai 2024 only Chrome is fixed by this)
4701
- '@supports (transition-behavior: allow-discrete)': {
4719
+ // keep the popover on the #top-layer while the fade-out runs (Chromium only; see `overlayTransitionSupportsQuery`)
4720
+ ...overlayTransitionSupportsQuery({
4702
4721
  transition: `${transition},${getTransition('overlay', duration, easing)} allow-discrete`,
4703
- },
4722
+ }),
4704
4723
  },
4705
4724
  },
4706
4725
  }, {
@@ -6091,6 +6110,7 @@ const getDialogBackdropTransitionJssStyle = (isVisible, backdrop = 'blur') => {
6091
6110
  height: '100dvh',
6092
6111
  visibility: 'inherit',
6093
6112
  pointerEvents: 'auto',
6113
+ overlay: 'auto',
6094
6114
  background: ref(colorBackdrop),
6095
6115
  ...(isBackdropBlur && {
6096
6116
  WebkitBackdropFilter: ref(blurFrosted),
@@ -6106,14 +6126,14 @@ const getDialogBackdropTransitionJssStyle = (isVisible, backdrop = 'blur') => {
6106
6126
  height: '0px',
6107
6127
  visibility: 'hidden', // element shall not be tabbable with keyboard after fade out transition has finished
6108
6128
  pointerEvents: 'none', // element can't be interacted with mouse
6129
+ overlay: 'none',
6109
6130
  background: 'transparent',
6110
6131
  }),
6111
6132
  transition,
6112
- // `allow-discrete` transition for ua-style `overlay` (supported browsers only) ensures dialog is rendered on
6113
- // #top-layer as long as fade-in or fade-out transition/animation is running
6114
- '@supports (transition-behavior: allow-discrete)': {
6133
+ // keep the dialog on the #top-layer while the fade-out runs (Chromium only; see `overlayTransitionSupportsQuery`)
6134
+ ...overlayTransitionSupportsQuery({
6115
6135
  transition: `${transition}, ${getTransition('overlay', duration, easing)} allow-discrete`,
6116
- },
6136
+ }),
6117
6137
  };
6118
6138
  };
6119
6139
  const getScrollerJssStyle = (position) => {
@@ -6141,13 +6161,17 @@ const getScrollerJssStyle = (position) => {
6141
6161
  overscrollBehaviorY: 'none',
6142
6162
  // TODO: check if smooth scrolling on iOS is given?
6143
6163
  background: background.light,
6164
+ // ensure a translate3d style is always applied on .scroller and .modal/.flyout/.sheet to create a new stacking
6165
+ // context and prevent a Chromium paint bug: when a dialog element is nested inside another (e.g. `p-modal` within
6166
+ // `p-flyout`)
6167
+ transform: 'translate3d(0,0,0)',
6144
6168
  };
6145
6169
  };
6146
6170
  const dialogBorderRadius = ref(radius3Xl);
6147
6171
  const dialogPaddingTop = ref(spacingFluidMd$1);
6148
6172
  const dialogPaddingBottom = `calc(${dialogBorderRadius} + ${ref(spacingFluidMd$1)})`;
6149
6173
  const dialogPaddingInline = ref(spacingFluidLg);
6150
- const dialogGridJssStyle = (clipPath = 'none') => {
6174
+ const dialogGridJssStyle = () => {
6151
6175
  return {
6152
6176
  position: 'relative',
6153
6177
  display: 'grid',
@@ -6156,8 +6180,12 @@ const dialogGridJssStyle = (clipPath = 'none') => {
6156
6180
  paddingTop: dialogPaddingTop,
6157
6181
  paddingBottom: dialogPaddingBottom,
6158
6182
  alignContent: 'flex-start',
6183
+ // Consumers set their own `clip-path` next to their corner `border-radius` (e.g. `inset(0 round …)`).
6159
6184
  // `overflow: clip` can't be used due to a Chromium bug that drops descendant backdrop-filter tiles (e.g. frosted p-tag); `clip-path` clips slotted content to the rounded corners without the faulty paint-containment box while keeping the scroll behavior intact
6160
- clipPath,
6185
+ // Chromium paint bug: when a dialog element is nested inside another (e.g. `p-modal` within `p-flyout`),
6186
+ // the inner dialog's grid content fails to render. Forcing a new compositing layer via `translate3d`
6187
+ // triggers a repaint and fixes it. Re-check periodically; remove once the upstream Chromium bug is resolved.
6188
+ transform: 'translate3d(0,0,0)',
6161
6189
  };
6162
6190
  };
6163
6191
  const getDialogColorJssStyle = () => {
@@ -6175,13 +6203,13 @@ const getDialogTransitionJssStyle = (isVisible, slideIn) => {
6175
6203
  ...(isVisible
6176
6204
  ? {
6177
6205
  opacity: 1,
6178
- transform: 'initial',
6206
+ transform: 'translate3d(0,0,0)',
6179
6207
  }
6180
6208
  : {
6181
6209
  opacity: 0,
6182
- transform: slideIn === '^' ? 'translateY(25vh)' : `translateX(${slideIn === '>' ? '-' : ''}100%)`,
6210
+ transform: slideIn === '^' ? 'translate3d(0,25vh,0)' : `translate3d(${slideIn === '>' ? '-' : ''}100%,0,0)`,
6183
6211
  '&:dir(rtl)': {
6184
- transform: slideIn === '^' ? 'translateY(25vh)' : `translateX(${slideIn === '>' ? '' : '-'}100%)`,
6212
+ transform: slideIn === '^' ? 'translate3d(0,25vh,0)' : `translate3d(${slideIn === '>' ? '' : '-'}100%,0,0)`,
6185
6213
  },
6186
6214
  }),
6187
6215
  transition: `${getTransition('opacity', duration, easing)}, ${getTransition('transform', duration, easing)}`,
@@ -7292,7 +7320,7 @@ const cssVarRefPaddingBottom$2 = '--ref-p-flyout-pb';
7292
7320
  * @css-variable {"name": "--ref-p-flyout-px", "description": "Exposes the internally used padding-inline of the Flyout as read only CSS variable. When slotting e.g. a media container, this variable can be used to stretch the element to the full horizontal size of the Flyout."}
7293
7321
  */
7294
7322
  const cssVarRefPaddingInline$2 = '--ref-p-flyout-px';
7295
- const getComponentCss$V = (isOpen, background, backdrop, position, hasHeader, hasFooter, hasSubFooter, footerBehavior) => {
7323
+ const getComponentCss$V = (isOpen, background, backdrop, position, hasHeader, hasFooter, hasSubFooter, footerBehavior, fullscreen) => {
7296
7324
  const isPositionStart = position === 'start';
7297
7325
  const isFooterFixed = footerBehavior === 'fixed';
7298
7326
  return getCss({
@@ -7343,35 +7371,49 @@ const getComponentCss$V = (isOpen, background, backdrop, position, hasHeader, ha
7343
7371
  },
7344
7372
  },
7345
7373
  flyout: {
7346
- ...dialogGridJssStyle(isPositionStart
7347
- ? `inset(0 round 0 ${dialogBorderRadius} ${dialogBorderRadius} 0)` // position 'start': round inline-end (right in LTR) corners only
7348
- : `inset(0 round ${dialogBorderRadius} 0 0 ${dialogBorderRadius})` // position 'end': round inline-start (left in LTR) corners only
7349
- ),
7374
+ ...dialogGridJssStyle(),
7350
7375
  ...getDialogColorJssStyle(),
7351
- width: ref(cssVariableWidth$2, 'auto'),
7352
- minWidth: '320px',
7353
- maxWidth: '100vw',
7354
- // `clip-path` uses physical corners, so mirror for RTL to keep parity with the logical border*Radius below
7355
- '&:dir(rtl)': {
7356
- clipPath: isPositionStart
7357
- ? `inset(0 round ${dialogBorderRadius} 0 0 ${dialogBorderRadius})`
7358
- : `inset(0 round 0 ${dialogBorderRadius} ${dialogBorderRadius} 0)`,
7359
- },
7360
- ...(isPositionStart
7376
+ ...buildResponsiveStyles(fullscreen, (fullscreenValue) => fullscreenValue
7361
7377
  ? {
7362
- borderStartEndRadius: dialogBorderRadius,
7363
- borderEndEndRadius: dialogBorderRadius,
7364
- ...forcedColorsMediaQuery({
7365
- borderInlineEnd: '2px solid CanvasText',
7366
- }),
7378
+ // fullscreen spans the whole viewport width, so corners are squared and corner clipping is disabled
7379
+ width: '100dvw',
7380
+ minWidth: 'auto',
7381
+ maxWidth: 'none',
7382
+ borderRadius: 0,
7383
+ clipPath: 'none',
7384
+ // the flyout touches both inline edges, so the inner-side HCM border is no longer needed
7385
+ '&:dir(rtl)': {
7386
+ clipPath: 'none',
7387
+ },
7367
7388
  }
7368
7389
  : {
7369
- borderStartStartRadius: dialogBorderRadius,
7370
- borderEndStartRadius: dialogBorderRadius,
7371
- // TODO: Fix needs to be implemented for Fullscreen (which is not available as prop for Flyout yet)
7372
- ...forcedColorsMediaQuery({
7373
- borderInlineStart: '2px solid CanvasText',
7374
- }),
7390
+ width: ref(cssVariableWidth$2, 'auto'),
7391
+ minWidth: '320px',
7392
+ maxWidth: '100vw',
7393
+ clipPath: isPositionStart
7394
+ ? `inset(0 round 0 ${dialogBorderRadius} ${dialogBorderRadius} 0)` // position 'start': round inline-end (right in LTR) corners only
7395
+ : `inset(0 round ${dialogBorderRadius} 0 0 ${dialogBorderRadius})`, // position 'end': round inline-start (left in LTR) corners only
7396
+ // `clip-path` uses physical corners, so mirror for RTL to keep parity with the logical border*Radius below
7397
+ '&:dir(rtl)': {
7398
+ clipPath: isPositionStart
7399
+ ? `inset(0 round ${dialogBorderRadius} 0 0 ${dialogBorderRadius})`
7400
+ : `inset(0 round 0 ${dialogBorderRadius} ${dialogBorderRadius} 0)`,
7401
+ },
7402
+ ...(isPositionStart
7403
+ ? {
7404
+ borderStartEndRadius: dialogBorderRadius,
7405
+ borderEndEndRadius: dialogBorderRadius,
7406
+ ...forcedColorsMediaQuery({
7407
+ borderInlineEnd: '2px solid CanvasText',
7408
+ }),
7409
+ }
7410
+ : {
7411
+ borderStartStartRadius: dialogBorderRadius,
7412
+ borderEndStartRadius: dialogBorderRadius,
7413
+ ...forcedColorsMediaQuery({
7414
+ borderInlineStart: '2px solid CanvasText',
7415
+ }),
7416
+ }),
7375
7417
  }),
7376
7418
  ...(isFooterFixed && {
7377
7419
  gridTemplateRows: hasHeader ? 'auto 1fr auto' : '1fr',
@@ -8165,7 +8207,7 @@ const getComponentCss$C = (isOpen, background, backdrop, fullscreen, hasDismissB
8165
8207
  },
8166
8208
  scroller: getScrollerJssStyle('fullscreen'),
8167
8209
  modal: {
8168
- ...dialogGridJssStyle(`inset(0 round ${dialogBorderRadius})`),
8210
+ ...dialogGridJssStyle(),
8169
8211
  ...getDialogColorJssStyle(),
8170
8212
  ...getDialogTransitionJssStyle(isOpen, '^'),
8171
8213
  ...buildResponsiveStyles(fullscreen, (fullscreenValue) => fullscreenValue
@@ -8185,6 +8227,7 @@ const getComponentCss$C = (isOpen, background, backdrop, fullscreen, hasDismissB
8185
8227
  placeSelf: 'center',
8186
8228
  margin: `${ref(cssVariableSpacingTop, 'clamp(16px, 10vh, 192px)')} ${gridExtendedOffsetBase} ${ref(cssVariableSpacingBottom, 'clamp(16px, 10vh, 192px)')}`, // horizontal margin is needed to ensure modal is placed on "Porsche Grid" when slotted content is wider than the viewport width
8187
8229
  borderRadius: dialogBorderRadius,
8230
+ clipPath: `inset(0 round ${dialogBorderRadius})`, // non-fullscreen has rounded corners, so clip slotted content to them
8188
8231
  ...forcedColorsMediaQuery({
8189
8232
  outline: '2px solid CanvasText',
8190
8233
  outlineOffset: '-2px',
@@ -9237,7 +9280,7 @@ const getComponentCss$n = (isOpen, background, hasDismissButton) => {
9237
9280
  },
9238
9281
  scroller: getScrollerJssStyle('fullscreen'),
9239
9282
  sheet: {
9240
- ...dialogGridJssStyle(`inset(0 round ${dialogBorderRadius} ${dialogBorderRadius} 0 0)`), // round top corners only
9283
+ ...dialogGridJssStyle(),
9241
9284
  ...getDialogColorJssStyle(),
9242
9285
  ...getDialogTransitionJssStyle(isOpen, '^'),
9243
9286
  width: '100%',
@@ -9245,6 +9288,7 @@ const getComponentCss$n = (isOpen, background, hasDismissButton) => {
9245
9288
  marginBlockStart: ref(spacingFluidLg), // ensures minimal space at the top to visualize paper sheet like border top radius in case sheet becomes scrollable
9246
9289
  borderTopLeftRadius: dialogBorderRadius,
9247
9290
  borderTopRightRadius: dialogBorderRadius,
9291
+ clipPath: `inset(0 round ${dialogBorderRadius} ${dialogBorderRadius} 0 0)`, // round top corners only
9248
9292
  ...forcedColorsMediaQuery({
9249
9293
  borderTop: '2px solid CanvasText',
9250
9294
  }),
@@ -1,3 +1,24 @@
1
+ function getHTMLElement(element, selector) {
2
+ return element?.querySelector(selector);
3
+ }
4
+
5
+ const transformSelectorToDirectChildSelector = (selector) => selector
6
+ .split(',')
7
+ .map((part) => `:scope>${part}`)
8
+ .join();
9
+
10
+ /* eslint-disable prefer-arrow/prefer-arrow-functions */
11
+ function getDirectChildHTMLElement(element, selector) {
12
+ // querySelector(All) doesn't work with :scope pseudo class and comma separator in jsdom, yet
13
+ // https://github.com/jsdom/jsdom/issues/3141
14
+ // therefore we got a workaround so it works nicely when consumed from jsdom-polyfill package
15
+ return (transformSelectorToDirectChildSelector(selector)
16
+ .split(',')
17
+ .map((sel) => getHTMLElement(element, sel))
18
+ .filter((x) => x)[0] || null // comma separated selector might return null, so we have to filter
19
+ );
20
+ }
21
+
1
22
  function _extends() {
2
23
  _extends = Object.assign ? Object.assign.bind() : function (target) {
3
24
  for (var i = 1; i < arguments.length; i++) {
@@ -3189,26 +3210,30 @@ const getTagNameWithoutPrefix = (host) => {
3189
3210
  return (tagNameWithoutPrefix || tagName); // return tagName as fallback for default tags
3190
3211
  };
3191
3212
 
3192
- function getHTMLElement(element, selector) {
3193
- return element?.querySelector(selector);
3194
- }
3195
-
3196
- const transformSelectorToDirectChildSelector = (selector) => selector
3197
- .split(',')
3198
- .map((part) => `:scope>${part}`)
3199
- .join();
3200
-
3201
- /* eslint-disable prefer-arrow/prefer-arrow-functions */
3202
- function getDirectChildHTMLElement(element, selector) {
3203
- // querySelector(All) doesn't work with :scope pseudo class and comma separator in jsdom, yet
3204
- // https://github.com/jsdom/jsdom/issues/3141
3205
- // therefore we got a workaround so it works nicely when consumed from jsdom-polyfill package
3206
- return (transformSelectorToDirectChildSelector(selector)
3207
- .split(',')
3208
- .map((sel) => getHTMLElement(element, sel))
3209
- .filter((x) => x)[0] || null // comma separated selector might return null, so we have to filter
3210
- );
3211
- }
3213
+ const parseCssTimeToMs = (value) => {
3214
+ const trimmed = value.trim();
3215
+ const num = Number.parseFloat(trimmed);
3216
+ if (Number.isNaN(num)) {
3217
+ return 0;
3218
+ }
3219
+ return trimmed.endsWith('ms') ? num : num * 1000; // seconds otherwise
3220
+ };
3221
+ /**
3222
+ * Returns the longest `transition-duration` + `transition-delay` (in ms) across an element's computed transitions.
3223
+ * Useful as a safety-net timeout when a `transitionend` event might not fire (e.g. with reduced motion or a 0 duration).
3224
+ *
3225
+ * @param {HTMLElement} element - The element to read the computed transition values from.
3226
+ * @returns {number} The maximum combined duration and delay in milliseconds.
3227
+ */
3228
+ const getMaxTransitionDurationMs = (element) => {
3229
+ const { transitionDuration, transitionDelay } = getComputedStyle(element);
3230
+ const durations = transitionDuration.split(',');
3231
+ const delays = transitionDelay.split(',');
3232
+ return durations.reduce((max, duration, index) => {
3233
+ const total = parseCssTimeToMs(duration) + parseCssTimeToMs(delays[index] ?? '0s');
3234
+ return total > max ? total : max;
3235
+ }, 0);
3236
+ };
3212
3237
 
3213
3238
  const hasSpecificDirectChildTag = (host, tag) => {
3214
3239
  const el = getDirectChildHTMLElement(host, ':only-child');
@@ -3455,6 +3480,18 @@ hasWindow$1 &&
3455
3480
  }
3456
3481
  });
3457
3482
 
3483
+ const showDialog = (dialog, scrollArea) => {
3484
+ // Must only be called when the dialog isn't already open and after the render cycle has finished (e.g. in
3485
+ // `componentDidRender()`), so visibility states are ready and the dismiss button can be focused correctly.
3486
+ // The "only when not already open" precondition is guaranteed by the caller (`createTopLayerController`'s `requestShow`
3487
+ // guards with `!isShown()`), since `showModal()` throws if the dialog is already open.
3488
+ scrollArea.scrollTo(0, 0); // reset scroll position each time dialog gets opened again
3489
+ dialog.inert = true; // This will prevent the autofocus of focusable elements inside the dialog (e.g. close button) element which is conflicting with our transition
3490
+ dialog.showModal(); // shows modal on `#top-layer`
3491
+ dialog.inert = false; // Re-enable focus on dialog element
3492
+ dialog.focus(); // set focus programmatically to dialog element to prevent transition bug in Safari
3493
+ };
3494
+
3458
3495
  const getCDNBaseURL = () => global.PORSCHE_DESIGN_SYSTEM_CDN_URL + "/porsche-design-system";
3459
3496
 
3460
3497
  const hasDocument = typeof document !== 'undefined';
@@ -3463,7 +3500,7 @@ const hasShowPickerSupport = () => (hasDocument &&
3463
3500
  'showPicker' in HTMLInputElement.prototype &&
3464
3501
  CSS.supports('selector(::-webkit-calendar-picker-indicator)'));
3465
3502
 
3466
- const prefix = `[Porsche Design System v${"4.2.0-rc.4"}]` // this part isn't covered by unit tests
3503
+ const prefix = `[Porsche Design System v${"4.2.0-rc.5"}]` // this part isn't covered by unit tests
3467
3504
  ;
3468
3505
  const consoleError$1 = (...messages) => {
3469
3506
  console.error(prefix, ...messages);
@@ -3484,6 +3521,83 @@ const supportsNativePopover = () => {
3484
3521
  const hasNativePopoverSupport = supportsNativePopover();
3485
3522
  // getter for easy mocking
3486
3523
  const getHasNativePopoverSupport = () => hasNativePopoverSupport;
3524
+
3525
+ // Single source of truth for the two CSS feature queries behind the "keep on #top-layer during fade-out" capability,
3526
+ // shared by the JS detection (`supportsOverlayTransition`) and the CSS `@supports` wrapper
3527
+ // (`overlayTransitionSupportsQuery`) so the two can never drift.
3528
+ const overlayFeature = 'overlay: auto';
3529
+ const allowDiscreteFeature = 'transition-behavior: allow-discrete';
3530
+ /**
3531
+ * Detects whether the browser can keep an element (e.g. a `dialog` or a `popover`) on the `#top-layer` during a
3532
+ * fade-out animation via the `overlay` property combined with `transition-behavior: allow-discrete`.
3533
+ *
3534
+ * BOTH capabilities are required: `transition-behavior: allow-discrete` is now widely supported (e.g. Firefox), but the
3535
+ * `overlay` property itself is Chromium-only. Firefox supports `allow-discrete` yet NOT `overlay`, so it would drop out
3536
+ * of the `#top-layer` immediately when leaving it and fall back to a high `z-index` — which breaks as soon as an
3537
+ * ancestor creates a new stacking context (e.g. `transform`/`isolation`, like a nested `p-modal` within `p-flyout`).
3538
+ *
3539
+ * For browsers lacking the `overlay` transition the element must be kept natively shown during the fade-out and only be
3540
+ * removed from the `#top-layer` once the transition has finished (see `createTopLayerController`).
3541
+ *
3542
+ * @returns {boolean} `true` if both `overlay` and `transition-behavior: allow-discrete` are supported.
3543
+ */
3544
+ const supportsOverlayTransition = () => typeof CSS !== 'undefined' && CSS.supports(overlayFeature) && CSS.supports(allowDiscreteFeature);
3545
+
3546
+ // Extra time added on top of the computed transition duration before hiding. The timer starts in `requestHide()`,
3547
+ // one frame before the transition actually begins, so this buffer biases the hide slightly *after* the visual
3548
+ // transition ends — preventing the element from leaving the #top-layer too early and flickering.
3549
+ const HIDE_BUFFER_MS = 50;
3550
+ /**
3551
+ * Creates a controller that manages an element's presence on the `#top-layer`, keeping it there during its fade-out
3552
+ * animation in browsers that don't support the `overlay` transition (Safari/Firefox). On hide it defers the native
3553
+ * removal (`dialog.close()` / `element.hidePopover()`) until the fade-out has finished, scheduled via a timeout derived
3554
+ * from the element's computed transition duration. In Chromium the native removal happens immediately because the
3555
+ * `overlay` + `allow-discrete` transition keeps the element on the `#top-layer` while it fades out.
3556
+ *
3557
+ * Both `requestShow` and `requestHide` are idempotent (guarded by `isShown`), so they can safely be called on every
3558
+ * render. State is scoped to the controller instance, so no shared registry is required.
3559
+ *
3560
+ * @param {TopLayerOptions} options - Element-specific hooks (show state, show/hide actions, element getter).
3561
+ * @returns {TopLayerController} The controller used to request show, request hide, and cancel.
3562
+ */
3563
+ const createTopLayerController = (options) => {
3564
+ const { getElement, isShown, show, hide } = options;
3565
+ let hideTimer;
3566
+ const cancel = () => {
3567
+ if (hideTimer) {
3568
+ clearTimeout(hideTimer);
3569
+ hideTimer = undefined;
3570
+ }
3571
+ };
3572
+ const requestShow = () => {
3573
+ cancel(); // cancel any pending deferred hide first, so re-opening during fade-out wins (element may still be shown)
3574
+ if (isShown()) {
3575
+ return; // `showModal()` / `showPopover()` throw if the element is already shown
3576
+ }
3577
+ show();
3578
+ };
3579
+ const requestHide = () => {
3580
+ if (!isShown()) {
3581
+ return; // already hidden (or a deferred hide already completed)
3582
+ }
3583
+ if (supportsOverlayTransition()) {
3584
+ hide(); // Chromium: `overlay` + `allow-discrete` keeps it on the #top-layer during the fade-out
3585
+ }
3586
+ else {
3587
+ // Safari/Firefox: keep it on the real #top-layer during the fade-out and hide once the transition has finished.
3588
+ cancel(); // drop any in-flight deferred hide before re-scheduling
3589
+ const element = getElement();
3590
+ const timeoutMs = (element ? getMaxTransitionDurationMs(element) : 0) + HIDE_BUFFER_MS;
3591
+ hideTimer = setTimeout(() => {
3592
+ cancel();
3593
+ if (isShown()) {
3594
+ hide();
3595
+ }
3596
+ }, timeoutMs);
3597
+ }
3598
+ };
3599
+ return { requestShow, requestHide, cancel };
3600
+ };
3487
3601
  const headerSlot = 'header';
3488
3602
  const anchorSlot = 'anchor';
3489
3603
 
@@ -4106,4 +4220,4 @@ const getTextTagType = (host, tag) => {
4106
4220
  return tag;
4107
4221
  };
4108
4222
 
4109
- export { AI_TAG_TRANSLATIONS, DISPLAY_TAGS, HEADING_TAGS, ItemType, TEXT_TAGS, anchorSlot, attributeMutationMap, buildCrestImgSrc, buildCrestSrcSet, buildFlagUrl, buildIconUrl, consoleError$1 as consoleError, createPaginationItems, createRange, crestSize, descriptionId, displaySizeToTagMap, getAiTagTranslation, getBannerAriaAttributes, getButtonAriaAttributes, getButtonBaseAriaAttributes, getButtonPureAriaAttributes, getCDNBaseURL, getComboboxAriaAttributes, getCurrentActivePage, getDirectChildHTMLElement, getDisplayTagType, getFieldsetAriaAttributes, getHTMLElement, getHasNativePopoverSupport, getHeadingTagType, getInlineNotificationAriaAttributes, getListboxAriaAttributes, getSanitizedActiveTabIndex, getSegmentedControlItemAriaAttributes, getStepperHorizontalIconName, getSvgUrl, getSwitchButtonAriaAttributes, getTagName, getTagNameWithoutPrefix, getTextTagType, getTotalPages, hasDocument, hasShowPickerSupport, hasSpecificDirectChildTag, hasVisibleIcon, hasWindow$1 as hasWindow, headerSlot, internalDrilldown, isCurrentInput, isDisabledOrLoading, isElementOfKind, isInfinitePagination, isListTypeOrdered, isSortable, isStateCompleteOrWarning, isUrl, labelId, mergeInputNativeAria, observedNodesMap, parseAndGetAriaAttributes, parseJSONAttribute, setAriaIDREF, supportsConstructableStylesheets, supportsNativePopover, tempDiv, tempIcon, tempLabel, traverseTreeAndUpdateState, updateDrilldownItemState };
4223
+ export { AI_TAG_TRANSLATIONS, DISPLAY_TAGS, HEADING_TAGS, ItemType, TEXT_TAGS, anchorSlot, attributeMutationMap, buildCrestImgSrc, buildCrestSrcSet, buildFlagUrl, buildIconUrl, consoleError$1 as consoleError, createPaginationItems, createRange, createTopLayerController, crestSize, descriptionId, displaySizeToTagMap, getAiTagTranslation, getBannerAriaAttributes, getButtonAriaAttributes, getButtonBaseAriaAttributes, getButtonPureAriaAttributes, getCDNBaseURL, getComboboxAriaAttributes, getCurrentActivePage, getDirectChildHTMLElement, getDisplayTagType, getFieldsetAriaAttributes, getHTMLElement, getHasNativePopoverSupport, getHeadingTagType, getInlineNotificationAriaAttributes, getListboxAriaAttributes, getMaxTransitionDurationMs, getSanitizedActiveTabIndex, getSegmentedControlItemAriaAttributes, getStepperHorizontalIconName, getSvgUrl, getSwitchButtonAriaAttributes, getTagName, getTagNameWithoutPrefix, getTextTagType, getTotalPages, hasDocument, hasShowPickerSupport, hasSpecificDirectChildTag, hasVisibleIcon, hasWindow$1 as hasWindow, headerSlot, internalDrilldown, isCurrentInput, isDisabledOrLoading, isElementOfKind, isInfinitePagination, isListTypeOrdered, isSortable, isStateCompleteOrWarning, isUrl, labelId, mergeInputNativeAria, observedNodesMap, parseAndGetAriaAttributes, parseJSONAttribute, setAriaIDREF, showDialog, supportsConstructableStylesheets, supportsNativePopover, supportsOverlayTransition, tempDiv, tempIcon, tempLabel, traverseTreeAndUpdateState, updateDrilldownItemState };
@@ -5,23 +5,23 @@ import { useEventCallback, usePrefix, useBrowserLayoutEffect, useMergedClass } f
5
5
  import { syncRef } from '../../utils.mjs';
6
6
  import { DSRFlyout } from '../dsr-components/flyout.mjs';
7
7
 
8
- const PFlyout = /*#__PURE__*/ forwardRef(({ aria, backdrop = 'blur', background = 'canvas', disableBackdropClick = false, footerBehavior = 'sticky', onDismiss, onMotionHiddenEnd, onMotionVisibleEnd, open = false, position = 'end', className, children, ...rest }, ref) => {
8
+ const PFlyout = /*#__PURE__*/ forwardRef(({ aria, backdrop = 'blur', background = 'canvas', disableBackdropClick = false, footerBehavior = 'sticky', fullscreen = false, onDismiss, onMotionHiddenEnd, onMotionVisibleEnd, open = false, position = 'end', className, children, ...rest }, ref) => {
9
9
  const elementRef = useRef(undefined);
10
10
  useEventCallback(elementRef, 'dismiss', onDismiss);
11
11
  useEventCallback(elementRef, 'motionHiddenEnd', onMotionHiddenEnd);
12
12
  useEventCallback(elementRef, 'motionVisibleEnd', onMotionVisibleEnd);
13
13
  const WebComponentTag = usePrefix('p-flyout');
14
- const propsToSync = [aria, backdrop, background, disableBackdropClick, footerBehavior, open, position];
14
+ const propsToSync = [aria, backdrop, background, disableBackdropClick, footerBehavior, fullscreen, open, position];
15
15
  useBrowserLayoutEffect(() => {
16
16
  const { current } = elementRef;
17
- ['aria', 'backdrop', 'background', 'disableBackdropClick', 'footerBehavior', 'open', 'position'].forEach((propName, i) => (current[propName] = propsToSync[i]));
17
+ ['aria', 'backdrop', 'background', 'disableBackdropClick', 'footerBehavior', 'fullscreen', 'open', 'position'].forEach((propName, i) => (current[propName] = propsToSync[i]));
18
18
  }, propsToSync);
19
19
  const props = {
20
20
  ...rest,
21
21
  // @ts-ignore
22
22
  ...(!process.browser
23
23
  ? {
24
- children: (jsx(DSRFlyout, { aria, backdrop, background, disableBackdropClick, footerBehavior, open, position, children })),
24
+ children: (jsx(DSRFlyout, { aria, backdrop, background, disableBackdropClick, footerBehavior, fullscreen, open, position, children })),
25
25
  }
26
26
  : {
27
27
  children,
@@ -4,7 +4,7 @@ import '../../provider.mjs';
4
4
  import { splitChildren } from '../../splitChildren.mjs';
5
5
  import { minifyCss } from '../../minifyCss.mjs';
6
6
  import { getBannerCss as getComponentCss$18 } from '../../../../../../components/dist/styles/esm/styles-entry.mjs';
7
- import { getBannerAriaAttributes } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
7
+ import { createTopLayerController, getBannerAriaAttributes } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
8
8
  import { NotificationBase } from './notification-base.mjs';
9
9
  import { PButton } from '../components/button.wrapper.mjs';
10
10
 
@@ -21,6 +21,12 @@ class DSRBanner extends Component {
21
21
  refDismiss;
22
22
  hasHeadingSlot;
23
23
  hasDescriptionSlot;
24
+ topLayer = createTopLayerController({
25
+ getElement: () => this.props.refPopover,
26
+ isShown: () => !!this.props.refPopover?.matches(':popover-open'),
27
+ show: () => this.props.refPopover?.showPopover(),
28
+ hide: () => this.props.refPopover?.hidePopover(),
29
+ });
24
30
  render() {
25
31
  const { children, namedSlotChildren, otherChildren } = splitChildren(this.props.children);
26
32
  const hasHeadingSlot = namedSlotChildren.filter(({ props: { slot } }) => slot === 'heading').length > 0;
@@ -3,7 +3,7 @@ import { splitChildren } from '../../splitChildren.mjs';
3
3
  import { Component } from 'react';
4
4
  import { minifyCss } from '../../minifyCss.mjs';
5
5
  import { getFlyoutCss as getComponentCss$V } from '../../../../../../components/dist/styles/esm/styles-entry.mjs';
6
- import { parseAndGetAriaAttributes } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
6
+ import { createTopLayerController, parseAndGetAriaAttributes, showDialog } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
7
7
  import { DialogBase } from './dialog-base.mjs';
8
8
 
9
9
  /**
@@ -23,12 +23,18 @@ class DSRFlyout extends Component {
23
23
  hasHeader;
24
24
  hasFooter;
25
25
  hasSubFooter;
26
+ topLayer = createTopLayerController({
27
+ getElement: () => this.props.dialog,
28
+ isShown: () => !!this.props.dialog?.open,
29
+ show: () => showDialog(this.props.dialog, this.props.scroller),
30
+ hide: () => this.props.dialog?.close(),
31
+ });
26
32
  render() {
27
33
  const { children, namedSlotChildren, otherChildren } = splitChildren(this.props.children);
28
34
  const hasHeader = namedSlotChildren.filter(({ props: { slot } }) => slot === 'header').length > 0;
29
35
  const hasFooter = namedSlotChildren.filter(({ props: { slot } }) => slot === 'footer').length > 0;
30
36
  const hasSubFooter = namedSlotChildren.filter(({ props: { slot } }) => slot === 'sub-footer').length > 0;
31
- const style = minifyCss(getComponentCss$V(this.props.open, this.props.background, this.props.backdrop, this.props.position, hasHeader, hasFooter, hasSubFooter, this.props.footerBehavior));
37
+ const style = minifyCss(getComponentCss$V(this.props.open, this.props.background, this.props.backdrop, this.props.position, hasHeader, hasFooter, hasSubFooter, this.props.footerBehavior, this.props.fullscreen));
32
38
  return (jsxs(Fragment, { children: [jsxs("template", { shadowroot: "open", shadowrootmode: "open", children: [jsx("style", { dangerouslySetInnerHTML: { __html: style } }), jsx(DialogBase, { host: null, dismissable: true, containerClass: "flyout", header: hasHeader ? jsx("slot", { name: "header" }) : undefined, footer: hasFooter ? jsx("slot", { name: "footer" }) : undefined, subFooter: hasSubFooter ? jsx("slot", { name: "sub-footer" }) : undefined, ariaAttributes: parseAndGetAriaAttributes({
33
39
  'aria-modal': true,
34
40
  ...{ 'aria-label': hasHeader ? namedSlotChildren.find(({ props: { slot } }) => slot === 'header')?.props.children : 'Flyout' },
@@ -3,7 +3,7 @@ import { splitChildren } from '../../splitChildren.mjs';
3
3
  import { Component } from 'react';
4
4
  import { minifyCss } from '../../minifyCss.mjs';
5
5
  import { getModalCss as getComponentCss$C } from '../../../../../../components/dist/styles/esm/styles-entry.mjs';
6
- import { parseAndGetAriaAttributes } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
6
+ import { createTopLayerController, parseAndGetAriaAttributes, showDialog } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
7
7
  import { DialogBase } from './dialog-base.mjs';
8
8
 
9
9
  /**
@@ -20,6 +20,12 @@ class DSRModal extends Component {
20
20
  footer;
21
21
  hasHeader;
22
22
  hasFooter;
23
+ topLayer = createTopLayerController({
24
+ getElement: () => this.props.dialog,
25
+ isShown: () => !!this.props.dialog?.open,
26
+ show: () => showDialog(this.props.dialog, this.props.scroller),
27
+ hide: () => this.props.dialog?.close(),
28
+ });
23
29
  render() {
24
30
  const { children, namedSlotChildren, otherChildren } = splitChildren(this.props.children);
25
31
  const hasHeader = namedSlotChildren.filter(({ props: { slot } }) => slot === 'header').length > 0;
@@ -3,7 +3,7 @@ import { splitChildren } from '../../splitChildren.mjs';
3
3
  import { Component } from 'react';
4
4
  import { minifyCss } from '../../minifyCss.mjs';
5
5
  import { getSheetCss as getComponentCss$n } from '../../../../../../components/dist/styles/esm/styles-entry.mjs';
6
- import { parseAndGetAriaAttributes } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
6
+ import { createTopLayerController, parseAndGetAriaAttributes, showDialog } from '../../../../../../components/dist/utils/esm/utils-entry.mjs';
7
7
  import { DialogBase } from './dialog-base.mjs';
8
8
 
9
9
  /**
@@ -17,6 +17,12 @@ class DSRSheet extends Component {
17
17
  dialog;
18
18
  scroller;
19
19
  hasHeader;
20
+ topLayer = createTopLayerController({
21
+ getElement: () => this.props.dialog,
22
+ isShown: () => !!this.props.dialog?.open,
23
+ show: () => showDialog(this.props.dialog, this.props.scroller),
24
+ hide: () => this.props.dialog?.close(),
25
+ });
20
26
  render() {
21
27
  const { children, namedSlotChildren, otherChildren } = splitChildren(this.props.children);
22
28
  const hasHeader = namedSlotChildren.filter(({ props: { slot } }) => slot === 'header').length > 0;