@justeattakeaway/pie-modal 0.6.1 → 0.8.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
- [2:38:36 PM] @custom-elements-manifest/analyzer: Created new manifest.
1
+ [11:02:50 AM] @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
+ ✓ 23 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 12.03 kB │ gzip: 3.79 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 23.71s
18
- [vite:dts] Declaration files built in 21869ms.
12
+ ✓ built in 27.07s
13
+ [vite:dts] Declaration files built in 24624ms.
19
14
  
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @justeattakeaway/pie-modal
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [Added] - New size prop for pie-modal ([#572](https://github.com/justeattakeaway/pie/pull/572)) by [@xander-marjoram](https://github.com/xander-marjoram)
8
+
9
+ [Added] - Visual regression test for each size
10
+ [Changed] - Modal story to include new size prop
11
+
12
+ ## 0.7.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [Added] - Modal backdrop functionality ([#559](https://github.com/justeattakeaway/pie/pull/559)) by [@kevinrodrigues](https://github.com/kevinrodrigues)
17
+
18
+ - [Added] - Scroll locking to modal ([#564](https://github.com/justeattakeaway/pie/pull/564)) by [@jamieomaguire](https://github.com/jamieomaguire)
19
+
3
20
  ## 0.6.1
4
21
 
5
22
  ### Patch Changes
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 C, LitElement as x } from "lit";
2
+ import { unsafeStatic as E, html as L } from "lit/static-html.js";
3
+ import { property as y, query as T } from "lit/decorators.js";
4
+ import { property as $ } from "lit/decorators/property.js";
5
+ var D = Object.defineProperty, B = Object.getOwnPropertyDescriptor, k = (i, e, t, n) => {
6
+ for (var o = n > 1 ? void 0 : n ? B(e, t) : e, d = i.length - 1, a; d >= 0; d--)
7
+ (a = i[d]) && (o = (n ? a(e, t, o) : a(o)) || o);
8
+ return n && o && D(e, t, o), o;
9
9
  };
10
- const O = (r) => {
11
- class t extends r {
10
+ const R = (i) => {
11
+ class e extends i {
12
12
  constructor() {
13
13
  super(...arguments), this.dir = "";
14
14
  }
@@ -22,135 +22,248 @@ 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 k([
30
+ $({ type: String })
31
+ ], e.prototype, "dir", 2), e;
32
+ }, S = (i, e, t) => function(o, d) {
33
+ const a = `#${d}`;
34
+ Object.defineProperty(o, d, {
35
35
  get() {
36
36
  return this[a];
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 m = this[a];
40
+ e.includes(f) ? this[a] = f : (console.error(
41
+ `<${i}> Invalid value "${f}" provided for property "${d}".`,
42
+ `Must be one of: ${e.join(" | ")}.`,
43
+ `Falling back to default value: "${t}"`
44
+ ), this[a] = t), this.requestUpdate(d, m);
45
45
  }
46
46
  });
47
- }, x = (r) => function(t, n) {
47
+ }, A = (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) {
53
+ set(d) {
54
54
  const a = this[o];
55
- (e == null || e === "") && console.error(`<${r}> Missing required attribute "${n}"`), this[o] = e, this.requestUpdate(n, a);
55
+ (d == null || typeof d == "string" && d.trim() === "") && console.error(`<${i}> Missing required attribute "${n}"`), this[o] = d, this.requestUpdate(n, a);
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)}
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;
64
58
  };
65
- class c extends v {
66
- constructor() {
67
- super(...arguments), this.propKeyValues = "";
68
- }
69
- // Renders a string such as 'size: small, isFullWidth: true'
70
- // as HTML such as:
71
- // <p class="c-webComponentTestWrapper-label"><b>size</b>: <code>small</code></p>
72
- // <p class="c-webComponentTestWrapper-label"><b>isFullWidth</b>: <code>true</code></p>
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>`;
77
- });
78
- }
79
- // eslint-disable-next-line class-methods-use-this
80
- render() {
81
- return m`
82
- <div class="c-webComponentTestWrapper">
83
- ${this._renderPropKeyValues()}
84
- <div class="c-webComponentTestWrapper-slot">
85
- <slot></slot>
86
- </div>
87
- </div>`;
88
- }
59
+ function q(i) {
60
+ if (Array.isArray(i)) {
61
+ for (var e = 0, t = Array(i.length); e < i.length; e++)
62
+ t[e] = i[e];
63
+ return t;
64
+ } else
65
+ return Array.from(i);
89
66
  }
90
- c.styles = g(z);
91
- 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;
67
+ var _ = !1;
68
+ if (typeof window < "u") {
69
+ var z = {
70
+ get passive() {
71
+ _ = !0;
72
+ }
73
+ };
74
+ window.addEventListener("testPassive", null, z), window.removeEventListener("testPassive", null, z);
75
+ }
76
+ var p = typeof window < "u" && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1), r = [], v = !1, P = -1, c = void 0, l = void 0, u = void 0, M = function(e) {
77
+ return r.some(function(t) {
78
+ return !!(t.options.allowTouchMove && t.options.allowTouchMove(e));
79
+ });
80
+ }, g = function(e) {
81
+ var t = e || window.event;
82
+ return M(t.target) || t.touches.length > 1 ? !0 : (t.preventDefault && t.preventDefault(), !1);
83
+ }, I = function(e) {
84
+ if (u === void 0) {
85
+ var t = !!e && e.reserveScrollBarGap === !0, n = window.innerWidth - document.documentElement.clientWidth;
86
+ if (t && n > 0) {
87
+ var o = parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right"), 10);
88
+ u = document.body.style.paddingRight, document.body.style.paddingRight = o + n + "px";
89
+ }
90
+ }
91
+ c === void 0 && (c = document.body.style.overflow, document.body.style.overflow = "hidden");
92
+ }, V = function() {
93
+ u !== void 0 && (document.body.style.paddingRight = u, u = void 0), c !== void 0 && (document.body.style.overflow = c, c = void 0);
94
+ }, j = function() {
95
+ return window.requestAnimationFrame(function() {
96
+ if (l === void 0) {
97
+ l = {
98
+ position: document.body.style.position,
99
+ top: document.body.style.top,
100
+ left: document.body.style.left
101
+ };
102
+ var e = window, t = e.scrollY, n = e.scrollX, o = e.innerHeight;
103
+ document.body.style.position = "fixed", document.body.style.top = -t, document.body.style.left = -n, setTimeout(function() {
104
+ return window.requestAnimationFrame(function() {
105
+ var d = o - window.innerHeight;
106
+ d && t >= o && (document.body.style.top = -(t + d));
107
+ });
108
+ }, 300);
109
+ }
110
+ });
111
+ }, F = function() {
112
+ if (l !== void 0) {
113
+ var e = -parseInt(document.body.style.top, 10), t = -parseInt(document.body.style.left, 10);
114
+ document.body.style.position = l.position, document.body.style.top = l.top, document.body.style.left = l.left, window.scrollTo(t, e), l = void 0;
115
+ }
116
+ }, H = function(e) {
117
+ return e ? e.scrollHeight - e.scrollTop <= e.clientHeight : !1;
118
+ }, N = function(e, t) {
119
+ var n = e.targetTouches[0].clientY - P;
120
+ return M(e.target) ? !1 : t && t.scrollTop === 0 && n > 0 || H(t) && n < 0 ? g(e) : (e.stopPropagation(), !0);
121
+ }, Y = function(e, t) {
122
+ if (!e) {
123
+ console.error("disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.");
124
+ return;
125
+ }
126
+ if (!r.some(function(o) {
127
+ return o.targetElement === e;
128
+ })) {
129
+ var n = {
130
+ targetElement: e,
131
+ options: t || {}
132
+ };
133
+ r = [].concat(q(r), [n]), p ? j() : I(t), p && (e.ontouchstart = function(o) {
134
+ o.targetTouches.length === 1 && (P = o.targetTouches[0].clientY);
135
+ }, e.ontouchmove = function(o) {
136
+ o.targetTouches.length === 1 && N(o, e);
137
+ }, v || (document.addEventListener("touchmove", g, _ ? { passive: !1 } : void 0), v = !0));
138
+ }
139
+ }, U = function(e) {
140
+ if (!e) {
141
+ console.error("enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.");
142
+ return;
143
+ }
144
+ r = r.filter(function(t) {
145
+ return t.targetElement !== e;
146
+ }), p && (e.ontouchstart = null, e.ontouchmove = null, v && r.length === 0 && (document.removeEventListener("touchmove", g, _ ? { passive: !1 } : void 0), v = !1)), p ? F() : V();
102
147
  };
103
- const p = "pie-modal";
104
- class d extends O(v) {
148
+ const X = `.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;box-shadow:var(--modal-elevation);font-family:var(--modal-font);background-color:var(--modal-bg-color);padding:0;--modal-max-inline-size: var(--modal-size-m);--modal-inline-size: 75%;max-inline-size:var(--modal-max-inline-size);inline-size:var(--modal-inline-size)}.c-modal[size=small]{--modal-max-inline-size: var(--modal-size-s)}.c-modal[size=large]{--modal-max-inline-size: var(--modal-size-l);--modal-inline-size: 100%}@media (min-width: 768px){.c-modal[size=large]{--modal-inline-size: 75%}}.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)}
149
+ `, W = ["h1", "h2", "h3", "h4", "h5", "h6"], G = ["small", "medium", "large"], w = "pie-modal-close", O = "pie-modal-open";
150
+ var J = Object.defineProperty, Q = Object.getOwnPropertyDescriptor, h = (i, e, t, n) => {
151
+ for (var o = n > 1 ? void 0 : n ? Q(e, t) : e, d = i.length - 1, a; d >= 0; d--)
152
+ (a = i[d]) && (o = (n ? a(e, t, o) : a(o)) || o);
153
+ return n && o && J(e, t, o), o;
154
+ };
155
+ const b = "pie-modal";
156
+ class s extends R(x) {
105
157
  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
- };
158
+ super(), this.isOpen = !1, this.headingLevel = "h2", this.size = "medium", this._triggerCloseModal = () => {
159
+ this.isOpen = !1;
160
+ }, this._handleDialogLightDismiss = (e) => {
161
+ var m;
162
+ const t = (m = this._dialog) == null ? void 0 : m.getBoundingClientRect(), {
163
+ top: n = 0,
164
+ bottom: o = 0,
165
+ left: d = 0,
166
+ right: a = 0
167
+ } = t || {};
168
+ if (n === 0 && o === 0 && d === 0 && a === 0)
169
+ return;
170
+ (e.clientY < n || e.clientY > o || e.clientX < d || e.clientX > a) && (this.isOpen = !1);
171
+ }, this._dispatchModalCloseEvent = () => {
172
+ const e = new CustomEvent(w, {
173
+ bubbles: !0,
174
+ composed: !0
175
+ });
176
+ this.dispatchEvent(e);
177
+ }, this._dispatchModalOpenEvent = () => {
178
+ const e = new CustomEvent(O, {
179
+ bubbles: !0,
180
+ composed: !0
181
+ });
182
+ this.dispatchEvent(e);
183
+ }, this.addEventListener("click", (e) => this._handleDialogLightDismiss(e));
184
+ }
185
+ firstUpdated(e) {
186
+ this._handleModalOpenStateOnFirstRender(e);
187
+ }
188
+ updated(e) {
189
+ this._handleModalOpenStateChanged(e);
190
+ }
191
+ /**
192
+ * Opens the dialog element and disables page scrolling
193
+ */
194
+ _handleModalOpened() {
195
+ var e;
196
+ Y(this), (e = this._dialog) == null || e.showModal();
197
+ }
198
+ /**
199
+ * Closes the dialog element and re-enables page scrolling
200
+ */
201
+ _handleModalClosed() {
202
+ var e;
203
+ U(this), (e = this._dialog) == null || e.close();
204
+ }
205
+ connectedCallback() {
206
+ super.connectedCallback(), document.addEventListener(O, this._handleModalOpened.bind(this)), document.addEventListener(w, this._handleModalClosed.bind(this));
207
+ }
208
+ disconnectedCallback() {
209
+ document.removeEventListener(O, this._handleModalOpened.bind(this)), document.removeEventListener(w, this._handleModalClosed.bind(this)), super.disconnectedCallback();
210
+ }
211
+ // Handles the value of the isOpen property on first render of the component
212
+ _handleModalOpenStateOnFirstRender(e) {
213
+ e.get("isOpen") === void 0 && this.isOpen && this._dispatchModalOpenEvent();
214
+ }
215
+ // Handles changes to the modal isOpen property by dispatching any appropriate events
216
+ _handleModalOpenStateChanged(e) {
217
+ const t = e.get("isOpen");
218
+ t !== void 0 && (t ? this._dispatchModalCloseEvent() : this._dispatchModalOpenEvent());
112
219
  }
113
220
  render() {
114
221
  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}">
222
+ heading: e,
223
+ headingLevel: t = "h2",
224
+ size: n
225
+ } = this, o = E(t);
226
+ return L`
227
+ <dialog
228
+ id="dialog"
229
+ size="${n}"
230
+ class="c-modal">
121
231
  <header>
122
- <${e} class="c-modal-heading">${n}</${e}>
232
+ <${o} class="c-modal-heading">${e}</${o}>
123
233
  <pie-icon-button
124
- @click="${this._handleCloseDialog}"
125
- variant="ghost-tertiary"
234
+ @click="${this._triggerCloseModal}"
235
+ variant="ghost-secondary"
126
236
  class="c-modal-closeBtn"></pie-icon-button>
127
237
  </header>
128
- <article>
129
- <div class="c-modal-content">
130
- <slot></slot>
131
- </div>
238
+ <article class="c-modal-content">
239
+ <slot></slot>
132
240
  </article>
133
241
  </dialog>
134
242
  `;
135
243
  }
136
244
  }
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);
245
+ s.styles = C(X);
246
+ h([
247
+ y({ type: Boolean })
248
+ ], s.prototype, "isOpen", 2);
249
+ h([
250
+ y({ type: String }),
251
+ A(b)
252
+ ], s.prototype, "heading", 2);
253
+ h([
254
+ y(),
255
+ S(b, W, "h2")
256
+ ], s.prototype, "headingLevel", 2);
257
+ h([
258
+ y(),
259
+ S(b, G, "medium")
260
+ ], s.prototype, "size", 2);
261
+ h([
262
+ T("dialog")
263
+ ], s.prototype, "_dialog", 2);
264
+ customElements.define(b, s);
153
265
  export {
154
- d as PieModal,
155
- j as headingLevels
266
+ s as PieModal,
267
+ W as headingLevels,
268
+ G as sizes
156
269
  };
@@ -1,27 +1,33 @@
1
+ export declare const headingLevels: readonly ["h1", "h2", "h3", "h4", "h5", "h6"];
2
+ export declare const sizes: readonly ["small", "medium", "large"];
1
3
  export interface ModalProps {
2
4
  /**
3
- * the heading of the modal .
5
+ * 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
+ * The HTML heading tag to use for the modal's heading. Can be h1-h6.
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
- * @default false
13
+ * When true, the modal will be open.
14
14
  */
15
- isOpen?: boolean;
15
+ isOpen: boolean;
16
+ /**
17
+ * The size of the modal; this controls how wide it will appear on the page.
18
+ */
19
+ size: typeof sizes[number];
16
20
  }
17
- /**
18
- * Modal heading levels/tags
19
- */
20
- export declare const headingLevels: Array<ModalProps['headingLevel']>;
21
21
  /**
22
22
  * Event name for when the modal is closed.
23
23
  *
24
24
  * @constant
25
25
  */
26
26
  export declare const ON_MODAL_CLOSE_EVENT = "pie-modal-close";
27
+ /**
28
+ * Event name for when the modal is opened.
29
+ *
30
+ * @constant
31
+ */
32
+ export declare const ON_MODAL_OPEN_EVENT = "pie-modal-open";
27
33
  //# 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,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,CAAwC,CAAC;AAErG;;;;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;AAC3E,eAAO,MAAM,KAAK,uCAAwC,CAAC;AAE3D,MAAM,WAAW,UAAU;IACvB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,YAAY,EAAE,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;IAC3C;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB;;OAEG;IACH,IAAI,EAAE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mBAAmB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { LitElement } from 'lit';
2
- import { ModalProps, headingLevels } from './defs';
3
- export { type ModalProps, headingLevels };
2
+ import type { DependentMap } from '@justeattakeaway/pie-webc-core';
3
+ import { ModalProps, headingLevels, sizes } from './defs';
4
+ export { type ModalProps, headingLevels, sizes };
4
5
  declare const componentSelector = "pie-modal";
5
6
  declare const PieModal_base: (new (...args: any[]) => {
6
7
  dir: string;
@@ -10,16 +11,48 @@ export declare class PieModal extends PieModal_base {
10
11
  isOpen: boolean;
11
12
  heading: string;
12
13
  headingLevel: ModalProps['headingLevel'];
13
- _dialog: HTMLDialogElement;
14
+ size: ModalProps['size'];
15
+ _dialog?: HTMLDialogElement;
16
+ constructor();
17
+ firstUpdated(changedProperties: DependentMap<ModalProps>): void;
18
+ updated(changedProperties: DependentMap<ModalProps>): void;
19
+ /**
20
+ * Opens the dialog element and disables page scrolling
21
+ */
22
+ private _handleModalOpened;
23
+ /**
24
+ * Closes the dialog element and re-enables page scrolling
25
+ */
26
+ private _handleModalClosed;
27
+ /**
28
+ * This is only to be used inside the component template as direct property
29
+ * reassignment is not allowed.
30
+ */
31
+ private _triggerCloseModal;
32
+ connectedCallback(): void;
33
+ disconnectedCallback(): void;
34
+ private _handleModalOpenStateOnFirstRender;
35
+ private _handleModalOpenStateChanged;
14
36
  render(): import("lit-html").TemplateResult;
15
- _handleCloseDialog: () => void;
37
+ /**
38
+ * Dismisses the modal on backdrop click
39
+ *
40
+ */
41
+ private _handleDialogLightDismiss;
16
42
  /**
17
43
  * Dispatch `ON_MODAL_CLOSE_EVENT` event.
18
44
  * To be used whenever we close the modal.
19
45
  *
20
46
  * @event
21
47
  */
22
- _onDialogClose: () => void;
48
+ private _dispatchModalCloseEvent;
49
+ /**
50
+ * Dispatch `ON_MODAL_OPEN_EVENT` event.
51
+ * To be used whenever we open the modal.
52
+ *
53
+ * @event
54
+ */
55
+ private _dispatchModalOpenEvent;
23
56
  static styles: import("lit").CSSResult;
24
57
  }
25
58
  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,EACV,aAAa,EAGb,KAAK,EACR,MAAM,QAAQ,CAAC;AAGhB,OAAO,EAAE,KAAK,UAAU,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;AAEjD,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;IAIhD,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAY;IAGpC,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;IA4BN;;;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.1",
3
+ "version": "0.8.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,12 @@
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-icon-button": "workspace:*",
26
+ "@justeattakeaway/pie-webc-core": "workspace:*",
27
+ "@types/body-scroll-lock": "3.1.0"
28
+ },
29
+ "peerDependencies": {
30
+ "body-scroll-lock": "4.0.0-beta.0"
26
31
  },
27
32
  "volta": {
28
33
  "extends": "../../../package.json"
package/src/defs.ts CHANGED
@@ -1,28 +1,35 @@
1
+ export const headingLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
2
+ export const sizes = ['small', 'medium', 'large'] as const;
3
+
1
4
  export interface ModalProps {
2
5
  /**
3
- * the heading of the modal .
6
+ * The text to display in the modal's heading.
4
7
  */
5
8
  heading: string;
6
9
  /**
7
- * the rendered heading tag of the modal header.
8
- * @default h2
10
+ * The HTML heading tag to use for the modal's heading. Can be h1-h6.
11
+ */
12
+ headingLevel: typeof headingLevels[number];
13
+ /**
14
+ * When true, the modal will be open.
9
15
  */
10
- headingLevel: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
16
+ isOpen: boolean;
11
17
  /**
12
- * If `true`, the modal will be opened.
13
- * @default false
18
+ * The size of the modal; this controls how wide it will appear on the page.
14
19
  */
15
- isOpen?: boolean;
20
+ size: typeof sizes[number];
16
21
  }
17
22
 
18
23
  /**
19
- * Modal heading levels/tags
24
+ * Event name for when the modal is closed.
25
+ *
26
+ * @constant
20
27
  */
21
- export const headingLevels: Array<ModalProps['headingLevel']> = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
28
+ export const ON_MODAL_CLOSE_EVENT = 'pie-modal-close';
22
29
 
23
30
  /**
24
- * Event name for when the modal is closed.
31
+ * Event name for when the modal is opened.
25
32
  *
26
33
  * @constant
27
34
  */
28
- export const ON_MODAL_CLOSE_EVENT = 'pie-modal-close';
35
+ export const ON_MODAL_OPEN_EVENT = 'pie-modal-open';
package/src/index.ts CHANGED
@@ -1,13 +1,22 @@
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,
12
+ headingLevels,
13
+ ON_MODAL_CLOSE_EVENT,
14
+ ON_MODAL_OPEN_EVENT,
15
+ sizes,
16
+ } from './defs';
8
17
 
9
18
  // Valid values available to consumers
10
- export { type ModalProps, headingLevels };
19
+ export { type ModalProps, headingLevels, sizes };
11
20
 
12
21
  const componentSelector = 'pie-modal';
13
22
 
@@ -23,39 +32,142 @@ export class PieModal extends RtlMixin(LitElement) {
23
32
  @validPropertyValues(componentSelector, headingLevels, 'h2')
24
33
  headingLevel: ModalProps['headingLevel'] = 'h2';
25
34
 
35
+ @property()
36
+ @validPropertyValues(componentSelector, sizes, 'medium')
37
+ size: ModalProps['size'] = 'medium';
38
+
26
39
  @query('dialog')
27
- _dialog: HTMLDialogElement;
40
+ _dialog?: HTMLDialogElement;
41
+
42
+ constructor () {
43
+ super();
44
+ this.addEventListener('click', (event) => this._handleDialogLightDismiss(event));
45
+ }
46
+
47
+ firstUpdated (changedProperties: DependentMap<ModalProps>) : void {
48
+ this._handleModalOpenStateOnFirstRender(changedProperties);
49
+ }
50
+
51
+ updated (changedProperties: DependentMap<ModalProps>) : void {
52
+ this._handleModalOpenStateChanged(changedProperties);
53
+ }
54
+
55
+ /**
56
+ * Opens the dialog element and disables page scrolling
57
+ */
58
+ private _handleModalOpened () : void {
59
+ disableBodyScroll(this);
60
+ // We require this because toggling the prop `isOpen` itself won't
61
+ // allow the dialog to open in the correct way (with the default background),
62
+ // the method `showModal()` needs to be invoked.
63
+ this._dialog?.showModal();
64
+ }
65
+
66
+ /**
67
+ * Closes the dialog element and re-enables page scrolling
68
+ */
69
+ private _handleModalClosed () : void {
70
+ enableBodyScroll(this);
71
+ // Closes the native dialog element
72
+ this._dialog?.close();
73
+ }
74
+
75
+ /**
76
+ * This is only to be used inside the component template as direct property
77
+ * reassignment is not allowed.
78
+ */
79
+ private _triggerCloseModal = () : void => {
80
+ this.isOpen = false;
81
+ };
82
+
83
+ connectedCallback () : void {
84
+ super.connectedCallback();
85
+ document.addEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
86
+ document.addEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
87
+ }
88
+
89
+ disconnectedCallback () : void {
90
+ document.removeEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
91
+ document.removeEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
92
+ super.disconnectedCallback();
93
+ }
94
+
95
+ // Handles the value of the isOpen property on first render of the component
96
+ private _handleModalOpenStateOnFirstRender (changedProperties: DependentMap<ModalProps>) : void {
97
+ // This ensures if the modal is open on first render, the scroll lock and backdrop are applied
98
+ const previousValue = changedProperties.get('isOpen');
99
+
100
+ if (previousValue === undefined && this.isOpen) {
101
+ this._dispatchModalOpenEvent();
102
+ }
103
+ }
104
+
105
+ // Handles changes to the modal isOpen property by dispatching any appropriate events
106
+ private _handleModalOpenStateChanged (changedProperties: DependentMap<ModalProps>) : void {
107
+ const previousValue = changedProperties.get('isOpen');
108
+
109
+ if (previousValue !== undefined) {
110
+ if (previousValue) {
111
+ this._dispatchModalCloseEvent();
112
+ } else {
113
+ this._dispatchModalOpenEvent();
114
+ }
115
+ }
116
+ }
28
117
 
29
118
  render () {
30
119
  const {
31
- isOpen,
32
120
  heading,
33
121
  headingLevel = 'h2',
122
+ size,
34
123
  } = this;
35
124
 
36
125
  const headingTag = unsafeStatic(headingLevel);
37
126
 
38
127
  return html`
39
- <dialog id="dialog" class="c-modal" ?open="${isOpen}">
128
+ <dialog
129
+ id="dialog"
130
+ size="${size}"
131
+ class="c-modal">
40
132
  <header>
41
133
  <${headingTag} class="c-modal-heading">${heading}</${headingTag}>
42
134
  <pie-icon-button
43
- @click="${this._handleCloseDialog}"
44
- variant="ghost-tertiary"
135
+ @click="${this._triggerCloseModal}"
136
+ variant="ghost-secondary"
45
137
  class="c-modal-closeBtn"></pie-icon-button>
46
138
  </header>
47
- <article>
48
- <div class="c-modal-content">
49
- <slot></slot>
50
- </div>
139
+ <article class="c-modal-content">
140
+ <slot></slot>
51
141
  </article>
52
142
  </dialog>
53
143
  `;
54
144
  }
55
145
 
56
- _handleCloseDialog = () => {
57
- this._dialog.close();
58
- this._onDialogClose();
146
+ /**
147
+ * Dismisses the modal on backdrop click
148
+ *
149
+ */
150
+ private _handleDialogLightDismiss = (event: MouseEvent) : void => {
151
+ const rect = this._dialog?.getBoundingClientRect();
152
+
153
+ const {
154
+ top = 0, bottom = 0, left = 0, right = 0,
155
+ } = rect || {};
156
+
157
+ // This means the dialog is not open due to clicking the close button so
158
+ // we want to escape early
159
+ if (top === 0 && bottom === 0 && left === 0 && right === 0) {
160
+ return;
161
+ }
162
+
163
+ const isClickOutsideDialog = event.clientY < top ||
164
+ event.clientY > bottom ||
165
+ event.clientX < left ||
166
+ event.clientX > right;
167
+
168
+ if (isClickOutsideDialog) {
169
+ this.isOpen = false;
170
+ }
59
171
  };
60
172
 
61
173
  /**
@@ -64,8 +176,27 @@ export class PieModal extends RtlMixin(LitElement) {
64
176
  *
65
177
  * @event
66
178
  */
67
- _onDialogClose = () => {
68
- const event = new CustomEvent(ON_MODAL_CLOSE_EVENT);
179
+ private _dispatchModalCloseEvent = () : void => {
180
+ const event = new CustomEvent(ON_MODAL_CLOSE_EVENT, {
181
+ bubbles: true,
182
+ composed: true,
183
+ });
184
+
185
+ this.dispatchEvent(event);
186
+ };
187
+
188
+ /**
189
+ * Dispatch `ON_MODAL_OPEN_EVENT` event.
190
+ * To be used whenever we open the modal.
191
+ *
192
+ * @event
193
+ */
194
+ private _dispatchModalOpenEvent = () : void => {
195
+ const event = new CustomEvent(ON_MODAL_OPEN_EVENT, {
196
+ bubbles: true,
197
+ composed: true,
198
+ });
199
+
69
200
  this.dispatchEvent(event);
70
201
  };
71
202
 
package/src/modal.scss CHANGED
@@ -1,6 +1,10 @@
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
6
+ $breakpoint-wide: 768px;
7
+
4
8
  --modal-size-s: 450px;
5
9
  --modal-size-m: 600px;
6
10
  --modal-size-l: 1080px;
@@ -13,13 +17,40 @@
13
17
 
14
18
  border-radius: var(--modal-border-radius);
15
19
  border: none;
20
+ box-shadow: var(--modal-elevation);
16
21
  font-family: var(--modal-font);
17
22
  background-color: var(--modal-bg-color);
18
23
 
19
24
  padding: 0;
20
- inline-size: var(--modal-size-m);
21
25
 
22
- box-shadow: var(--modal-elevation);
26
+ --modal-max-inline-size: var(--modal-size-m);
27
+ --modal-inline-size: 75%;
28
+
29
+ max-inline-size: var(--modal-max-inline-size);
30
+ inline-size: var(--modal-inline-size);
31
+
32
+ &[size='small'] {
33
+ --modal-max-inline-size: var(--modal-size-s);
34
+ }
35
+
36
+ &[size='medium'] {
37
+ /* Same as default styles */
38
+ }
39
+
40
+ &[size='large'] {
41
+ --modal-max-inline-size: var(--modal-size-l);
42
+ --modal-inline-size: 100%;
43
+
44
+ @media (min-width: $breakpoint-wide) {
45
+ --modal-inline-size: 75%;
46
+ }
47
+ }
48
+
49
+ // We need to pull in the token directly here because the
50
+ // pseudo element `::backdrop` doesn't seem to pick up custom css properties.
51
+ &::backdrop {
52
+ background: #{dt.$color-overlay};
53
+ }
23
54
 
24
55
  & .c-modal-heading {
25
56
  // Modal header Custom Props
@@ -52,11 +83,14 @@
52
83
 
53
84
  padding-block: var(--modal-content-padding-block-start) var(--modal-content-padding);
54
85
  padding-inline: var(--modal-content-padding);
86
+
87
+ overflow-y: scroll;
88
+ 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
89
  }
56
90
 
57
91
  & .c-modal-closeBtn {
58
92
  position: absolute;
59
- right: var(--dt-spacing-d);
60
- top: var(--dt-spacing-d);
93
+ inset-inline-end: var(--dt-spacing-d);
94
+ inset-block-start: var(--dt-spacing-d);
61
95
  }
62
96
  }
@@ -1,8 +1,25 @@
1
1
  import { test, expect } from '@sand4rt/experimental-ct-web';
2
- import * as events from 'events';
2
+ import { PieIconButton } from '@justeattakeaway/pie-icon-button';
3
3
  import { PieModal } from '@/index';
4
4
  import { headingLevels } from '@/defs';
5
5
 
6
+ // Mount any components that are used inside of pie-modal so that
7
+ // they have been registered with the browser before the tests run.
8
+ // There is likely a nicer way to do this but this will temporarily
9
+ // unblock tests.
10
+ test.beforeEach(async ({ page, mount }) => {
11
+ await mount(
12
+ PieIconButton,
13
+ {},
14
+ );
15
+
16
+ // Removing the element so it's not present in the tests (but is still registered in the DOM)
17
+ await page.evaluate(() => {
18
+ const element : Element | null = document.querySelector('pie-icon-button');
19
+ element?.remove();
20
+ });
21
+ });
22
+
6
23
  headingLevels.forEach((headingLevel) => test(`should render the correct heading tag based on the value of headingLevel: ${headingLevel}`, async ({ mount }) => {
7
24
  const props = {
8
25
  heading: 'Modal Header',
@@ -21,33 +38,55 @@ headingLevels.forEach((headingLevel) => test(`should render the correct heading
21
38
  headingLevel,
22
39
  };
23
40
 
41
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
42
+ // @ts-ignore // Added this as we want to deliberately test with invalid headingLevel (which is an invalid type based on ModalProps)
24
43
  const component = await mount(PieModal, { props });
25
44
 
26
45
  // h2 is the default / fallback value
27
46
  await expect(component.locator('h2.c-modal-heading')).toContainText(props.heading);
28
47
  }));
