@justeattakeaway/pie-modal 0.0.0-snapshot-release-20231123112917
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.
- package/README.md +121 -0
- package/declaration.d.ts +9 -0
- package/package.json +50 -0
- package/src/defs.ts +150 -0
- package/src/index.ts +481 -0
- package/src/modal.scss +321 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img align="center" src="../../../readme_image.png" height="200" alt="">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/@justeattakeaway/pie-modal">
|
|
7
|
+
<img alt="GitHub Workflow Status" src="https://img.shields.io/npm/v/@justeattakeaway/pie-modal.svg">
|
|
8
|
+
</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
# Table of Contents
|
|
12
|
+
|
|
13
|
+
1. [Introduction](#pie-modal)
|
|
14
|
+
2. [Installation](#installation)
|
|
15
|
+
3. [Importing the component](#importing-the-component)
|
|
16
|
+
4. [Peer Dependencies](#peer-dependencies)
|
|
17
|
+
5. [Props](#props)
|
|
18
|
+
6. [Legacy browser support](#legacy-browser-support)
|
|
19
|
+
7. [Contributing](#contributing)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## pie-modal
|
|
23
|
+
|
|
24
|
+
`pie-modal` is a Web Component built using the Lit library. It offers a simple and accessible modal component for web applications, which uses the native HTML `dialog` element under the hood.
|
|
25
|
+
|
|
26
|
+
This component can be easily integrated into various frontend frameworks and customized through a set of properties.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
To install `pie-modal` in your application, run the following on your command line:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# npm
|
|
35
|
+
$ npm i @justeattakeaway/pie-modal
|
|
36
|
+
|
|
37
|
+
# yarn
|
|
38
|
+
$ yarn add @justeattakeaway/pie-modal
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For full information on using PIE components as part of an application, check out the [Getting Started Guide](https://github.com/justeattakeaway/pie/wiki/Getting-started-with-PIE-Web-Components).
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Importing the component
|
|
45
|
+
|
|
46
|
+
#### JavaScript
|
|
47
|
+
```js
|
|
48
|
+
// Default – for Native JS Applications, Vue, Angular, Svelte, etc.
|
|
49
|
+
import { PieModal } from '@justeattakeaway/pie-modal';
|
|
50
|
+
|
|
51
|
+
// If you don't need to reference the imported object, you can simply
|
|
52
|
+
// import the module which registers the component as a custom element.
|
|
53
|
+
import '@justeattakeaway/pie-modal';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### React
|
|
57
|
+
```js
|
|
58
|
+
// React
|
|
59
|
+
// For React, you will need to import our React-specific component build
|
|
60
|
+
// which wraps the web component using @lit/react
|
|
61
|
+
import { PieModal } from '@justeattakeaway/pie-modal/dist/react';
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> [!NOTE]
|
|
65
|
+
> When using the React version of the component, please make sure to also
|
|
66
|
+
> include React as a [peer dependency](#peer-dependencies) in your project.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## Peer Dependencies
|
|
70
|
+
|
|
71
|
+
> [!IMPORTANT]
|
|
72
|
+
> When using `pie-modal`, you will also need to include a couple of dependencies to ensure the component renders as expected. See [the PIE Wiki](https://github.com/justeattakeaway/pie/wiki/Getting-started-with-PIE-Web-Components#expected-dependencies) for more information and how to include these in your application.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Props
|
|
76
|
+
|
|
77
|
+
| Property | Type | Default | Description |
|
|
78
|
+
|-------------------------------|-----------|----------------|-------------|
|
|
79
|
+
| aria | `Object` | `undefined` | An object representing the aria labels for the `close` & `back` buttons within the modal as well as the `isLoading` state labels (`aria-label` & `aria-busy`). |
|
|
80
|
+
| hasBackButton | `Boolean` | `false` | If true, the modal includes a back button which closes the modal when clicked. |
|
|
81
|
+
| hasStackedActions | `Boolean` | `false` | If true, the action buttons will be stacked (full width) at narrow breakpoints. |
|
|
82
|
+
| heading | `String` | n/a (Required) | The heading text of the modal. |
|
|
83
|
+
| headingLevel | `String` | `h2` | The HTML tag to use for the modal's heading (can be `h1`-`h6`). |
|
|
84
|
+
| isDismissible | `Boolean` | `false` | If true, the modal includes a close button and can be dismissed by clicking on the backdrop or pressing the `Esc` key. |
|
|
85
|
+
| isFooterPinned | `Boolean` | `true` | When false, the modal footer will scroll with the content inside the modal body. |
|
|
86
|
+
| isFullWidthBelowMid | `Boolean` | `false` | If true and the page is narrower than the mid breakpoint, a **medium-sized** modal will take up the full width of the screen. |
|
|
87
|
+
| isLoading | `Boolean` | `false` | When true, displays a loading spinner in the modal |
|
|
88
|
+
| isOpen | `Boolean` | `false` | Controls if the modal element is open or closed |
|
|
89
|
+
| leadingAction | `Object` | `undefined` | An object representing the leading action of the modal. It has the following properties:<br>- text (required): The text to display on the leading action button.<br>- variant: The variant of the leading action button. Please see [pie-button](https://github.com/justeattakeaway/pie/blob/main/packages/components/pie-button/README.md#props) for options. Defaults to primary if not provided.<br>- ariaLabel: The ARIA label for the leading action button. Example: `leadingAction = { text: "Example Text", variant: "secondary", ariaLabel: "Example Aria Label" }` |
|
|
90
|
+
| position | `String` | `center` | The position of the modal; this controls where it will appear on the page. Can be `top` or `center`. |
|
|
91
|
+
| returnFocusAfterCloseSelector | `String` | `undefined` | If provided, focus will be sent to the first element that matches this selector when the modal is closed. If not provided, the `dialog` element will return focus to the element that opened the modal. |
|
|
92
|
+
| size | `String` | `medium` | Determines the maximum width of the modal. Large modals will expand to fill the entire page width at narrow viewports. Can be `small`, `medium` or `large`. |
|
|
93
|
+
| supportingAction | `Object` | `undefined` | An object representing the supporting action of the modal. It has the following properties:<br>- text (required): The text to display on the supporting action button.<br>- variant: The variant of the supporting action button. Please see [pie-button](https://github.com/justeattakeaway/pie/blob/main/packages/components/pie-button/README.md#props) for options. Defaults to ghost if not provided.<br>- ariaLabel: The ARIA label for the supporting action button. Example: `supportingAction = { text: "Example Text", variant: "ghost", ariaLabel: "Example Aria Label" }` |
|
|
94
|
+
|
|
95
|
+
In your markup or JSX, you can then use these to set the properties for the `pie-modal` component:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<!-- Native HTML -->
|
|
99
|
+
<pie-modal heading='My Awesome Heading' headingLevel='h3'>Click me!</pie-modal>
|
|
100
|
+
|
|
101
|
+
<!-- JSX -->
|
|
102
|
+
<PieModal heading='My Awesome Heading' headingLevel='h3'>Click me!</PieModal>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Legacy browser support
|
|
106
|
+
|
|
107
|
+
`pie-modal` uses the Dialog element which might not be supported by legacy browsers.
|
|
108
|
+
|
|
109
|
+
To support them, `pie-modal` uses the [dialog-polyfill](https://github.com/GoogleChrome/dialog-polyfill) package. It works automatically and doesn't need any setup.
|
|
110
|
+
|
|
111
|
+
The polyfill comes with a few limitations, as noted on its [documentation page](https://github.com/GoogleChrome/dialog-polyfill#limitations):
|
|
112
|
+
- Dialogs should not be contained by parents that create a stacking context
|
|
113
|
+
- The browser's chrome may not always be accessible via the tab key
|
|
114
|
+
- Changes to the CSS top/bottom values while open aren't retained
|
|
115
|
+
|
|
116
|
+
For more details, check the package documentation mentioned above.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
## Contributing
|
|
120
|
+
|
|
121
|
+
Check out our [contributing guide](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide) for more information on [local development](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#local-development) and how to run specific [component tests](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#testing).
|
package/declaration.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@justeattakeaway/pie-modal",
|
|
3
|
+
"version": "0.0.0-snapshot-release-20231123112917",
|
|
4
|
+
"description": "PIE design system modal built using web components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"dist",
|
|
12
|
+
"**/*.d.ts"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "yarn build:wrapper pie-modal && run -T vite build",
|
|
16
|
+
"lint:scripts": "run -T eslint .",
|
|
17
|
+
"lint:scripts:fix": "run -T eslint . --fix",
|
|
18
|
+
"lint:style": "run -T stylelint ./src/**/*.{css,scss}",
|
|
19
|
+
"lint:style:fix": "yarn lint:style --fix",
|
|
20
|
+
"watch": "run -T vite build --watch",
|
|
21
|
+
"test": "echo \"Error: no test specified\" && exit 0",
|
|
22
|
+
"test:ci": "yarn test",
|
|
23
|
+
"test:browsers": "npx playwright test -c ./playwright-lit.config.ts",
|
|
24
|
+
"test:browsers:ci": "yarn test:browsers",
|
|
25
|
+
"test:visual": "run -T cross-env-shell PERCY_TOKEN=${PERCY_TOKEN_PIE_MODAL} percy exec --allowed-hostname cloudfront.net -- npx playwright test -c ./playwright-lit-visual.config.ts",
|
|
26
|
+
"test:visual:ci": "yarn test:visual"
|
|
27
|
+
},
|
|
28
|
+
"author": "Just Eat Takeaway.com - Design System Team",
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@justeat/pie-design-tokens": "5.9.0",
|
|
32
|
+
"@justeattakeaway/pie-components-config": "0.6.0",
|
|
33
|
+
"@types/body-scroll-lock": "3.1.2"
|
|
34
|
+
},
|
|
35
|
+
"volta": {
|
|
36
|
+
"extends": "../../../package.json"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@justeattakeaway/pie-button": "0.40.0",
|
|
40
|
+
"@justeattakeaway/pie-icon-button": "0.22.0",
|
|
41
|
+
"@justeattakeaway/pie-icons-webc": "0.11.1",
|
|
42
|
+
"@justeattakeaway/pie-spinner": "0.3.0",
|
|
43
|
+
"@justeattakeaway/pie-webc-core": "0.12.0",
|
|
44
|
+
"body-scroll-lock": "3.1.5",
|
|
45
|
+
"dialog-polyfill": "0.5.6"
|
|
46
|
+
},
|
|
47
|
+
"sideEffects": [
|
|
48
|
+
"dist/*.js"
|
|
49
|
+
]
|
|
50
|
+
}
|
package/src/defs.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Variant } from '@justeattakeaway/pie-button/src/defs.ts';
|
|
2
|
+
|
|
3
|
+
export const headingLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
|
|
4
|
+
export const sizes = ['small', 'medium', 'large'] as const;
|
|
5
|
+
export const positions = ['top', 'center'] as const;
|
|
6
|
+
|
|
7
|
+
export type AriaProps = {
|
|
8
|
+
close?: string;
|
|
9
|
+
back?: string;
|
|
10
|
+
loading?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ActionProps = {
|
|
14
|
+
/**
|
|
15
|
+
* The text to display inside the button.
|
|
16
|
+
*/
|
|
17
|
+
text: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The button variant.
|
|
21
|
+
*/
|
|
22
|
+
variant?: Variant;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The ARIA label for the button.
|
|
26
|
+
*/
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ModalProps = {
|
|
31
|
+
/**
|
|
32
|
+
* The ARIA labels used for the modal close and back buttons, as well as loading state.
|
|
33
|
+
*/
|
|
34
|
+
aria?: AriaProps;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* When true, the modal will have a back button. This currently behaves the same as the close button.
|
|
38
|
+
*/
|
|
39
|
+
hasBackButton: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* When true, the modal will have a back button. This currently behaves the same as the close button.
|
|
43
|
+
*/
|
|
44
|
+
hasStackedActions: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The text to display in the modal's heading.
|
|
48
|
+
*/
|
|
49
|
+
heading: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The HTML heading tag to use for the modal's heading. Can be h1-h6.
|
|
53
|
+
*/
|
|
54
|
+
headingLevel: typeof headingLevels[number];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* When true, the modal will be open.
|
|
58
|
+
*/
|
|
59
|
+
isOpen: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* When set to `true`:
|
|
63
|
+
* 1. The close button within the modal will be visible.
|
|
64
|
+
* 2. The user can dismiss the modal via the ESCAPE key, clicking the backdrop
|
|
65
|
+
* or via a close button.
|
|
66
|
+
*
|
|
67
|
+
* When set to `false`:
|
|
68
|
+
* 1. The close button within the modal will be hidden.
|
|
69
|
+
* 2. The user can NOT dismiss the modal via the ESCAPE key or clicking the backdrop.
|
|
70
|
+
*
|
|
71
|
+
*/
|
|
72
|
+
isDismissible: boolean;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* When false, the modal footer will scroll with the content inside the modal body.
|
|
76
|
+
*/
|
|
77
|
+
isFooterPinned: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* This controls whether a *medium-sized* modal will cover the full width of the page when below the mid breakpoint.
|
|
81
|
+
*/
|
|
82
|
+
isFullWidthBelowMid: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* When true, displays a loading spinner in the modal.
|
|
86
|
+
*/
|
|
87
|
+
isLoading: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The leading action configuration for the modal.
|
|
91
|
+
*/
|
|
92
|
+
leadingAction: ActionProps;
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* The position of the modal; this controls where it will appear on the page.
|
|
96
|
+
*/
|
|
97
|
+
position: typeof positions[number];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The selector for the element that you would like focus to be returned to when the modal is closed, e.g., #skipToMain
|
|
101
|
+
*/
|
|
102
|
+
returnFocusAfterCloseSelector?: string;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The size of the modal; this controls how wide it will appear on the page.
|
|
106
|
+
*/
|
|
107
|
+
size: typeof sizes[number];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The supporting action configuration for the modal.
|
|
111
|
+
*/
|
|
112
|
+
supportingAction: ActionProps;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Event name for when the modal is closed.
|
|
117
|
+
*
|
|
118
|
+
* @constant
|
|
119
|
+
*/
|
|
120
|
+
export const ON_MODAL_CLOSE_EVENT = 'pie-modal-close';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Event name for when the modal is opened.
|
|
124
|
+
*
|
|
125
|
+
* @constant
|
|
126
|
+
*/
|
|
127
|
+
export const ON_MODAL_OPEN_EVENT = 'pie-modal-open';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Event name for when the modal back button is clicked.
|
|
131
|
+
*
|
|
132
|
+
* @constant
|
|
133
|
+
*/
|
|
134
|
+
export const ON_MODAL_BACK_EVENT = 'pie-modal-back';
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Event name for when the modal leading action is clicked.
|
|
138
|
+
*
|
|
139
|
+
* @constant
|
|
140
|
+
*/
|
|
141
|
+
export const ON_MODAL_LEADING_ACTION_CLICK = 'pie-modal-leading-action-click';
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Event name for when the modal supporting action is clicked.
|
|
145
|
+
*
|
|
146
|
+
* @constant
|
|
147
|
+
*/
|
|
148
|
+
export const ON_MODAL_SUPPORTING_ACTION_CLICK = 'pie-modal-supporting-action-click';
|
|
149
|
+
|
|
150
|
+
export type ModalActionType = 'leading' | 'supporting';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LitElement, nothing, TemplateResult, unsafeCSS, PropertyValues,
|
|
3
|
+
} from 'lit';
|
|
4
|
+
import { html, unsafeStatic } from 'lit/static-html.js';
|
|
5
|
+
import { property, query } from 'lit/decorators.js';
|
|
6
|
+
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
|
|
7
|
+
|
|
8
|
+
import '@justeattakeaway/pie-button';
|
|
9
|
+
import '@justeattakeaway/pie-icon-button';
|
|
10
|
+
import {
|
|
11
|
+
requiredProperty, RtlMixin, validPropertyValues, defineCustomElement,
|
|
12
|
+
} from '@justeattakeaway/pie-webc-core';
|
|
13
|
+
import '@justeattakeaway/pie-icons-webc/IconClose';
|
|
14
|
+
import '@justeattakeaway/pie-icons-webc/IconChevronLeft';
|
|
15
|
+
import '@justeattakeaway/pie-icons-webc/IconChevronRight';
|
|
16
|
+
import '@justeattakeaway/pie-spinner';
|
|
17
|
+
|
|
18
|
+
import styles from './modal.scss?inline';
|
|
19
|
+
import {
|
|
20
|
+
type AriaProps,
|
|
21
|
+
type ActionProps,
|
|
22
|
+
type ModalProps,
|
|
23
|
+
type ModalActionType,
|
|
24
|
+
headingLevels,
|
|
25
|
+
positions,
|
|
26
|
+
sizes,
|
|
27
|
+
ON_MODAL_BACK_EVENT,
|
|
28
|
+
ON_MODAL_CLOSE_EVENT,
|
|
29
|
+
ON_MODAL_OPEN_EVENT,
|
|
30
|
+
ON_MODAL_LEADING_ACTION_CLICK,
|
|
31
|
+
ON_MODAL_SUPPORTING_ACTION_CLICK,
|
|
32
|
+
} from './defs';
|
|
33
|
+
|
|
34
|
+
// Valid values available to consumers
|
|
35
|
+
export * from './defs';
|
|
36
|
+
|
|
37
|
+
const componentSelector = 'pie-modal';
|
|
38
|
+
|
|
39
|
+
export interface ModalEventDetail {
|
|
40
|
+
targetModal: PieModal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @tagname pie-modal
|
|
45
|
+
* @event {CustomEvent} pie-modal-open - when the modal is opened.
|
|
46
|
+
* @event {CustomEvent} pie-modal-close - when the modal is closed.
|
|
47
|
+
* @event {CustomEvent} pie-modal-back - when the modal back button is clicked.
|
|
48
|
+
* @event {CustomEvent} pie-modal-leading-action-click - when the modal leading action is clicked.
|
|
49
|
+
* @event {CustomEvent} pie-modal-supporting-action-click - when the modal supporting action is clicked.
|
|
50
|
+
*/
|
|
51
|
+
export class PieModal extends RtlMixin(LitElement) implements ModalProps {
|
|
52
|
+
@property({ type: Object })
|
|
53
|
+
public aria!: AriaProps;
|
|
54
|
+
|
|
55
|
+
@property({ type: String })
|
|
56
|
+
@requiredProperty(componentSelector)
|
|
57
|
+
public heading!: string;
|
|
58
|
+
|
|
59
|
+
@property()
|
|
60
|
+
@validPropertyValues(componentSelector, headingLevels, 'h2')
|
|
61
|
+
public headingLevel: ModalProps['headingLevel'] = 'h2';
|
|
62
|
+
|
|
63
|
+
@property({ type: Boolean })
|
|
64
|
+
public hasBackButton = false;
|
|
65
|
+
|
|
66
|
+
@property({ type: Boolean })
|
|
67
|
+
public hasStackedActions = false;
|
|
68
|
+
|
|
69
|
+
@property({ type: Boolean, reflect: true })
|
|
70
|
+
public isDismissible = false;
|
|
71
|
+
|
|
72
|
+
@property({ type: Boolean })
|
|
73
|
+
public isFooterPinned = true;
|
|
74
|
+
|
|
75
|
+
@property({ type: Boolean })
|
|
76
|
+
public isFullWidthBelowMid = false;
|
|
77
|
+
|
|
78
|
+
@property({ type: Boolean, reflect: true })
|
|
79
|
+
public isLoading = false;
|
|
80
|
+
|
|
81
|
+
@property({ type: Boolean })
|
|
82
|
+
public isOpen = false;
|
|
83
|
+
|
|
84
|
+
@property({ type: Object })
|
|
85
|
+
public leadingAction!: ActionProps;
|
|
86
|
+
|
|
87
|
+
@property()
|
|
88
|
+
@validPropertyValues(componentSelector, positions, 'center')
|
|
89
|
+
public position: ModalProps['position'] = 'center';
|
|
90
|
+
|
|
91
|
+
@property()
|
|
92
|
+
public returnFocusAfterCloseSelector?: string;
|
|
93
|
+
|
|
94
|
+
@property()
|
|
95
|
+
@validPropertyValues(componentSelector, sizes, 'medium')
|
|
96
|
+
public size: ModalProps['size'] = 'medium';
|
|
97
|
+
|
|
98
|
+
@property({ type: Object })
|
|
99
|
+
public supportingAction!: ActionProps;
|
|
100
|
+
|
|
101
|
+
@query('dialog')
|
|
102
|
+
private _dialog?: HTMLDialogElement;
|
|
103
|
+
|
|
104
|
+
private _backButtonClicked = false;
|
|
105
|
+
|
|
106
|
+
// Renders a `CSSResult` generated from SCSS by Vite
|
|
107
|
+
static styles = unsafeCSS(styles);
|
|
108
|
+
|
|
109
|
+
connectedCallback () : void {
|
|
110
|
+
super.connectedCallback();
|
|
111
|
+
this.addEventListener('click', (event) => this._handleDialogLightDismiss(event));
|
|
112
|
+
document.addEventListener(ON_MODAL_OPEN_EVENT, (event) => this._handleModalOpened(<CustomEvent>event));
|
|
113
|
+
document.addEventListener(ON_MODAL_CLOSE_EVENT, (event) => this._handleModalClosed(<CustomEvent>event));
|
|
114
|
+
document.addEventListener(ON_MODAL_BACK_EVENT, (event) => this._handleModalClosed(<CustomEvent>event));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
disconnectedCallback () : void {
|
|
118
|
+
document.removeEventListener(ON_MODAL_OPEN_EVENT, (event) => this._handleModalOpened(<CustomEvent>event));
|
|
119
|
+
document.removeEventListener(ON_MODAL_CLOSE_EVENT, (event) => this._handleModalClosed(<CustomEvent>event));
|
|
120
|
+
document.removeEventListener(ON_MODAL_BACK_EVENT, (event) => this._handleModalClosed(<CustomEvent>event));
|
|
121
|
+
super.disconnectedCallback();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async firstUpdated (changedProperties: PropertyValues<this>) : Promise<void> {
|
|
125
|
+
super.firstUpdated(changedProperties);
|
|
126
|
+
|
|
127
|
+
if (this._dialog) {
|
|
128
|
+
const dialogPolyfill = await import('dialog-polyfill').then((module) => module.default);
|
|
129
|
+
dialogPolyfill.registerDialog(this._dialog);
|
|
130
|
+
this._dialog.addEventListener('cancel', (event) => this._handleDialogCancelEvent(event));
|
|
131
|
+
this._dialog.addEventListener('close', () => {
|
|
132
|
+
this.isOpen = false;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._handleModalOpenStateOnFirstRender(changedProperties);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
updated (changedProperties: PropertyValues<this>) : void {
|
|
140
|
+
super.updated(changedProperties);
|
|
141
|
+
this._handleModalOpenStateChanged(changedProperties);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Opens the dialog element and disables page scrolling
|
|
146
|
+
*/
|
|
147
|
+
private _handleModalOpened (event: CustomEvent): void {
|
|
148
|
+
const { targetModal } = event.detail;
|
|
149
|
+
|
|
150
|
+
if (targetModal === this) {
|
|
151
|
+
const modalScrollContainer = this._dialog?.querySelector('.c-modal-scrollContainer');
|
|
152
|
+
|
|
153
|
+
if (modalScrollContainer) {
|
|
154
|
+
disableBodyScroll(modalScrollContainer);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this._dialog?.hasAttribute('open') || !this._dialog?.isConnected) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The ::backdrop pseudoelement is only shown if the modal is opened via JS
|
|
162
|
+
this._dialog?.showModal();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Closes the dialog element and re-enables page scrolling
|
|
168
|
+
*/
|
|
169
|
+
private _handleModalClosed (event: CustomEvent): void {
|
|
170
|
+
const { targetModal } = event.detail;
|
|
171
|
+
|
|
172
|
+
if (targetModal === this) {
|
|
173
|
+
const modalScrollContainer = this._dialog?.querySelector('.c-modal-scrollContainer');
|
|
174
|
+
|
|
175
|
+
if (modalScrollContainer) {
|
|
176
|
+
enableBodyScroll(modalScrollContainer);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this._dialog?.close();
|
|
180
|
+
this._returnFocus();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Prevents the user from dismissing the dialog via the `cancel`
|
|
186
|
+
* event (ESC key) when `isDismissible` is set to false.
|
|
187
|
+
*
|
|
188
|
+
* @param {Event} event - The event object.
|
|
189
|
+
*/
|
|
190
|
+
private _handleDialogCancelEvent = (event: Event) : void => {
|
|
191
|
+
if (!this.isDismissible) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Handles the value of the isOpen property on first render of the component
|
|
197
|
+
private _handleModalOpenStateOnFirstRender (changedProperties: PropertyValues<this>) : void {
|
|
198
|
+
// This ensures if the modal is open on first render, the scroll lock and backdrop are applied
|
|
199
|
+
const previousValue = changedProperties.get('isOpen');
|
|
200
|
+
|
|
201
|
+
if (previousValue === undefined && this.isOpen) {
|
|
202
|
+
this._dispatchModalCustomEvent(ON_MODAL_OPEN_EVENT);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handles changes to the modal isOpen property by dispatching any appropriate events
|
|
207
|
+
private _handleModalOpenStateChanged (changedProperties: PropertyValues<this>) : void {
|
|
208
|
+
const wasPreviouslyOpen = changedProperties.get('isOpen');
|
|
209
|
+
|
|
210
|
+
if (wasPreviouslyOpen !== undefined) {
|
|
211
|
+
if (wasPreviouslyOpen) {
|
|
212
|
+
if (this._backButtonClicked) {
|
|
213
|
+
// Reset the flag
|
|
214
|
+
this._backButtonClicked = false;
|
|
215
|
+
this._dispatchModalCustomEvent(ON_MODAL_BACK_EVENT);
|
|
216
|
+
} else {
|
|
217
|
+
this._dispatchModalCustomEvent(ON_MODAL_CLOSE_EVENT);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
this._dispatchModalCustomEvent(ON_MODAL_OPEN_EVENT);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private _handleActionClick (actionType: ModalActionType) : void {
|
|
226
|
+
if (actionType === 'leading') {
|
|
227
|
+
this._dialog?.close('leading');
|
|
228
|
+
this._dispatchModalCustomEvent(ON_MODAL_LEADING_ACTION_CLICK);
|
|
229
|
+
} else if (actionType === 'supporting') {
|
|
230
|
+
this._dialog?.close('supporting');
|
|
231
|
+
this._dispatchModalCustomEvent(ON_MODAL_SUPPORTING_ACTION_CLICK);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Return focus to the specified element, providing the selector is valid
|
|
237
|
+
* and the chosen element can be found.
|
|
238
|
+
*/
|
|
239
|
+
private _returnFocus () : void {
|
|
240
|
+
const selector = this.returnFocusAfterCloseSelector?.trim();
|
|
241
|
+
|
|
242
|
+
if (selector) {
|
|
243
|
+
(document.querySelector(selector) as HTMLElement)?.focus();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Template for the close button element. Called within the
|
|
249
|
+
* main render function.
|
|
250
|
+
*
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
253
|
+
private renderCloseButton (): TemplateResult {
|
|
254
|
+
return html`
|
|
255
|
+
<pie-icon-button
|
|
256
|
+
@click="${() => { this.isOpen = false; }}"
|
|
257
|
+
variant="ghost-secondary"
|
|
258
|
+
class="c-modal-closeBtn"
|
|
259
|
+
aria-label="${this.aria?.close || nothing}"
|
|
260
|
+
data-test-id="modal-close-button">
|
|
261
|
+
<icon-close></icon-close>
|
|
262
|
+
</pie-icon-button>`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Template for the back button element. Called within the
|
|
267
|
+
* main render function.
|
|
268
|
+
*
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
private renderBackButton () : TemplateResult {
|
|
272
|
+
return html`
|
|
273
|
+
<pie-icon-button
|
|
274
|
+
@click="${() => { this._backButtonClicked = true; this.isOpen = false; }}"
|
|
275
|
+
variant="ghost-secondary"
|
|
276
|
+
class="c-modal-backBtn"
|
|
277
|
+
aria-label="${this.aria?.back || nothing}"
|
|
278
|
+
data-test-id="modal-back-button">
|
|
279
|
+
${this.isRTL ? html`<icon-chevron-right></icon-chevron-right>` : html`<icon-chevron-left></icon-chevron-left>`}
|
|
280
|
+
</pie-icon-button>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Render leadingAction button depending on prop availability.
|
|
286
|
+
*
|
|
287
|
+
* 1. If the prop `leadingAction` is not provided, the button is not rendered.
|
|
288
|
+
* 2. If the prop `leadingAction` is provided but any of the optional properties
|
|
289
|
+
* are not provided, they fall back to their default values.
|
|
290
|
+
*
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
private renderLeadingAction () : TemplateResult | typeof nothing {
|
|
294
|
+
const { text, variant = 'primary', ariaLabel } = this.leadingAction;
|
|
295
|
+
|
|
296
|
+
if (!text) {
|
|
297
|
+
return nothing;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return html`
|
|
301
|
+
<pie-button
|
|
302
|
+
variant="${variant}"
|
|
303
|
+
aria-label="${ariaLabel || nothing}"
|
|
304
|
+
type="submit"
|
|
305
|
+
?isFullWidth="${this.hasStackedActions}"
|
|
306
|
+
@click="${() => this._handleActionClick('leading')}"
|
|
307
|
+
data-test-id="modal-leading-action">
|
|
308
|
+
${text}
|
|
309
|
+
</pie-button>
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Render supportingAction button depending on prop availability.
|
|
315
|
+
*
|
|
316
|
+
* 1. If the prop `supportingAction` is not provided, the button is not rendered.
|
|
317
|
+
* 2. If the prop `supportingAction` is provided but any of the optional properties
|
|
318
|
+
* are not provided, they fall back to their default values.
|
|
319
|
+
* 3. If `supportingAction` is provided but not `leadingAction`, log a warning and do
|
|
320
|
+
* not render `supportingAction`.
|
|
321
|
+
*
|
|
322
|
+
* @private
|
|
323
|
+
*/
|
|
324
|
+
private renderSupportingAction (): TemplateResult | typeof nothing {
|
|
325
|
+
const { text, variant = 'ghost', ariaLabel } = this.supportingAction;
|
|
326
|
+
|
|
327
|
+
if (!text) {
|
|
328
|
+
return nothing;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!this.leadingAction) {
|
|
332
|
+
console.warn('Use `leadingAction` instead of `supportingAction`. `supportingAction` is being ignored.');
|
|
333
|
+
return nothing;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return html`
|
|
337
|
+
<pie-button
|
|
338
|
+
variant="${variant}"
|
|
339
|
+
aria-label="${ariaLabel || nothing}"
|
|
340
|
+
type="reset"
|
|
341
|
+
?isFullWidth="${this.hasStackedActions}"
|
|
342
|
+
@click="${() => this._handleActionClick('supporting')}"
|
|
343
|
+
data-test-id="modal-supporting-action">
|
|
344
|
+
${text}
|
|
345
|
+
</pie-button>
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Renders the modal inner content and footer of the modal.
|
|
351
|
+
* @private
|
|
352
|
+
*/
|
|
353
|
+
private renderModalContentAndFooter (): TemplateResult {
|
|
354
|
+
return html`
|
|
355
|
+
<article class="c-modal-scrollContainer c-modal-content c-modal-content--scrollable">
|
|
356
|
+
<div class="c-modal-contentInner">
|
|
357
|
+
<slot></slot>
|
|
358
|
+
</div>
|
|
359
|
+
${this.isLoading ? html`<pie-spinner size="xlarge" variant="secondary"></pie-spinner>` : nothing}
|
|
360
|
+
</article>
|
|
361
|
+
<footer class="c-modal-footer">
|
|
362
|
+
${this.leadingAction ? this.renderLeadingAction() : nothing}
|
|
363
|
+
${this.supportingAction ? this.renderSupportingAction() : nothing}
|
|
364
|
+
</footer>`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public render () {
|
|
368
|
+
const {
|
|
369
|
+
aria,
|
|
370
|
+
hasBackButton,
|
|
371
|
+
hasStackedActions,
|
|
372
|
+
heading,
|
|
373
|
+
headingLevel = 'h2',
|
|
374
|
+
isDismissible,
|
|
375
|
+
isFooterPinned,
|
|
376
|
+
isFullWidthBelowMid,
|
|
377
|
+
isLoading,
|
|
378
|
+
leadingAction,
|
|
379
|
+
position,
|
|
380
|
+
size,
|
|
381
|
+
supportingAction,
|
|
382
|
+
} = this;
|
|
383
|
+
|
|
384
|
+
const headingTag = unsafeStatic(headingLevel);
|
|
385
|
+
|
|
386
|
+
return html`
|
|
387
|
+
<dialog
|
|
388
|
+
id="dialog"
|
|
389
|
+
class="c-modal"
|
|
390
|
+
size="${size}"
|
|
391
|
+
position="${position}"
|
|
392
|
+
?hasActions=${leadingAction || supportingAction}
|
|
393
|
+
?hasBackButton=${hasBackButton}
|
|
394
|
+
?hasStackedActions=${hasStackedActions}
|
|
395
|
+
?isDismissible=${isDismissible}
|
|
396
|
+
?isFooterPinned=${isFooterPinned}
|
|
397
|
+
?isFullWidthBelowMid=${isFullWidthBelowMid}
|
|
398
|
+
?isLoading=${isLoading}
|
|
399
|
+
aria-busy="${isLoading ? 'true' : 'false'}"
|
|
400
|
+
aria-label="${(isLoading && aria?.loading) || nothing}"
|
|
401
|
+
data-test-id="pie-modal">
|
|
402
|
+
<header class="c-modal-header">
|
|
403
|
+
${hasBackButton ? this.renderBackButton() : nothing}
|
|
404
|
+
<${headingTag} class="c-modal-heading">
|
|
405
|
+
${heading}
|
|
406
|
+
</${headingTag}>
|
|
407
|
+
${isDismissible ? this.renderCloseButton() : nothing}
|
|
408
|
+
</header>
|
|
409
|
+
${
|
|
410
|
+
// We need to wrap the remaining content in a shared scrollable container if the footer is not pinned
|
|
411
|
+
isFooterPinned
|
|
412
|
+
? this.renderModalContentAndFooter()
|
|
413
|
+
: html`
|
|
414
|
+
<div class="c-modal-scrollContainer">
|
|
415
|
+
${this.renderModalContentAndFooter()}
|
|
416
|
+
</div>
|
|
417
|
+
`
|
|
418
|
+
}
|
|
419
|
+
</dialog>`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Dismisses the modal on backdrop click if `isDismissible` is `true`.
|
|
424
|
+
* @param {MouseEvent} event - the click event targetting the modal/backdrop
|
|
425
|
+
*/
|
|
426
|
+
private _handleDialogLightDismiss = (event: MouseEvent) : void => {
|
|
427
|
+
if (!this.isDismissible) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const rect = this._dialog?.getBoundingClientRect();
|
|
432
|
+
|
|
433
|
+
const {
|
|
434
|
+
top = 0, bottom = 0, left = 0, right = 0,
|
|
435
|
+
} = rect || {};
|
|
436
|
+
|
|
437
|
+
// This means the dialog is not open due to clicking the close button so
|
|
438
|
+
// we want to escape early
|
|
439
|
+
if (top === 0 && bottom === 0 && left === 0 && right === 0) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const isClickOutsideDialog = event.clientY < top ||
|
|
444
|
+
event.clientY > bottom ||
|
|
445
|
+
event.clientX < left ||
|
|
446
|
+
event.clientX > right;
|
|
447
|
+
|
|
448
|
+
if (isClickOutsideDialog) {
|
|
449
|
+
this.isOpen = false;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Note: We should aim to have a shareable event helper system to allow
|
|
455
|
+
* us to share this across components in-future.
|
|
456
|
+
*
|
|
457
|
+
* Dispatch a custom event.
|
|
458
|
+
*
|
|
459
|
+
* To be used whenever we have behavioural events we want to
|
|
460
|
+
* bubble up through the modal.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} eventType
|
|
463
|
+
*/
|
|
464
|
+
private _dispatchModalCustomEvent (eventType: string): void {
|
|
465
|
+
const event = new CustomEvent<ModalEventDetail>(eventType, {
|
|
466
|
+
bubbles: true,
|
|
467
|
+
composed: true,
|
|
468
|
+
detail: { targetModal: this },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
this.dispatchEvent(event);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
defineCustomElement(componentSelector, PieModal);
|
|
476
|
+
|
|
477
|
+
declare global {
|
|
478
|
+
interface HTMLElementTagNameMap {
|
|
479
|
+
[componentSelector]: PieModal;
|
|
480
|
+
}
|
|
481
|
+
}
|
package/src/modal.scss
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
@use '@justeattakeaway/pie-css/scss' as p;
|
|
2
|
+
@use '@justeat/pie-design-tokens/dist/jet.scss' as dt;
|
|
3
|
+
@use 'dialog-polyfill/dist/dialog-polyfill.css';
|
|
4
|
+
|
|
5
|
+
*,
|
|
6
|
+
*:before,
|
|
7
|
+
*:after {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.c-modal {
|
|
12
|
+
// Custom Property Declarations
|
|
13
|
+
// These are defined here instead of :host to encapsulate them inside Shadow DOM
|
|
14
|
+
$breakpoint-wide: 768px;
|
|
15
|
+
|
|
16
|
+
--modal-size-s: 450px;
|
|
17
|
+
--modal-size-m: 600px;
|
|
18
|
+
--modal-size-l: 1080px;
|
|
19
|
+
|
|
20
|
+
// The base modal defaults
|
|
21
|
+
--modal-border-radius: var(--dt-radius-rounded-d);
|
|
22
|
+
--modal-font: var(--dt-font-interactive-m-family);
|
|
23
|
+
--modal-bg-color: var(--dt-color-container-default);
|
|
24
|
+
--modal-elevation: var(--dt-elevation-04);
|
|
25
|
+
|
|
26
|
+
&:focus-visible {
|
|
27
|
+
outline: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// We need to override the icon sizes at different screen sizes regardless of size prop passed in
|
|
31
|
+
pie-icon-button {
|
|
32
|
+
@media (max-width: calc($breakpoint-wide - 1px)) {
|
|
33
|
+
--btn-dimension: 40px;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
&[open] {
|
|
39
|
+
// We only apply this when the modal is open,
|
|
40
|
+
// otherwise it interferes with the native
|
|
41
|
+
// `display: none;` on the dialog element.
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
border-radius: var(--modal-border-radius);
|
|
47
|
+
border: none;
|
|
48
|
+
box-shadow: var(--modal-elevation);
|
|
49
|
+
font-family: var(--modal-font);
|
|
50
|
+
background-color: var(--modal-bg-color);
|
|
51
|
+
|
|
52
|
+
padding: 0;
|
|
53
|
+
|
|
54
|
+
--modal-margin-none: var(--dt-spacing-none);
|
|
55
|
+
--modal-margin-small: var(--dt-spacing-g);
|
|
56
|
+
--modal-margin-large: var(--dt-spacing-j);
|
|
57
|
+
--modal-margin-block: var(--modal-margin-small);
|
|
58
|
+
|
|
59
|
+
@media (min-width: $breakpoint-wide) {
|
|
60
|
+
--modal-margin-block: var(--modal-margin-large);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The initial values for these variables apply to the medium-sized modal
|
|
64
|
+
// Other sizes will update the variables as needed
|
|
65
|
+
--modal-block-size: fit-content;
|
|
66
|
+
--modal-inline-size: 75%;
|
|
67
|
+
--modal-max-block-size: calc(100vh - calc(var(--modal-margin-block) * 2));
|
|
68
|
+
--modal-max-inline-size: var(--modal-size-m);
|
|
69
|
+
|
|
70
|
+
block-size: var(--modal-block-size);
|
|
71
|
+
inline-size: var(--modal-inline-size);
|
|
72
|
+
max-block-size: var(--modal-max-block-size);
|
|
73
|
+
max-inline-size: var(--modal-max-inline-size);
|
|
74
|
+
|
|
75
|
+
&[size='small'] {
|
|
76
|
+
--modal-max-inline-size: var(--modal-size-s);
|
|
77
|
+
|
|
78
|
+
@media (min-width: $breakpoint-wide) {
|
|
79
|
+
--modal-margin-block: var(--modal-margin-large);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&[size='medium'] {
|
|
84
|
+
/* Same as default styles */
|
|
85
|
+
&[isfullwidthbelowmid] {
|
|
86
|
+
@media (max-width: calc($breakpoint-wide - 1px)) {
|
|
87
|
+
--modal-margin-block: var(--modal-margin-none);
|
|
88
|
+
--modal-border-radius: var(--dt-radius-rounded-none);
|
|
89
|
+
--modal-block-size: 100%;
|
|
90
|
+
--modal-inline-size: 100%;
|
|
91
|
+
|
|
92
|
+
// In this case, the modal must exceed the previous maximum width
|
|
93
|
+
--modal-max-inline-size: 100%;
|
|
94
|
+
|
|
95
|
+
// this ensures the content container spans the full height
|
|
96
|
+
> .c-modal-scrollContainer {
|
|
97
|
+
block-size: 100%;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
&[size='large'] {
|
|
104
|
+
--modal-inline-size: 75%;
|
|
105
|
+
--modal-max-inline-size: var(--modal-size-l);
|
|
106
|
+
--modal-margin-block: var(--modal-margin-large);
|
|
107
|
+
|
|
108
|
+
@media (max-width: calc($breakpoint-wide - 1px)) {
|
|
109
|
+
--modal-margin-block: var(--modal-margin-none);
|
|
110
|
+
--modal-border-radius: var(--dt-radius-rounded-none);
|
|
111
|
+
--modal-block-size: 100%;
|
|
112
|
+
--modal-inline-size: 100%;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&[position='top'] {
|
|
117
|
+
margin-block-start: var(--dt-spacing-j);
|
|
118
|
+
max-block-size: calc(100% - var(--dt-spacing-j) * 2);
|
|
119
|
+
|
|
120
|
+
&[size='large'],
|
|
121
|
+
&[isfullwidthbelowmid][size='medium'] {
|
|
122
|
+
@media (max-width: calc($breakpoint-wide - 1px)) {
|
|
123
|
+
margin-block-start: var(--dt-spacing-none);
|
|
124
|
+
max-block-size: 100%;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// We need to pull in the token directly here because the
|
|
130
|
+
// pseudo element `::backdrop` doesn't seem to pick up custom css properties.
|
|
131
|
+
&::backdrop {
|
|
132
|
+
background: #{dt.$color-overlay};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
& .c-modal-footer {
|
|
136
|
+
--modal-button-spacing: var(--dt-spacing-d);
|
|
137
|
+
--modal-footer-padding: var(--dt-spacing-d);
|
|
138
|
+
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-flow: row-reverse;
|
|
141
|
+
flex-wrap: wrap;
|
|
142
|
+
gap: var(--modal-button-spacing);
|
|
143
|
+
padding: var(--modal-footer-padding);
|
|
144
|
+
|
|
145
|
+
@media (min-width: $breakpoint-wide) {
|
|
146
|
+
--modal-footer-padding: var(--dt-spacing-e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// When hasStackedActions is set
|
|
151
|
+
// change the direction of the footer flex container so buttons are full width of modal
|
|
152
|
+
&[hasstackedactions] {
|
|
153
|
+
& .c-modal-footer {
|
|
154
|
+
// TODO: Move breakpoint sizes into shared CSS component utilities
|
|
155
|
+
@media (max-width: calc($breakpoint-wide - 1px)) {
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
& .c-modal-header {
|
|
162
|
+
display: grid;
|
|
163
|
+
grid-template-areas: 'back heading close';
|
|
164
|
+
grid-template-columns: minmax(0, max-content) minmax(0, 1fr) minmax(0, max-content);
|
|
165
|
+
align-items: start;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
& .c-modal-heading {
|
|
169
|
+
--modal-header-font-size: calc(var(--dt-font-heading-m-size--narrow) * 1px);
|
|
170
|
+
--modal-header-font-line-height: calc(var(--dt-font-heading-m-line-height--narrow) * 1px);
|
|
171
|
+
--modal-header-font-weight: var(--dt-font-heading-m-weight);
|
|
172
|
+
|
|
173
|
+
font-size: var(--modal-header-font-size);
|
|
174
|
+
line-height: var(--modal-header-font-line-height);
|
|
175
|
+
font-weight: var(--modal-header-font-weight);
|
|
176
|
+
margin: 0;
|
|
177
|
+
grid-area: heading;
|
|
178
|
+
|
|
179
|
+
margin-inline: var(--dt-spacing-d);
|
|
180
|
+
margin-block: 14px; // This is deliberately not a custom property
|
|
181
|
+
|
|
182
|
+
@media (min-width: $breakpoint-wide) {
|
|
183
|
+
--modal-header-font-size: calc(var(--dt-font-heading-m-size--wide) * 1px);
|
|
184
|
+
--modal-header-font-line-height: calc(var(--dt-font-heading-m-line-height--wide) * 1px);
|
|
185
|
+
margin-inline: var(--dt-spacing-e);
|
|
186
|
+
margin-block: 20px; // This is deliberately not a custom property
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Ensure correct padding when there is a back button in front of the heading
|
|
191
|
+
&[hasbackbutton] .c-modal-heading {
|
|
192
|
+
margin-inline-start: var(--dt-spacing-b);
|
|
193
|
+
|
|
194
|
+
@media (min-width: $breakpoint-wide) {
|
|
195
|
+
margin-inline-start: var(--dt-spacing-c);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Ensure correct padding when there is a close button behind the heading
|
|
200
|
+
&[isdismissible] .c-modal-heading {
|
|
201
|
+
margin-inline-end: var(--dt-spacing-d);
|
|
202
|
+
|
|
203
|
+
@media (min-width: $breakpoint-wide) {
|
|
204
|
+
margin-inline-end: var(--dt-spacing-e);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
& .c-modal-backBtn {
|
|
209
|
+
grid-area: back;
|
|
210
|
+
margin-block: var(--dt-spacing-b);
|
|
211
|
+
margin-inline: var(--dt-spacing-b) var(--dt-spacing-none);
|
|
212
|
+
|
|
213
|
+
@media (min-width: $breakpoint-wide) {
|
|
214
|
+
margin-block: var(--dt-spacing-c);
|
|
215
|
+
margin-inline: var(--dt-spacing-c) var(--dt-spacing-none);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
& .c-modal-closeBtn {
|
|
220
|
+
grid-area: close;
|
|
221
|
+
margin-block: var(--dt-spacing-b);
|
|
222
|
+
margin-inline: var(--dt-spacing-none) var(--dt-spacing-b);
|
|
223
|
+
|
|
224
|
+
@media (min-width: $breakpoint-wide) {
|
|
225
|
+
margin-block: var(--dt-spacing-c);
|
|
226
|
+
margin-inline: var(--dt-spacing-none) var(--dt-spacing-c);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
& .c-modal-content {
|
|
231
|
+
// Modal content Custom Props
|
|
232
|
+
--modal-content-font-size: calc(var(--dt-font-size-16) * 1px);
|
|
233
|
+
--modal-content-font-weight: var(--dt-font-weight-regular);
|
|
234
|
+
--modal-content-line-height: calc(var(--dt-font-size-16-line-height) * 1px);
|
|
235
|
+
--modal-content-padding-block: var(--dt-spacing-a);
|
|
236
|
+
--modal-content-padding-inline: var(--dt-spacing-d);
|
|
237
|
+
|
|
238
|
+
@media (min-width: $breakpoint-wide) {
|
|
239
|
+
--modal-content-padding-inline: var(--dt-spacing-e);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
position: relative;
|
|
243
|
+
min-block-size: var(--dt-spacing-j);
|
|
244
|
+
|
|
245
|
+
font-size: var(--modal-content-font-size);
|
|
246
|
+
line-height: var(--modal-content-line-height);
|
|
247
|
+
font-weight: var(--modal-content-font-weight);
|
|
248
|
+
|
|
249
|
+
padding-inline: var(--modal-content-padding-inline);
|
|
250
|
+
padding-block: var(--modal-content-padding-block);
|
|
251
|
+
flex-grow: 1;
|
|
252
|
+
|
|
253
|
+
&--scrollable {
|
|
254
|
+
background:
|
|
255
|
+
// Scroll shadow cover
|
|
256
|
+
// A top-to-bottom opacity gradient from transparent to the component background colour
|
|
257
|
+
linear-gradient(to bottom, transparent, var(--dt-color-container-default) 75%) center bottom,
|
|
258
|
+
// Scroll shadow
|
|
259
|
+
linear-gradient(transparent, var(--dt-color-border-strong)) center bottom;
|
|
260
|
+
background-repeat: no-repeat;
|
|
261
|
+
background-size: 100% 48px, 100% 12px;
|
|
262
|
+
|
|
263
|
+
// The shadow cover is an opacity gradient which is attached to the bottom of the scrollable element
|
|
264
|
+
// and scrolls with it, so as you reach the bottom of the content the more opaque portion covers
|
|
265
|
+
// (and therefore hides) the shadow. This gives the effect of the shadow fading away.
|
|
266
|
+
// The shadow itself does not move as you scroll.
|
|
267
|
+
background-attachment: local, scroll;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
& > .c-modal-scrollContainer {
|
|
272
|
+
display: flex;
|
|
273
|
+
flex-direction: column;
|
|
274
|
+
block-size: auto;
|
|
275
|
+
overflow-y: auto;
|
|
276
|
+
|
|
277
|
+
// These are the shadows used to indicate scrolling above and below content
|
|
278
|
+
--bg-scroll-end: linear-gradient(rgba(255, 255, 255, 0), var(--dt-color-container-default) 70%) 0 100%;
|
|
279
|
+
--bg-scroll-bottom: radial-gradient(farthest-corner at 50% 100%, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0)) 0 100%;
|
|
280
|
+
|
|
281
|
+
// Sizes of the scroll shadows
|
|
282
|
+
--bg-size-scroll-end: 100% 40px;
|
|
283
|
+
--bg-size-scroll-bottom: 100% 8px;
|
|
284
|
+
|
|
285
|
+
background: var(--bg-scroll-end), var(--bg-scroll-bottom);
|
|
286
|
+
background-repeat: no-repeat;
|
|
287
|
+
background-size: var(--bg-size-scroll-end), var(--bg-size-scroll-bottom);
|
|
288
|
+
|
|
289
|
+
background-attachment: local, scroll;
|
|
290
|
+
|
|
291
|
+
.c-modal-content {
|
|
292
|
+
flex-shrink: 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
&[isfooterpinned] .c-modal-content {
|
|
297
|
+
overflow-y: auto;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
&[isLoading] .c-modal-content {
|
|
301
|
+
pie-spinner {
|
|
302
|
+
position: absolute;
|
|
303
|
+
left: 50%;
|
|
304
|
+
top: 50%;
|
|
305
|
+
transform: translate(-50%, -50%);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
& .c-modal-contentInner {
|
|
309
|
+
display: none;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// removes the scroll shadow effect if "background-attachment" isn't supported.
|
|
314
|
+
// @supports (background-attachment: local) isn't used as Safari 14 recognizes the property but it has no effect.
|
|
315
|
+
// thus, "aspect-ratio" is used instead as it's only supported in Safari>=15
|
|
316
|
+
@supports not (aspect-ratio: 1 / 1) {
|
|
317
|
+
.c-modal-scrollContainer {
|
|
318
|
+
background: none;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|