@justeattakeaway/pie-modal 0.13.0 → 0.16.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.
package/src/modal.scss CHANGED
@@ -33,10 +33,24 @@
33
33
  --modal-bg-color: var(--dt-color-container-default);
34
34
  --modal-elevation: var(--dt-elevation-04);
35
35
 
36
+ // TODO: This should be moved into global CSS typography setting
37
+ // This should be imported by consuming apps and set on the application body
38
+ text-rendering: optimizelegibility;
39
+ -webkit-font-smoothing: antialiased;
40
+ -moz-font-smoothing: antialiased;
41
+
36
42
  &:focus-visible {
37
43
  outline: none;
38
44
  }
39
45
 
46
+ // We need to override the icon sizes at different screen sizes regardless of size prop passed in
47
+ pie-icon-button {
48
+ @media (max-width: $breakpoint-wide) {
49
+ --btn-dimension: 40px;
50
+ }
51
+ }
52
+
53
+
40
54
  &[open] {
41
55
  // We only apply this when the modal is open,
42
56
  // otherwise it interferes with the native
@@ -110,6 +124,16 @@
110
124
  }
111
125
  }
112
126
 
127
+ &[position='top'] {
128
+ margin-block-start: var(--dt-spacing-j);
129
+
130
+ &[isfullwidthbelowmid] {
131
+ @media (max-width: calc($breakpoint-wide - 1px)) {
132
+ margin-block-start: var(--dt-spacing-none);
133
+ }
134
+ }
135
+ }
136
+
113
137
  // We need to pull in the token directly here because the
114
138
  // pseudo element `::backdrop` doesn't seem to pick up custom css properties.
115
139
  &::backdrop {
@@ -131,24 +155,51 @@
131
155
  }
132
156
  }
133
157
 
134
- & .c-modal-header {
135
- --modal-header-padding: var(--dt-spacing-e);
158
+ // When hasStackedActions is set
159
+ // change the direction of the footer flex container so buttons are full width of modal
160
+ &[hasstackedactions] {
161
+ & .c-modal-footer {
162
+ // TODO: Move breakpoint sizes into shared CSS component utilities
163
+ @media (max-width: calc($breakpoint-wide - 1px)) {
164
+ flex-direction: column;
165
+ }
166
+ }
167
+ }
136
168
 
137
- padding-inline: var(--modal-header-padding);
138
- padding-block: var(--modal-header-padding);
169
+ & .c-modal-header {
170
+ padding-inline: var(--dt-spacing-d);
171
+ padding-block: 14px; // This is deliberately not a custom property
172
+ display: grid;
173
+ grid-template-areas:
174
+ 'back heading close'
175
+ '. heading .';
176
+ grid-template-columns: minmax(0, max-content) minmax(0, 1fr) minmax(0, max-content);
139
177
  align-items: center;
140
- display: flex;
178
+
179
+ @media (min-width: $breakpoint-wide) {
180
+ padding-inline: var(--dt-spacing-e);
181
+ padding-block: 20px; // This is deliberately not a custom property
182
+ }
141
183
  }
142
184
 
143
185
  &[hasbackbutton] .c-modal-header {
144
- padding-block: var(--dt-spacing-c);
145
- padding-inline-start: var(--dt-spacing-c);
186
+ padding-block: var(--dt-spacing-b);
187
+ padding-inline-start: var(--dt-spacing-b);
188
+
189
+ @media (min-width: $breakpoint-wide) {
190
+ padding-block: var(--dt-spacing-c);
191
+ padding-inline-start: var(--dt-spacing-c);
192
+ }
146
193
  }
147
194
 
148
195
  &[isdismissible] .c-modal-header {
149
- justify-content: space-between;
150
- padding-block: var(--dt-spacing-c);
151
- padding-inline-end: var(--dt-spacing-c);
196
+ padding-block: var(--dt-spacing-b);
197
+ padding-inline-end: var(--dt-spacing-b);
198
+
199
+ @media (min-width: $breakpoint-wide) {
200
+ padding-block: var(--dt-spacing-c);
201
+ padding-inline-end: var(--dt-spacing-c);
202
+ }
152
203
  }
153
204
 
154
205
  & .c-modal-heading {
@@ -160,16 +211,59 @@
160
211
  line-height: var(--modal-header-font-line-height);
161
212
  font-weight: var(--modal-header-font-weight);
162
213
  margin: 0;
214
+ grid-area: heading;
163
215
  }
164
216
 
165
217
  // Ensure correct padding when there is a back button in front of the heading
166
218
  &[hasbackbutton] .c-modal-heading {
167
- margin-inline-start: var(--dt-spacing-c);
219
+ padding-inline-start: var(--dt-spacing-b);
220
+
221
+ @media (min-width: $breakpoint-wide) {
222
+ padding-inline-start: var(--dt-spacing-c);
223
+ }
168
224
  }
169
225
 
170
226
  // Ensure correct padding when there is a close button behind the heading
171
227
  &[isdismissible] .c-modal-heading {
172
- margin-inline-end: var(--dt-spacing-e);
228
+ padding-inline-end: var(--dt-spacing-d);
229
+
230
+ @media (min-width: $breakpoint-wide) {
231
+ padding-inline-end: var(--dt-spacing-e);
232
+ }
233
+ }
234
+
235
+ & .c-modal-backBtn {
236
+ grid-area: back;
237
+ }
238
+
239
+ & .c-modal-closeBtn {
240
+ grid-area: close;
241
+ }
242
+
243
+ &[isfooterpinned] .c-modal-content,
244
+ & .c-modal-scrollContainer {
245
+ overflow-y: auto;
246
+ }
247
+
248
+ & .c-modal-scrollContainer {
249
+ // These are the shadows used to indicate scrolling above and below content
250
+ --bg-scroll-start: linear-gradient(var(--dt-color-container-default) 30%, rgba(255, 255, 255, 0));
251
+ --bg-scroll-end: linear-gradient(rgba(255, 255, 255, 0), var(--dt-color-container-default) 70%) 0 100%;
252
+ --bg-scroll-top: radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
253
+ --bg-scroll-bottom: radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0)) 0 100%;
254
+
255
+ // Sizes of the scroll shadows
256
+ --bg-size-scroll-start: 100% 40px;
257
+ --bg-size-scroll-end: 100% 40px;
258
+ --bg-size-scroll-top: 100% 16px;
259
+ --bg-size-scroll-bottom: 100% 16px;
260
+
261
+ background: var(--bg-scroll-start), var(--bg-scroll-end), var(--bg-scroll-top), var(--bg-scroll-bottom);
262
+ background-repeat: no-repeat;
263
+ background-color: var(--dt-color-container-default);
264
+ background-size: var(--bg-size-scroll-start), var(--bg-size-scroll-end), var(--bg-size-scroll-top), var(--bg-size-scroll-bottom);
265
+
266
+ background-attachment: local, local, scroll, scroll;
173
267
  }