29
48
 
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,
49
+ test.describe('`Pie Modal is closed`', () => {
50
+ test.describe('when via the close button click', () => {
51
+ test('should dispatch event `pie-modal-close`', async ({ mount, page }) => {
52
+ const messages: string[] = [];
53
+ await mount(
54
+ PieModal,
55
+ {
56
+ props: {
57
+ isOpen: true,
58
+ },
59
+ on: {
60
+ click: (event: string) => messages.push(event),
61
+ },
38
62
  },
39
- },
40
- );
63
+ );
64
+
65
+ await page.locator('.c-modal-closeBtn').click();
41
66
 
42
- await component.update({
43
- on: {
44
- click: (event: string) => messages.push(event),
45
- },
67
+ expect(messages).toHaveLength(1);
46
68
  });
69
+ });
70
+
71
+ test.describe('when via the backdrop click', () => {
72
+ test('should dispatch event `pie-modal-close`', async ({ mount, page }) => {
73
+ const messages: string[] = [];
74
+ await mount(
75
+ PieModal,
76
+ {
77
+ props: {
78
+ isOpen: true,
79
+ },
80
+ on: {
81
+ click: (event: string) => messages.push(event),
82
+ },
83
+ },
84
+ );
47
85
 
48
- await component.locator('.c-modal-closeBtn').click({ });
86
+ await page.locator('#dialog').click();
49
87
 
50
- expect(messages).toHaveLength(1);
88
+ expect(messages).toHaveLength(1);
89
+ });
51
90
  });
52
91
  });
