@lumx/react 3.11.0 → 3.11.1-alpha.1

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.0",
10
- "@lumx/icons": "^3.11.0",
9
+ "@lumx/core": "^3.11.1-alpha.1",
10
+ "@lumx/icons": "^3.11.1-alpha.1",
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.0"
113
+ "version": "3.11.1-alpha.1"
114
114
  }
@@ -14,11 +14,17 @@ import {
14
14
  Popover,
15
15
  Size,
16
16
  Elevation,
17
+ Message,
18
+ FlexBox,
19
+ FlexBoxProps,
20
+ IconButtonProps,
17
21
  } from '@lumx/react';
18
22
  import range from 'lodash/range';
19
23
  import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
20
24
  import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
21
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';
22
28
 
23
29
  export default {
24
30
  title: 'LumX components/popover/Popover',
@@ -110,164 +116,153 @@ export const Placements = {
110
116
  ],
111
117
  };
112
118
 
113
- export const WithUpdatingChildren = () => {
114
- const anchorRef = useRef(null);
115
- const [isOpen, setIsOpen] = useState(false);
116
-
117
- const toggleOpen = () => setIsOpen(!isOpen);
118
-
119
- const [text, setText] = useState('Long loading text with useless words');
120
- useEffect(() => {
121
- if (isOpen) {
122
- const timer = setTimeout(() => {
123
- setText('Text');
124
- }, 1000);
125
- return () => clearTimeout(timer);
126
- }
127
- setText('Long loading text with useless words');
128
- return undefined;
129
- }, [isOpen]);
130
-
131
- return (
132
- <div style={{ float: 'right' }} className="lumx-spacing-margin-right-huge">
133
- <IconButton
134
- label="Notifications"
135
- className="lumx-spacing-margin-right-huge"
136
- ref={anchorRef}
137
- emphasis={Emphasis.low}
138
- icon={mdiBell}
139
- size={Size.m}
140
- onClick={toggleOpen}
141
- />
142
- <Popover
143
- closeOnClickAway
144
- closeOnEscape
145
- isOpen={isOpen}
146
- anchorRef={anchorRef}
147
- placement={Placement.BOTTOM_END}
148
- onClose={toggleOpen}
149
- fitWithinViewportHeight
150
- >
151
- <List>
152
- <ListItem before={<Icon icon={mdiAccount} />} className="lumx-spacing-margin-right-huge">
153
- <span>{text}</span>
154
- </ListItem>
155
- </List>
156
- </Popover>
157
- </div>
158
- );
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
+ ],
159
154
  };
160
155
 
