@adobe-commerce/elsie 1.3.1-alpha007 → 1.3.1-alpha008

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.3.1-alpha007",
3
+ "version": "1.3.1-alpha008",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -10,10 +10,10 @@
10
10
  /* https://cssguidelin.es/#bem-like-naming */
11
11
 
12
12
  .dropin-modal {
13
- height: 100vh;
14
- width: 100vw;
15
13
  position: fixed;
16
14
  top: 0;
15
+ right: 0;
16
+ bottom: 0;
17
17
  left: 0;
18
18
  z-index: 99999;
19
19
  overflow: auto;
@@ -11,7 +11,7 @@ import { Meta, StoryObj } from '@storybook/preact';
11
11
  import { Modal as component, ModalProps } from './Modal';
12
12
  import { useState } from 'preact/hooks';
13
13
  import { Button } from '../Button';
14
- import { expect, userEvent, within } from '@storybook/test';
14
+ import { expect, userEvent, within, waitFor } from '@storybook/test';
15
15
 
16
16
  const meta: Meta<ModalProps> = {
17
17
  title: 'Components/Modal',
@@ -107,13 +107,21 @@ export const SmallModal: Story = {
107
107
  const canvas = within(canvasElement);
108
108
  await userEvent.click(canvas.getByRole('button'));
109
109
 
110
+ const portalRoot = await waitFor(() => {
111
+ const root = document.querySelector('[data-portal-root]') as HTMLDivElement;
112
+ expect(root).toBeTruthy();
113
+ return root;
114
+ });
115
+
116
+ await expect(portalRoot).toBeVisible();
117
+
110
118
  const modal = document.querySelector(
111
119
  '.dropin-modal__body'
112
120
  ) as HTMLDivElement;
113
121
 
114
122
  await expect(modal).toBeVisible();
115
123
 
116
- await expect(await canvas.findByText('Small modal')).toBeVisible();
124
+ expect(portalRoot.querySelector('h4')?.innerText).toBe('Small modal');
117
125
 
118
126
  const closeButton = document.querySelector(
119
127
  '.dropin-modal__header-close-button'
@@ -148,13 +156,21 @@ export const MediumModal: Story = {
148
156
  const canvas = within(canvasElement);
149
157
  await userEvent.click(canvas.getByRole('button'));
150
158
 
159
+ const portalRoot = await waitFor(() => {
160
+ const root = document.querySelector('[data-portal-root]') as HTMLDivElement;
161
+ expect(root).toBeTruthy();
162
+ return root;
163
+ });
164
+
165
+ await expect(portalRoot).toBeVisible();
166
+
151
167
  const modal = document.querySelector(
152
168
  '.dropin-modal__body'
153
169
  ) as HTMLDivElement;
154
170
 
155
171
  await expect(modal).toBeVisible();
156
172
 
157
- await expect(await canvas.findByText('Medium modal')).toBeVisible();
173
+ expect(portalRoot.querySelector('h3')?.innerText).toBe('Medium modal');
158
174
 
159
175
  const closeButton = document.querySelector(
160
176
  '.dropin-modal__header-close-button'
@@ -187,11 +203,17 @@ export const FullModal: Story = {
187
203
  const canvas = within(canvasElement);
188
204
  await userEvent.click(canvas.getByRole('button'));
189
205
 
206
+ const portalRoot = await waitFor(() => {
207
+ const root = document.querySelector('[data-portal-root]') as HTMLDivElement;
208
+ expect(root).toBeTruthy();
209
+ return root;
210
+ });
211
+
190
212
  const modal = document.querySelector(
191
213
  '.dropin-modal__body'
192
214
  ) as HTMLDivElement;
193
215
 
194
- await expect(modal).toBeVisible();
216
+ await expect(portalRoot).toBeVisible();
195
217
 
196
218
  const closeButton = document.querySelector(
197
219
  '.dropin-modal__header-close-button'
@@ -18,6 +18,7 @@ import {
18
18
  import { Button } from '../Button';
19
19
  import { Close as CloseSVG } from '@adobe-commerce/elsie/icons';
20
20
  import { VNode } from 'preact';
21
+ import { Portal } from '../Portal/Portal';
21
22
 
22
23
  import '@adobe-commerce/elsie/components/Modal/Modal.css';
23
24
 
@@ -106,52 +107,54 @@ export const Modal: FunctionComponent<ModalProps> = ({
106
107
  }, []);
107
108
 
108
109
  return (
109
- <div
110
- className={classes([
111
- 'dropin-modal',
112
- ['dropin-modal--dim', backgroundDim],
113
- ])}
114
- >
110
+ <Portal>
115
111
  <div
116
- {...props}
117
112
  className={classes([
118
- 'dropin-modal__body',
119
- [`dropin-modal__body--${size}`, size],
120
- className,
113
+ 'dropin-modal',
114
+ ['dropin-modal--dim', backgroundDim],
121
115
  ])}
122
116
  >
123
117
  <div
118
+ {...props}
124
119
  className={classes([
125
- 'dropin-modal__header',
126
- ['dropin-modal__header-title', !!title],
120
+ 'dropin-modal__body',
121
+ [`dropin-modal__body--${size}`, size],
122
+ className,
127
123
  ])}
128
124
  >
129
- {title && (
130
- <div className={classes(['dropin-modal__header-title-content'])}>
131
- {title}
132
- </div>
133
- )}
134
-
135
- {showCloseButton && (
136
- <Button
137
- aria-label={translations.modalCloseLabel}
138
- variant="tertiary"
139
- className="dropin-modal__header-close-button"
140
- onClick={handleOnClose}
141
- icon={<CloseSVG />}
142
- />
143
- )}
144
- </div>
145
-
146
- <div
147
- className={classes([
148
- 'dropin-modal__content',
149
- ['dropin-modal__body--centered', centered],
150
- ])}
151
- >
152
- {children}
125
+ <div
126
+ className={classes([
127
+ 'dropin-modal__header',
128
+ ['dropin-modal__header-title', !!title],
129
+ ])}
130
+ >
131
+ {title && (
132
+ <div className={classes(['dropin-modal__header-title-content'])}>
133
+ {title}
134
+ </div>
135
+ )}
136
+
137
+ {showCloseButton && (
138
+ <Button
139
+ aria-label={translations.modalCloseLabel}
140
+ variant="tertiary"
141
+ className="dropin-modal__header-close-button"
142
+ onClick={handleOnClose}
143
+ icon={<CloseSVG />}
144
+ />
145
+ )}
146
+ </div>
147
+
148
+ <div
149
+ className={classes([
150
+ 'dropin-modal__content',
151
+ ['dropin-modal__body--centered', centered],
152
+ ])}
153
+ >
154
+ {children}
155
+ </div>
153
156
  </div>
154
157
  </div>
155
- </div>
158
+ </Portal>
156
159
  );
157
160
  };
@@ -0,0 +1,145 @@
1
+ /********************************************************************
2
+ * Copyright 2024 Adobe
3
+ * All Rights Reserved.
4
+ *
5
+ * NOTICE: Adobe permits you to use, modify, and distribute this
6
+ * file in accordance with the terms of the Adobe license agreement
7
+ * accompanying it.
8
+ *******************************************************************/
9
+
10
+ import { Meta, StoryObj } from '@storybook/preact';
11
+ import { Portal } from './Portal';
12
+ import { Button } from '../Button';
13
+ import { useState } from 'preact/compat';
14
+
15
+ const meta: Meta<typeof Portal> = {
16
+ title: 'Components/Portal',
17
+ component: Portal,
18
+ parameters: {
19
+ layout: 'centered',
20
+ },
21
+ tags: ['autodocs'],
22
+ };
23
+
24
+ export default meta;
25
+ type Story = StoryObj<typeof Portal>;
26
+
27
+ /**
28
+ * ```ts
29
+ * import { Portal } from '@/elsie/components/Portal';
30
+ *
31
+ * <Portal>
32
+ * <div>👋 Howdy, I'm Howdy!</div>
33
+ * </Portal>
34
+ * ```
35
+ */
36
+
37
+ // Portal with dynamic content
38
+ export const DynamicContent: Story = {
39
+ render: () => {
40
+ const [isOpen, setIsOpen] = useState(false);
41
+ const [count, setCount] = useState(0);
42
+
43
+ return (
44
+ <div style={{ border: '2px dashed #ccc', padding: '20px' }}>
45
+ <Button onClick={() => setIsOpen(!isOpen)}>
46
+ {isOpen ? 'Close Portal' : 'Open Portal'}
47
+ </Button>
48
+
49
+ {isOpen && (
50
+ <Portal>
51
+ <div style={{
52
+ position: 'fixed',
53
+ top: '50%',
54
+ left: '50%',
55
+ transform: 'translate(-50%, -50%)',
56
+ background: 'white',
57
+ padding: '20px',
58
+ border: '1px solid #ccc',
59
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
60
+ }}>
61
+ <p>Portal content with counter: {count}</p>
62
+ <Button onClick={() => setCount(c => c + 1)}>
63
+ Increment Counter
64
+ </Button>
65
+ <Button
66
+ variant="tertiary"
67
+ onClick={() => setIsOpen(false)}
68
+ style={{ marginLeft: '8px' }}
69
+ >
70
+ Close
71
+ </Button>
72
+ </div>
73
+ </Portal>
74
+ )}
75
+ </div>
76
+ );
77
+ },
78
+ };
79
+
80
+ // Portal with nested portals
81
+ export const NestedPortals: Story = {
82
+ render: () => {
83
+ const [isOuterOpen, setOuterOpen] = useState(false);
84
+ const [isInnerOpen, setInnerOpen] = useState(false);
85
+
86
+ return (
87
+ <div style={{ border: '2px dashed #ccc', padding: '20px' }}>
88
+ <Button onClick={() => setOuterOpen(!isOuterOpen)}>
89
+ {isOuterOpen ? 'Close Outer Portal' : 'Open Outer Portal'}
90
+ </Button>
91
+
92
+ {isOuterOpen && (
93
+ <Portal>
94
+ <div style={{
95
+ position: 'fixed',
96
+ top: '50%',
97
+ left: '50%',
98
+ transform: 'translate(-50%, -50%)',
99
+ background: 'white',
100
+ padding: '20px',
101
+ border: '1px solid #ccc',
102
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
103
+ }}>
104
+ <p>Outer Portal Content</p>
105
+ <Button onClick={() => setInnerOpen(!isInnerOpen)}>
106
+ {isInnerOpen ? 'Close Inner Portal' : 'Open Inner Portal'}
107
+ </Button>
108
+
109
+ {isInnerOpen && (
110
+ <Portal>
111
+ <div style={{
112
+ position: 'fixed',
113
+ top: '60%',
114
+ left: '50%',
115
+ transform: 'translate(-50%, -50%)',
116
+ background: 'white',
117
+ padding: '20px',
118
+ border: '1px solid #ccc',
119
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
120
+ }}>
121
+ <p>Inner Portal Content</p>
122
+ <Button
123
+ variant="tertiary"
124
+ onClick={() => setInnerOpen(false)}
125
+ >
126
+ Close Inner Portal
127
+ </Button>
128
+ </div>
129
+ </Portal>
130
+ )}
131
+
132
+ <Button
133
+ variant="tertiary"
134
+ onClick={() => setOuterOpen(false)}
135
+ style={{ marginLeft: '8px' }}
136
+ >
137
+ Close Outer Portal
138
+ </Button>
139
+ </div>
140
+ </Portal>
141
+ )}
142
+ </div>
143
+ );
144
+ },
145
+ };
@@ -0,0 +1,43 @@
1
+ /********************************************************************
2
+ * Copyright 2024 Adobe
3
+ * All Rights Reserved.
4
+ *
5
+ * NOTICE: Adobe permits you to use, modify, and distribute this
6
+ * file in accordance with the terms of the Adobe license agreement
7
+ * accompanying it.
8
+ *******************************************************************/
9
+
10
+ import { ComponentChildren, render } from 'preact';
11
+ import { FunctionComponent, useEffect, useRef } from 'preact/compat';
12
+
13
+ interface PortalProps {
14
+ children: ComponentChildren;
15
+ }
16
+
17
+ export const Portal: FunctionComponent<PortalProps> = ({ children }) => {
18
+ const portalRoot = useRef<HTMLDivElement | null>(null);
19
+
20
+ useEffect(() => {
21
+ // Create portal root if it doesn't exist
22
+ if (!portalRoot.current) {
23
+ portalRoot.current = document.createElement('div');
24
+ portalRoot.current.setAttribute('data-portal-root', '');
25
+ portalRoot.current.classList.add('dropin-design');
26
+ document.body.appendChild(portalRoot.current);
27
+ }
28
+
29
+ // Render children into portal root
30
+ render(children, portalRoot.current);
31
+
32
+ // Cleanup
33
+ return () => {
34
+ if (portalRoot.current) {
35
+ render(null, portalRoot.current);
36
+ portalRoot.current.remove();
37
+ portalRoot.current = null;
38
+ }
39
+ };
40
+ }, [children]);
41
+
42
+ return null;
43
+ };