@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/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.0",
10
- "@lumx/icons": "^3.11.1-alpha.0",
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.0"
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, { CSSProperties, useEffect, useRef, useState } from '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, FlexBoxProps, IconButtonProps,
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('Long loading text with useless words');
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('Long loading text with useless words');
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
- <List>
165
- <ListItem before={<Icon icon={mdiAccount} />} className="lumx-spacing-margin-right-huge">
166
- <span>{text}</span>
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
- export const WithScrollingPopover = () => {
178
- const anchorRef = useRef(null);
179
- const [isOpen, setIsOpen] = useState(false);
180
-
181
- const toggleOpen = () => setIsOpen(!isOpen);
182
-
183
- return (
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
- const maxWidthAnchorRef = useRef(null);
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
- return (
246
- <div style={container}>
247
- <div>
248
- <Chip ref={maxWidthAnchorRef} size={Size.s}>
249
- Anchor
250
- </Chip>
251
- </div>
252
- <Popover anchorRef={maxWidthAnchorRef} fitToAnchorWidth="maxWidth" isOpen placement="top">
253
- <div style={demoPopperStyle}>Popover maxWidth</div>
254
- </Popover>
255
- <div>
256
- <Chip ref={widthSmallAnchorRef} size={Size.s}>
257
- Anchor
258
- </Chip>
259
- </div>
260
- <Popover anchorRef={widthSmallAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
261
- <div style={demoPopperStyle}>Popover width small anchor</div>
262
- </Popover>
263
- <div>
264
- <Chip ref={widthLargeAnchorRef} size={Size.s}>
265
- VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
266
- </Chip>
267
- </div>
268
- <Popover anchorRef={widthLargeAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
269
- <div style={demoPopperStyle}>Popover width large anchor</div>
270
- </Popover>
271
- <div>
272
- <Chip ref={minWidthAnchorRef} size={Size.s}>
273
- VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
274
- </Chip>
275
- </div>
276
- <Popover anchorRef={minWidthAnchorRef} fitToAnchorWidth="minWidth" isOpen placement="top">
277
- <div style={demoPopperStyle}>Popover minWidth</div>
278
- </Popover>
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
- <Popover anchorRef={defaultWidthAnchorRef} isOpen placement="top">
285
- <div style={demoPopperStyle}>Popover default</div>
286
- </Popover>
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 && styles.popover
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.visibility = 'hidden';
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
- }