@khanacademy/wonder-blocks-popover 3.2.8 → 3.2.10

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.
@@ -16,8 +16,9 @@ import PopoverContentCore from "./popover-content-core";
16
16
  import PopoverContext from "./popover-context";
17
17
  import PopoverAnchor from "./popover-anchor";
18
18
  import PopoverDialog from "./popover-dialog";
19
- import FocusManager from "./focus-manager";
20
19
  import PopoverEventListener from "./popover-event-listener";
20
+ import InitialFocus from "./initial-focus";
21
+ import FocusManager from "./focus-manager";
21
22
 
22
23
  type PopoverContents =
23
24
  | React.ReactElement<React.ComponentProps<typeof PopoverContent>>
@@ -101,6 +102,20 @@ type Props = AriaProps &
101
102
  * Whether to show the popover tail or not. Defaults to true.
102
103
  */
103
104
  showTail: boolean;
105
+ /**
106
+ * Optional property to enable the portal functionality of popover.
107
+ * This is very handy in cases where the Popover can't be easily
108
+ * injected into the DOM structure and requires portaling to
109
+ * the trigger location.
110
+ *
111
+ * Set to "true" by default.
112
+ *
113
+ * CAUTION: Turning off portal could cause some clipping issues
114
+ * especially around legacy code with usage of z-indexing,
115
+ * Use caution when turning this functionality off and ensure
116
+ * your content does not get clipped or hidden.
117
+ */
118
+ portal?: boolean;
104
119
  }>;
105
120
 
106
121
  type State = Readonly<{
@@ -121,6 +136,7 @@ type State = Readonly<{
121
136
  type DefaultProps = Readonly<{
122
137
  placement: Props["placement"];
123
138
  showTail: Props["showTail"];
139
+ portal: Props["portal"];
124
140
  }>;
125
141
 
126
142
  /**
@@ -150,6 +166,7 @@ export default class Popover extends React.Component<Props, State> {
150
166
  static defaultProps: DefaultProps = {
151
167
  placement: "top",
152
168
  showTail: true,
169
+ portal: true,
153
170
  };
154
171
 
155
172
  /**
@@ -255,33 +272,54 @@ export default class Popover extends React.Component<Props, State> {
255
272
  }
256
273
 
257
274
  renderPopper(uniqueId: string): React.ReactNode {
258
- const {initialFocusId, placement, showTail} = this.props;
275
+ const {
276
+ initialFocusId,
277
+ placement,
278
+ showTail,
279
+ portal,
280
+ "aria-label": ariaLabel,
281
+ } = this.props;
259
282
  const {anchorElement} = this.state;
260
283
 
261
- return (
262
- <FocusManager
263
- anchorElement={anchorElement}
264
- initialFocusId={initialFocusId}
265
- >
266
- <TooltipPopper
284
+ const ariaDescribedBy = ariaLabel ? undefined : `${uniqueId}-content`;
285
+ const ariaLabelledBy = ariaLabel ? undefined : `${uniqueId}-title`;
286
+
287
+ const popperContent = (
288
+ <TooltipPopper anchorElement={anchorElement} placement={placement}>
289
+ {(props: PopperElementProps) => (
290
+ <PopoverDialog
291
+ {...props}
292
+ aria-label={ariaLabel}
293
+ aria-describedby={ariaDescribedBy}
294
+ aria-labelledby={ariaLabelledBy}
295
+ id={uniqueId}
296
+ onUpdate={(placement) => this.setState({placement})}
297
+ showTail={showTail}
298
+ >
299
+ {this.renderContent(uniqueId)}
300
+ </PopoverDialog>
301
+ )}
302
+ </TooltipPopper>
303
+ );
304
+
305
+ if (portal) {
306
+ return (
307
+ <FocusManager
267
308
  anchorElement={anchorElement}
268
- placement={placement}
309
+ initialFocusId={initialFocusId}
269
310
  >
270
- {(props: PopperElementProps) => (
271
- <PopoverDialog
272
- {...props}
273
- aria-describedby={`${uniqueId}-content`}
274
- aria-labelledby={`${uniqueId}-title`}
275
- id={uniqueId}
276
- onUpdate={(placement) => this.setState({placement})}
277
- showTail={showTail}
278
- >
279
- {this.renderContent(uniqueId)}
280
- </PopoverDialog>
281
- )}
282
- </TooltipPopper>
283
- </FocusManager>
284
- );
311
+ {popperContent}
312
+ </FocusManager>
313
+ );
314
+ } else {
315
+ return (
316
+ // Ensures the user is focused on the first available element
317
+ // when popover is rendered without the focus manager.
318
+ <InitialFocus initialFocusId={initialFocusId}>
319
+ {popperContent}
320
+ </InitialFocus>
321
+ );
322
+ }
285
323
  }
286
324
 
287
325
  getHost(): Element | null | undefined {
@@ -295,10 +333,29 @@ export default class Popover extends React.Component<Props, State> {
295
333
  );
296
334
  }
297
335
 
336
+ renderPortal(uniqueId: string, opened: boolean) {
337
+ if (!opened) {
338
+ return null;
339
+ }
340
+
341
+ const {portal} = this.props;
342
+ const popperHost = this.getHost();
343
+
344
+ // Attach the popover to a Portal
345
+ if (portal && popperHost) {
346
+ return ReactDOM.createPortal(
347
+ this.renderPopper(uniqueId),
348
+ popperHost,
349
+ );
350
+ }
351
+
352
+ // Otherwise, append the dialog next to the trigger element
353
+ return this.renderPopper(uniqueId);
354
+ }
355
+
298
356
  render(): React.ReactNode {
299
357
  const {children, dismissEnabled, id} = this.props;
300
358
  const {opened, placement} = this.state;
301
- const popperHost = this.getHost();
302
359
 
303
360
  return (
304
361
  <PopoverContext.Provider
@@ -319,12 +376,7 @@ export default class Popover extends React.Component<Props, State> {
319
376
  >
320
377
  {children}
321
378
  </PopoverAnchor>
322
- {popperHost &&
323
- opened &&
324
- ReactDOM.createPortal(
325
- this.renderPopper(uniqueId),
326
- popperHost,
327
- )}
379
+ {this.renderPortal(uniqueId, opened)}
328
380
  </React.Fragment>
329
381
  )}
330
382
  </IDProvider>