@justeattakeaway/pie-modal 0.9.0 → 0.11.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,4 +1,4 @@
1
- import { LitElement } from 'lit';
1
+ import { LitElement, TemplateResult } from 'lit';
2
2
  import type { DependentMap } from '@justeattakeaway/pie-webc-core';
3
3
  import { ModalProps, headingLevels, sizes } from './defs';
4
4
  export { type ModalProps, headingLevels, sizes };
@@ -7,13 +7,18 @@ declare const PieModal_base: (new (...args: any[]) => {
7
7
  dir: string;
8
8
  isRTL: boolean;
9
9
  }) & typeof LitElement;
10
- export declare class PieModal extends PieModal_base {
11
- isOpen: boolean;
10
+ export declare class PieModal extends PieModal_base implements ModalProps {
12
11
  heading: string;
13
12
  headingLevel: ModalProps['headingLevel'];
13
+ isDismissible: boolean;
14
+ isFullWidthBelowMid: boolean;
15
+ isOpen: boolean;
16
+ returnFocusAfterCloseSelector?: string;
14
17
  size: ModalProps['size'];
15
18
  _dialog?: HTMLDialogElement;
16
19
  constructor();
20
+ connectedCallback(): void;
21
+ disconnectedCallback(): void;
17
22
  firstUpdated(changedProperties: DependentMap<ModalProps>): void;
18
23
  updated(changedProperties: DependentMap<ModalProps>): void;
19
24
  /**
@@ -25,34 +30,45 @@ export declare class PieModal extends PieModal_base {
25
30
  */
26
31
  private _handleModalClosed;
27
32
  /**
28
- * This is only to be used inside the component template as direct property
29
- * reassignment is not allowed.
33
+ * Prevents the user from dismissing the dialog via the `cancel`
34
+ * event (ESC key) when `isDismissible` is set to false.
35
+ *
36
+ * @param {Event} event - The event object.
30
37
  */
31
- private _triggerCloseModal;
32
- connectedCallback(): void;
33
- disconnectedCallback(): void;
38
+ private _handleDialogCancelEvent;
34
39
  private _handleModalOpenStateOnFirstRender;
35
40
  private _handleModalOpenStateChanged;
36
- render(): import("lit-html").TemplateResult;
37
41
  /**
38
- * Dismisses the modal on backdrop click
39
- *
42
+ * Return focus to the specified element, providing the selector is valid
43
+ * and the chosen element can be found.
44
+ * Fails silently.
45
+ */
46
+ private _returnFocus;
47
+ render(): TemplateResult;
48
+ /**
49
+ * Dismisses the modal on backdrop click if `isDismissible` is `true`.
50
+ * @param {MouseEvent} event - the click event targetting the modal/backdrop
40
51
  */
41
52
  private _handleDialogLightDismiss;
42
53
  /**
43
- * Dispatch `ON_MODAL_CLOSE_EVENT` event.
44
- * To be used whenever we close the modal.
54
+ * Note: We should aim to have a shareable event helper system to allow
55
+ * us to share this across components in-future.
56
+ *
57
+ * Dispatch a custom event.
58
+ *
59
+ * To be used whenever we have behavioural events we want to
60
+ * bubble up through the modal.
45
61
  *
46
- * @event
62
+ * @param {string} eventType
47
63
  */
48
- private _dispatchModalCloseEvent;
64
+ private _dispatchModalCustomEvent;
49
65
  /**
50
- * Dispatch `ON_MODAL_OPEN_EVENT` event.
51
- * To be used whenever we open the modal.
66
+ * Template for the close button element. Called within the
67
+ * main render function.
52
68
  *
53
- * @event
69
+ * @private
54
70
  */
55
- private _dispatchModalOpenEvent;
71
+ private renderCloseButton;
56
72
  static styles: import("lit").CSSResult;
57
73
  }
58
74
  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,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;IAEvC,MAAM,UAAS;IAIf,OAAO,EAAG,MAAM,CAAC;IAIjB,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC,CAAQ;IAIhD,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAY;IAGvC,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,UAAU,EAAW,cAAc,EACtC,MAAM,KAAK,CAAC;AAMb,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,iDAAiD,CAAC;AAEzD,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,aAAqB,YAAW,UAAU;IAG7D,OAAO,EAAG,MAAM,CAAC;IAIjB,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC,CAAQ;IAGhD,aAAa,UAAS;IAGtB,mBAAmB,UAAS;IAG5B,MAAM,UAAS;IAGf,6BAA6B,CAAC,EAAE,MAAM,CAAC;IAIvC,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAY;IAGvC,OAAO,CAAC,EAAE,iBAAiB,CAAC;;IAOhC,iBAAiB,IAAM,IAAI;IAM3B,oBAAoB,IAAM,IAAI;IAM9B,YAAY,CAAE,iBAAiB,EAAE,YAAY,CAAC,UAAU,CAAC,GAAI,IAAI;IASjE,OAAO,CAAE,iBAAiB,EAAE,YAAY,CAAC,UAAU,CAAC,GAAI,IAAI;IAI5D;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB,CAI9B;IAGF,OAAO,CAAC,kCAAkC;IAU1C,OAAO,CAAC,4BAA4B;IAYpC;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAQpB,MAAM;IA2BN;;;OAGG;IACH,OAAO,CAAC,yBAAyB,CAyB/B;IAEF;;;;;;;;;;OAUG;IACH,OAAO,CAAC,yBAAyB,CAO/B;IAEF;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAWzB,MAAM,CAAC,MAAM,0BAAqB;CACrC;AAID,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC;KACjC;CACJ"}
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justeattakeaway/pie-modal",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "PIE design system modal built using web components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,6 +23,7 @@
23
23
  "devDependencies": {
24
24
  "@justeattakeaway/pie-components-config": "workspace:*",
25
25
  "@justeattakeaway/pie-icon-button": "workspace:*",
26
+ "@justeattakeaway/pie-icons-webc": "workspace:*",
26
27
  "@justeattakeaway/pie-webc-core": "workspace:*",
27
28
  "@types/body-scroll-lock": "3.1.0"
28
29
  },
package/src/defs.ts CHANGED
@@ -6,14 +6,40 @@ export interface ModalProps {
6
6
  * The text to display in the modal's heading.
7
7
  */
8
8
  heading: string;
9
+
9
10
  /**
10
11
  * The HTML heading tag to use for the modal's heading. Can be h1-h6.
11
12
  */
12
13
  headingLevel: typeof headingLevels[number];
14
+
13
15
  /**
14
16
  * When true, the modal will be open.
15
17
  */
16
18
  isOpen: boolean;
19
+
20
+ /**
21
+ * When set to `true`:
22
+ * 1. The close button within the modal will be visible.
23
+ * 2. The user can dismiss the modal via the ESCAPE key, clicking the backdrop
24
+ * or via a close button.
25
+ *
26
+ * When set to `false`:
27
+ * 1. The close button within the modal will be hidden.
28
+ * 2. The user can NOT dismiss the modal via the ESCAPE key or clicking the backdrop.
29
+ *
30
+ */
31
+ isDismissible: boolean;
32
+
33
+ /**
34
+ * This controls whether a *medium-sized* modal will cover the full width of the page when below the mid breakpoint.
35
+ */
36
+ isFullWidthBelowMid: boolean;
37
+
38
+ /**
39
+ * The selector for the element that you would like focus to be returned to when the modal is closed, e.g., #skipToMain
40
+ */
41
+ returnFocusAfterCloseSelector?: string;
42
+
17
43
  /**
18
44
  * The size of the modal; this controls how wide it will appear on the page.
19
45
  */
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { LitElement, unsafeCSS } from 'lit';
1
+ import {
2
+ LitElement, nothing, TemplateResult, unsafeCSS,
3
+ } from 'lit';
2
4
  import { html, unsafeStatic } from 'lit/static-html.js';
3
5
  import { property, query } from 'lit/decorators.js';
4
6
  import {
@@ -6,6 +8,7 @@ import {
6
8
  } from '@justeattakeaway/pie-webc-core';
7
9
  import type { DependentMap } from '@justeattakeaway/pie-webc-core';
8
10
  import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
11
+ import '@justeattakeaway/pie-icons-webc/icons/IconClose';
9
12
  import styles from './modal.scss?inline';
10
13
  import {
11
14
  ModalProps,
@@ -20,10 +23,7 @@ export { type ModalProps, headingLevels, sizes };
20
23
 
21
24
  const componentSelector = 'pie-modal';
22
25
 
23
- export class PieModal extends RtlMixin(LitElement) {
24
- @property({ type: Boolean })
25
- public isOpen = false;
26
-
26
+ export class PieModal extends RtlMixin(LitElement) implements ModalProps {
27
27
  @property({ type: String })
28
28
  @requiredProperty(componentSelector)
29
29
  public heading!: string;
@@ -32,6 +32,18 @@ export class PieModal extends RtlMixin(LitElement) {
32
32
  @validPropertyValues(componentSelector, headingLevels, 'h2')
33
33
  public headingLevel: ModalProps['headingLevel'] = 'h2';
34
34
 
35
+ @property({ type: Boolean, reflect: true })
36
+ public isDismissible = false;
37
+
38
+ @property({ type: Boolean })
39
+ public isFullWidthBelowMid = false;
40
+
41
+ @property({ type: Boolean })
42
+ public isOpen = false;
43
+
44
+ @property()
45
+ public returnFocusAfterCloseSelector?: string;
46
+
35
47
  @property()
36
48
  @validPropertyValues(componentSelector, sizes, 'medium')
37
49
  public size: ModalProps['size'] = 'medium';
@@ -44,8 +56,25 @@ export class PieModal extends RtlMixin(LitElement) {
44
56
  this.addEventListener('click', (event) => this._handleDialogLightDismiss(event));
45
57
  }
46
58
 
59
+ connectedCallback () : void {
60
+ super.connectedCallback();
61
+ document.addEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
62
+ document.addEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
63
+ }
64
+
65
+ disconnectedCallback () : void {
66
+ document.removeEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
67
+ document.removeEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
68
+ super.disconnectedCallback();
69
+ }
70
+
47
71
  firstUpdated (changedProperties: DependentMap<ModalProps>) : void {
72
+ this._dialog?.addEventListener('cancel', (event) => this._handleDialogCancelEvent(event));
48
73
  this._handleModalOpenStateOnFirstRender(changedProperties);
74
+
75
+ this._dialog?.addEventListener('close', () => {
76
+ this.isOpen = false;
77
+ });
49
78
  }
50
79
 
51
80
  updated (changedProperties: DependentMap<ModalProps>) : void {
@@ -57,9 +86,10 @@ export class PieModal extends RtlMixin(LitElement) {
57
86
  */
58
87
  private _handleModalOpened () : void {
59
88
  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.
89
+ if (this._dialog?.hasAttribute('open') || !this._dialog?.isConnected) {
90
+ return;
91
+ }
92
+ // The ::backdrop pseudoelement is only shown if the modal is opened via JS
63
93
  this._dialog?.showModal();
64
94
  }
65
95
 
@@ -68,37 +98,29 @@ export class PieModal extends RtlMixin(LitElement) {
68
98
  */
69
99
  private _handleModalClosed () : void {
70
100
  enableBodyScroll(this);
71
- // Closes the native dialog element
72
101
  this._dialog?.close();
102
+ this._returnFocus();
73
103
  }
74
104
 
75
105
  /**
76
- * This is only to be used inside the component template as direct property
77
- * reassignment is not allowed.
106
+ * Prevents the user from dismissing the dialog via the `cancel`
107
+ * event (ESC key) when `isDismissible` is set to false.
108
+ *
109
+ * @param {Event} event - The event object.
78
110
  */
79
- private _triggerCloseModal = () : void => {
80
- this.isOpen = false;
111
+ private _handleDialogCancelEvent = (event: Event) : void => {
112
+ if (!this.isDismissible) {
113
+ event.preventDefault();
114
+ }
81
115
  };
82
116
 
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
117
  // Handles the value of the isOpen property on first render of the component
96
118
  private _handleModalOpenStateOnFirstRender (changedProperties: DependentMap<ModalProps>) : void {
97
119
  // This ensures if the modal is open on first render, the scroll lock and backdrop are applied
98
120
  const previousValue = changedProperties.get('isOpen');
99
121
 
100
122
  if (previousValue === undefined && this.isOpen) {
101
- this._dispatchModalOpenEvent();
123
+ this._dispatchModalCustomEvent(ON_MODAL_OPEN_EVENT);
102
124
  }
103
125
  }
104
126
 
@@ -108,18 +130,32 @@ export class PieModal extends RtlMixin(LitElement) {
108
130
 
109
131
  if (previousValue !== undefined) {
110
132
  if (previousValue) {
111
- this._dispatchModalCloseEvent();
133
+ this._dispatchModalCustomEvent(ON_MODAL_CLOSE_EVENT);
112
134
  } else {
113
- this._dispatchModalOpenEvent();
135
+ this._dispatchModalCustomEvent(ON_MODAL_OPEN_EVENT);
114
136
  }
115
137
  }
116
138
  }
117
139
 
140
+ /**
141
+ * Return focus to the specified element, providing the selector is valid
142
+ * and the chosen element can be found.
143
+ * Fails silently.
144
+ */
145
+ private _returnFocus () : void {
146
+ const selector = this.returnFocusAfterCloseSelector?.trim();
147
+
148
+ if (selector) {
149
+ (document.querySelector(selector) as HTMLElement)?.focus();
150
+ }
151
+ }
152
+
118
153
  render () {
119
154
  const {
120
155
  heading,
121
156
  headingLevel = 'h2',
122
157
  size,
158
+ isFullWidthBelowMid,
123
159
  } = this;
124
160
 
125
161
  const headingTag = unsafeStatic(headingLevel);
@@ -127,14 +163,12 @@ export class PieModal extends RtlMixin(LitElement) {
127
163
  return html`
128
164
  <dialog
129
165
  id="dialog"
166
+ class="c-modal"
130
167
  size="${size}"
131
- class="c-modal">
168
+ ?isFullWidthBelowMid=${isFullWidthBelowMid}>
132
169
  <header>
133
170
  <${headingTag} class="c-modal-heading">${heading}</${headingTag}>
134
- <pie-icon-button
135
- @click="${this._triggerCloseModal}"
136
- variant="ghost-secondary"
137
- class="c-modal-closeBtn"></pie-icon-button>
171
+ ${this.isDismissible ? this.renderCloseButton() : nothing}
138
172
  </header>
139
173
  <article class="c-modal-content">
140
174
  <slot></slot>
@@ -144,10 +178,14 @@ export class PieModal extends RtlMixin(LitElement) {
144
178
  }
145
179
 
146
180
  /**
147
- * Dismisses the modal on backdrop click
148
- *
181
+ * Dismisses the modal on backdrop click if `isDismissible` is `true`.
182
+ * @param {MouseEvent} event - the click event targetting the modal/backdrop
149
183
  */
150
184
  private _handleDialogLightDismiss = (event: MouseEvent) : void => {
185
+ if (!this.isDismissible) {
186
+ return;
187
+ }
188
+
151
189
  const rect = this._dialog?.getBoundingClientRect();
152
190
 
153
191
  const {
@@ -161,9 +199,9 @@ export class PieModal extends RtlMixin(LitElement) {
161
199
  }
162
200
 
163
201
  const isClickOutsideDialog = event.clientY < top ||
164
- event.clientY > bottom ||
165
- event.clientX < left ||
166
- event.clientX > right;
202
+ event.clientY > bottom ||
203
+ event.clientX < left ||
204
+ event.clientX > right;
167
205
 
168
206
  if (isClickOutsideDialog) {
169
207
  this.isOpen = false;
@@ -171,13 +209,18 @@ export class PieModal extends RtlMixin(LitElement) {
171
209
  };
172
210
 
173
211
  /**
174
- * Dispatch `ON_MODAL_CLOSE_EVENT` event.
175
- * To be used whenever we close the modal.
212
+ * Note: We should aim to have a shareable event helper system to allow
213
+ * us to share this across components in-future.
214
+ *
215
+ * Dispatch a custom event.
176
216
  *
177
- * @event
217
+ * To be used whenever we have behavioural events we want to
218
+ * bubble up through the modal.
219
+ *
220
+ * @param {string} eventType
178
221
  */
179
- private _dispatchModalCloseEvent = () : void => {
180
- const event = new CustomEvent(ON_MODAL_CLOSE_EVENT, {
222
+ private _dispatchModalCustomEvent = (eventType: string) : void => {
223
+ const event = new CustomEvent(eventType, {
181
224
  bubbles: true,
182
225
  composed: true,
183
226
  });
@@ -186,19 +229,20 @@ export class PieModal extends RtlMixin(LitElement) {
186
229
  };
187
230
 
188
231
  /**
189
- * Dispatch `ON_MODAL_OPEN_EVENT` event.
190
- * To be used whenever we open the modal.
232
+ * Template for the close button element. Called within the
233
+ * main render function.
191
234
  *
192
- * @event
235
+ * @private
193
236
  */
194
- private _dispatchModalOpenEvent = () : void => {
195
- const event = new CustomEvent(ON_MODAL_OPEN_EVENT, {
196
- bubbles: true,
197
- composed: true,
198
- });
199
-
200
- this.dispatchEvent(event);
201
- };
237
+ private renderCloseButton (): TemplateResult {
238
+ return html`
239
+ <pie-icon-button
240
+ @click="${() => { this.isOpen = false; }}"
241
+ variant="ghost-secondary"
242
+ class="c-modal-closeBtn"
243
+ data-test-id="modal-close-button"><icon-close /></pie-icon-button>
244
+ `;
245
+ }
202
246
 
203
247
  // Renders a `CSSResult` generated from SCSS by Vite
204
248
  static styles = unsafeCSS(styles);
package/src/modal.scss CHANGED
@@ -35,6 +35,11 @@
35
35
 
36
36
  &[size='medium'] {
37
37
  /* Same as default styles */
38
+ &[isfullwidthbelowmid] {
39
+ @media (max-width: $breakpoint-wide) {
40
+ --modal-inline-size: 100%;
41
+ }
42
+ }
38
43
  }
39
44
 
40
45
  &[size='large'] {