53
92
 
@@ -1,20 +1,90 @@
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-testing/src/helpers/components/web-component-test-wrapper/WebComponentTestWrapper.ts';
6
+ import { PieIconButton } from '@justeattakeaway/pie-icon-button';
3
7
  import { PieModal } from '@/index';
4
- import { getLitPercyOptions } from '@justeattakeaway/pie-webc-core/src/test-helpers/percy-lit-options.ts';
8
+ import { ModalProps, sizes } from '@/defs';
5
9
 
6
- const propIsOpenValues = [{ heading: 'Modal Heading', isOpen: true }, { isOpen: false }];
10
+ // Renders a <pie-modal> HTML string with the given prop values
11
+ const renderTestPieModal = ({
12
+ heading = 'This is a modal heading',
13
+ headingLevel = 'h2',
14
+ size = 'medium',
15
+ isOpen = true,
16
+ } : Partial<ModalProps> = {}) => `<pie-modal ${isOpen ? 'isOpen' : ''} heading="${heading}" headingLevel="${headingLevel}" size="${size}"></pie-modal>`;
17
+
18
+ // Creates a <ol> with a large number of <li> nodes for testing page scrolling
19
+ const createTestPageHTML = () => `<ol>
20
+ ${'<li>List item</li>'.repeat(200)}
21
+ </ol>`;
22
+
23
+ // Mount any components that are used inside of pie-modal so that
24
+ // they have been registered with the browser before the tests run.
25
+ // There is likely a nicer way to do this but this will temporarily
26
+ // unblock tests.
27
+ test.beforeEach(async ({ page, mount }) => {
28
+ await mount(
29
+ PieIconButton,
30
+ {},
31
+ );
32
+
33
+ // Removing the element so it's not present in the tests (but is still registered in the DOM)
34
+ await page.evaluate(() => {
35
+ const element : Element | null = document.querySelector('pie-icon-button');
36
+ element?.remove();
37
+ });
38
+ });
39
+
40
+ test('Should not be able to scroll when modal is open', async ({ page, mount }) => {
41
+ const modalComponent = renderTestPieModal();
7
42
 
8
- propIsOpenValues.forEach((props) => test(`should render Modal correctly when prop isOpen is set to ${props.isOpen}`, async ({ page, mount }) => {
9
43
  await mount(
10
- PieModal,
44
+ WebComponentTestWrapper,
11
45
  {
12
- props,
46
+ props: {
47
+ pageMode: true,
48
+ },
13
49
  slots: {
14
- default: 'Hello, this is the Pie Modal!',
50
+ component: modalComponent,
51
+ pageMarkup: createTestPageHTML(),
15
52
  },
16
53
  },
17
54
  );
18
55
 
19
- await percySnapshot(page, `PIE Modal when isOpen is set to ${props.isOpen}`, getLitPercyOptions());
20
- }));
56
+ // Scroll 800 pixels down the page
57
+ await page.mouse.wheel(0, 800);
58
+
59
+ await page.waitForTimeout(3000); // The mouse.wheel function causes scrolling, but doesn't wait for the scroll to finish before returning.
60
+
61
+ await percySnapshot(page, 'Modal - scroll locking');
62
+ });
63
+
64
+ test('should not render when isOpen = false', async ({ page, mount }) => {
65
+ await mount(PieModal, {
66
+ props: {
67
+ heading: 'This is a modal heading',
68
+ headingLevel: 'h2',
69
+ isOpen: false,
70
+ size: 'medium',
71
+ },
72
+ });
73
+
74
+ await percySnapshot(page, 'Modal - isOpen = false');
75
+ });
76
+
77
+ sizes.forEach((size) => {
78
+ test(`should render correctly with size = ${size}`, async ({ page, mount }) => {
79
+ await mount(PieModal, {
80
+ props: {
81
+ heading: 'This is a modal heading',
82
+ headingLevel: 'h2',
83
+ isOpen: true,
84
+ size,
85
+ },
86
+ });
87
+
88
+ await percySnapshot(page, `Modal - size = ${size}`);
89
+ });
90
+ });