@adobe-commerce/elsie 1.3.1-alpha01 → 1.3.1-alpha010
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 +4 -4
- package/src/components/CartItem/CartItem.css +0 -4
- package/src/components/Modal/Modal.css +2 -2
- package/src/components/Modal/Modal.stories.tsx +26 -4
- package/src/components/Modal/Modal.tsx +40 -37
- package/src/components/Portal/Portal.stories.tsx +145 -0
- package/src/components/Portal/Portal.tsx +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe-commerce/elsie",
|
|
3
|
-
"version": "1.3.1-
|
|
3
|
+
"version": "1.3.1-alpha010",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"description": "Domain Package SDK",
|
|
6
6
|
"engines": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@adobe-commerce/event-bus": "~1.0.0",
|
|
30
|
-
"@adobe-commerce/fetch-graphql": "~1.
|
|
31
|
-
"@adobe-commerce/recaptcha": "~1.0.
|
|
30
|
+
"@adobe-commerce/fetch-graphql": "~1.1.0",
|
|
31
|
+
"@adobe-commerce/recaptcha": "~1.0.1",
|
|
32
32
|
"@adobe-commerce/storefront-design": "~1.0.0",
|
|
33
|
-
"@dropins/build-tools": "~1.0.
|
|
33
|
+
"@dropins/build-tools": "~1.0.1",
|
|
34
34
|
"preact": "~10.22.1",
|
|
35
35
|
"vite-plugin-banner": "^0.8.0"
|
|
36
36
|
},
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
<
|
|
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-
|
|
119
|
-
[
|
|
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-
|
|
126
|
-
[
|
|
120
|
+
'dropin-modal__body',
|
|
121
|
+
[`dropin-modal__body--${size}`, size],
|
|
122
|
+
className,
|
|
127
123
|
])}
|
|
128
124
|
>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
</
|
|
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
|
+
};
|