161
- export const WithScrollingPopover = () => {
162
- const anchorRef = useRef(null);
163
- const [isOpen, setIsOpen] = useState(false);
156
+ /**
157
+ * Testing update of the popover on anchor and popover resize and move
158
+ */
159
+ export const TestUpdatingChildrenAndMovingAnchor = {
160
+ render() {
161
+ const anchorRef = useRef(null);
162
+ const [isOpen, setIsOpen] = useState(false);
163
+
164
+ const toggleOpen = () => setIsOpen(!isOpen);
164
165
 
165
- const toggleOpen = () => setIsOpen(!isOpen);
166
+ const [text, setText] = useState('Initial large span of text');
167
+ const [anchorSize, setAnchorSize] = useState<IconButtonProps['size']>('m');
168
+ const [anchorPosition, setAnchorPosition] = useState<FlexBoxProps['vAlign']>('center');
169
+ useEffect(() => {
170
+ if (isOpen) {
171
+ const timers = [
172
+ setTimeout(() => setText('Text'), 1000),
173
+ setTimeout(() => setAnchorSize('s'), 1500),
174
+ setTimeout(() => setAnchorPosition('left'), 2000),
175
+ ];
176
+ return () => timers.forEach(clearTimeout);
177
+ }
178
+ setText('Initial large span of text');
179
+ setAnchorSize('m');
180
+ setAnchorPosition('center');
181
+ return undefined;
182
+ }, [isOpen]);
166
183
 
167
- return (
168
- <div style={{ float: 'right' }} className="lumx-spacing-margin-right-huge">
169
- <IconButton
170
- label="Notifications"
171
- className="lumx-spacing-margin-right-huge"
172
- ref={anchorRef}
173
- emphasis={Emphasis.low}
174
- icon={mdiBell}
175
- size={Size.m}
176
- onClick={toggleOpen}
177
- />
178
- <Popover
179
- closeOnClickAway
180
- closeOnEscape
181
- isOpen={isOpen}
182
- anchorRef={anchorRef}
183
- placement={Placement.BOTTOM}
184
- onClose={toggleOpen}
185
- fitWithinViewportHeight
186
- >
187
- <List style={{ overflowY: 'auto' }}>
188
- {range(100).map((n: number) => {
189
- return (
190
- <ListItem
191
- key={`key-${n}`}
192
- before={<Icon icon={mdiAccount} />}
193
- className="lumx-spacing-margin-right-huge"
194
- >
195
- <span>{`List item ${n} and some text`}</span>
196
- </ListItem>
197
- );
198
- })}
199
- </List>
200
- </Popover>
201
- </div>
202
- );
184
+ return (
185
+ <FlexBox orientation="vertical" gap="huge">
186
+ <Message kind="info">
187
+ Test popover text resize (after 1sec), anchor resize (after 1.5sec) and anchor move (after 2sec)
188
+ </Message>
189
+ <FlexBox orientation="horizontal" vAlign={anchorPosition}>
190
+ <IconButton
191
+ label="Notifications"
192
+ className="lumx-spacing-margin-right-huge"
193
+ ref={anchorRef}
194
+ emphasis={Emphasis.low}
195
+ icon={mdiBell}
196
+ size={anchorSize}
197
+ onClick={toggleOpen}
198
+ />
199
+ <Popover
200
+ closeOnClickAway
201
+ closeOnEscape
202
+ isOpen={isOpen}
203
+ anchorRef={anchorRef}
204
+ placement={Placement.BOTTOM_END}
205
+ onClose={toggleOpen}
206
+ fitWithinViewportHeight
207
+ hasArrow
208
+ >
209
+ <Text as="p" className="lumx-spacing-padding-huge">
210
+ {text}
211
+ </Text>
212
+ </Popover>
213
+ </FlexBox>
214
+ </FlexBox>
215
+ );
216
+ },
217
+ parameters: { chromatic: { disable: true } },
203
218
  };
204
219
 
205
- export const FitToAnchorWidth = () => {
206
- const demoPopperStyle = {
207
- alignItems: 'center',
208
- display: 'flex',
209
- height: 100,
210
- justifyContent: 'center',
211
- width: 200,
212
- };
213
-
214
- const container = {
215
- alignItems: 'center',
216
- display: 'flex',
217
- justifyContent: 'center',
218
- flexDirection: 'column',
219
- gap: 150,
220
- marginTop: 150,
221
- } 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);
222
227
 
223
- const maxWidthAnchorRef = useRef(null);
224
- const widthSmallAnchorRef = useRef(null);
225
- const widthLargeAnchorRef = useRef(null);
226
- const minWidthAnchorRef = useRef(null);
227
- const defaultWidthAnchorRef = useRef(null);
228
+ const toggleOpen = () => setIsOpen(!isOpen);
228
229
 
229
- return (
230
- <div style={container}>
231
- <div>
232
- <Chip ref={maxWidthAnchorRef} size={Size.s}>
233
- Anchor
234
- </Chip>
235
- </div>
236
- <Popover anchorRef={maxWidthAnchorRef} fitToAnchorWidth="maxWidth" isOpen placement="top">
237
- <div style={demoPopperStyle}>Popover maxWidth</div>
238
- </Popover>
239
- <div>
240
- <Chip ref={widthSmallAnchorRef} size={Size.s}>
241
- Anchor
242
- </Chip>
243
- </div>
244
- <Popover anchorRef={widthSmallAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
245
- <div style={demoPopperStyle}>Popover width small anchor</div>
246
- </Popover>
247
- <div>
248
- <Chip ref={widthLargeAnchorRef} size={Size.s}>
249
- VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
250
- </Chip>
251
- </div>
252
- <Popover anchorRef={widthLargeAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
253
- <div style={demoPopperStyle}>Popover width large anchor</div>
254
- </Popover>
255
- <div>
256
- <Chip ref={minWidthAnchorRef} size={Size.s}>
257
- VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
258
- </Chip>
259
- </div>
260
- <Popover anchorRef={minWidthAnchorRef} fitToAnchorWidth="minWidth" isOpen placement="top">
261
- <div style={demoPopperStyle}>Popover minWidth</div>
262
- </Popover>
263
- <div>
264
- <Chip ref={defaultWidthAnchorRef} size={Size.s}>
265
- VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
266
- </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>
267
264
  </div>
268
- <Popover anchorRef={defaultWidthAnchorRef} isOpen placement="top">
269
- <div style={demoPopperStyle}>Popover default</div>
270
- </Popover>
271
- </div>
272
- );
265
+ );
266
+ },
267
+ parameters: { chromatic: { disable: true } },
273
268
  };
