@lumx/react 3.11.1-alpha.0 → 3.11.1-alpha.2
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/index.js +2 -148
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/popover/Popover.stories.tsx +97 -118
- package/src/components/popover/Popover.tsx +1 -1
- package/src/components/popover/usePopoverStyle.tsx +1 -7
- package/src/components/popover/PositionObserver.ts +0 -155
package/package.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"url": "https://github.com/lumapps/design-system/issues"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@lumx/core": "^3.11.1-alpha.
|
|
10
|
-
"@lumx/icons": "^3.11.1-alpha.
|
|
9
|
+
"@lumx/core": "^3.11.1-alpha.2",
|
|
10
|
+
"@lumx/icons": "^3.11.1-alpha.2",
|
|
11
11
|
"@popperjs/core": "^2.5.4",
|
|
12
12
|
"body-scroll-lock": "^3.1.5",
|
|
13
13
|
"classnames": "^2.3.2",
|
|
@@ -110,5 +110,5 @@
|
|
|
110
110
|
"build:storybook": "storybook build"
|
|
111
111
|
},
|
|
112
112
|
"sideEffects": false,
|
|
113
|
-
"version": "3.11.1-alpha.
|
|
113
|
+
"version": "3.11.1-alpha.2"
|
|
114
114
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
-
import React, {
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import { mdiAccount, mdiBell } from '@lumx/icons';
|
|
5
5
|
import {
|
|
@@ -15,12 +15,16 @@ import {
|
|
|
15
15
|
Size,
|
|
16
16
|
Elevation,
|
|
17
17
|
Message,
|
|
18
|
-
FlexBox,
|
|
18
|
+
FlexBox,
|
|
19
|
+
FlexBoxProps,
|
|
20
|
+
IconButtonProps,
|
|
19
21
|
} from '@lumx/react';
|
|
20
22
|
import range from 'lodash/range';
|
|
21
23
|
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
|
|
22
24
|
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
23
25
|
import { withChromaticForceScreenSize } from '@lumx/react/stories/decorators/withChromaticForceScreenSize';
|
|
26
|
+
import { FitAnchorWidth } from '@lumx/react/components/popover/constants';
|
|
27
|
+
import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
|
|
24
28
|
|
|
25
29
|
export default {
|
|
26
30
|
title: 'LumX components/popover/Popover',
|
|
@@ -112,6 +116,46 @@ export const Placements = {
|
|
|
112
116
|
],
|
|
113
117
|
};
|
|
114
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Demo all fitAnchorWidth configurations
|
|
121
|
+
*/
|
|
122
|
+
export const FitToAnchorWidth = {
|
|
123
|
+
render({ anchorText, fitAnchorWidth }: any) {
|
|
124
|
+
const anchorRef = useRef(null);
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
<Chip className="lumx-spacing-margin-huge" ref={anchorRef} size="s">
|
|
128
|
+
{anchorText}
|
|
129
|
+
</Chip>
|
|
130
|
+
<Popover
|
|
131
|
+
isOpen
|
|
132
|
+
className="lumx-spacing-padding"
|
|
133
|
+
placement="top"
|
|
134
|
+
anchorRef={anchorRef}
|
|
135
|
+
fitToAnchorWidth={fitAnchorWidth}
|
|
136
|
+
>
|
|
137
|
+
Popover {fitAnchorWidth}
|
|
138
|
+
</Popover>
|
|
139
|
+
</>
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
decorators: [
|
|
143
|
+
withCombinations({
|
|
144
|
+
combinations: {
|
|
145
|
+
cols: {
|
|
146
|
+
'Small Anchor': { anchorText: 'Small' },
|
|
147
|
+
'Large Anchor': { anchorText: 'Very very very very large anchor' },
|
|
148
|
+
},
|
|
149
|
+
rows: { key: 'fitAnchorWidth', options: withUndefined(Object.values(FitAnchorWidth)) },
|
|
150
|
+
},
|
|
151
|
+
cellStyle: { padding: 16 },
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Testing update of the popover on anchor and popover resize and move
|
|
158
|
+
*/
|
|
115
159
|
export const TestUpdatingChildrenAndMovingAnchor = {
|
|
116
160
|
render() {
|
|
117
161
|
const anchorRef = useRef(null);
|
|
@@ -119,7 +163,7 @@ export const TestUpdatingChildrenAndMovingAnchor = {
|
|
|
119
163
|
|
|
120
164
|
const toggleOpen = () => setIsOpen(!isOpen);
|
|
121
165
|
|
|
122
|
-
const [text, setText] = useState('
|
|
166
|
+
const [text, setText] = useState('Initial large span of text');
|
|
123
167
|
const [anchorSize, setAnchorSize] = useState<IconButtonProps['size']>('m');
|
|
124
168
|
const [anchorPosition, setAnchorPosition] = useState<FlexBoxProps['vAlign']>('center');
|
|
125
169
|
useEffect(() => {
|
|
@@ -131,7 +175,7 @@ export const TestUpdatingChildrenAndMovingAnchor = {
|
|
|
131
175
|
];
|
|
132
176
|
return () => timers.forEach(clearTimeout);
|
|
133
177
|
}
|
|
134
|
-
setText('
|
|
178
|
+
setText('Initial large span of text');
|
|
135
179
|
setAnchorSize('m');
|
|
136
180
|
setAnchorPosition('center');
|
|
137
181
|
return undefined;
|
|
@@ -160,12 +204,11 @@ export const TestUpdatingChildrenAndMovingAnchor = {
|
|
|
160
204
|
placement={Placement.BOTTOM_END}
|
|
161
205
|
onClose={toggleOpen}
|
|
162
206
|
fitWithinViewportHeight
|
|
207
|
+
hasArrow
|
|
163
208
|
>
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
</ListItem>
|
|
168
|
-
</List>
|
|
209
|
+
<Text as="p" className="lumx-spacing-padding-huge">
|
|
210
|
+
{text}
|
|
211
|
+
</Text>
|
|
169
212
|
</Popover>
|
|
170
213
|
</FlexBox>
|
|
171
214
|
</FlexBox>
|
|
@@ -174,116 +217,52 @@ export const TestUpdatingChildrenAndMovingAnchor = {
|
|
|
174
217
|
parameters: { chromatic: { disable: true } },
|
|
175
218
|
};
|
|
176
219
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
<div style={{ float: 'right' }} className="lumx-spacing-margin-right-huge">
|
|
185
|
-
<IconButton
|
|
186
|
-
label="Notifications"
|
|
187
|
-
className="lumx-spacing-margin-right-huge"
|
|
188
|
-
ref={anchorRef}
|
|
189
|
-
emphasis={Emphasis.low}
|
|
190
|
-
icon={mdiBell}
|
|
191
|
-
size={Size.m}
|
|
192
|
-
onClick={toggleOpen}
|
|
193
|
-
/>
|
|
194
|
-
<Popover
|
|
195
|
-
closeOnClickAway
|
|
196
|
-
closeOnEscape
|
|
197
|
-
isOpen={isOpen}
|
|
198
|
-
anchorRef={anchorRef}
|
|
199
|
-
placement={Placement.BOTTOM}
|
|
200
|
-
onClose={toggleOpen}
|
|
201
|
-
fitWithinViewportHeight
|
|
202
|
-
>
|
|
203
|
-
<List style={{ overflowY: 'auto' }}>
|
|
204
|
-
{range(100).map((n: number) => {
|
|
205
|
-
return (
|
|
206
|
-
<ListItem
|
|
207
|
-
key={`key-${n}`}
|
|
208
|
-
before={<Icon icon={mdiAccount} />}
|
|
209
|
-
className="lumx-spacing-margin-right-huge"
|
|
210
|
-
>
|
|
211
|
-
<span>{`List item ${n} and some text`}</span>
|
|
212
|
-
</ListItem>
|
|
213
|
-
);
|
|
214
|
-
})}
|
|
215
|
-
</List>
|
|
216
|
-
</Popover>
|
|
217
|
-
</div>
|
|
218
|
-
);
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
export const FitToAnchorWidth = () => {
|
|
222
|
-
const demoPopperStyle = {
|
|
223
|
-
alignItems: 'center',
|
|
224
|
-
display: 'flex',
|
|
225
|
-
height: 100,
|
|
226
|
-
justifyContent: 'center',
|
|
227
|
-
width: 200,
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const container = {
|
|
231
|
-
alignItems: 'center',
|
|
232
|
-
display: 'flex',
|
|
233
|
-
justifyContent: 'center',
|
|
234
|
-
flexDirection: 'column',
|
|
235
|
-
gap: 150,
|
|
236
|
-
marginTop: 150,
|
|
237
|
-
} as const;
|
|
220
|
+
/**
|
|
221
|
+
* Testing popover with scroll inside
|
|
222
|
+
*/
|
|
223
|
+
export const TestScrollingPopover = {
|
|
224
|
+
render() {
|
|
225
|
+
const anchorRef = useRef(null);
|
|
226
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
238
227
|
|
|
239
|
-
|
|
240
|
-
const widthSmallAnchorRef = useRef(null);
|
|
241
|
-
const widthLargeAnchorRef = useRef(null);
|
|
242
|
-
const minWidthAnchorRef = useRef(null);
|
|
243
|
-
const defaultWidthAnchorRef = useRef(null);
|
|
228
|
+
const toggleOpen = () => setIsOpen(!isOpen);
|
|
244
229
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<div>
|
|
280
|
-
<Chip ref={defaultWidthAnchorRef} size={Size.s}>
|
|
281
|
-
VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
|
|
282
|
-
</Chip>
|
|
230
|
+
return (
|
|
231
|
+
<div style={{ float: 'right' }} className="lumx-spacing-margin-right-huge">
|
|
232
|
+
<IconButton
|
|
233
|
+
label="Notifications"
|
|
234
|
+
className="lumx-spacing-margin-right-huge"
|
|
235
|
+
ref={anchorRef}
|
|
236
|
+
emphasis={Emphasis.low}
|
|
237
|
+
icon={mdiBell}
|
|
238
|
+
size={Size.m}
|
|
239
|
+
onClick={toggleOpen}
|
|
240
|
+
/>
|
|
241
|
+
<Popover
|
|
242
|
+
closeOnClickAway
|
|
243
|
+
closeOnEscape
|
|
244
|
+
isOpen={isOpen}
|
|
245
|
+
anchorRef={anchorRef}
|
|
246
|
+
placement={Placement.BOTTOM_END}
|
|
247
|
+
onClose={toggleOpen}
|
|
248
|
+
fitWithinViewportHeight
|
|
249
|
+
>
|
|
250
|
+
<List style={{ overflowY: 'auto' }}>
|
|
251
|
+
{range(100).map((n: number) => {
|
|
252
|
+
return (
|
|
253
|
+
<ListItem
|
|
254
|
+
key={`key-${n}`}
|
|
255
|
+
before={<Icon icon={mdiAccount} />}
|
|
256
|
+
className="lumx-spacing-margin-right-huge"
|
|
257
|
+
>
|
|
258
|
+
<span>{`List item ${n} and some text`}</span>
|
|
259
|
+
</ListItem>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</List>
|
|
263
|
+
</Popover>
|
|
283
264
|
</div>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
</div>
|
|
288
|
-
);
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
parameters: { chromatic: { disable: true } },
|
|
289
268
|
};
|
|
@@ -154,7 +154,7 @@ const _InnerPopover = forwardRef<PopoverProps, HTMLDivElement>((props, ref) => {
|
|
|
154
154
|
const clickAwayRefs = useRef([popoverRef, anchorRef]);
|
|
155
155
|
const mergedRefs = useMergeRefs<HTMLDivElement>(setPopperElement, ref, popoverRef);
|
|
156
156
|
|
|
157
|
-
return isOpen
|
|
157
|
+
return isOpen
|
|
158
158
|
? renderPopover(
|
|
159
159
|
<Component
|
|
160
160
|
{...forwardedProps}
|
|
@@ -6,7 +6,6 @@ import { DOCUMENT, WINDOW } from '@lumx/react/constants';
|
|
|
6
6
|
import { PopoverProps } from '@lumx/react/components/popover/Popover';
|
|
7
7
|
import { usePopper } from '@lumx/react/hooks/usePopper';
|
|
8
8
|
import { ARROW_SIZE, FitAnchorWidth, Placement } from './constants';
|
|
9
|
-
import PositionObserver from './PositionObserver';
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Popper js modifier to fit popover min width to the anchor width.
|
|
@@ -153,17 +152,12 @@ export function usePopoverStyle({
|
|
|
153
152
|
});
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
// On anchor move
|
|
157
|
-
const positionObserver = new PositionObserver(limitedUpdate);
|
|
158
|
-
positionObserver.observe(anchorElement);
|
|
159
|
-
|
|
160
155
|
// On anchor or popover resize
|
|
161
156
|
const resizeObserver = new ResizeObserver(limitedUpdate);
|
|
162
157
|
resizeObserver.observe(anchorElement);
|
|
163
158
|
resizeObserver.observe(popperElement);
|
|
164
159
|
return () => {
|
|
165
160
|
resizeObserver.disconnect();
|
|
166
|
-
positionObserver.disconnect();
|
|
167
161
|
};
|
|
168
162
|
}, [anchorRef, popperElement, update]);
|
|
169
163
|
|
|
@@ -177,7 +171,7 @@ export function usePopoverStyle({
|
|
|
177
171
|
}
|
|
178
172
|
|
|
179
173
|
// Do not show the popover while it's not properly placed
|
|
180
|
-
if (!newStyles.transform) newStyles.
|
|
174
|
+
if (!newStyles.transform) newStyles.opacity = 0;
|
|
181
175
|
|
|
182
176
|
return newStyles;
|
|
183
177
|
}, [style, styles.popper, zIndex, fitWithinViewportHeight]);
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { DOCUMENT, WINDOW } from '@lumx/react/constants';
|
|
2
|
-
|
|
3
|
-
export type PositionObserverCallback = (entries: PositionObserverEntry[], observer: PositionObserver) => void;
|
|
4
|
-
|
|
5
|
-
export type PositionObserverEntry = {
|
|
6
|
-
target: Element;
|
|
7
|
-
boundingClientRect: DOMRect;
|
|
8
|
-
clientHeight: number;
|
|
9
|
-
clientWidth: number;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const errorString = 'PositionObserver Error';
|
|
13
|
-
|
|
14
|
-
const ROOT = DOCUMENT?.documentElement;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The PositionObserver class is a utility class that observes the position
|
|
18
|
-
* of DOM elements and triggers a callback when their position changes.
|
|
19
|
-
*/
|
|
20
|
-
export default class PositionObserver {
|
|
21
|
-
public entries: Map<Element, PositionObserverEntry>;
|
|
22
|
-
|
|
23
|
-
_tick: number;
|
|
24
|
-
|
|
25
|
-
_callback: PositionObserverCallback;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The constructor takes two arguments, a `callback`, which is called
|
|
29
|
-
* whenever the position of an observed element changes and an `options` object.
|
|
30
|
-
* The callback function should take an array of `PositionObserverEntry` objects
|
|
31
|
-
* as its only argument, but it's not required.
|
|
32
|
-
*
|
|
33
|
-
* @param callback the callback that applies to all targets of this observer
|
|
34
|
-
*/
|
|
35
|
-
constructor(callback: PositionObserverCallback) {
|
|
36
|
-
this.entries = new Map();
|
|
37
|
-
this._callback = callback;
|
|
38
|
-
this._tick = 0;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Start observing the position of the specified element.
|
|
43
|
-
* If the element is not currently attached to the DOM,
|
|
44
|
-
* it will NOT be added to the entries.
|
|
45
|
-
*
|
|
46
|
-
* @param target an `Element` target
|
|
47
|
-
*/
|
|
48
|
-
public observe = (target: Element) => {
|
|
49
|
-
if (!(target instanceof Element)) {
|
|
50
|
-
throw new Error(`${errorString}: ${target} is not an instance of Element.`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!ROOT?.contains(target)) return;
|
|
54
|
-
|
|
55
|
-
this._new(target).then(({ boundingClientRect }) => {
|
|
56
|
-
if (ROOT && boundingClientRect && !this.entries.has(target)) {
|
|
57
|
-
const { clientWidth, clientHeight } = ROOT;
|
|
58
|
-
|
|
59
|
-
this.entries.set(target, {
|
|
60
|
-
target,
|
|
61
|
-
boundingClientRect,
|
|
62
|
-
clientWidth,
|
|
63
|
-
clientHeight,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!this._tick) this._tick = requestAnimationFrame(this._runCallback);
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Private method responsible for all the heavy duty,
|
|
73
|
-
* the observer's runtime.
|
|
74
|
-
*/
|
|
75
|
-
private _runCallback = () => {
|
|
76
|
-
/* istanbul ignore if @preserve - a guard must be set */
|
|
77
|
-
if (!ROOT || !this.entries.size) return;
|
|
78
|
-
const { clientWidth, clientHeight } = ROOT;
|
|
79
|
-
|
|
80
|
-
const queue = new Promise<PositionObserverEntry[]>((resolve) => {
|
|
81
|
-
const updates: PositionObserverEntry[] = [];
|
|
82
|
-
this.entries.forEach(
|
|
83
|
-
({ target, boundingClientRect: oldBoundingBox, clientWidth: oldWidth, clientHeight: oldHeight }) => {
|
|
84
|
-
/* istanbul ignore if @preserve - a guard must be set when target has been removed */
|
|
85
|
-
if (!ROOT.contains(target)) return;
|
|
86
|
-
|
|
87
|
-
this._new(target).then(({ boundingClientRect, isIntersecting }) => {
|
|
88
|
-
/* istanbul ignore if @preserve - make sure to only count visible entries */
|
|
89
|
-
if (!isIntersecting) return;
|
|
90
|
-
const { left, top } = boundingClientRect;
|
|
91
|
-
|
|
92
|
-
/* istanbul ignore else @preserve - only schedule entries that changed position */
|
|
93
|
-
if (
|
|
94
|
-
oldBoundingBox.top !== top ||
|
|
95
|
-
oldBoundingBox.left !== left ||
|
|
96
|
-
oldWidth !== clientWidth ||
|
|
97
|
-
oldHeight !== clientHeight
|
|
98
|
-
) {
|
|
99
|
-
const newEntry = {
|
|
100
|
-
target,
|
|
101
|
-
boundingClientRect,
|
|
102
|
-
clientHeight,
|
|
103
|
-
clientWidth,
|
|
104
|
-
};
|
|
105
|
-
this.entries.set(target, newEntry);
|
|
106
|
-
updates.push(newEntry);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
resolve(updates);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
this._tick = requestAnimationFrame(async () => {
|
|
116
|
-
// execute the queue
|
|
117
|
-
const updates = await queue;
|
|
118
|
-
|
|
119
|
-
// only execute the callback if position actually changed
|
|
120
|
-
/* istanbul ignore else @preserve */
|
|
121
|
-
if (updates.length) this._callback(updates, this);
|
|
122
|
-
|
|
123
|
-
this._runCallback();
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check intersection status and resolve it
|
|
129
|
-
* right away.
|
|
130
|
-
*
|
|
131
|
-
* @param target an `Element` target
|
|
132
|
-
*/
|
|
133
|
-
private _new = (target: Element) => {
|
|
134
|
-
return new Promise<IntersectionObserverEntry>((resolve) => {
|
|
135
|
-
if (!WINDOW?.IntersectionObserver) return;
|
|
136
|
-
|
|
137
|
-
const intersectionObserver = new IntersectionObserver(([entry], ob) => {
|
|
138
|
-
ob.disconnect();
|
|
139
|
-
|
|
140
|
-
resolve(entry);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
intersectionObserver.observe(target);
|
|
144
|
-
});
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Immediately stop observing all elements.
|
|
149
|
-
*/
|
|
150
|
-
public disconnect = () => {
|
|
151
|
-
cancelAnimationFrame(this._tick);
|
|
152
|
-
this.entries.clear();
|
|
153
|
-
this._tick = 0;
|
|
154
|
-
};
|
|
155
|
-
}
|