@basic-ui/core 0.0.28 → 0.0.32
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/build/cjs/index.js +44 -21
- package/build/cjs/index.js.map +1 -1
- package/build/esm/FocusLock/useFocusLock.js +21 -7
- package/build/esm/FocusLock/useFocusLock.js.map +1 -1
- package/build/esm/Menu/Menu.js +0 -3
- package/build/esm/Menu/Menu.js.map +1 -1
- package/build/esm/Menu/MenuButton.js +7 -5
- package/build/esm/Menu/MenuButton.js.map +1 -1
- package/build/esm/Menu/MenuList.js +8 -5
- package/build/esm/Menu/MenuList.js.map +1 -1
- package/build/esm/Menu/context.d.ts +0 -1
- package/build/esm/Menu/context.js.map +1 -1
- package/build/esm/Tooltip/Tooltip.d.ts +1 -0
- package/build/esm/Tooltip/Tooltip.js +10 -3
- package/build/esm/Tooltip/Tooltip.js.map +1 -1
- package/build/esm/hooks/useId.d.ts +1 -0
- package/build/esm/hooks/useId.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +11 -11
- package/package.json +4 -3
- package/src/Accordion/Accordion.story.tsx +72 -0
- package/src/Accordion/Accordion.tsx +51 -0
- package/src/Accordion/AccordionBody.tsx +53 -0
- package/src/Accordion/AccordionHeader.tsx +165 -0
- package/src/Accordion/AccordionItem.tsx +43 -0
- package/src/Accordion/context.ts +35 -0
- package/src/Accordion/index.ts +4 -0
- package/src/Accordion/scopeQuery.ts +7 -0
- package/src/Accordion/styles.css +21 -0
- package/src/CheckBox/CheckBox.tsx +41 -0
- package/src/CheckBox/index.ts +1 -0
- package/src/ComboBox/ComboBox.story.tsx +118 -0
- package/src/ComboBox/Combobox.tsx +153 -0
- package/src/ComboBox/ComboboxButton.tsx +60 -0
- package/src/ComboBox/ComboboxInput.tsx +178 -0
- package/src/ComboBox/ComboboxLabel.tsx +32 -0
- package/src/ComboBox/ComboboxList.tsx +47 -0
- package/src/ComboBox/ComboboxOption.tsx +107 -0
- package/src/ComboBox/ComboboxPopover.tsx +58 -0
- package/src/ComboBox/cities.ts +23194 -0
- package/src/ComboBox/context.ts +33 -0
- package/src/ComboBox/hooks.tsx +428 -0
- package/src/ComboBox/index.ts +8 -0
- package/src/ComboBox/makeHash.ts +19 -0
- package/src/ComboBox/scopeQuery.ts +6 -0
- package/src/ComboBox/styles.css +30 -0
- package/src/FocusLock/FocusLock.tsx +59 -0
- package/src/FocusLock/index.ts +1 -0
- package/src/FocusLock/tabUtils.ts +28 -0
- package/src/FocusLock/useFocusLock.ts +61 -0
- package/src/List/List.story.tsx +17 -0
- package/src/List/List.tsx +17 -0
- package/src/List/ListItem.tsx +23 -0
- package/src/List/context.ts +19 -0
- package/src/List/index.ts +2 -0
- package/src/Menu/.gitkeep +0 -0
- package/src/Menu/Menu.story.tsx +158 -0
- package/src/Menu/Menu.tsx +60 -0
- package/src/Menu/MenuButton.tsx +83 -0
- package/src/Menu/MenuItem.tsx +83 -0
- package/src/Menu/MenuList.tsx +201 -0
- package/src/Menu/MenuPopover.tsx +25 -0
- package/src/Menu/context.ts +32 -0
- package/src/Menu/index.ts +5 -0
- package/src/Menu/scope.ts +7 -0
- package/src/Menu/styles.css +42 -0
- package/src/Modal/Modal.story.tsx +242 -0
- package/src/Modal/Modal.tsx +42 -0
- package/src/Modal/ModalBackdrop.tsx +72 -0
- package/src/Modal/NavDrawer.story.tsx +157 -0
- package/src/Modal/index.ts +2 -0
- package/src/Modal/styles.css +46 -0
- package/src/Popover/.gitkeep +0 -0
- package/src/Popper/Popper.story.tsx +267 -0
- package/src/Popper/Popper.tsx +149 -0
- package/src/Popper/PopperArrow.tsx +36 -0
- package/src/Popper/context.ts +9 -0
- package/src/Popper/index.ts +3 -0
- package/src/Popper/styles.css +60 -0
- package/src/Portal/Portal.tsx +20 -0
- package/src/Portal/index.ts +1 -0
- package/src/RadioButton/RadioButton.story.tsx +73 -0
- package/src/RadioButton/RadioButton.tsx +48 -0
- package/src/RadioButton/RadioGroup.tsx +56 -0
- package/src/RadioButton/context.ts +19 -0
- package/src/RadioButton/index.ts +2 -0
- package/src/SkipNav/SkipNav.tsx +16 -0
- package/src/SkipNav/index.tsx +1 -0
- package/src/Spinner/Spinner.story.tsx +30 -0
- package/src/Spinner/Spinner.tsx +112 -0
- package/src/Spinner/SpinnerButton.tsx +48 -0
- package/src/Spinner/context.ts +21 -0
- package/src/Spinner/index.ts +2 -0
- package/src/Spinner/styles.css +23 -0
- package/src/Tabs/Tab.story.tsx +78 -0
- package/src/Tabs/Tab.tsx +131 -0
- package/src/Tabs/TabList.tsx +63 -0
- package/src/Tabs/TabPanel.tsx +52 -0
- package/src/Tabs/TabPanels.tsx +30 -0
- package/src/Tabs/Tabs.tsx +47 -0
- package/src/Tabs/context.ts +30 -0
- package/src/Tabs/index.tsx +5 -0
- package/src/Tabs/scopeQuery.ts +6 -0
- package/src/Tabs/styles.css +0 -0
- package/src/Tooltip/.gitkeep +0 -0
- package/src/Tooltip/Tooltip.story.tsx +43 -0
- package/src/Tooltip/Tooltip.tsx +48 -0
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/stateMachine.ts +185 -0
- package/src/Tooltip/useTooltip.ts +121 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAutoFocus.ts +13 -0
- package/src/hooks/useChildrenCounter.ts +50 -0
- package/src/hooks/useControlledState.ts +37 -0
- package/src/hooks/useFocusReturn.ts +23 -0
- package/src/hooks/useFocusState.ts +28 -0
- package/src/hooks/useGestureHandlers.ts +217 -0
- package/src/hooks/useId.ts +18 -0
- package/src/hooks/useMeasure.ts +33 -0
- package/src/hooks/useOnClickOutside.ts +32 -0
- package/src/hooks/useOnKeyDown.ts +18 -0
- package/src/hooks/useReducerMachine.ts +59 -0
- package/src/hooks/useRemoveBodyScroll.ts +37 -0
- package/src/hooks/useScope.ts +51 -0
- package/src/hooks/useThrottle.ts +19 -0
- package/src/index.ts +19 -0
- package/src/utils/assignRef.ts +27 -0
- package/src/utils/clamp.ts +3 -0
- package/src/utils/createSubscription.ts +16 -0
- package/src/utils/getCircularIndex.ts +7 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/rubberBandClamp.ts +25 -0
- package/src/utils/wrapEvent.ts +20 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext, useContext, MutableRefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ItemObject = { text: string; value: any; id: string | undefined };
|
|
4
|
+
|
|
5
|
+
// MenuRoot
|
|
6
|
+
export interface MenuContextProps {
|
|
7
|
+
buttonRef: MutableRefObject<HTMLButtonElement | null>;
|
|
8
|
+
menuListIdRef: MutableRefObject<undefined | string>;
|
|
9
|
+
openWithArrowKeyRef: MutableRefObject<string | null>;
|
|
10
|
+
onChange?: (
|
|
11
|
+
e:
|
|
12
|
+
| React.KeyboardEvent<HTMLElement>
|
|
13
|
+
| React.MouseEvent<HTMLElement>
|
|
14
|
+
| React.PointerEvent<HTMLElement>,
|
|
15
|
+
isOpen: boolean
|
|
16
|
+
) => void;
|
|
17
|
+
open: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const menuContext = createContext<MenuContextProps>(null as any);
|
|
21
|
+
export const { Provider: MenuProvider } = menuContext;
|
|
22
|
+
export const useMenuContext = () => useContext(menuContext);
|
|
23
|
+
|
|
24
|
+
// MenuList
|
|
25
|
+
export interface MenuListContextProps {
|
|
26
|
+
navigationItem: HTMLElement | undefined;
|
|
27
|
+
onNavigate: undefined | ((idx: HTMLElement) => void);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const menuListContext = createContext<MenuListContextProps>(null as any);
|
|
31
|
+
export const MenuListProvider = menuListContext.Provider;
|
|
32
|
+
export const useMenuListContext = () => useContext(menuListContext);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[data-menu-item] {
|
|
2
|
+
padding: 8px;
|
|
3
|
+
list-style: none;
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
[data-menu-item][data-highlighted] {
|
|
8
|
+
background-color: #e7e7e7;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
[data-menu-item][data-highlighted]:hover {
|
|
12
|
+
background-color: #d7d7d7;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[data-menu-item]:hover {
|
|
16
|
+
background-color: #f3f3f3;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-menu-item][data-disabled] {
|
|
20
|
+
background-color: #ffffff;
|
|
21
|
+
color: #777;
|
|
22
|
+
cursor: not-allowed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-menu-list] {
|
|
26
|
+
margin: 0;
|
|
27
|
+
padding: 0;
|
|
28
|
+
box-shadow: 0px 2px 6px hsla(0, 0%, 0%, 0.15);
|
|
29
|
+
border-radius: 3px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-popper-placement='top'] [data-menu-list] {
|
|
33
|
+
transform-origin: bottom center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[data-popper-placement='bottom'] [data-menu-list] {
|
|
37
|
+
transform-origin: top center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[data-menu-list]:focus {
|
|
41
|
+
outline: none;
|
|
42
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { storiesOf } from '@storybook/react';
|
|
3
|
+
import { useSpring, animated } from 'react-spring';
|
|
4
|
+
import { Modal, ModalBackdrop } from './';
|
|
5
|
+
import { Portal } from '../Portal';
|
|
6
|
+
import './styles.css';
|
|
7
|
+
|
|
8
|
+
const stories = storiesOf('Components/Modal', module);
|
|
9
|
+
|
|
10
|
+
const LoremIpsum = ({ numOfParagraphs = 20 }) => {
|
|
11
|
+
const content = [];
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < numOfParagraphs; i++) {
|
|
14
|
+
content.push(
|
|
15
|
+
<p key={`paragraph_${i}`}>
|
|
16
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
|
|
17
|
+
vestibulum sapien nec mauris placerat, et facilisis massa placerat.
|
|
18
|
+
Curabitur sagittis condimentum lectus vel aliquam. Sed ultrices, metus
|
|
19
|
+
sed suscipit venenatis, metus lectus vestibulum neque, eget sodales
|
|
20
|
+
justo sapien in ante. In a neque mollis, volutpat est a, vehicula lacus.
|
|
21
|
+
Praesent lectus justo, tempor a laoreet in, consectetur nec sapien.
|
|
22
|
+
Phasellus non venenatis erat. Maecenas eget mi sodales, euismod tortor
|
|
23
|
+
vitae, ultricies quam. Sed varius nunc id tincidunt porttitor. Morbi
|
|
24
|
+
lectus massa, malesuada at lorem ut, semper volutpat orci. Donec mauris
|
|
25
|
+
eros, faucibus ut egestas a, ultricies vel tortor. Phasellus mi lectus,
|
|
26
|
+
consectetur a risus at, faucibus porttitor ligula. Maecenas felis eros,
|
|
27
|
+
porttitor vel placerat eget, malesuada ac leo. Cras a eros id dui
|
|
28
|
+
porttitor ullamcorper sit amet quis lorem.
|
|
29
|
+
</p>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return <>{content}</>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const Wrapper = ({ children }) => {
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ maxWidth: 900, margin: '0 auto' }}>
|
|
39
|
+
<div
|
|
40
|
+
style={{
|
|
41
|
+
boxSizing: 'border-box',
|
|
42
|
+
display: 'flex',
|
|
43
|
+
alignItems: 'flex-start',
|
|
44
|
+
padding: '96px 48px',
|
|
45
|
+
justifyContent: 'space-around',
|
|
46
|
+
width: '100%',
|
|
47
|
+
position: 'relative',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<a href="#" onClick={(e) => e.preventDefault()}>
|
|
51
|
+
Link
|
|
52
|
+
</a>
|
|
53
|
+
<div style={{ minHeight: 120, width: 100 }}>{children}</div>
|
|
54
|
+
<a href="#" onClick={(e) => e.preventDefault()}>
|
|
55
|
+
Link
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
<div>
|
|
59
|
+
<LoremIpsum />
|
|
60
|
+
</div>
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
boxSizing: 'border-box',
|
|
64
|
+
display: 'flex',
|
|
65
|
+
alignItems: 'flex-start',
|
|
66
|
+
padding: '96px 48px',
|
|
67
|
+
justifyContent: 'space-around',
|
|
68
|
+
width: '100%',
|
|
69
|
+
position: 'relative',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<a href="#" onClick={(e) => e.preventDefault()}>
|
|
73
|
+
Link
|
|
74
|
+
</a>
|
|
75
|
+
<div style={{ minHeight: 120, width: 100 }}>{children}</div>
|
|
76
|
+
<a href="#" onClick={(e) => e.preventDefault()}>
|
|
77
|
+
Link
|
|
78
|
+
</a>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const SimpleModalControlled = () => {
|
|
85
|
+
const [open, setOpen] = useState(false);
|
|
86
|
+
|
|
87
|
+
const handleClose = () => {
|
|
88
|
+
setOpen(false);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<button onClick={() => setOpen(!open)}>Open modal</button>
|
|
94
|
+
{open && (
|
|
95
|
+
<Portal>
|
|
96
|
+
<ModalBackdrop onClose={handleClose}>
|
|
97
|
+
<Modal trapFocus={true}>
|
|
98
|
+
<button>Start button</button>
|
|
99
|
+
<LoremIpsum />
|
|
100
|
+
<button>End button</button>
|
|
101
|
+
</Modal>
|
|
102
|
+
</ModalBackdrop>
|
|
103
|
+
</Portal>
|
|
104
|
+
)}
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const NestedModalControlled = () => {
|
|
110
|
+
const [open, setOpen] = useState(false);
|
|
111
|
+
const [open2, setOpen2] = useState(false);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<button onClick={() => setOpen(!open)}>Open modal</button>
|
|
116
|
+
{open && (
|
|
117
|
+
<Portal>
|
|
118
|
+
<ModalBackdrop onClose={() => setOpen(false)}>
|
|
119
|
+
<Modal trapFocus={true}>
|
|
120
|
+
<button onClick={() => setOpen2(true)}>Open another modal</button>
|
|
121
|
+
{open2 && (
|
|
122
|
+
<Portal>
|
|
123
|
+
<ModalBackdrop onClose={() => setOpen2(false)}>
|
|
124
|
+
<Modal trapFocus={true}>
|
|
125
|
+
<LoremIpsum numOfParagraphs={1} />
|
|
126
|
+
<button onClick={() => setOpen2(false)}>Close</button>
|
|
127
|
+
</Modal>
|
|
128
|
+
</ModalBackdrop>
|
|
129
|
+
</Portal>
|
|
130
|
+
)}
|
|
131
|
+
<LoremIpsum numOfParagraphs={1} />
|
|
132
|
+
<button onClick={() => setOpen(false)}>Close</button>
|
|
133
|
+
</Modal>
|
|
134
|
+
</ModalBackdrop>
|
|
135
|
+
</Portal>
|
|
136
|
+
)}
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const SimpleModalControlledAnimated = () => {
|
|
142
|
+
const [open, setOpen] = useState(false);
|
|
143
|
+
const [pointerEvents, setPointerEvents] = useState('none');
|
|
144
|
+
const [{ scale, opacity }, set] = useSpring(() => ({
|
|
145
|
+
scale: 0.97,
|
|
146
|
+
opacity: 0,
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
const handleClose = () => {
|
|
150
|
+
const t = Date.now();
|
|
151
|
+
setPointerEvents('none');
|
|
152
|
+
set({
|
|
153
|
+
scale: 0.97,
|
|
154
|
+
opacity: 0,
|
|
155
|
+
config: {
|
|
156
|
+
mass: 1,
|
|
157
|
+
tension: 1600,
|
|
158
|
+
friction: 50,
|
|
159
|
+
clamp: true,
|
|
160
|
+
},
|
|
161
|
+
onRest: () => {
|
|
162
|
+
console.log('Close in: ', Date.now() - t);
|
|
163
|
+
setOpen(false);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleOpen = () => {
|
|
169
|
+
const t = Date.now();
|
|
170
|
+
setOpen(true);
|
|
171
|
+
setPointerEvents('auto');
|
|
172
|
+
set({
|
|
173
|
+
scale: 1,
|
|
174
|
+
opacity: 1,
|
|
175
|
+
config: {
|
|
176
|
+
mass: 1,
|
|
177
|
+
tension: 1050,
|
|
178
|
+
friction: 50,
|
|
179
|
+
clamp: true,
|
|
180
|
+
},
|
|
181
|
+
onRest: () => {
|
|
182
|
+
console.log('Opened in: ', Date.now() - t);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<>
|
|
189
|
+
<button onClick={handleOpen}>Open modal</button>
|
|
190
|
+
{open && (
|
|
191
|
+
<Portal>
|
|
192
|
+
<animated.div
|
|
193
|
+
style={{
|
|
194
|
+
opacity,
|
|
195
|
+
position: 'fixed',
|
|
196
|
+
boxSizing: 'border-box',
|
|
197
|
+
top: '0',
|
|
198
|
+
left: '0',
|
|
199
|
+
right: '0',
|
|
200
|
+
bottom: '0',
|
|
201
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
202
|
+
pointerEvents: 'none',
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
<ModalBackdrop onClose={handleClose} style={{ pointerEvents }}>
|
|
206
|
+
<Modal
|
|
207
|
+
as={animated.div}
|
|
208
|
+
style={{
|
|
209
|
+
transform: scale.to((x: number) => `scale(${x}, ${x * x})`),
|
|
210
|
+
opacity,
|
|
211
|
+
transformOrigin: 'center top',
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
<button>Start button</button>
|
|
215
|
+
Hello world
|
|
216
|
+
<LoremIpsum />
|
|
217
|
+
<button>End button</button>
|
|
218
|
+
</Modal>
|
|
219
|
+
</ModalBackdrop>
|
|
220
|
+
</Portal>
|
|
221
|
+
)}
|
|
222
|
+
</>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
stories.add('Simple modal, controlled', () => (
|
|
227
|
+
<Wrapper>
|
|
228
|
+
<SimpleModalControlled />
|
|
229
|
+
</Wrapper>
|
|
230
|
+
));
|
|
231
|
+
|
|
232
|
+
stories.add('Nested modal, controlled', () => (
|
|
233
|
+
<Wrapper>
|
|
234
|
+
<NestedModalControlled />
|
|
235
|
+
</Wrapper>
|
|
236
|
+
));
|
|
237
|
+
|
|
238
|
+
stories.add('Simple modal, controlled, animated', () => (
|
|
239
|
+
<Wrapper>
|
|
240
|
+
<SimpleModalControlledAnimated />
|
|
241
|
+
</Wrapper>
|
|
242
|
+
));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ReactNode, forwardRef, useRef } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { useAutoFocus, useFocusReturn, useRemoveBodyScroll } from '../hooks';
|
|
4
|
+
import { FocusLock } from '../FocusLock';
|
|
5
|
+
import { assignMultipleRefs } from '../utils';
|
|
6
|
+
|
|
7
|
+
export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
as?: React.ElementType<any>;
|
|
9
|
+
innerAs?: React.ElementType<any>;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
trapFocus?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|
16
|
+
(
|
|
17
|
+
{ as: Comp = 'div', children, trapFocus = true, style = {}, ...otherProps },
|
|
18
|
+
ref
|
|
19
|
+
) => {
|
|
20
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
useFocusReturn(trapFocus);
|
|
23
|
+
useRemoveBodyScroll(trapFocus);
|
|
24
|
+
useAutoFocus(trapFocus, modalRef);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<FocusLock childRef={modalRef} enabled={trapFocus}>
|
|
28
|
+
<Comp
|
|
29
|
+
ref={assignMultipleRefs(ref, modalRef)}
|
|
30
|
+
data-modal-container=""
|
|
31
|
+
role="dialog"
|
|
32
|
+
aria-modal="true"
|
|
33
|
+
style={style}
|
|
34
|
+
tabIndex={0}
|
|
35
|
+
{...otherProps}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</Comp>
|
|
39
|
+
</FocusLock>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useRef, forwardRef } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { assignMultipleRefs } from '../utils/assignRef';
|
|
4
|
+
import { wrapEvent } from '../utils/wrapEvent';
|
|
5
|
+
|
|
6
|
+
export interface ModalBackdropProps
|
|
7
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
as?: React.ElementType<any>;
|
|
9
|
+
innerAs?: React.ElementType<any>;
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
disableCloseOnClick?: boolean;
|
|
13
|
+
disableEscapeKeyDown?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ModalBackdrop = forwardRef<HTMLDivElement, ModalBackdropProps>(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
as: Comp = 'div',
|
|
20
|
+
onClose,
|
|
21
|
+
onClick,
|
|
22
|
+
onMouseDown,
|
|
23
|
+
onKeyDown,
|
|
24
|
+
disableCloseOnClick = false,
|
|
25
|
+
disableEscapeKeyDown = false,
|
|
26
|
+
...otherProps
|
|
27
|
+
},
|
|
28
|
+
forwardedRef
|
|
29
|
+
) => {
|
|
30
|
+
const ref = useRef();
|
|
31
|
+
const mouseDownTargetRef = useRef<EventTarget | null>(null);
|
|
32
|
+
|
|
33
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
34
|
+
// Ignore the events not coming from the "backdrop"
|
|
35
|
+
// We don't want to close the dialog when clicking the dialog content.
|
|
36
|
+
if (e.target !== e.currentTarget) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Make sure the event starts and ends on the same DOM element.
|
|
41
|
+
if (e.target !== mouseDownTargetRef.current) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
mouseDownTargetRef.current = null;
|
|
46
|
+
!disableCloseOnClick && onClose?.();
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
51
|
+
mouseDownTargetRef.current = e.target;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
55
|
+
if (e.key === 'Escape') {
|
|
56
|
+
!disableEscapeKeyDown && onClose?.();
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Comp
|
|
63
|
+
ref={assignMultipleRefs(ref, forwardedRef)}
|
|
64
|
+
data-modal-root=""
|
|
65
|
+
onClick={wrapEvent(onClick, handleClick)}
|
|
66
|
+
onMouseDown={wrapEvent(onMouseDown, handleMouseDown)}
|
|
67
|
+
onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
|
|
68
|
+
{...otherProps}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { storiesOf } from '@storybook/react';
|
|
3
|
+
import { useSpring, animated } from 'react-spring';
|
|
4
|
+
import { Modal, ModalBackdrop } from './';
|
|
5
|
+
import { Portal } from '../Portal';
|
|
6
|
+
import './styles.css';
|
|
7
|
+
|
|
8
|
+
const stories = storiesOf('Components/Modal/NavDrawer', module);
|
|
9
|
+
|
|
10
|
+
const LoremIpsum = ({ numOfParagraphs = 20 }) => {
|
|
11
|
+
const content = [];
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < numOfParagraphs; i++) {
|
|
14
|
+
content.push(
|
|
15
|
+
<p key={`paragraph_${i}`}>
|
|
16
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
|
|
17
|
+
vestibulum sapien nec mauris placerat, et facilisis massa placerat.
|
|
18
|
+
Curabitur sagittis condimentum lectus vel aliquam. Sed ultrices, metus
|
|
19
|
+
sed suscipit venenatis, metus lectus vestibulum neque, eget sodales
|
|
20
|
+
justo sapien in ante. In a neque mollis, volutpat est a, vehicula lacus.
|
|
21
|
+
Praesent lectus justo, tempor a laoreet in, consectetur nec sapien.
|
|
22
|
+
Phasellus non venenatis erat. Maecenas eget mi sodales, euismod tortor
|
|
23
|
+
vitae, ultricies quam. Sed varius nunc id tincidunt porttitor. Morbi
|
|
24
|
+
lectus massa, malesuada at lorem ut, semper volutpat orci. Donec mauris
|
|
25
|
+
eros, faucibus ut egestas a, ultricies vel tortor. Phasellus mi lectus,
|
|
26
|
+
consectetur a risus at, faucibus porttitor ligula. Maecenas felis eros,
|
|
27
|
+
porttitor vel placerat eget, malesuada ac leo. Cras a eros id dui
|
|
28
|
+
porttitor ullamcorper sit amet quis lorem.
|
|
29
|
+
</p>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return <>{content}</>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const Wrapper = ({ children }) => {
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ maxWidth: 900, margin: '0 auto' }}>
|
|
39
|
+
<div
|
|
40
|
+
style={{
|
|
41
|
+
boxSizing: 'border-box',
|
|
42
|
+
display: 'flex',
|
|
43
|
+
alignItems: 'flex-start',
|
|
44
|
+
padding: '96px 48px',
|
|
45
|
+
justifyContent: 'space-around',
|
|
46
|
+
width: '100%',
|
|
47
|
+
position: 'relative',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<a href="#" onClick={(e) => e.preventDefault()}>
|
|
51
|
+
Link
|
|
52
|
+
</a>
|
|
53
|
+
<div style={{ minHeight: 120, width: 100 }}>{children}</div>
|
|
54
|
+
<a href="#" onClick={(e) => e.preventDefault()}>
|
|
55
|
+
Link
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
<div>
|
|
59
|
+
<LoremIpsum />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const SimpleModalControlledAnimated = () => {
|
|
66
|
+
const [open, setOpen] = useState(false);
|
|
67
|
+
const [pointerEvents, setPointerEvents] = useState<PointerEventsProperty>(
|
|
68
|
+
'none'
|
|
69
|
+
);
|
|
70
|
+
const [{ transformX, opacity }, set] = useSpring(() => ({
|
|
71
|
+
transformX: -120,
|
|
72
|
+
opacity: 0,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const handleClose = () => {
|
|
76
|
+
const t = Date.now();
|
|
77
|
+
setPointerEvents('none');
|
|
78
|
+
set({
|
|
79
|
+
transformX: -120,
|
|
80
|
+
opacity: 0,
|
|
81
|
+
config: {
|
|
82
|
+
mass: 1,
|
|
83
|
+
tension: 820,
|
|
84
|
+
friction: 50,
|
|
85
|
+
clamp: true,
|
|
86
|
+
},
|
|
87
|
+
onRest: () => {
|
|
88
|
+
console.log('Close in: ', Date.now() - t);
|
|
89
|
+
setOpen(false);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleOpen = () => {
|
|
95
|
+
const t = Date.now();
|
|
96
|
+
setOpen(true);
|
|
97
|
+
setPointerEvents('auto');
|
|
98
|
+
set({
|
|
99
|
+
transformX: 0,
|
|
100
|
+
opacity: 1,
|
|
101
|
+
config: {
|
|
102
|
+
mass: 1,
|
|
103
|
+
tension: 770,
|
|
104
|
+
friction: 50,
|
|
105
|
+
clamp: true,
|
|
106
|
+
},
|
|
107
|
+
onRest: () => {
|
|
108
|
+
console.log('Opened in: ', Date.now() - t);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const trans = (x: number) => `translateX(${x}%)`;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<button onClick={handleOpen}>Open modal</button>
|
|
118
|
+
{open && (
|
|
119
|
+
<Portal>
|
|
120
|
+
<animated.div
|
|
121
|
+
style={{
|
|
122
|
+
opacity,
|
|
123
|
+
position: 'fixed',
|
|
124
|
+
boxSizing: 'border-box',
|
|
125
|
+
top: '0',
|
|
126
|
+
left: '0',
|
|
127
|
+
right: '0',
|
|
128
|
+
bottom: '0',
|
|
129
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
130
|
+
pointerEvents: 'none',
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
<ModalBackdrop onClose={handleClose} style={{ pointerEvents }}>
|
|
134
|
+
<Modal
|
|
135
|
+
as={animated.div}
|
|
136
|
+
className="nav-drawer-left"
|
|
137
|
+
style={{
|
|
138
|
+
transform: transformX.to(trans),
|
|
139
|
+
opacity,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
<button>This is a cool button</button>
|
|
143
|
+
Hello world
|
|
144
|
+
<LoremIpsum />
|
|
145
|
+
</Modal>
|
|
146
|
+
</ModalBackdrop>
|
|
147
|
+
</Portal>
|
|
148
|
+
)}
|
|
149
|
+
</>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
stories.add('NavDrawer, controlled, animated', () => (
|
|
154
|
+
<Wrapper>
|
|
155
|
+
<SimpleModalControlledAnimated />
|
|
156
|
+
</Wrapper>
|
|
157
|
+
));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[data-modal-root] {
|
|
2
|
+
position: fixed;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
top: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
right: 0;
|
|
7
|
+
bottom: 0;
|
|
8
|
+
overflow-y: auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
[data-modal-container] {
|
|
12
|
+
position: relative;
|
|
13
|
+
left: 0;
|
|
14
|
+
right: 0;
|
|
15
|
+
margin: 96px auto;
|
|
16
|
+
width: 100%;
|
|
17
|
+
max-width: 600px;
|
|
18
|
+
background-color: #fff;
|
|
19
|
+
box-shadow: 0px 2px 6px hsla(0, 0%, 0%, 0.15);
|
|
20
|
+
border-radius: 3px;
|
|
21
|
+
overflow-y: auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[data-modal-backdrop] {
|
|
25
|
+
position: fixed;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
top: 0;
|
|
28
|
+
left: 0;
|
|
29
|
+
right: 0;
|
|
30
|
+
bottom: 0;
|
|
31
|
+
background-color: rgba(0, 0, 0, 0.3);
|
|
32
|
+
pointer-events: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.nav-drawer-left {
|
|
36
|
+
position: relative;
|
|
37
|
+
left: 0;
|
|
38
|
+
right: 0;
|
|
39
|
+
margin: 0;
|
|
40
|
+
width: 260px;
|
|
41
|
+
background-color: #fff;
|
|
42
|
+
box-shadow: 0px 2px 6px hsla(0, 0%, 0%, 0.15);
|
|
43
|
+
border-radius: 0px;
|
|
44
|
+
max-height: 100%;
|
|
45
|
+
overflow-y: auto;
|
|
46
|
+
}
|
|
File without changes
|