@@ -137,7 +137,6 @@ const _InnerPopover = forwardRef<PopoverProps, HTMLDivElement>((props, ref) => {
137
137
  fitWithinViewportHeight,
138
138
  boundaryRef,
139
139
  anchorRef,
140
- children,
141
140
  placement,
142
141
  style,
143
142
  zIndex,
@@ -74,7 +74,6 @@ type Options = Pick<
74
74
  | 'fitWithinViewportHeight'
75
75
  | 'boundaryRef'
76
76
  | 'anchorRef'
77
- | 'children'
78
77
  | 'placement'
79
78
  | 'style'
80
79
  | 'zIndex'
@@ -97,13 +96,11 @@ export function usePopoverStyle({
97
96
  fitWithinViewportHeight,
98
97
  boundaryRef,
99
98
  anchorRef,
100
- children,
101
99
  placement,
102
100
  style,
103
101
  zIndex,
104
102
  }: Options): Output {
105
103
  const [popperElement, setPopperElement] = useState<null | HTMLElement>(null);
106
-
107
104
  const [arrowElement, setArrowElement] = useState<null | HTMLElement>(null);
108
105
 
109
106
  const actualOffset: [number, number] = [offset?.along ?? 0, (offset?.away ?? 0) + (hasArrow ? ARROW_SIZE : 0)];
@@ -137,9 +134,32 @@ export function usePopoverStyle({
137
134
  }
138
135
 
139
136
  const { styles, attributes, state, update } = usePopper(anchorRef.current, popperElement, { placement, modifiers });
137
+
138
+ // Auto update popover
140
139
  useEffect(() => {
141
- update?.();
142
- }, [children, update]);
140
+ const { current: anchorElement } = anchorRef;
141
+ if (!update || !popperElement || !anchorElement || !WINDOW?.ResizeObserver) {
142
+ return undefined;
143
+ }
144
+
145
+ // Only update once per frame
146
+ let frame: number | undefined;
147
+ function limitedUpdate() {
148
+ if (frame) return;
149
+ frame = requestAnimationFrame(() => {
150
+ update?.();
151
+ frame = undefined;
152
+ });
153
+ }
154
+
155
+ // On anchor or popover resize
156
+ const resizeObserver = new ResizeObserver(limitedUpdate);
157
+ resizeObserver.observe(anchorElement);
158
+ resizeObserver.observe(popperElement);
159
+ return () => {
160
+ resizeObserver.disconnect();
161
+ };
162
+ }, [anchorRef, popperElement, update]);
143
163
 
144
164
  const position = state?.placement ?? placement;
145
165
 
@@ -150,8 +170,12 @@ export function usePopoverStyle({
150
170
  newStyles.maxHeight = WINDOW?.innerHeight || DOCUMENT?.documentElement.clientHeight;
151
171
  }
152
172
 
173
+ // Do not show the popover while it's not properly placed
174
+ if (!newStyles.transform) newStyles.visibility = 'hidden';
175
+
153
176
  return newStyles;
154
177
  }, [style, styles.popper, zIndex, fitWithinViewportHeight]);
178
+
155
179
  return {
156
180
  styles: { arrow: styles.arrow, popover: popoverStyle },
157
181
  attributes,