From 31f99648884a294ba808e1ae9b0da6c2dcaed505 Mon Sep 17 00:00:00 2001
From: Adam Kudrna
Date: Tue, 30 Jan 2024 18:52:30 +0100
Subject: [PATCH] Feat(web): Introduce option to disable scrolling inside
`Modal` #DS-732
Scrolling inside `ModalDialog` can now be turned off by adding the
`ModalDialog--nonScrolling` modifier class.
---
.../web/src/scss/components/Modal/README.md | 28 ++-
.../web/src/scss/components/Modal/_Modal.scss | 43 ++--
.../scss/components/Modal/_ModalDialog.scss | 53 ++++-
.../web/src/scss/components/Modal/_theme.scss | 15 +-
.../web/src/scss/components/Modal/index.html | 214 +++++++++++-------
5 files changed, 226 insertions(+), 127 deletions(-)
diff --git a/packages/web/src/scss/components/Modal/README.md b/packages/web/src/scss/components/Modal/README.md
index 8c81822afb..60a1b960a5 100644
--- a/packages/web/src/scss/components/Modal/README.md
+++ b/packages/web/src/scss/components/Modal/README.md
@@ -60,6 +60,9 @@ This is useful for Modals with dynamic content, e.g. a list of items that can be
```
+👉 Please note the preferred height options are ignored when scrolling inside ModalDialog is
+[turned off](#disable-scrolling-inside-modaldialog).
+
👉 Please note the custom height values are considered **preferred:** Modal will not expand beyond the viewport height.
### Custom Max Height
@@ -82,6 +85,8 @@ You can use the custom property `--modal-max-height-tablet` to override the max
```
+👉 Please note the max height is ignored when scrolling inside ModalDialog is [turned off](#disable-scrolling-inside-modaldialog).
+
👉 Please note the max height on mobile screens is currently not customizable. Let us know if you need this feature! 🙏
## ModalDialog
@@ -246,7 +251,7 @@ Use our JavaScript plugin to open your Modal, e.g.:
```
-## Disable Modal closing on backdrop click
+## Disable Modal Closing on Backdrop Click
Disable modal close when clicking on the backdrop.
You can still close modal with close buttons or ESC key.
@@ -280,6 +285,27 @@ scrolling, e.g.:
```
+### Disable Scrolling Inside ModalDialog
+
+Scrolling inside ModalDialog can be turned off by adding the `ModalDialog--nonScrollable` modifier class:
+
+```html
+
+
+
+```
+
+This way, the ModalBody will expand to fit the height of its content and the whole ModalDialog will scroll in case the
+content is longer than user's viewport.
+
+👉 Please note that this modifier class can produce unexpected results when used in combination with ScrollView.
+
+#### ⚠️ DEPRECATION NOTICE
+
+The `.ModalDialog--nonScrollable` modifier will be removed in the next major release and the ModalDialog will be made
+non-scrollable by default. It will be possible to re-enable the inside scrolling by adding the
+`.ModalDialog--scrollable` modifier class (which will remain the recommended default usage).
+
## Stacking Modals
Multiple Modals can be open at the same time. That means, you can open a Modal from another Modal, and they will display
diff --git a/packages/web/src/scss/components/Modal/_Modal.scss b/packages/web/src/scss/components/Modal/_Modal.scss
index 4e0c3d2d48..a2af9d2200 100644
--- a/packages/web/src/scss/components/Modal/_Modal.scss
+++ b/packages/web/src/scss/components/Modal/_Modal.scss
@@ -1,10 +1,11 @@
// 1. In order to be transitioned, the visibility of the dialog is controlled through `opacity` and `visibility`
// properties instead of the default `display`.
-// 2. Use a `::before` pseudo-element instead of `::backdrop` to make fading possible.
-// 3. Allow scrolling through the backdrop.
-// 4. Restore text selection after `all: unset`.
-// 5. For alignments other than `top`, prefer the `bottom` CSS property to enable smooth transitions between the uniform
-// and docked variants (see `_ModalDialog.scss`).
+// 2. Simply use Modal background color instead of `::backdrop` to make fading possible. (A pseudo-element cannot be
+// used because it covers the scrollbar of the ModalDialog with the inside scrolling turned off.)
+// 3. Use background gradient to prevent background from flickering in Safari.
+// 4. Clip overflow during transition of the docked variant on mobile screens.
+// 5. Allow scrolling through the modal. Has no effect unless the modal is taller than the viewport.
+// 6. Restore text selection after `all: unset`.
@use 'sass:map';
@use '../../tools/breakpoint';
@@ -19,6 +20,8 @@
z-index: 1;
display: flex; // 1.
padding-block: theme.$padding-y;
+ overflow: hidden; // 4.
+ background: linear-gradient(#{theme.$backdrop-background-color}, #{theme.$backdrop-background-color}); // 8.
visibility: hidden; // 1.
opacity: 0; // 1.
pointer-events: none; // 1.
@@ -29,29 +32,16 @@
background-color: transparent;
}
- // 2.
- &::before {
- content: '';
- position: fixed;
- inset: 0;
- z-index: -2;
- background-color: theme.$backdrop-background-color;
- visibility: hidden;
- opacity: 0;
- }
-
@media (prefers-reduced-motion: no-preference) {
transition-property: visibility, opacity;
transition-duration: theme.$transition-duration;
-
- // 2.
- &::before {
- transition: inherit;
- }
}
}
-// 5.
+.Modal:has(.ModalDialog--nonScrollable) {
+ overflow-y: auto; // 5.
+}
+
.Modal--center,
.Modal:not(.Modal--top, .Modal--bottom) {
--modal-top: auto;
@@ -80,13 +70,6 @@
visibility: visible;
opacity: 1;
- user-select: text; // 4.
+ user-select: text; // 6.
pointer-events: auto;
-
- // 2.
- &::before {
- visibility: visible;
- opacity: 1;
- pointer-events: none; // 3.
- }
}
diff --git a/packages/web/src/scss/components/Modal/_ModalDialog.scss b/packages/web/src/scss/components/Modal/_ModalDialog.scss
index ae011ef526..cbbf30c771 100644
--- a/packages/web/src/scss/components/Modal/_ModalDialog.scss
+++ b/packages/web/src/scss/components/Modal/_ModalDialog.scss
@@ -6,6 +6,7 @@
// 5. Unfortunately, the open state cannot be part of the parent `-docked-modal-dialog()` mixin because it's not
// possible to generate selector for the feature class scenario.
// 6. Override bottom padding of parent `.Modal` in the docked variant.
+// 7. Override the min-height of the expanded docked variant.
@use 'sass:map';
@use '../../settings/feature-flags';
@@ -53,13 +54,12 @@
width: theme.$dialog-uniform-width;
max-width: calc(100% - #{theme.$padding-x});
height: theme.$dialog-uniform-height;
- max-height: theme.$dialog-uniform-max-height;
+ min-height: unset; // 7. @deprecated Can be removed once the uniform modal dialog has been made default.
border-radius: theme.$dialog-border-radius;
}
@include breakpoint.up(map.get(theme.$breakpoints, tablet)) {
- height: theme.$dialog-height-tablet;
- max-height: theme.$dialog-uniform-max-height-tablet;
+ height: theme.$dialog-uniform-height-tablet;
}
@include breakpoint.up(map.get(theme.$breakpoints, desktop)) {
@@ -67,6 +67,16 @@
}
}
+@mixin -scrollable-uniform-modal-dialog($from-breakpoint) {
+ @if $from-breakpoint == 0 {
+ max-height: theme.$dialog-uniform-max-height;
+ }
+
+ @include breakpoint.up(map.get(theme.$breakpoints, tablet)) {
+ max-height: theme.$dialog-uniform-max-height-tablet;
+ }
+}
+
@mixin -docked-modal-dialog() {
@include breakpoint.down(map.get(theme.$breakpoints, tablet)) {
--modal-top: auto;
@@ -76,8 +86,7 @@
width: theme.$dialog-width;
max-width: none;
- height: theme.$dialog-height;
- max-height: theme.$dialog-max-height;
+ height: theme.$dialog-docked-height;
border-radius: theme.$dialog-border-radius theme.$dialog-border-radius 0 0;
}
}
@@ -91,10 +100,23 @@
@mixin -expand-docked-modal-dialog() {
@include breakpoint.down(map.get(theme.$breakpoints, tablet)) {
- height: theme.$dialog-max-height;
+ min-height: theme.$dialog-docked-expanded-height;
}
}
+@mixin -scrollable-docked-modal-dialog() {
+ @include breakpoint.down(map.get(theme.$breakpoints, tablet)) {
+ max-height: theme.$dialog-docked-expanded-height;
+ }
+}
+
+// @deprecated The non-scrollable modal dialog is deprecated and will be removed in the next major release.
+// Migration: In CSS, make the modal dialog non-scrollable by default so scrolling inside is turned on by a modifier class.
+@mixin -non-scrollable-modal-dialog() {
+ height: auto;
+ max-height: none;
+}
+
.ModalDialog {
@include -modal-dialog();
@@ -102,9 +124,12 @@
// Migration: Remove the feature flag and make the uniform dialog the default.
@if feature-flags.$modal-enable-uniform-dialog {
@include -uniform-modal-dialog($from-breakpoint: map.get(theme.$breakpoints, mobile));
+ @include -scrollable-uniform-modal-dialog($from-breakpoint: map.get(theme.$breakpoints, mobile));
} @else {
@include -docked-modal-dialog();
+ @include -scrollable-docked-modal-dialog();
@include -uniform-modal-dialog($from-breakpoint: map.get(theme.$breakpoints, tablet));
+ @include -scrollable-uniform-modal-dialog($from-breakpoint: map.get(theme.$breakpoints, tablet));
[open] > & {
@include -open-docked-modal-dialog();
@@ -126,22 +151,36 @@
.ModalDialog--dockOnMobile.ModalDialog--expandOnMobile {
@include -expand-docked-modal-dialog();
}
+
+ .ModalDialog--nonScrollable {
+ @include -non-scrollable-modal-dialog();
+ }
} @else {
.spirit-feature-modal-enable-uniform-dialog .ModalDialog {
@include -uniform-modal-dialog($from-breakpoint: map.get(theme.$breakpoints, mobile));
+ @include -scrollable-uniform-modal-dialog($from-breakpoint: map.get(theme.$breakpoints, mobile));
}
.spirit-feature-modal-enable-uniform-dialog .ModalDialog--dockOnMobile {
@include -docked-modal-dialog();
+ @include -scrollable-docked-modal-dialog();
}
.spirit-feature-modal-enable-uniform-dialog [open] > .ModalDialog--dockOnMobile {
@include -open-docked-modal-dialog();
}
- // stylelint-disable-next-line selector-max-class -- Only target the docked variant.
+ // stylelint-disable selector-max-class, no-descending-specificity -- Only target the docked variant.
.ModalDialog--expandOnMobile,
.spirit-feature-modal-enable-uniform-dialog .ModalDialog--dockOnMobile.ModalDialog--expandOnMobile {
@include -expand-docked-modal-dialog();
}
+ // stylelint-enable selector-max-class
+
+ // @deprecated The non-scrollable modal dialog is deprecated and will be removed in the next major release.
+ // Migration: In CSS, make the modal dialog non-scrollable by default so scrolling inside is turned on by a modifier class.
+ .ModalDialog--nonScrollable,
+ .spirit-feature-modal-enable-uniform-dialog .ModalDialog--nonScrollable {
+ @include -non-scrollable-modal-dialog();
+ }
}
diff --git a/packages/web/src/scss/components/Modal/_theme.scss b/packages/web/src/scss/components/Modal/_theme.scss
index 3f24094c78..929519ff58 100644
--- a/packages/web/src/scss/components/Modal/_theme.scss
+++ b/packages/web/src/scss/components/Modal/_theme.scss
@@ -19,21 +19,22 @@ $common-padding-x-tablet: tokens.$space-800;
$dialog-width: 100%;
$dialog-width-tablet: 640px;
$dialog-width-desktop: 680px;
-$dialog-height: var(--modal-preferred-height-mobile, min-content);
-$dialog-height-tablet: var(--modal-preferred-height-tablet, min-content);
-$dialog-max-height: calc(100dvh - #{tokens.$space-1100});
-$dialog-max-height-tablet: min(var(--modal-max-height-tablet, 600px), calc(100dvh - #{2 * tokens.$space-600}));
$dialog-text-color: tokens.$text-primary-default;
$dialog-border-radius: tokens.$radius-200;
$dialog-background-color: tokens.$background-basic;
$dialog-shadow: tokens.$shadow-300;
-// @deprecated The "uniform" Header padding is deprecated and will be removed in the next major release.
+// @deprecated The "uniform" dialog dimensions are deprecated and will be removed in the next major release.
// Migration: Rename the variables containing the `uniform` keyword to remove it.
$dialog-uniform-width: 640px;
$dialog-uniform-height: var(--modal-preferred-height-mobile, min-content);
-$dialog-uniform-max-height: min(var(--modal-max-height-mobile, 600px), calc(100dvh - #{2 * tokens.$space-600}));
-$dialog-uniform-max-height-tablet: min(var(--modal-max-height-tablet, 600px), calc(100dvh - #{2 * tokens.$space-600}));
+$dialog-uniform-height-tablet: var(--modal-preferred-height-tablet, min-content);
+$dialog-uniform-max-height: min(var(--modal-max-height-mobile, 600px), calc(100dvh - #{2 * $padding-y}));
+$dialog-uniform-max-height-tablet: min(var(--modal-max-height-tablet, 600px), calc(100dvh - #{2 * $padding-y}));
+
+$dialog-docked-height: var(--modal-preferred-height-mobile, min-content);
+$dialog-docked-margin-top: tokens.$space-1100;
+$dialog-docked-expanded-height: calc(100dvh - #{$dialog-docked-margin-top});
// ModalHeader
$header-gap: tokens.$space-400;
diff --git a/packages/web/src/scss/components/Modal/index.html b/packages/web/src/scss/components/Modal/index.html
index 74a62e3f03..54617ff63e 100644
--- a/packages/web/src/scss/components/Modal/index.html
+++ b/packages/web/src/scss/components/Modal/index.html
@@ -15,9 +15,22 @@
footerElement.classList.add(`ModalFooter--${event.target.value}`);
};
+ const toggleDockOnMobile = (selector, dependingInputSelector) => {
+ const dependingInputElement = document.querySelector(dependingInputSelector);
+ const dependingLabelElement = dependingInputElement.closest('label');
+
+ document.querySelector(selector).classList.toggle('ModalDialog--dockOnMobile');
+ dependingInputElement.disabled = !dependingInputElement.disabled;
+ dependingLabelElement.classList.toggle('Checkbox--disabled');
+ }
+
const toggleExpandOnMobile = (selector) => {
document.querySelector(selector).classList.toggle('ModalDialog--expandOnMobile');
}
+
+ const toggleScrolling = (selector) => {
+ document.querySelector(selector).classList.toggle('ModalDialog--nonScrollable');
+ }
@@ -559,6 +572,100 @@