174
268
 
175
269
  & .c-modal-content {
@@ -198,7 +292,7 @@
198
292
  --spinner-animation-iteration-count: infinite;
199
293
 
200
294
  position: relative;
201
- min-block-size: 60px;
295
+ min-block-size: var(--dt-spacing-j);
202
296
 
203
297
  font-size: var(--modal-content-font-size);
204
298
  line-height: var(--modal-content-line-height);
@@ -207,8 +301,6 @@
207
301
  padding-inline: var(--modal-content-padding);
208
302
  padding-block: var(--modal-content-padding-block);
209
303
 
210
- overflow-y: auto;
211
-
212
304
  &--scrollable {
213
305
  background:
214
306
  // Scroll shadow cover
@@ -258,8 +350,4 @@
258
350
  opacity: 0;
259
351
  }
260
352
  }
261
-
262
- & .c-modal-closeBtn {
263
- margin-inline-start: auto;
264
- }
265
353
  }
@@ -5,7 +5,7 @@ import { PieIconButton } from '@justeattakeaway/pie-icon-button';
5
5
  import {
6
6
  WebComponentTestWrapper,
7
7
  } from '@justeattakeaway/pie-webc-testing/src/helpers/components/web-component-test-wrapper/WebComponentTestWrapper.ts';
8
- import { renderTestPieModal } from '../helpers/index.ts';
8
+ import { createScrollablePageHTML, renderTestPieModal } from '../helpers/index.ts';
9
9
 
10
10
  import { PieModal } from '@/index';
11
11
  import {
@@ -14,7 +14,7 @@ import {
14
14
  headingLevels,
15
15
  } from '@/defs';
16
16
 
17
- const modalSelector = '[data-test-id="pie-modal"]';
17
+ const componentSelector = '[data-test-id="pie-modal"]';
18
18
  const backButtonSelector = '[data-test-id="modal-back-button"]';
19
19
  const closeButtonSelector = '[data-test-id="modal-close-button"]';
20
20
 
@@ -38,7 +38,7 @@ test.describe('modal', () => {
38
38
  });
39
39
 
40
40
  // Act
41
- const modal = page.locator(modalSelector);
41
+ const modal = page.locator(componentSelector);
42
42
 
43
43
  // Assert
44
44
  expect(modal).toBeVisible();
@@ -109,7 +109,7 @@ test.describe('When modal is closed', () => {
109
109
  },
110
110
  });
