@justeattakeaway/pie-modal 0.6.0 → 0.7.0

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.
@@ -1,19 +1,14 @@
1
- [3:10:46 PM] @custom-elements-manifest/analyzer: Created new manifest.
1
+ [4:04:52 PM] @custom-elements-manifest/analyzer: Created new manifest.
2
2
  react wrapper has been added!
3
3
  vite v4.3.9 building for production...
4
4
  transforming...
5
- ✓ 28 modules transformed.
5
+ ✓ 30 modules transformed.
6
6
  rendering chunks...
7
7
  computing gzip size...
8
- dist/index.js  7.43 kB │ gzip: 2.38 kB
8
+ dist/index.js 13.55 kB │ gzip: 4.16 kB
9
9
  dist/react.js 59.04 kB │ gzip: 15.92 kB
10
10
  
11
11
  [vite:dts] Start generate declaration files...
12
- src/index.ts:27:9 - error TS2564: Property '_dialog' has no initializer and is not definitely assigned in the constructor.
13
-
14
- 27 _dialog: HTMLDialogElement;
15
-    ~~~~~~~
16
-
17
- ✓ built in 28.25s
18
- [vite:dts] Declaration files built in 26028ms.
12
+ ✓ built in 28.39s
13
+ [vite:dts] Declaration files built in 26000ms.
19
14
  
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @justeattakeaway/pie-modal
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [Added] - Modal backdrop functionality ([#559](https://github.com/justeattakeaway/pie/pull/559)) by [@kevinrodrigues](https://github.com/kevinrodrigues)
8
+
9
+ - [Added] - Scroll locking to modal ([#564](https://github.com/justeattakeaway/pie/pull/564)) by [@jamieomaguire](https://github.com/jamieomaguire)
10
+
11
+ ## 0.6.1
12
+
13
+ ### Patch Changes
14
+
15
+ - [Changed] - Updated defs to use different array type syntax ([#566](https://github.com/justeattakeaway/pie/pull/566)) by [@ashleynolan](https://github.com/ashleynolan)
16
+
3
17
  ## 0.6.0
4
18
 
5
19
  ### Minor Changes
package/README.md CHANGED
@@ -17,65 +17,92 @@
17
17
 
18
18
  # pie-modal
19
19
 
20
- This modal is a Web Component built using Lit.
20
+ `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.
21
+
22
+ This component can be easily integrated into various frontend frameworks and customized through a set of properties.
21
23
 
22
24
  ## Local development
23
25
 
24
- Install dependencies at the root
25
- ```
26
+ Install the dependencies. Note that this, and the following commands below, should be run from the **root of the monorepo**:
27
+
28
+ ```bash
26
29
  yarn
27
30
  ```
28
31
 
29
- Navigate to this folder, compile with TypeScript and build with Vite
30
- ```
31
- cd packages/components/pie-modal
32
- yarn build
33
- ```
32
+ To build the `pie-modal` package, run the following command:
34
33
 
35
- Compile and watch for changes (auto-compiles `dist` on save)
34
+ ```bash
35
+ yarn build --filter=pie-modal
36
36
  ```
37
- yarn watch
37
+
38
+ If you'd like to develop using the component storybook, then you should build the component in `watch` mode, and run storybook in a separate terminal tab:
39
+
40
+ ```bash
41
+ yarn watch --filter=pie-modal
42
+
43
+ # in a separate terminal tab, run
44
+ yarn dev --filter=pie-storybook
38
45
  ```
39
46
 
40
47
  ### Importing the component
41
48
 
42
- ```javascript
49
+ ```js
43
50
  // default
44
- import { PieModal } from '@justeattakeaway/pie-button';
51
+ import { PieModal } from '@justeattakeaway/pie-modal';
45
52
 
46
53
  // react
47
- import { PieModal } from '@justeattakeaway/pie-button/dist/react';
54
+ import { PieModal } from '@justeattakeaway/pie-modal/dist/react';
48
55
  ```
49
56
 
50
57
  ## Props
51
58
 
52
- | Property | Type | Default | Description |
53
- |----------|-----------|---------|-------------------------------------------------|
54
- | isOpen | `Boolean` | `false` | Controls if the modal element is open or closed |
55
- | heading* | `String` | - | Sets the heading of the modal |
56
- | headingLevel | `String` | `h2` | Allows you to set the heading tag (from `h1` to `h6`) |
59
+ | Property | Type | Default | Description |
60
+ |----------------|-----------|---------|-------------------------------------------------------|
61
+ | isOpen | `Boolean` | `false` | Controls if the modal element is open or closed |
62
+ | heading* | `String` | - | Sets the heading of the modal |
63
+ | headingLevel | `String` | `h2` | Allows you to set the heading tag (from `h1` to `h6`) |
64
+
57
65
 
66
+ In your markup or JSX, you can then use these to set the properties for the `pie-modal` component:
67
+
68
+ ```html
69
+ <!-- Native HTML -->
70
+ <pie-modal heading='My Awesome Heading' headingLevel='h3'>Click me!</pie-modal>
71
+
72
+ <!-- JSX -->
73
+ <PieModal heading='My Awesome Heading' headingLevel='h3'>Click me!</PieModal>
74
+ ```
58
75
 
59
76
  ## Testing
60
77
 
61
- ### Visual tests
78
+ ### Browser tests
79
+
80
+ To run the browser tests, run the following command from the root of the monorepo:
62
81
 
63
- Run at the root of the monorepo:
82
+ ```bash
83
+ yarn test:browsers --filter=pie-modal
64
84
  ```
85
+
86
+ ### Visual tests
87
+
88
+ To run the visual regression tests, run the following command from the root of the monorepo:
89
+
90
+ ```bash
65
91
  yarn test:visual --filter=pie-modal
66
92
  ```
67
93
 
68
94
  Note: To run these locally, you will need to ensure that any environment variables required are set up on your machine to mirror those on CI (such as Percy tokens). How you achieve this will differ between operating systems.
69
95
 
70
- ### Setup via bash:
96
+ #### Setup via bash
71
97
 
72
- ```
98
+ ```bash
73
99
  export PERCY_TOKEN_PIE_MODAL=abcde
74
100
  ```
75
101
 
76
- ### Setup via package.json:
102
+ #### Setup via package.json
77
103
 
78
104
  Under scripts `test:visual` replace the environment variable with the below:
79
105
 
80
- ```
106
+ ```bash
81
107
  PERCY_TOKEN_PIE_MODAL=abcde
108
+ ```
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { unsafeCSS as g, LitElement as v, html as m } from "lit";
2
- import { unsafeStatic as u, html as b } from "lit/static-html.js";
3
- import { property as s, query as _ } from "lit/decorators.js";
4
- import { property as w } from "lit/decorators/property.js";
5
- var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, P = (r, t, n, o) => {
6
- for (var e = o > 1 ? void 0 : o ? $(t, n) : t, a = r.length - 1, i; a >= 0; a--)
7
- (i = r[a]) && (e = (o ? i(t, n, e) : i(e)) || e);
8
- return o && e && y(t, n, e), e;
1
+ import { unsafeCSS as T, LitElement as $, html as w } from "lit";
2
+ import { unsafeStatic as D, html as z } from "lit/static-html.js";
3
+ import { property as u, query as B } from "lit/decorators.js";
4
+ import { property as k } from "lit/decorators/property.js";
5
+ var V = Object.defineProperty, R = Object.getOwnPropertyDescriptor, A = (i, e, t, n) => {
6
+ for (var o = n > 1 ? void 0 : n ? R(e, t) : e, r = i.length - 1, d; r >= 0; r--)
7
+ (d = i[r]) && (o = (n ? d(e, t, o) : d(o)) || o);
8
+ return n && o && V(e, t, o), o;
9
9
  };
10
- const O = (r) => {
11
- class t extends r {
10
+ const W = (i) => {
11
+ class e extends i {
12
12
  constructor() {
13
13
  super(...arguments), this.dir = "";
14
14
  }
@@ -22,135 +22,282 @@ const O = (r) => {
22
22
  * will not be reactive and is only computed once
23
23
  */
24
24
  get isRTL() {
25
- var o;
26
- return this.dir === "ltr" ? !1 : this.dir === "rtl" || ((o = window == null ? void 0 : window.getComputedStyle(this)) == null ? void 0 : o.direction) === "rtl";
25
+ var n;
26
+ return this.dir === "ltr" ? !1 : this.dir === "rtl" || ((n = window == null ? void 0 : window.getComputedStyle(this)) == null ? void 0 : n.direction) === "rtl";
27
27
  }
28
28
  }
29
- return P([
30
- w({ type: String })
31
- ], t.prototype, "dir", 2), t;
32
- }, C = (r, t, n) => function(o, e) {
33
- const a = `#${e}`;
34
- Object.defineProperty(o, e, {
29
+ return A([
30
+ k({ type: String })
31
+ ], e.prototype, "dir", 2), e;
32
+ }, j = (i, e, t) => function(o, r) {
33
+ const d = `#${r}`;
34
+ Object.defineProperty(o, r, {
35
35
  get() {
36
- return this[a];
36
+ return this[d];
37
37
  },
38
- set(i) {
39
- const f = this[a];
40
- t.includes(i) ? this[a] = i : (console.error(
41
- `<${r}> Invalid value "${i}" provided for property "${e}".`,
42
- `Must be one of: ${t.join(" | ")}.`,
43
- `Falling back to default value: "${n}"`
44
- ), this[a] = n), this.requestUpdate(e, f);
38
+ set(f) {
39
+ const h = this[d];
40
+ e.includes(f) ? this[d] = f : (console.error(
41
+ `<${i}> Invalid value "${f}" provided for property "${r}".`,
42
+ `Must be one of: ${e.join(" | ")}.`,
43
+ `Falling back to default value: "${t}"`
44
+ ), this[d] = t), this.requestUpdate(r, h);
45
45
  }
46
46
  });
47
- }, x = (r) => function(t, n) {
47
+ }, q = (i) => function(t, n) {
48
48
  const o = `#${n}`;
49
49
  Object.defineProperty(t, n, {
50
50
  get() {
51
51
  return this[o];
52
52
  },
53
- set(e) {
54
- const a = this[o];
55
- (e == null || e === "") && console.error(`<${r}> Missing required attribute "${n}"`), this[o] = e, this.requestUpdate(n, a);
53
+ set(r) {
54
+ const d = this[o];
55
+ (r == null || typeof r == "string" && r.trim() === "") && console.error(`<${i}> Missing required attribute "${n}"`), this[o] = r, this.requestUpdate(n, d);
56
56
  }
57
57
  });
58
- }, z = `.c-webComponentTestWrapper{padding-block:var(--dt-spacing-c);padding-inline:var(--dt-spacing-e);font-family:var(--dt-font-interactive-m-family);font-size:calc(var(--dt-font-size-20) * 1px);border:1px solid var(--dt-color-background-dark);display:grid;grid-template-columns:1fr 1fr}.c-webComponentTestWrapper-label{margin-block:var(--dt-spacing-c)}.c-webComponentTestWrapper-slot{padding:var(--dt-spacing-c);border:1px dashed var(--dt-color-background-dark);grid-column:1/3;margin-block-start:var(--dt-spacing-c)}
58
+ }, I = `.c-webComponentTestWrapper{padding-block:var(--dt-spacing-c);padding-inline:var(--dt-spacing-e);font-family:var(--dt-font-interactive-m-family);font-size:calc(var(--dt-font-size-20) * 1px);border:1px solid var(--dt-color-background-dark);display:grid;grid-template-columns:1fr 1fr}.c-webComponentTestWrapper-label{margin-block:var(--dt-spacing-c)}.c-webComponentTestWrapper-slot{padding:var(--dt-spacing-c);border:1px dashed var(--dt-color-background-dark);grid-column:1/3;margin-block-start:var(--dt-spacing-c)}
59
59
  `;
60
- var D = Object.defineProperty, T = Object.getOwnPropertyDescriptor, E = (r, t, n, o) => {
61
- for (var e = o > 1 ? void 0 : o ? T(t, n) : t, a = r.length - 1, i; a >= 0; a--)
62
- (i = r[a]) && (e = (o ? i(t, n, e) : i(e)) || e);
63
- return o && e && D(t, n, e), e;
60
+ var F = Object.defineProperty, H = Object.getOwnPropertyDescriptor, E = (i, e, t, n) => {
61
+ for (var o = n > 1 ? void 0 : n ? H(e, t) : e, r = i.length - 1, d; r >= 0; r--)
62
+ (d = i[r]) && (o = (n ? d(e, t, o) : d(o)) || o);
63
+ return n && o && F(e, t, o), o;
64
64
  };
65
- class c extends v {
65
+ class b extends $ {
66
66
  constructor() {
67
- super(...arguments), this.propKeyValues = "";
67
+ super(...arguments), this.propKeyValues = "", this.pageMode = !1;
68
68
  }
69
69
  // Renders a string such as 'size: small, isFullWidth: true'
70
70
  // as HTML such as:
71
71
  // <p class="c-webComponentTestWrapper-label"><b>size</b>: <code>small</code></p>
72
72
  // <p class="c-webComponentTestWrapper-label"><b>isFullWidth</b>: <code>true</code></p>
73
73
  _renderPropKeyValues() {
74
- return this.propKeyValues.split(",").map((t) => {
75
- const [n, o] = t.split(":");
76
- return m`<p class="c-webComponentTestWrapper-label"><b>${n}</b>: <code>${o}</code></p>`;
74
+ return this.propKeyValues.split(",").map((e) => {
75
+ const [t, n] = e.split(":");
76
+ return w`<p class="c-webComponentTestWrapper-label"><b>${t}</b>: <code>${n}</code></p>`;
77
77
  });
78
78
  }
79
- // eslint-disable-next-line class-methods-use-this
80
79
  render() {
81
- return m`
80
+ return this.pageMode ? w`
81
+ <div>
82
+ <slot name="component"></slot>
83
+ <slot name="pageMarkup"></slot>
84
+ </div>` : w`
82
85
  <div class="c-webComponentTestWrapper">
83
86
  ${this._renderPropKeyValues()}
84
87
  <div class="c-webComponentTestWrapper-slot">
85
- <slot></slot>
88
+ <slot name="component"></slot>
86
89
  </div>
87
90
  </div>`;
88
91
  }
89
92
  }
90
- c.styles = g(z);
93
+ b.styles = T(I);
94
+ E([
95
+ u({ type: String })
96
+ ], b.prototype, "propKeyValues", 2);
91
97
  E([
92
- s({ type: String })
93
- ], c.prototype, "propKeyValues", 2);
94
- const h = "web-component-test-wrapper";
95
- customElements.get(h) || customElements.define(h, c);
96
- const S = `.c-modal{--modal-size-s: 450px;--modal-size-m: 600px;--modal-size-l: 1080px;--modal-border-radius: var(--dt-radius-rounded-d);--modal-font: var(--dt-font-interactive-m-family);--modal-bg-color: var(--dt-color-container-default);--modal-elevation: var(--dt-elevation-04);border-radius:var(--modal-border-radius);border:none;font-family:var(--modal-font);background-color:var(--modal-bg-color);padding:0;inline-size:var(--modal-size-m);box-shadow:var(--modal-elevation)}.c-modal .c-modal-heading{--modal-header-font-size: calc(var(--dt-font-heading-m-size--wide) * 1px);--modal-header-font-line-height: calc(var(--dt-font-heading-m-line-height--wide) * 1px);--modal-header-font-weight: var(--dt-font-heading-m-weight);--modal-header-padding: var(--dt-spacing-e);--modal-header-padding-block-end: var(--dt-spacing-d);font-size:var(--modal-header-font-size);line-height:var(--modal-header-font-line-height);font-weight:var(--modal-header-font-weight);margin:0;padding-block:var(--modal-header-padding) var(--modal-header-padding-block-end);padding-inline:var(--modal-header-padding)}.c-modal .c-modal-content{--modal-content-font-size: calc(var(--dt-font-size-16) * 1px);--modal-content-font-weight: var(--dt-font-weight-regular);--modal-content-line-height: calc(var(--dt-font-size-16-line-height) * 1px);--modal-content-padding: var(--dt-spacing-e);--modal-content-padding-block-start: var(--dt-spacing-a);font-size:var(--modal-content-font-size);line-height:var(--modal-content-line-height);font-weight:var(--modal-content-font-weight);padding-block:var(--modal-content-padding-block-start) var(--modal-content-padding);padding-inline:var(--modal-content-padding)}.c-modal .c-modal-closeBtn{position:absolute;right:var(--dt-spacing-d);top:var(--dt-spacing-d)}
97
- `, j = ["h1", "h2", "h3", "h4", "h5", "h6"], L = "pie-modal-close";
98
- var W = Object.defineProperty, V = Object.getOwnPropertyDescriptor, l = (r, t, n, o) => {
99
- for (var e = o > 1 ? void 0 : o ? V(t, n) : t, a = r.length - 1, i; a >= 0; a--)
100
- (i = r[a]) && (e = (o ? i(t, n, e) : i(e)) || e);
101
- return o && e && W(t, n, e), e;
98
+ u({ type: Boolean })
99
+ ], b.prototype, "pageMode", 2);
100
+ const C = "web-component-test-wrapper";
101
+ customElements.get(C) || customElements.define(C, b);
102
+ function N(i) {
103
+ if (Array.isArray(i)) {
104
+ for (var e = 0, t = Array(i.length); e < i.length; e++)
105
+ t[e] = i[e];
106
+ return t;
107
+ } else
108
+ return Array.from(i);
109
+ }
110
+ var P = !1;
111
+ if (typeof window < "u") {
112
+ var M = {
113
+ get passive() {
114
+ P = !0;
115
+ }
116
+ };
117
+ window.addEventListener("testPassive", null, M), window.removeEventListener("testPassive", null, M);
118
+ }
119
+ var v = typeof window < "u" && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1), s = [], m = !1, x = -1, c = void 0, a = void 0, p = void 0, L = function(e) {
120
+ return s.some(function(t) {
121
+ return !!(t.options.allowTouchMove && t.options.allowTouchMove(e));
122
+ });
123
+ }, g = function(e) {
124
+ var t = e || window.event;
125
+ return L(t.target) || t.touches.length > 1 ? !0 : (t.preventDefault && t.preventDefault(), !1);
126
+ }, Y = function(e) {
127
+ if (p === void 0) {
128
+ var t = !!e && e.reserveScrollBarGap === !0, n = window.innerWidth - document.documentElement.clientWidth;
129
+ if (t && n > 0) {
130
+ var o = parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right"), 10);
131
+ p = document.body.style.paddingRight, document.body.style.paddingRight = o + n + "px";
132
+ }
133
+ }
134
+ c === void 0 && (c = document.body.style.overflow, document.body.style.overflow = "hidden");
135
+ }, U = function() {
136
+ p !== void 0 && (document.body.style.paddingRight = p, p = void 0), c !== void 0 && (document.body.style.overflow = c, c = void 0);
137
+ }, X = function() {
138
+ return window.requestAnimationFrame(function() {
139
+ if (a === void 0) {
140
+ a = {
141
+ position: document.body.style.position,
142
+ top: document.body.style.top,
143
+ left: document.body.style.left
144
+ };
145
+ var e = window, t = e.scrollY, n = e.scrollX, o = e.innerHeight;
146
+ document.body.style.position = "fixed", document.body.style.top = -t, document.body.style.left = -n, setTimeout(function() {
147
+ return window.requestAnimationFrame(function() {
148
+ var r = o - window.innerHeight;
149
+ r && t >= o && (document.body.style.top = -(t + r));
150
+ });
151
+ }, 300);
152
+ }
153
+ });
154
+ }, K = function() {
155
+ if (a !== void 0) {
156
+ var e = -parseInt(document.body.style.top, 10), t = -parseInt(document.body.style.left, 10);
157
+ document.body.style.position = a.position, document.body.style.top = a.top, document.body.style.left = a.left, window.scrollTo(t, e), a = void 0;
158
+ }
159
+ }, G = function(e) {
160
+ return e ? e.scrollHeight - e.scrollTop <= e.clientHeight : !1;
161
+ }, J = function(e, t) {
162
+ var n = e.targetTouches[0].clientY - x;
163
+ return L(e.target) ? !1 : t && t.scrollTop === 0 && n > 0 || G(t) && n < 0 ? g(e) : (e.stopPropagation(), !0);
164
+ }, Q = function(e, t) {
165
+ if (!e) {
166
+ console.error("disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.");
167
+ return;
168
+ }
169
+ if (!s.some(function(o) {
170
+ return o.targetElement === e;
171
+ })) {
172
+ var n = {
173
+ targetElement: e,
174
+ options: t || {}
175
+ };
176
+ s = [].concat(N(s), [n]), v ? X() : Y(t), v && (e.ontouchstart = function(o) {
177
+ o.targetTouches.length === 1 && (x = o.targetTouches[0].clientY);
178
+ }, e.ontouchmove = function(o) {
179
+ o.targetTouches.length === 1 && J(o, e);
180
+ }, m || (document.addEventListener("touchmove", g, P ? { passive: !1 } : void 0), m = !0));
181
+ }
182
+ }, Z = function(e) {
183
+ if (!e) {
184
+ console.error("enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.");
185
+ return;
186
+ }
187
+ s = s.filter(function(t) {
188
+ return t.targetElement !== e;
189
+ }), v && (e.ontouchstart = null, e.ontouchmove = null, m && s.length === 0 && (document.removeEventListener("touchmove", g, P ? { passive: !1 } : void 0), m = !1)), v ? K() : U();
102
190
  };
103
- const p = "pie-modal";
104
- class d extends O(v) {
191
+ const ee = `.c-modal{--modal-size-s: 450px;--modal-size-m: 600px;--modal-size-l: 1080px;--modal-border-radius: var(--dt-radius-rounded-d);--modal-font: var(--dt-font-interactive-m-family);--modal-bg-color: var(--dt-color-container-default);--modal-elevation: var(--dt-elevation-04);border-radius:var(--modal-border-radius);border:none;font-family:var(--modal-font);background-color:var(--modal-bg-color);padding:0;inline-size:var(--modal-size-m);box-shadow:var(--modal-elevation)}.c-modal::backdrop{background:rgba(0,0,0,.5)}.c-modal .c-modal-heading{--modal-header-font-size: calc(var(--dt-font-heading-m-size--wide) * 1px);--modal-header-font-line-height: calc(var(--dt-font-heading-m-line-height--wide) * 1px);--modal-header-font-weight: var(--dt-font-heading-m-weight);--modal-header-padding: var(--dt-spacing-e);--modal-header-padding-block-end: var(--dt-spacing-d);font-size:var(--modal-header-font-size);line-height:var(--modal-header-font-line-height);font-weight:var(--modal-header-font-weight);margin:0;padding-block:var(--modal-header-padding) var(--modal-header-padding-block-end);padding-inline:var(--modal-header-padding)}.c-modal .c-modal-content{--modal-content-font-size: calc(var(--dt-font-size-16) * 1px);--modal-content-font-weight: var(--dt-font-weight-regular);--modal-content-line-height: calc(var(--dt-font-size-16-line-height) * 1px);--modal-content-padding: var(--dt-spacing-e);--modal-content-padding-block-start: var(--dt-spacing-a);font-size:var(--modal-content-font-size);line-height:var(--modal-content-line-height);font-weight:var(--modal-content-font-weight);padding-block:var(--modal-content-padding-block-start) var(--modal-content-padding);padding-inline:var(--modal-content-padding);overflow-y:scroll;max-block-size:300px}.c-modal .c-modal-closeBtn{position:absolute;inset-inline-end:var(--dt-spacing-d);inset-block-start:var(--dt-spacing-d)}
192
+ `, te = ["h1", "h2", "h3", "h4", "h5", "h6"], _ = "pie-modal-close", O = "pie-modal-open";
193
+ var oe = Object.defineProperty, ne = Object.getOwnPropertyDescriptor, y = (i, e, t, n) => {
194
+ for (var o = n > 1 ? void 0 : n ? ne(e, t) : e, r = i.length - 1, d; r >= 0; r--)
195
+ (d = i[r]) && (o = (n ? d(e, t, o) : d(o)) || o);
196
+ return n && o && oe(e, t, o), o;
197
+ };
198
+ const S = "pie-modal";
199
+ class l extends W($) {
105
200
  constructor() {
106
- super(...arguments), this.isOpen = !1, this.headingLevel = "h2", this._handleCloseDialog = () => {
107
- this._dialog.close(), this._onDialogClose();
108
- }, this._onDialogClose = () => {
109
- const t = new CustomEvent(L);
110
- this.dispatchEvent(t);
111
- };
201
+ super(), this.isOpen = !1, this.headingLevel = "h2", this._triggerCloseModal = () => {
202
+ this.isOpen = !1;
203
+ }, this._handleDialogLightDismiss = (e) => {
204
+ var h;
205
+ const t = (h = this._dialog) == null ? void 0 : h.getBoundingClientRect(), {
206
+ top: n = 0,
207
+ bottom: o = 0,
208
+ left: r = 0,
209
+ right: d = 0
210
+ } = t || {};
211
+ if (n === 0 && o === 0 && r === 0 && d === 0)
212
+ return;
213
+ (e.clientY < n || e.clientY > o || e.clientX < r || e.clientX > d) && (this.isOpen = !1);
214
+ }, this._dispatchModalCloseEvent = () => {
215
+ const e = new CustomEvent(_, {
216
+ bubbles: !0,
217
+ composed: !0
218
+ });
219
+ this.dispatchEvent(e);
220
+ }, this._dispatchModalOpenEvent = () => {
221
+ const e = new CustomEvent(O, {
222
+ bubbles: !0,
223
+ composed: !0
224
+ });
225
+ this.dispatchEvent(e);
226
+ }, this.addEventListener("click", (e) => this._handleDialogLightDismiss(e));
227
+ }
228
+ firstUpdated(e) {
229
+ this._handleModalOpenStateOnFirstRender(e);
230
+ }
231
+ updated(e) {
232
+ this._handleModalOpenStateChanged(e);
233
+ }
234
+ /**
235
+ * Opens the dialog element and disables page scrolling
236
+ */
237
+ _handleModalOpened() {
238
+ var e;
239
+ Q(this), (e = this._dialog) == null || e.showModal();
240
+ }
241
+ /**
242
+ * Closes the dialog element and re-enables page scrolling
243
+ */
244
+ _handleModalClosed() {
245
+ var e;
246
+ Z(this), (e = this._dialog) == null || e.close();
247
+ }
248
+ connectedCallback() {
249
+ super.connectedCallback(), document.addEventListener(O, this._handleModalOpened.bind(this)), document.addEventListener(_, this._handleModalClosed.bind(this));
250
+ }
251
+ disconnectedCallback() {
252
+ document.removeEventListener(O, this._handleModalOpened.bind(this)), document.removeEventListener(_, this._handleModalClosed.bind(this)), super.disconnectedCallback();
253
+ }
254
+ // Handles the value of the isOpen property on first render of the component
255
+ _handleModalOpenStateOnFirstRender(e) {
256
+ e.get("isOpen") === void 0 && this.isOpen && this._dispatchModalOpenEvent();
257
+ }
258
+ // Handles changes to the modal isOpen property by dispatching any appropriate events
259
+ _handleModalOpenStateChanged(e) {
260
+ const t = e.get("isOpen");
261
+ t !== void 0 && (t ? this._dispatchModalCloseEvent() : this._dispatchModalOpenEvent());
112
262
  }
113
263
  render() {
114
264
  const {
115
- isOpen: t,
116
- heading: n,
117
- headingLevel: o = "h2"
118
- } = this, e = u(o);
119
- return b`
120
- <dialog id="dialog" class="c-modal" ?open="${t}">
265
+ heading: e,
266
+ headingLevel: t = "h2"
267
+ } = this, n = D(t);
268
+ return z`
269
+ <dialog id="dialog" class="c-modal">
121
270
  <header>
122
- <${e} class="c-modal-heading">${n}</${e}>
271
+ <${n} class="c-modal-heading">${e}</${n}>
123
272
  <pie-icon-button
124
- @click="${this._handleCloseDialog}"
273
+ @click="${this._triggerCloseModal}"
125
274
  variant="ghost-tertiary"
126
275
  class="c-modal-closeBtn"></pie-icon-button>
127
276
  </header>
128
- <article>
129
- <div class="c-modal-content">
130
- <slot></slot>
131
- </div>
277
+ <article class="c-modal-content">
278
+ <slot></slot>
132
279
  </article>
133
280
  </dialog>
134
281
  `;
135
282
  }
136
283
  }
137
- d.styles = g(S);
138
- l([
139
- s({ type: Boolean })
140
- ], d.prototype, "isOpen", 2);
141
- l([
142
- s({ type: String }),
143
- x(p)
144
- ], d.prototype, "heading", 2);
145
- l([
146
- s(),
147
- C(p, j, "h2")
148
- ], d.prototype, "headingLevel", 2);
149
- l([
150
- _("dialog")
151
- ], d.prototype, "_dialog", 2);
152
- customElements.define(p, d);
284
+ l.styles = T(ee);
285
+ y([
286
+ u({ type: Boolean })
287
+ ], l.prototype, "isOpen", 2);
288
+ y([
289
+ u({ type: String }),
290
+ q(S)
291
+ ], l.prototype, "heading", 2);
292
+ y([
293
+ u(),
294
+ j(S, te, "h2")
295
+ ], l.prototype, "headingLevel", 2);
296
+ y([
297
+ B("dialog")
298
+ ], l.prototype, "_dialog", 2);
299
+ customElements.define(S, l);
153
300
  export {
154
- d as PieModal,
155
- j as headingLevels
301
+ l as PieModal,
302
+ te as headingLevels
156
303
  };
@@ -1,27 +1,30 @@
1
+ export declare const headingLevels: readonly ["h1", "h2", "h3", "h4", "h5", "h6"];
1
2
  export interface ModalProps {
2
3
  /**
3
- * the heading of the modal .
4
+ * (Required) The text to display in the modal's heading.
4
5
  */
5
6
  heading: string;
6
7
  /**
7
- * the rendered heading tag of the modal header.
8
- * @default h2
8
+ * (Optional) The HTML heading tag to use for the modal's heading. Can be H1-H6.
9
+ * @default "h2"
9
10
  */
10
- headingLevel: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
11
+ headingLevel: typeof headingLevels[number];
11
12
  /**
12
- * If `true`, the modal will be opened.
13
+ * (Optional) When true, the modal will be open.
13
14
  * @default false
14
15
  */
15
- isOpen?: boolean;
16
+ isOpen: boolean;
16
17
  }
17
- /**
18
- * Modal heading levels/tags
19
- */
20
- export declare const headingLevels: ModalProps['headingLevel'][];
21
18
  /**
22
19
  * Event name for when the modal is closed.
23
20
  *
24
21
  * @constant
25
22
  */
26
23
  export declare const ON_MODAL_CLOSE_EVENT = "pie-modal-close";
24
+ /**
25
+ * Event name for when the modal is opened.
26
+ *
27
+ * @constant
28
+ */
29
+ export declare const ON_MODAL_OPEN_EVENT = "pie-modal-open";
27
30
  //# sourceMappingURL=defs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"defs.d.ts","sourceRoot":"","sources":["../../../src/defs.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACvB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IACtD;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAAU,CAAC,cAAc,CAAC,EAAyC,CAAC;AAEhG;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC"}
1
+ {"version":3,"file":"defs.d.ts","sourceRoot":"","sources":["../../../src/defs.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,+CAAgD,CAAC;AAE3E,MAAM,WAAW,UAAU;IACvB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,EAAE,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;IAC3C;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mBAAmB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { LitElement } from 'lit';
2
+ import type { DependentMap } from '@justeattakeaway/pie-webc-core';
2
3
  import { ModalProps, headingLevels } from './defs';
3
4
  export { type ModalProps, headingLevels };
4
5
  declare const componentSelector = "pie-modal";
@@ -10,16 +11,47 @@ export declare class PieModal extends PieModal_base {
10
11
  isOpen: boolean;
11
12
  heading: string;
12
13
  headingLevel: ModalProps['headingLevel'];
13
- _dialog: HTMLDialogElement;
14
+ _dialog?: HTMLDialogElement;
15
+ constructor();
16
+ firstUpdated(changedProperties: DependentMap<ModalProps>): void;
17
+ updated(changedProperties: DependentMap<ModalProps>): void;
18
+ /**
19
+ * Opens the dialog element and disables page scrolling
20
+ */
21
+ private _handleModalOpened;
22
+ /**
23
+ * Closes the dialog element and re-enables page scrolling
24
+ */
25
+ private _handleModalClosed;
26
+ /**
27
+ * This is only to be used inside the component template as direct property
28
+ * reassignment is not allowed.
29
+ */
30
+ private _triggerCloseModal;
31
+ connectedCallback(): void;
32
+ disconnectedCallback(): void;
33
+ private _handleModalOpenStateOnFirstRender;
34
+ private _handleModalOpenStateChanged;
14
35
  render(): import("lit-html").TemplateResult;
15
- _handleCloseDialog: () => void;
36
+ /**
37
+ * Dismisses the modal on backdrop click
38
+ *
39
+ */
40
+ private _handleDialogLightDismiss;
16
41
  /**
17
42
  * Dispatch `ON_MODAL_CLOSE_EVENT` event.
18
43
  * To be used whenever we close the modal.
19
44
  *
20
45
  * @event
21
46
  */
22
- _onDialogClose: () => void;
47
+ private _dispatchModalCloseEvent;
48
+ /**
49
+ * Dispatch `ON_MODAL_OPEN_EVENT` event.
50
+ * To be used whenever we open the modal.
51
+ *
52
+ * @event
53
+ */
54
+ private _dispatchModalOpenEvent;
23
55
  static styles: import("lit").CSSResult;
24
56
  }
25
57
  declare global {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAM5C,OAAO,EAAE,UAAU,EAAE,aAAa,EAAwB,MAAM,QAAQ,CAAC;AAGzE,OAAO,EAAE,KAAK,UAAU,EAAE,aAAa,EAAE,CAAC;AAE1C,QAAA,MAAM,iBAAiB,cAAc,CAAC;;;;;AAEtC,qBAAa,QAAS,SAAQ,aAAoB;IAE1C,MAAM,UAAS;IAIf,OAAO,EAAG,MAAM,CAAC;IAIjB,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC,CAAQ;IAGhD,OAAO,EAAE,iBAAiB,CAAC;IAE/B,MAAM;IA2BN,kBAAkB,aAGhB;IAEF;;;;;OAKG;IACH,cAAc,aAGZ;IAGF,MAAM,CAAC,MAAM,0BAAqB;CACrC;AAID,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC;KACjC;CACJ"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAM5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGnE,OAAO,EACH,UAAU,EAAE,aAAa,EAC5B,MAAM,QAAQ,CAAC;AAGhB,OAAO,EAAE,KAAK,UAAU,EAAE,aAAa,EAAE,CAAC;AAE1C,QAAA,MAAM,iBAAiB,cAAc,CAAC;;;;;AAEtC,qBAAa,QAAS,SAAQ,aAAoB;IAE1C,MAAM,UAAS;IAIf,OAAO,EAAG,MAAM,CAAC;IAIjB,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC,CAAQ;IAGhD,OAAO,CAAC,EAAE,iBAAiB,CAAC;;IAOhC,YAAY,CAAE,iBAAiB,EAAE,YAAY,CAAC,UAAU,CAAC,GAAI,IAAI;IAIjE,OAAO,CAAE,iBAAiB,EAAE,YAAY,CAAC,UAAU,CAAC,GAAI,IAAI;IAI5D;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAQ1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;;OAGG;IACH,OAAO,CAAC,kBAAkB,CAExB;IAEF,iBAAiB,IAAM,IAAI;IAM3B,oBAAoB,IAAM,IAAI;IAO9B,OAAO,CAAC,kCAAkC;IAU1C,OAAO,CAAC,4BAA4B;IAYpC,MAAM;IAwBN;;;OAGG;IACH,OAAO,CAAC,yBAAyB,CAqB/B;IAEF;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB,CAO9B;IAEF;;;;;OAKG;IACH,OAAO,CAAC,uBAAuB,CAO7B;IAGF,MAAM,CAAC,MAAM,0BAAqB;CACrC;AAID,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC;KACjC;CACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justeattakeaway/pie-modal",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "PIE design system modal built using web components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,11 @@
22
22
  "license": "Apache-2.0",
23
23
  "devDependencies": {
24
24
  "@justeattakeaway/pie-components-config": "workspace:*",
25
- "@justeattakeaway/pie-webc-core": "workspace:*"
25
+ "@justeattakeaway/pie-webc-core": "workspace:*",
26
+ "@types/body-scroll-lock": "3.1.0"
27
+ },
28
+ "peerDependencies": {
29
+ "body-scroll-lock": "4.0.0-beta.0"
26
30
  },
27
31
  "volta": {
28
32
  "extends": "../../../package.json"
package/src/defs.ts CHANGED
@@ -1,28 +1,32 @@
1
+ export const headingLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
2
+
1
3
  export interface ModalProps {
2
4
  /**
3
- * the heading of the modal .
5
+ * (Required) The text to display in the modal's heading.
4
6
  */
5
7
  heading: string;
6
8
  /**
7
- * the rendered heading tag of the modal header.
8
- * @default h2
9
+ * (Optional) The HTML heading tag to use for the modal's heading. Can be H1-H6.
10
+ * @default "h2"
9
11
  */
10
- headingLevel: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
12
+ headingLevel: typeof headingLevels[number];
11
13
  /**
12
- * If `true`, the modal will be opened.
14
+ * (Optional) When true, the modal will be open.
13
15
  * @default false
14
16
  */
15
- isOpen?: boolean;
17
+ isOpen: boolean;
16
18
  }
17
19
 
18
20
  /**
19
- * Modal heading levels/tags
21
+ * Event name for when the modal is closed.
22
+ *
23
+ * @constant
20
24
  */
21
- export const headingLevels: ModalProps['headingLevel'][] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
25
+ export const ON_MODAL_CLOSE_EVENT = 'pie-modal-close';
22
26
 
23
27
  /**
24
- * Event name for when the modal is closed.
28
+ * Event name for when the modal is opened.
25
29
  *
26
30
  * @constant
27
31
  */
28
- export const ON_MODAL_CLOSE_EVENT = 'pie-modal-close';
32
+ export const ON_MODAL_OPEN_EVENT = 'pie-modal-open';
package/src/index.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { LitElement, unsafeCSS } from 'lit';
2
2
  import { html, unsafeStatic } from 'lit/static-html.js';
3
- import { property, query } from 'lit/decorators.js'; // eslint-disable-line import/no-extraneous-dependencies
4
- import { RtlMixin, validPropertyValues, requiredProperty } from '@justeattakeaway/pie-webc-core';
5
-
3
+ import { property, query } from 'lit/decorators.js';
4
+ import {
5
+ RtlMixin, validPropertyValues, requiredProperty,
6
+ } from '@justeattakeaway/pie-webc-core';
7
+ import type { DependentMap } from '@justeattakeaway/pie-webc-core';
8
+ import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
6
9
  import styles from './modal.scss?inline';
7
- import { ModalProps, headingLevels, ON_MODAL_CLOSE_EVENT } from './defs';
10
+ import {
11
+ ModalProps, headingLevels, ON_MODAL_CLOSE_EVENT, ON_MODAL_OPEN_EVENT,
12
+ } from './defs';
8
13
 
9
14
  // Valid values available to consumers
10
15
  export { type ModalProps, headingLevels };
@@ -24,11 +29,86 @@ export class PieModal extends RtlMixin(LitElement) {
24
29
  headingLevel: ModalProps['headingLevel'] = 'h2';
25
30
 
26
31
  @query('dialog')
27
- _dialog: HTMLDialogElement;
32
+ _dialog?: HTMLDialogElement;
33
+
34
+ constructor () {
35
+ super();
36
+ this.addEventListener('click', (event) => this._handleDialogLightDismiss(event));
37
+ }
38
+
39
+ firstUpdated (changedProperties: DependentMap<ModalProps>) : void {
40
+ this._handleModalOpenStateOnFirstRender(changedProperties);
41
+ }
42
+
43
+ updated (changedProperties: DependentMap<ModalProps>) : void {
44
+ this._handleModalOpenStateChanged(changedProperties);
45
+ }
46
+
47
+ /**
48
+ * Opens the dialog element and disables page scrolling
49
+ */
50
+ private _handleModalOpened () : void {
51
+ disableBodyScroll(this);
52
+ // We require this because toggling the prop `isOpen` itself won't
53
+ // allow the dialog to open in the correct way (with the default background),
54
+ // the method `showModal()` needs to be invoked.
55
+ this._dialog?.showModal();
56
+ }
57
+
58
+ /**
59
+ * Closes the dialog element and re-enables page scrolling
60
+ */
61
+ private _handleModalClosed () : void {
62
+ enableBodyScroll(this);
63
+ // Closes the native dialog element
64
+ this._dialog?.close();
65
+ }
66
+
67
+ /**
68
+ * This is only to be used inside the component template as direct property
69
+ * reassignment is not allowed.
70
+ */
71
+ private _triggerCloseModal = () : void => {
72
+ this.isOpen = false;
73
+ };
74
+
75
+ connectedCallback () : void {
76
+ super.connectedCallback();
77
+ document.addEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
78
+ document.addEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
79
+ }
80
+
81
+ disconnectedCallback () : void {
82
+ document.removeEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
83
+ document.removeEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
84
+ super.disconnectedCallback();
85
+ }
86
+
87
+ // Handles the value of the isOpen property on first render of the component
88
+ private _handleModalOpenStateOnFirstRender (changedProperties: DependentMap<ModalProps>) : void {
89
+ // This ensures if the modal is open on first render, the scroll lock and backdrop are applied
90
+ const previousValue = changedProperties.get('isOpen');
91
+
92
+ if (previousValue === undefined && this.isOpen) {
93
+ this._dispatchModalOpenEvent();
94
+ }
95
+ }
96
+
97
+ // Handles changes to the modal isOpen property by dispatching any appropriate events
98
+ private _handleModalOpenStateChanged (changedProperties: DependentMap<ModalProps>) : void {
99
+ const previousValue = changedProperties.get('isOpen');
100
+
101
+ if (previousValue !== undefined) {
102
+ if (previousValue) {
103
+ this._dispatchModalCloseEvent();
104
+ } else {
105
+ this._dispatchModalOpenEvent();
106
+ }
107
+ }
108
+ }
28
109
 
29
110
  render () {
30
111
  const {
31
- isOpen,
32
112
  heading,
33
113
  headingLevel = 'h2',
34
114
  } = this;
@@ -36,26 +116,46 @@ export class PieModal extends RtlMixin(LitElement) {
36
116
  const headingTag = unsafeStatic(headingLevel);
37
117
 
38
118
  return html`
39
- <dialog id="dialog" class="c-modal" ?open="${isOpen}">
119
+ <dialog id="dialog" class="c-modal">
40
120
  <header>
41
121
  <${headingTag} class="c-modal-heading">${heading}</${headingTag}>
42
122
  <pie-icon-button
43
- @click="${this._handleCloseDialog}"
123
+ @click="${this._triggerCloseModal}"
44
124
  variant="ghost-tertiary"
45
125
  class="c-modal-closeBtn"></pie-icon-button>
46
126
  </header>
47
- <article>
48
- <div class="c-modal-content">
49
- <slot></slot>
50
- </div>
127
+ <article class="c-modal-content">
128
+ <slot></slot>
51
129
  </article>
52
130
  </dialog>
53
131
  `;
54
132
  }
55
133
 
56
- _handleCloseDialog = () => {
57
- this._dialog.close();
58
- this._onDialogClose();
134
+ /**
135
+ * Dismisses the modal on backdrop click
136
+ *
137
+ */
138
+ private _handleDialogLightDismiss = (event: MouseEvent) : void => {
139
+ const rect = this._dialog?.getBoundingClientRect();
140
+
141
+ const {
142
+ top = 0, bottom = 0, left = 0, right = 0,
143
+ } = rect || {};
144
+
145
+ // This means the dialog is not open due to clicking the close button so
146
+ // we want to escape early
147
+ if (top === 0 && bottom === 0 && left === 0 && right === 0) {
148
+ return;
149
+ }
150
+
151
+ const isClickOutsideDialog = event.clientY < top ||
152
+ event.clientY > bottom ||
153
+ event.clientX < left ||
154
+ event.clientX > right;
155
+
156
+ if (isClickOutsideDialog) {
157
+ this.isOpen = false;
158
+ }
59
159
  };
60
160
 
61
161
  /**
@@ -64,8 +164,27 @@ export class PieModal extends RtlMixin(LitElement) {
64
164
  *
65
165
  * @event
66
166
  */
67
- _onDialogClose = () => {
68
- const event = new CustomEvent(ON_MODAL_CLOSE_EVENT);
167
+ private _dispatchModalCloseEvent = () : void => {
168
+ const event = new CustomEvent(ON_MODAL_CLOSE_EVENT, {
169
+ bubbles: true,
170
+ composed: true,
171
+ });
172
+
173
+ this.dispatchEvent(event);
174
+ };
175
+
176
+ /**
177
+ * Dispatch `ON_MODAL_OPEN_EVENT` event.
178
+ * To be used whenever we open the modal.
179
+ *
180
+ * @event
181
+ */
182
+ private _dispatchModalOpenEvent = () : void => {
183
+ const event = new CustomEvent(ON_MODAL_OPEN_EVENT, {
184
+ bubbles: true,
185
+ composed: true,
186
+ });
187
+
69
188
  this.dispatchEvent(event);
70
189
  };
71
190
 
package/src/modal.scss CHANGED
@@ -1,3 +1,5 @@
1
+ @use '@justeat/pie-design-tokens/dist/jet.scss' as dt;
2
+
1
3
  .c-modal {
2
4
  // Custom Property Declarations
3
5
  // These are defined here instead of :host to encapsulate them inside Shadow DOM
@@ -21,6 +23,12 @@
21
23
 
22
24
  box-shadow: var(--modal-elevation);
23
25
 
26
+ // We need to pull in the token directly here because the
27
+ // pseudo element `::backdrop` doesn't seem to pick up custom css properties.
28
+ &::backdrop {
29
+ background: #{dt.$color-overlay};
30
+ }
31
+
24
32
  & .c-modal-heading {
25
33
  // Modal header Custom Props
26
34
  --modal-header-font-size: calc(var(--dt-font-heading-m-size--wide) * 1px);
@@ -52,11 +60,14 @@
52
60
 
53
61
  padding-block: var(--modal-content-padding-block-start) var(--modal-content-padding);
54
62
  padding-inline: var(--modal-content-padding);
63
+
64
+ overflow-y: scroll;
65
+ max-block-size: 300px; // This is just a placeholder before we add the proper container styles to slotted content to demonstrate scrollable slot content
55
66
  }
56
67
 
57
68
  & .c-modal-closeBtn {
58
69
  position: absolute;
59
- right: var(--dt-spacing-d);
60
- top: var(--dt-spacing-d);
70
+ inset-inline-end: var(--dt-spacing-d);
71
+ inset-block-start: var(--dt-spacing-d);
61
72
  }
62
73
  }
@@ -1,5 +1,4 @@
1
1
  import { test, expect } from '@sand4rt/experimental-ct-web';
2
- import * as events from 'events';
3
2
  import { PieModal } from '@/index';
4
3
  import { headingLevels } from '@/defs';
5
4
 
@@ -21,33 +20,57 @@ headingLevels.forEach((headingLevel) => test(`should render the correct heading
21
20
  headingLevel,
22
21
  };
23
22
 
23
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
24
+ // @ts-ignore // Added this as we want to deliberately test with invalid headingLevel (which is an invalid type based on ModalProps)
24
25
  const component = await mount(PieModal, { props });
25
26
 
26
27
  // h2 is the default / fallback value
27
28
  await expect(component.locator('h2.c-modal-heading')).toContainText(props.heading);
28
29
  }));
29
30
 
30
- test.describe('`When the Pie Modal is closed`', () => {
31
- test.skip('should dispatch event `pie-modal-close`', async ({ mount }) => {
32
- const messages: string[] = [];
33
- const component = await mount(
34
- PieModal,
35
- {
36
- props: {
37
- isOpen: true,
31
+ test.describe('`Pie Modal is closed`', () => {
32
+ test.describe('when via the close button click', () => {
33
+ test.skip('should dispatch event `pie-modal-close`', async ({ mount, page }) => {
34
+ const messages: string[] = [];
35
+ await mount(
36
+ PieModal,
37
+ {
38
+ props: {
39
+ isOpen: true
40
+ },
41
+ on: {
42
+ click: (event: string) => messages.push(event),
43
+ },
38
44
  },
39
- },
40
- );
45
+ );
41
46
 
42
- await component.update({
43
- on: {
44
- click: (event: string) => messages.push(event),
45
- },
47
+ await page.locator('.c-modal-closeBtn').click();
48
+
49
+
50
+ expect(messages).toHaveLength(1);
46
51
  });
52
+ });
47
53
 
48
- await component.locator('.c-modal-closeBtn').click({ });
54
+ test.describe('when via the backdrop click', () => {
55
+ test.skip('should dispatch event `pie-modal-close`', async ({ mount, page }) => {
56
+ const messages: string[] = [];
57
+ await mount(
58
+ PieModal,
59
+ {
60
+ props: {
61
+ isOpen: true,
62
+ },
63
+ on: {
64
+ click: (event: string) => messages.push(event),
65
+ },
66
+ },
67
+ );
49
68
 
50
- expect(messages).toHaveLength(1);
69
+ await page.locator('#dialog').click();
70
+
71
+
72
+ expect(messages).toHaveLength(1);
73
+ });
51
74
  });
52
75
  });
53
76
 
@@ -1,7 +1,10 @@
1
1
  import { test } from '@sand4rt/experimental-ct-web';
2
2
  import percySnapshot from '@percy/playwright';
3
+ import {
4
+ WebComponentTestWrapper,
5
+ } from '@justeattakeaway/pie-webc-core/src/test-helpers/components/web-component-test-wrapper/WebComponentTestWrapper.ts';
6
+ import { WebComponentPropValues } from '@justeattakeaway/pie-webc-core/src/test-helpers/defs';
3
7
  import { PieModal } from '@/index';
4
- import { getLitPercyOptions } from '@justeattakeaway/pie-webc-core/src/test-helpers/percy-lit-options.ts';
5
8
 
6
9
  const propIsOpenValues = [{ heading: 'Modal Heading', isOpen: true }, { isOpen: false }];
7
10
 
@@ -16,5 +19,50 @@ propIsOpenValues.forEach((props) => test(`should render Modal correctly when pro
16
19
  },
17
20
  );
18
21
 
19
- await percySnapshot(page, `PIE Modal when isOpen is set to ${props.isOpen}`, getLitPercyOptions());
22
+ await percySnapshot(page, `PIE Modal when isOpen is set to ${props.isOpen}`);
20
23
  }));
24
+
25
+ const renderTestPieModal = (propVals: WebComponentPropValues) => `<pie-modal ${propVals.isOpen ? 'isOpen' : ''} heading="${propVals.heading}" headingLevel="${propVals.headingLevel}"></pie-modal>`;
26
+
27
+ // Creates a <ul> with a large number of <li> nodes for testing page scrolling
28
+ const createTestPageHTML = () : string => {
29
+ let list = '<ul>';
30
+
31
+ for (let i = 0; i < 200; i++) {
32
+ list += `<li>Item ${i}</li>`;
33
+ }
34
+
35
+ list += '</ul>';
36
+
37
+ return list;
38
+ };
39
+
40
+ test('Should not be able to scroll when modal is open', async ({ page, mount }) => {
41
+ const props: WebComponentPropValues = {
42
+ heading: 'I am a Modal!',
43
+ headingLevel: 'h2',
44
+ isOpen: true,
45
+ };
46
+
47
+ const modalComponent = renderTestPieModal(props);
48
+
49
+ await mount(
50
+ WebComponentTestWrapper,
51
+ {
52
+ props: {
53
+ pageMode: true,
54
+ },
55
+ slots: {
56
+ component: modalComponent,
57
+ pageMarkup: createTestPageHTML(),
58
+ },
59
+ },
60
+ );
61
+
62
+ // Scroll 800 pixels down the page
63
+ await page.mouse.wheel(0, 800);
64
+
65
+ await page.waitForTimeout(3000); // The mouse.wheel function causes scrolling, but doesn't wait for the scroll to finish before returning.
66
+
67
+ await percySnapshot(page, 'PIE Modal scroll locking');
68
+ });