@justeattakeaway/pie-modal 0.9.0 → 0.10.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 +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;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;IAGvC,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,CAKqC;IAG9D,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.10.0",
4
4
  "description": "PIE design system modal built using web components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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 {
@@ -21,9 +23,6 @@ export { type ModalProps, headingLevels, sizes };
21
23
  const componentSelector = 'pie-modal';
22
24
 
23
25
  export class PieModal extends RtlMixin(LitElement) {
24
- @property({ type: Boolean })
25
- public isOpen = false;
26
-
27
26
  @property({ type: String })
28
27
  @requiredProperty(componentSelector)
29
28
  public heading!: string;
@@ -32,6 +31,18 @@ export class PieModal extends RtlMixin(LitElement) {
32
31
  @validPropertyValues(componentSelector, headingLevels, 'h2')
33
32
  public headingLevel: ModalProps['headingLevel'] = 'h2';
34
33
 
34
+ @property({ type: Boolean, reflect: true })
35
+ public isDismissible = false;
36
+
37
+ @property({ type: Boolean })
38
+ public isFullWidthBelowMid = false;
39
+
40
+ @property({ type: Boolean })
41
+ public isOpen = false;
42
+
43
+ @property()
44
+ public returnFocusAfterCloseSelector?: string;
45
+
35
46
  @property()
36
47
  @validPropertyValues(componentSelector, sizes, 'medium')
37
48
  public size: ModalProps['size'] = 'medium';
@@ -44,8 +55,25 @@ export class PieModal extends RtlMixin(LitElement) {
44
55
  this.addEventListener('click', (event) => this._handleDialogLightDismiss(event));
45
56
  }
46
57
 
58
+ connectedCallback () : void {
59
+ super.connectedCallback();
60
+ document.addEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
61
+ document.addEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
62
+ }
63
+
64
+ disconnectedCallback () : void {
65
+ document.removeEventListener(ON_MODAL_OPEN_EVENT, this._handleModalOpened.bind(this));
66
+ document.removeEventListener(ON_MODAL_CLOSE_EVENT, this._handleModalClosed.bind(this));
67
+ super.disconnectedCallback();
68
+ }
69
+
47
70
  firstUpdated (changedProperties: DependentMap<ModalProps>) : void {
71
+ this._dialog?.addEventListener('cancel', (event) => this._handleDialogCancelEvent(event));
48
72
  this._handleModalOpenStateOnFirstRender(changedProperties);
73
+
74
+ this._dialog?.addEventListener('close', () => {
75
+ this.isOpen = false;
76
+ });
49
77
  }
50
78
 
51
79
  updated (changedProperties: DependentMap<ModalProps>) : void {
@@ -57,9 +85,10 @@ export class PieModal extends RtlMixin(LitElement) {
57
85
  */
58
86
  private _handleModalOpened () : void {
59
87
  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.
88
+ if (this._dialog?.hasAttribute('open') || !this._dialog?.isConnected) {
89
+ return;
90
+ }
91
+ // The ::backdrop pseudoelement is only shown if the modal is opened via JS
63
92
  this._dialog?.showModal();
64
93
  }
65
94
 
@@ -68,37 +97,29 @@ export class PieModal extends RtlMixin(LitElement) {
68
97
  */
69
98
  private _handleModalClosed () : void {
70
99
  enableBodyScroll(this);
71
- // Closes the native dialog element
72
100
  this._dialog?.close();
101
+ this._returnFocus();
73
102
  }
74
103
 
75
104
  /**
76
- * This is only to be used inside the component template as direct property
77
- * reassignment is not allowed.
105
+ * Prevents the user from dismissing the dialog via the `cancel`
106
+ * event (ESC key) when `isDismissible` is set to false.
107
+ *
108
+ * @param {Event} event - The event object.
78
109
  */
79
- private _triggerCloseModal = () : void => {
80
- this.isOpen = false;
110
+ private _handleDialogCancelEvent = (event: Event) : void => {
111
+ if (!this.isDismissible) {
112
+ event.preventDefault();
113
+ }
81
114
  };
82
115
 
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
116
  // Handles the value of the isOpen property on first render of the component
96
117
  private _handleModalOpenStateOnFirstRender (changedProperties: DependentMap<ModalProps>) : void {
97
118
  // This ensures if the modal is open on first render, the scroll lock and backdrop are applied
98
119
  const previousValue = changedProperties.get('isOpen');
99
120
 
100
121
  if (previousValue === undefined && this.isOpen) {
101
- this._dispatchModalOpenEvent();
122
+ this._dispatchModalCustomEvent(ON_MODAL_OPEN_EVENT);
102
123
  }
103
124
  }
104
125
 
@@ -108,18 +129,32 @@ export class PieModal extends RtlMixin(LitElement) {
108
129
 
109
130
  if (previousValue !== undefined) {
110
131
  if (previousValue) {
111
- this._dispatchModalCloseEvent();
132
+ this._dispatchModalCustomEvent(ON_MODAL_CLOSE_EVENT);
112
133
  } else {
113
- this._dispatchModalOpenEvent();
134
+ this._dispatchModalCustomEvent(ON_MODAL_OPEN_EVENT);
114
135
  }
115
136
  }
116
137
  }
117
138
 
139
+ /**
140
+ * Return focus to the specified element, providing the selector is valid
141
+ * and the chosen element can be found.
142
+ * Fails silently.
143
+ */
144
+ private _returnFocus () : void {
145
+ const selector = this.returnFocusAfterCloseSelector?.trim();
146
+
147
+ if (selector) {
148
+ (document.querySelector(selector) as HTMLElement)?.focus();
149
+ }
150
+ }
151
+
118
152
  render () {
119
153
  const {
120
154
  heading,
121
155
  headingLevel = 'h2',
122
156
  size,
157
+ isFullWidthBelowMid,
123
158
  } = this;
124
159
 
125
160
  const headingTag = unsafeStatic(headingLevel);
@@ -127,14 +162,12 @@ export class PieModal extends RtlMixin(LitElement) {
127
162
  return html`
128
163
  <dialog
129
164
  id="dialog"
165
+ class="c-modal"
130
166
  size="${size}"
131
- class="c-modal">
167
+ ?isFullWidthBelowMid=${isFullWidthBelowMid}>
132
168
  <header>
133
169
  <${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>
170
+ ${this.isDismissible ? this.renderCloseButton() : nothing}
138
171
  </header>
139
172
  <article class="c-modal-content">
140
173
  <slot></slot>
@@ -144,10 +177,14 @@ export class PieModal extends RtlMixin(LitElement) {
144
177
  }
145
178
 
146
179
  /**
147
- * Dismisses the modal on backdrop click
148
- *
180
+ * Dismisses the modal on backdrop click if `isDismissible` is `true`.
181
+ * @param {MouseEvent} event - the click event targetting the modal/backdrop
149
182
  */
150
183
  private _handleDialogLightDismiss = (event: MouseEvent) : void => {
184
+ if (!this.isDismissible) {
185
+ return;
186
+ }
187
+
151
188
  const rect = this._dialog?.getBoundingClientRect();
152
189
 
153
190
  const {
@@ -161,9 +198,9 @@ export class PieModal extends RtlMixin(LitElement) {
161
198
  }
162
199
 
163
200
  const isClickOutsideDialog = event.clientY < top ||
164
- event.clientY > bottom ||
165
- event.clientX < left ||
166
- event.clientX > right;
201
+ event.clientY > bottom ||
202
+ event.clientX < left ||
203
+ event.clientX > right;
167
204
 
168
205
  if (isClickOutsideDialog) {
169
206
  this.isOpen = false;
@@ -171,13 +208,18 @@ export class PieModal extends RtlMixin(LitElement) {
171
208
  };
172
209
 
173
210
  /**
174
- * Dispatch `ON_MODAL_CLOSE_EVENT` event.
175
- * To be used whenever we close the modal.
211
+ * Note: We should aim to have a shareable event helper system to allow
212
+ * us to share this across components in-future.
213
+ *
214
+ * Dispatch a custom event.
176
215
  *
177
- * @event
216
+ * To be used whenever we have behavioural events we want to
217
+ * bubble up through the modal.
218
+ *
219
+ * @param {string} eventType
178
220
  */
179
- private _dispatchModalCloseEvent = () : void => {
180
- const event = new CustomEvent(ON_MODAL_CLOSE_EVENT, {
221
+ private _dispatchModalCustomEvent = (eventType: string) : void => {
222
+ const event = new CustomEvent(eventType, {
181
223
  bubbles: true,
182
224
  composed: true,
183
225
  });
@@ -186,19 +228,17 @@ export class PieModal extends RtlMixin(LitElement) {
186
228
  };
187
229
 
188
230
  /**
189
- * Dispatch `ON_MODAL_OPEN_EVENT` event.
190
- * To be used whenever we open the modal.
231
+ * Template for the close button element. Called within the
232
+ * main render function.
191
233
  *
192
- * @event
234
+ * @private
193
235
  */
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
- };
236
+ private renderCloseButton = () : TemplateResult => html`
237
+ <pie-icon-button
238
+ @click="${() => { this.isOpen = false; }}"
239
+ variant="ghost-secondary"
240
+ class="c-modal-closeBtn"
241
+ data-test-id="modal-close-button"></pie-icon-button>`;
202
242
 
203
243
  // Renders a `CSSResult` generated from SCSS by Vite
204
244
  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'] {