111
111
 
112
- const modal = page.locator(modalSelector);
112
+ const modal = page.locator(componentSelector);
113
113
 
114
114
  // Act
115
115
  await page.click(closeButtonSelector);
@@ -159,7 +159,7 @@ test.describe('When modal is closed', () => {
159
159
  });
160
160
 
161
161
  // Act
162
- await page.click(modalSelector, { position: { x: -10, y: -10 } }); // Click outside dialog
162
+ await page.click(componentSelector, { position: { x: -10, y: -10 } }); // Click outside dialog
163
163
 
164
164
  // Assert
165
165
  expect(events).toHaveLength(1); // TODO - Event object is null for this test
@@ -174,7 +174,7 @@ test.describe('When modal is closed', () => {
174
174
  },
175
175
  });
176
176
 
177
- const modal = await page.locator(modalSelector);
177
+ const modal = await page.locator(componentSelector);
178
178
 
179
179
  // Act
180
180
  await modal.click({ position: { x: -10, y: -10 } }); // Click outside dialog
@@ -355,7 +355,7 @@ test.describe('`isDismissible` prop', () => {
355
355
  // Act
356
356
  await page.click('body');
357
357
 
358
- const element = await page.locator(modalSelector);
358
+ const element = await page.locator(componentSelector);
359
359
 
360
360
  const styles = await element.evaluate((modal) => {
361
361
  const computedStyles = window.getComputedStyle(modal);
@@ -377,7 +377,7 @@ test.describe('`isDismissible` prop', () => {
377
377
  },
378
378
  });
379
379
 
380
- const modal = await page.locator(modalSelector);
380
+ const modal = await page.locator(componentSelector);
381
381
 
382
382
  // Act
383
383
  await page.keyboard.press('Escape');
@@ -419,7 +419,7 @@ test.describe('`isDismissible` prop', () => {
419
419
  // Act
420
420
  await page.locator('body').click();
421
421
 
422
- const element = await page.locator(modalSelector);
422
+ const element = await page.locator(componentSelector);
423
423
 
424
424
  const styles = await element.evaluate((modal) => {
425
425
  const computedStyles = window.getComputedStyle(modal);
@@ -443,7 +443,7 @@ test.describe('`isDismissible` prop', () => {
443
443
 
444
444
  // Act
445
445
  await page.keyboard.press('Escape');
446
- const modal = await page.locator(modalSelector);
446
+ const modal = await page.locator(componentSelector);
447
447
 
448
448
  // Assert
449
449
  await expect(modal).toBeVisible();
@@ -451,6 +451,94 @@ test.describe('`isDismissible` prop', () => {
451
451
  });
452
452
  });
453
453
 
454
+ test.describe('isOpen prop', () => {
455
+ test('should not render open when isOpen = false', async ({ mount, page }) => {
456
+ // Arrange
457
+ await mount(PieModal, {
458
+ props: {
459
+ isOpen: false,
460
+ },
461
+ });
462
+
463
+ // Assert
464
+ await expect(page.locator(componentSelector)).not.toBeVisible();
465
+ });
466
+
467
+ test('should render open when isOpen = true', async ({ mount, page }) => {
468
+ // Arrange
469
+ await mount(PieModal, {
470
+ props: {
471
+ isOpen: true,
472
+ },
473
+ });
474
+
475
+ // Assert
476
+ await expect(page.locator(componentSelector)).toBeVisible();
477
+ });
478
+ });
479
+
480
+ test.describe('scrolling logic', () => {
481
+ test('Should not be able to scroll when isOpen = true', async ({ page, mount }) => {
482
+ // Arrange
483
+ const modalComponent = renderTestPieModal();
484
+
485
+ await mount(
486
+ WebComponentTestWrapper,
487
+ {
488
+ props: {
489
+ pageMode: true,
490
+ },
491
+ slots: {
492
+ component: modalComponent,
493
+ pageMarkup: createScrollablePageHTML(),
494
+ },
495
+ },
496
+ );
497
+
498
+ // Act
499
+ // Scroll 800 pixels down the page
500
+ await page.mouse.wheel(0, 5000);
501
+
502
+ // The mouse.wheel function causes scrolling, but doesn't wait for the scroll to finish before returning.
503
+ await page.waitForTimeout(3000);
504
+
505
+ // Assert
506
+ await expect.soft(page.getByText('Top of page copy')).toBeInViewport();
507
+ await expect(page.getByText('Bottom of page copy')).not.toBeInViewport();
508
+ });
509
+
510
+ test('Should scroll to the bottom when Pie Modal is closed', async ({ page, mount }) => {
511
+ // Arrange
512
+ const modalComponent = renderTestPieModal();
513
+
514
+ await mount(
515
+ WebComponentTestWrapper,
516
+ {
517
+ props: {
518
+ pageMode: true,
519
+ },
520
+ slots: {
521
+ component: modalComponent,
522
+ pageMarkup: createScrollablePageHTML(),
523
+ },
524
+ },
525
+ );
526
+
527
+ // Act
528
+ await page.locator('[data-test-id="modal-close-button"]').click();
529
+
530
+ // Scroll 800 pixels down the page
531
+ await page.mouse.wheel(0, 5000);
532
+
533
+ // The mouse.wheel function causes scrolling, but doesn't wait for the scroll to finish before returning.
534
+ await page.waitForTimeout(3000);
535
+
536
+ // Assert
537
+ await expect.soft(page.getByText('Top of page copy')).not.toBeInViewport();
538
+ await expect(page.getByText('Bottom of page copy')).toBeInViewport();
539
+ });
540
+ });
541
+
454
542
  test.describe('`hasBackButton` prop', () => {
455
543
  test.describe('when `true`', () => {
456
544
  test('should make the modal contain a back button', async ({ mount }) => {
@@ -519,10 +607,20 @@ test.describe('actions', () => {
519
607
  props: {
520
608
  heading: 'Modal Header',
521
609
  isOpen: true,
610
+ leadingAction: {
611
+ text: 'Confirm',
612
+ variant: 'primary',
613
+ ariaLabel: 'Descriptive message',
614
+ },
615
+ supportingAction: {
616
+ text: 'Cancel',
617
+ variant: 'ghost',
618
+ ariaLabel: 'Descriptive message',
619
+ },
522
620
  },
523
621
  });
524
622
 
525
- const modal = await page.locator(modalSelector);
623
+ const modal = await page.locator(componentSelector);
526
624
 
527
625
  // Act
528
626
  await page.click(buttonSelector);
@@ -537,13 +635,23 @@ test.describe('actions', () => {
537
635
  props: {
538
636
  heading: 'Modal Header',
539
637
  isOpen: true,
638
+ leadingAction: {
639
+ text: 'Confirm',
640
+ variant: 'primary',
641
+ ariaLabel: 'Descriptive message',
642
+ },
643
+ supportingAction: {
644
+ text: 'Cancel',
645
+ variant: 'ghost',
646
+ ariaLabel: 'Descriptive message',
647
+ },
540
648
  },
541
649
  });
542
650
 
543
651
  // Act
544
652
  await page.click(buttonSelector);
545
653
  const returnValue = await page.$eval(
546
- modalSelector,
654
+ componentSelector,
547
655
  (dialog : HTMLDialogElement) => dialog.returnValue,
548
656
  );
549
657
 
@@ -553,3 +661,156 @@ test.describe('actions', () => {
553
661
  });
554
662
  });
555
663
  });
664
+
665
+ test.describe('Props: `aria`', () => {
666
+ test.describe('when aria exist', () => {
667
+ test('should render component elements with the correct aria-labels', async ({ mount }) => {
668
+ // Arrange
669
+ const component = await mount(PieModal, {
670
+ props: {
671
+ isOpen: true,
672
+ isDismissible: true,
673
+ isLoading: true,
674
+ hasBackButton: true,
675
+ aria: {
676
+ close: 'Close label info',
677
+ back: 'Back label info',
678
+ loading: 'Loading label info',
679
+ },
680
+ },
681
+ });
682
+
683
+ // Act
684
+ // Close button
685
+ const closeButton = await component.locator(closeButtonSelector);
686
+ const ariaCloseLabel = await closeButton.getAttribute('aria-label');
687
+
688
+ // Back button
689
+ const backButton = await component.locator(backButtonSelector);
690
+ const ariaBackLabel = await backButton.getAttribute('aria-label');
691
+
692
+ // Assert
693
+ await expect(ariaCloseLabel).toBe('Close label info');
694
+ await expect(ariaBackLabel).toBe('Back label info');
695
+ });
696
+
697
+ test.describe('when modal `isloading` is true', () => {
698
+ test('should render component with the correct aria values: `aria-label` & `aria-busy`', async ({ mount }) => {
699
+ // Arrange
700
+ const component = await mount(PieModal, {
701
+ props: {
702
+ isOpen: true,
703
+ isLoading: true,
704
+ aria: {
705
+ loading: 'Loading label info',
706
+ },
707
+ },
708
+ });
709
+
710
+ // Loading state
711
+ const pieModalComponent = await component.locator(componentSelector);
712
+ const ariaLoadingLabel = await pieModalComponent.getAttribute('aria-label');
713
+ const ariaLoadingBusy = await pieModalComponent.getAttribute('aria-busy');
714
+
715
+ // Assert
716
+ await expect(ariaLoadingLabel).toBe('Loading label info');
717
+ await expect(ariaLoadingBusy).toBe('true');
718
+ });
719
+ });
720
+
721
+ test.describe('when modal `isLoading` is dynamically changing from `isLoading: true` to `isLoading: false`', () => {
722
+ test('should dynamically add, remove, and update `arial-label` & `aria-busy` labels', async ({ mount }) => {
723
+ // Arrange
724
+ const component = await mount(PieModal, {
725
+ props: {
726
+ isOpen: true,
727
+ isLoading: true,
728
+ aria: {
729
+ loading: 'Loading label info',
730
+ },
731
+ },
732
+ });
733
+
734
+ const pieModalComponent = await component.locator(componentSelector);
735
+ let ariaLoadingLabel = await pieModalComponent.getAttribute('aria-label');
736
+ let ariaLoadingBusy = await pieModalComponent.getAttribute('aria-busy');
737
+
738
+ // Assert: When `isLoading: true`
739
+ await expect(ariaLoadingLabel).toBe('Loading label info');
740
+ await expect(ariaLoadingBusy).toBe('true');
741
+
742
+ await component.update({ props: { isLoading: false } });
743
+
744
+ ariaLoadingLabel = await pieModalComponent.getAttribute('aria-label');
745
+ ariaLoadingBusy = await pieModalComponent.getAttribute('aria-busy');
746
+
747
+ // Assert: When `isLoading: false`
748
+ await expect(ariaLoadingLabel).toBeNull();
749
+ await expect(ariaLoadingBusy).toBe('false');
750
+ });
751
+ });
752
+ });
753
+
754
+ test.describe('when aria does not exist', () => {
755
+ test('should not render the aria-labels', async ({ mount }) => {
756
+ // Arrange
757
+ const component = await mount(PieModal, {
758
+ props: {
759
+ isOpen: true,
760
+ isDismissible: true,
761
+ hasBackButton: true,
762
+ },
763
+ });
764
+
765
+ // Act
766
+ // Close button
767
+ const closeButton = await component.locator(closeButtonSelector);
768
+ const ariaCloseLabel = await closeButton.getAttribute('aria-label');
769
+
770
+ // Back button
771
+ const backButton = await component.locator(backButtonSelector);
772
+ const ariaBackLabel = await backButton.getAttribute('aria-label');
773
+
774
+ // Assert
775
+ await expect(ariaCloseLabel).toBe(null);
776
+ await expect(ariaBackLabel).toBe(null);
777
+ });
778
+ });
779
+
780
+ test.describe('when modal `isloading` is false', () => {
781
+ test('should not render aria-label', async ({ mount }) => {
782
+ // Arrange
783
+ const component = await mount(PieModal, {
784
+ props: {
785
+ isOpen: true,
786
+ isLoading: false,
787
+ },
788
+ });
789
+
790
+ // Loading state
791
+ const pieModalComponent = await component.locator(componentSelector);
792
+ const ariaLoadingLabel = await pieModalComponent.getAttribute('aria-label');
793
+
794
+ // Assert
795
+ await expect(ariaLoadingLabel).toBe(null);
796
+ });
797
+
798
+ test('should set `aria-busy` to `false`', async ({ mount }) => {
799
+ // Arrange
800
+ const component = await mount(PieModal, {
801
+ props: {
802
+ isOpen: true,
803
+ isLoading: false,
804
+ },
805
+ });
806
+
807
+ // Loading state
808
+ const pieModalComponent = await component.locator(componentSelector);
809
+ const ariaLoadingBusy = await pieModalComponent.getAttribute('aria-busy');
810
+
811
+ // Assert
812
+ await expect(ariaLoadingBusy).toBe('false');
813
+ });
814
+ });
815
+ });
816
+
@@ -22,8 +22,10 @@ export const renderTestPieModal = ({
22
22
  // Creates some test page markup to test scroll locking
23
23
  export const createScrollablePageHTML = () => `<div>
24
24
  <h1>Test Page</h1>
25
+ <p>Top of page copy</p>
25
26
  <p> Test copy </p>
26
27
  <ol>
27
28
  ${'<li>List item</li>'.repeat(200)}
29
+ <li>Bottom of page copy</li>
28
30
  </ol>
29
31
  </div>`;