@khanacademy/wonder-blocks-popover 6.3.18 → 7.0.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @khanacademy/wonder-blocks-popover
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 4aef2da: Refactor `Popover` to use the `wonder-blocks-floating` package (floating-ui) instead of PopperJS and the `wonder-blocks-tooltip` dependency.
8
+
9
+ Breaking / behavior changes:
10
+
11
+ - `Popover` now positions its content with floating-ui via the `Floating` component. The `@popperjs/core` and `react-popper` peer dependencies (and the `@khanacademy/wonder-blocks-tooltip` / `@khanacademy/wonder-blocks-modal` dependencies) have been removed.
12
+ - `PopoverContentCore` no longer renders the popover "bubble" chrome (background, border, border radius and shadow). That chrome now comes from `Floating`, so a `PopoverContentCore` rendered on its own — outside a `Popover` — no longer looks like a popover bubble and needs its own container styling.
13
+ - The popover tail is now rendered by `Floating` (as its arrow) instead of `TooltipTail`. The `showTail` prop is unchanged, but the tail's markup and styling now come from `Floating`.
14
+ - The popover is positioned with floating-ui's `fixed` strategy, so it is no longer clipped by scrolling or `overflow`-clipping ancestors the way it could be before.
15
+ - Focus management is now handled by floating-ui's non-modal focus manager rather than the previous custom implementation. The popover remains non-modal and focus is not trapped or cycled (it never was): tabbing past the last focusable element in the popover moves focus on to the next element after the trigger, and `dismissEnabled` popovers close when focus leaves them. The mechanism behind that is floating-ui's focus guards rather than Popover's own keydown handling.
16
+ - `rootBoundary` and `viewportPadding` are now remapped onto floating-ui's `flip`/`shift` middleware (prop names unchanged).
17
+ - `autoUpdate` is deprecated and now a no-op: the popover always keeps its position in sync with the anchor (floating-ui's `autoUpdate`).
18
+ - `initialFocusDelay` is deprecated and now a no-op: initial focus is applied synchronously when the popover opens.
19
+ - `onClose` is now called at most once per open/close cycle. floating-ui can request a close through several channels (escape, outside click, focus out), so the callback is guarded against firing more than once for a single dismissal.
20
+ - `testId` is now actually applied. It was previously declared on `Popover`'s props but never used; it is now forwarded to the floating element.
21
+
22
+ - 4aef2da: `Popover` no longer uses `ReactDOM.findDOMNode` (removed in React 19) to find its trigger's element. The trigger now has to do one of two things for the popover to anchor to it, and most triggers already do both:
23
+
24
+ - Attach the ref it is given to its element. `Popover` only passes a ref to triggers that can receive one (host elements and `React.forwardRef` components), so a plain function component doesn't get React's "Function components cannot be given refs" error.
25
+ - Spread the props it is given (`id`, `aria-controls`, `aria-expanded`, the `onClick` handler that opens the popover, and the attribute that identifies the anchor) onto its element. The anchor element is then resolved from the DOM.
26
+
27
+ The trigger can be any component type (host element, `React.forwardRef` component, class component or plain function component). A trigger that does neither, e.g. a function component that ignores its props, is no longer anchored. Before, `findDOMNode` anchored the popover to the trigger's first DOM node regardless.
28
+
29
+ A trigger's own `ref` is merged rather than replaced, so it keeps resolving the same element.
30
+
31
+ ### Patch Changes
32
+
33
+ - 4aef2da: - Updates `PopoverAnchor`, `PopoverContent`, and `PopoverContentCore` to pass `forwardRef`
34
+
35
+ - Updates different components to be function components instead of class based components.
36
+
37
+ - Updated dependencies [4aef2da]
38
+ - Updated dependencies [4aef2da]
39
+ - Updated dependencies [4aef2da]
40
+ - Updated dependencies [4aef2da]
41
+ - Updated dependencies [4aef2da]
42
+ - Updated dependencies [4aef2da]
43
+ - Updated dependencies [4aef2da]
44
+ - Updated dependencies [4aef2da]
45
+ - @khanacademy/wonder-blocks-floating@0.1.0
46
+
47
+ ## 6.3.19
48
+
49
+ ### Patch Changes
50
+
51
+ - Updated dependencies [c56e31b]
52
+ - @khanacademy/wonder-blocks-modal@8.9.0
53
+ - @khanacademy/wonder-blocks-tooltip@4.2.8
54
+
3
55
  ## 6.3.18
4
56
 
5
57
  ### Patch Changes
@@ -1,19 +1,22 @@
1
1
  import * as React from "react";
2
- import type { AriaProps } from "@khanacademy/wonder-blocks-core";
3
- type Props = AriaProps & {
4
- /**
5
- * Callback to be invoked when the anchored content is mounted.
6
- * This provides a reference to the anchored content, which can then be
7
- * used for calculating popover content positioning.
8
- */
9
- anchorRef: (arg1?: HTMLElement) => unknown;
2
+ /**
3
+ * The element that triggers the popover dialog. This is also used as reference
4
+ * to position the dialog itself.
5
+ *
6
+ * The ref it receives comes from `Floating` and resolves the element the popover
7
+ * is anchored to. It is passed along to the trigger, alongside the attribute
8
+ * that lets `Floating` resolve that element from the DOM instead, so that a
9
+ * trigger only has to do one of the two (see `Floating`'s `children` prop).
10
+ */
11
+ declare const PopoverAnchor: React.ForwardRefExoticComponent<Readonly<import("@khanacademy/wonder-blocks-core").AriaAttributes> & Readonly<{
12
+ role?: import("@khanacademy/wonder-blocks-core").AriaRole;
13
+ }> & {
10
14
  /**
11
15
  * The element that triggers the popover. This element will be used to
12
16
  * position the popover. It can be either a Node or a function using the
13
17
  * children-as-function pattern to pass an open function for use anywhere
14
18
  * within children. The latter provides a lot of flexibility in terms of
15
- * what actions may trigger the `Popover` to launch the
16
- * [PopoverDialog](#PopoverDialog).
19
+ * what actions may trigger the `Popover` to launch the popover dialog.
17
20
  */
18
21
  children: React.ReactElement<any> | ((arg1: {
19
22
  open: () => void;
@@ -26,13 +29,11 @@ type Props = AriaProps & {
26
29
  * Called when the anchor is clicked
27
30
  */
28
31
  onClick: () => void;
29
- };
30
- /**
31
- * The element that triggers the popover dialog. This is also used as reference
32
- * to position the dialog itself.
33
- */
34
- export default class PopoverAnchor extends React.Component<Props> {
35
- componentDidMount(): void;
36
- render(): React.ReactNode;
37
- }
38
- export {};
32
+ /**
33
+ * Injected by `Floating` to identify the trigger's element in the DOM. It is
34
+ * passed along to the trigger so that `Floating` can find that element and
35
+ * use it as the reference (anchor) element.
36
+ */
37
+ "data-wb-floating-reference"?: string;
38
+ } & React.RefAttributes<HTMLElement>>;
39
+ export default PopoverAnchor;
@@ -1,6 +1,36 @@
1
1
  import * as React from "react";
2
- import type { AriaProps, StyleType } from "@khanacademy/wonder-blocks-core";
3
- type Props = AriaProps & {
2
+ import type { StyleType } from "@khanacademy/wonder-blocks-core";
3
+ /**
4
+ * This is the base popover container. It’s used internally by all the variants.
5
+ * Also, it can be used to create flexible popovers.
6
+ *
7
+ * ### Presentation
8
+ *
9
+ * `PopoverContentCore` only lays out the popover's contents (max width,
10
+ * padding and the optional close button). The popover "bubble" chrome —
11
+ * background, border, border radius, shadow and the tail — is drawn by the
12
+ * `Floating` component that `Popover` renders it inside.
13
+ *
14
+ * This means that when `PopoverContentCore` is rendered **standalone**, outside
15
+ * of a `Popover` — which is how the examples on this page are rendered — it has
16
+ * no bubble around it and you need to supply your own container styling. Inside
17
+ * a `Popover` it looks like a popover with no extra work.
18
+ *
19
+ * ### Usage
20
+ *
21
+ * ```jsx
22
+ * import {PopoverContentCore} from "@khanacademy/wonder-blocks-popover";
23
+ *
24
+ * <PopoverContentCore>
25
+ * <>
26
+ * Some custom layout
27
+ * </>
28
+ * </PopoverContentCore>
29
+ * ```
30
+ */
31
+ declare const PopoverContentCore: React.ForwardRefExoticComponent<Readonly<import("@khanacademy/wonder-blocks-core").AriaAttributes> & Readonly<{
32
+ role?: import("@khanacademy/wonder-blocks-core").AriaRole;
33
+ }> & {
4
34
  /**
5
35
  * The content to render inside the popover.
6
36
  */
@@ -25,29 +55,5 @@ type Props = AriaProps & {
25
55
  * Test ID used for e2e testing.
26
56
  */
27
57
  testId?: string;
28
- };
29
- type DefaultProps = {
30
- closeButtonLight: Props["closeButtonLight"];
31
- closeButtonVisible: Props["closeButtonVisible"];
32
- };
33
- /**
34
- * This is the base popover container. It’s used internally by all the variants.
35
- * Also, it can be used to create flexible popovers.
36
- *
37
- * ### Usage
38
- *
39
- * ```jsx
40
- * import {PopoverContentCore} from "@khanacademy/wonder-blocks-popover";
41
- *
42
- * <PopoverContentCore>
43
- * <>
44
- * Some custom layout
45
- * </>
46
- * </PopoverContentCore>
47
- * ```
48
- */
49
- export default class PopoverContentCore extends React.Component<Props> {
50
- static defaultProps: DefaultProps;
51
- render(): React.ReactNode;
52
- }
53
- export {};
58
+ } & React.RefAttributes<HTMLElement>>;
59
+ export default PopoverContentCore;
@@ -1,7 +1,24 @@
1
1
  import * as React from "react";
2
- import type { AriaProps, StyleType } from "@khanacademy/wonder-blocks-core";
3
- import type { PopoverContextType } from "./popover-context";
4
- type CommonProps = AriaProps & {
2
+ import type { StyleType } from "@khanacademy/wonder-blocks-core";
3
+ /**
4
+ * This is the container that is consumed by all the predefined variations. Its
5
+ * main responsibility is populate the contents depending on the variation used.
6
+ *
7
+ * ### Usage
8
+ *
9
+ * ```jsx
10
+ * import {PopoverContent} from "@khanacademy/wonder-blocks-popover";
11
+ *
12
+ * <PopoverContent
13
+ * closeButtonVisible
14
+ * content="Some content for the popover"
15
+ * title="Popover with text only"
16
+ * />
17
+ * ```
18
+ */
19
+ declare const PopoverContent: React.ForwardRefExoticComponent<Readonly<import("@khanacademy/wonder-blocks-core").AriaAttributes> & Readonly<{
20
+ role?: import("@khanacademy/wonder-blocks-core").AriaRole;
21
+ }> & {
5
22
  /**
6
23
  * The content to render inside the popover.
7
24
  */
@@ -48,8 +65,7 @@ type CommonProps = AriaProps & {
48
65
  * @ignore
49
66
  */
50
67
  uniqueId?: string;
51
- };
52
- type Props = CommonProps & {
68
+ } & {
53
69
  /**
54
70
  * Decorate the popover with an illustrated icon. It cannot be used at the
55
71
  * same time with image.
@@ -65,36 +81,5 @@ type Props = CommonProps & {
65
81
  * the same time with icon.
66
82
  */
67
83
  image?: React.ReactElement<React.ComponentProps<"img">> | React.ReactElement<React.ComponentProps<"svg">>;
68
- };
69
- type DefaultProps = {
70
- closeButtonVisible: Props["closeButtonVisible"];
71
- };
72
- /**
73
- * This is the container that is consumed by all the predefined variations. Its
74
- * main responsibility is populate the contents depending on the variation used.
75
- *
76
- * ### Usage
77
- *
78
- * ```jsx
79
- * import {PopoverContent} from "@khanacademy/wonder-blocks-popover";
80
- *
81
- * <PopoverContent
82
- * closeButtonVisible
83
- * content="Some content for the popover"
84
- * title="Popover with text only"
85
- * />
86
- * ```
87
- */
88
- export default class PopoverContent extends React.Component<Props> {
89
- static defaultProps: DefaultProps;
90
- componentDidMount(): void;
91
- /**
92
- * Runtime validation in case we try to use an invalid shape
93
- */
94
- validateProps({ placement }: PopoverContextType): void;
95
- maybeRenderImage: (context: PopoverContextType) => React.ReactElement;
96
- maybeRenderIcon: () => React.ReactElement;
97
- maybeRenderActions: (close: () => unknown) => React.ReactElement;
98
- render(): React.ReactNode;
99
- }
100
- export {};
84
+ } & React.RefAttributes<HTMLElement>>;
85
+ export default PopoverContent;
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import type { Placement } from "@khanacademy/wonder-blocks-tooltip";
2
+ import type { Placement } from "../util/types";
3
3
  export type PopoverContextType = {
4
4
  /**
5
5
  * Facilitates passing the `onClose` handler from the Popover down to its
@@ -7,10 +7,10 @@ export type PopoverContextType = {
7
7
  */
8
8
  close?: () => unknown;
9
9
  /**
10
- * Facilitates passing this value from Popover (via TooltipPopper) down to
11
- * PopoverContent. This is needed here to reposition the illustration to the
12
- * start or the end of the content, in case the popper changes its
13
- * placement.
10
+ * Facilitates passing this value from Popover (via the Floating component)
11
+ * down to PopoverContent. This is needed here to reposition the
12
+ * illustration to the start or the end of the content, in case the floating
13
+ * element changes its placement.
14
14
  */
15
15
  placement?: Placement;
16
16
  };
@@ -1,13 +1,20 @@
1
1
  import * as React from "react";
2
- import type { AriaProps } from "@khanacademy/wonder-blocks-core";
3
- import type { Placement, PopperElementProps } from "@khanacademy/wonder-blocks-tooltip";
2
+ import type { Placement } from "../util/types";
4
3
  import PopoverContent from "./popover-content";
5
4
  import PopoverContentCore from "./popover-content-core";
6
- type Props = AriaProps &
7
5
  /**
8
- * Required to correctly position the elements inside the dialog
9
- * @ignore
10
- */ PopperElementProps & {
6
+ * This is an internal component that we use to render the dialog wrapper that
7
+ * appears when a popover shows. It wraps the popover content (of type
8
+ * [PopoverContent](#PopoverContent) or
9
+ * [PopoverContentCore](#PopoverContentCore)) and provides the `dialog` role
10
+ * along with its accessibility wiring.
11
+ *
12
+ * Positioning and the tail/arrow are handled by the `Floating` component, so
13
+ * this component only provides the semantic dialog container.
14
+ */
15
+ declare const PopoverDialog: React.ForwardRefExoticComponent<Readonly<import("@khanacademy/wonder-blocks-core").AriaAttributes> & Readonly<{
16
+ role?: import("@khanacademy/wonder-blocks-core").AriaRole;
17
+ }> & {
11
18
  /**
12
19
  * The content to render inside the dialog.
13
20
  */
@@ -17,30 +24,9 @@ type Props = AriaProps &
17
24
  */
18
25
  id?: string;
19
26
  /**
20
- * Called when popper changes its placement
21
- */
22
- onUpdate: (placement: Placement) => unknown;
23
- /**
24
- * Whether to show the popover tail or not.
27
+ * The current placement of the popover, reflected on the `data-placement`
28
+ * attribute.
25
29
  */
26
- showTail: boolean;
27
- };
28
- /**
29
- * This is an internal component that we use to render the stuff that appears
30
- * when a popover shows. It's composed by two elements: The popover content,
31
- * that can be of type [PopoverContent](#PopoverContent) or
32
- * [PopoverContentCore](#PopoverContentCore), and the
33
- * [TooltipTail](#TooltipTail).
34
- *
35
- * The main difference with [TooltipBubble](#TooltipBubble) is that bubble
36
- * handles hover states and PopoverDialog doesn't need to handle any states at
37
- * all (for now). Also, PopoverDialog needs to coordinate different background
38
- * colors for the content and tail components.
39
- *
40
- * Note that without explicit positioning, the tail will not be centered.
41
- */
42
- export default class PopoverDialog extends React.Component<Props> {
43
- componentDidUpdate(prevProps: Props): void;
44
- render(): React.ReactNode;
45
- }
46
- export {};
30
+ placement: Placement;
31
+ } & React.RefAttributes<HTMLElement>>;
32
+ export default PopoverDialog;
@@ -1,22 +1,22 @@
1
1
  import * as React from "react";
2
2
  import type { AriaProps } from "@khanacademy/wonder-blocks-core";
3
- import type { Placement } from "@khanacademy/wonder-blocks-tooltip";
4
- import type { RootBoundary } from "@popperjs/core";
5
3
  import PopoverContent from "./popover-content";
6
4
  import PopoverContentCore from "./popover-content-core";
5
+ import type { Placement, RootBoundary } from "../util/types";
7
6
  type PopoverContents = React.ReactElement<React.ComponentProps<typeof PopoverContent>> | React.ReactElement<React.ComponentProps<typeof PopoverContentCore>>;
8
- type Props = AriaProps & Readonly<{
9
- /**
10
- * Whether the popover should update its position when the anchor
11
- * element changes size or position. Defaults to false.
12
- */
13
- autoUpdate?: boolean;
7
+ type Props = AriaProps & {
14
8
  /**
15
9
  * The element that triggers the popover. This element will be used to
16
10
  * position the popover. It can be either a Node or a function using the
17
11
  * children-as-function pattern to pass an open function for use anywhere
18
12
  * within children. The latter provides a lot of flexibility in terms of
19
13
  * what actions may trigger the `Popover` to launch the popover dialog.
14
+ *
15
+ * The trigger can be any component type, but it has to either attach the
16
+ * ref it is given or spread the props it is given onto its element, so
17
+ * that the popover can be anchored to it. Spreading the props is
18
+ * recommended, since they include the ARIA attributes and the `onClick`
19
+ * handler that opens the popover. See the "Custom Triggers" example.
20
20
  */
21
21
  children: React.ReactElement<any> | ((arg1: {
22
22
  open: () => void;
@@ -35,8 +35,9 @@ type Props = AriaProps & Readonly<{
35
35
  }) => PopoverContents);
36
36
  /**
37
37
  * Where the popover should try to appear in relation to the trigger element.
38
+ * Defaults to "top".
38
39
  */
39
- placement: Placement;
40
+ placement?: Placement;
40
41
  /**
41
42
  * When enabled, user can hide the popover content by pressing the `esc` key
42
43
  * or clicking/tapping outside of it.
@@ -67,13 +68,6 @@ type Props = AriaProps & Readonly<{
67
68
  * popover content will be used.
68
69
  */
69
70
  initialFocusId?: string;
70
- /**
71
- * The delay in milliseconds before the initial focus is set.
72
- * This allows any active event listeners to finish before focusing.
73
- *
74
- * Defaults to 0.
75
- */
76
- initialFocusDelay?: number;
77
71
  /**
78
72
  * Renders the popover when true, renders nothing when false.
79
73
  *
@@ -93,7 +87,7 @@ type Props = AriaProps & Readonly<{
93
87
  /**
94
88
  * Whether to show the popover tail or not. Defaults to true.
95
89
  */
96
- showTail: boolean;
90
+ showTail?: boolean;
97
91
  /**
98
92
  * Optional property to enable the portal functionality of popover.
99
93
  * This is very handy in cases where the Popover can't be easily
@@ -109,47 +103,45 @@ type Props = AriaProps & Readonly<{
109
103
  */
110
104
  portal?: boolean;
111
105
  /**
112
- * Optional property to set what the root boundary is for the popper behavior.
113
- * This is set to "viewport" by default, causing the popper to be positioned based
114
- * on the user's viewport. If set to "document", it will position itself based
115
- * on where there is available room within the document body.
106
+ * Optional property to set what the root boundary is for the positioning
107
+ * behavior. This is set to "viewport" by default, causing the popover to be
108
+ * positioned based on the user's viewport. If set to "document", it will
109
+ * position itself based on where there is available room within the
110
+ * document body.
116
111
  */
117
112
  rootBoundary?: RootBoundary;
118
113
  /**
119
114
  * If `rootBoundary` is `viewport`, this padding value is used to provide
120
- * spacing between the popper and the viewport. If not provided, default
115
+ * spacing between the popover and the viewport. If not provided, default
121
116
  * spacing of 12px is applied.
122
117
  */
123
118
  viewportPadding?: number;
124
- }>;
125
- type State = Readonly<{
126
119
  /**
127
- * Keeps a reference of the dialog state
128
- */
129
- opened: boolean;
130
- /**
131
- * Anchor element DOM reference
120
+ * Whether the popover should update its position when the anchor
121
+ * element changes size or position.
122
+ *
123
+ * @deprecated The popover now always keeps its position in sync with the
124
+ * anchor element (via floating-ui's `autoUpdate`), so this prop no longer
125
+ * has any effect.
132
126
  */
133
- anchorElement?: HTMLElement;
127
+ autoUpdate?: boolean;
134
128
  /**
135
- * Current popper placement
129
+ * The delay in milliseconds before the initial focus is set.
130
+ *
131
+ * @deprecated Initial focus is now handled by floating-ui and is applied
132
+ * synchronously when the popover opens, so this prop no longer has any
133
+ * effect.
136
134
  */
137
- placement: Placement;
138
- }>;
139
- type DefaultProps = Readonly<{
140
- placement: Props["placement"];
141
- showTail: Props["showTail"];
142
- portal: Props["portal"];
143
- rootBoundary: Props["rootBoundary"];
144
- }>;
135
+ initialFocusDelay?: number;
136
+ };
145
137
  /**
146
138
  * Popovers provide additional information that is related to a particular
147
139
  * element and/or content. They can include text, links, icons and
148
140
  * illustrations. The main difference with `Tooltip` is that they must be
149
141
  * dismissed by clicking an element.
150
142
  *
151
- * This component uses the `PopoverPopper` component to position the
152
- * `PopoverContentCore` component according to the children it is wrapping.
143
+ * This component uses the `Floating` component (powered by floating-ui) to
144
+ * position the popover content according to the element it is wrapping.
153
145
  *
154
146
  * ### Usage
155
147
  *
@@ -165,35 +157,5 @@ type DefaultProps = Readonly<{
165
157
  * </Popover>
166
158
  * ```
167
159
  */
168
- export default class Popover extends React.Component<Props, State> {
169
- static defaultProps: DefaultProps;
170
- /**
171
- * Used to sync the `opened` state when Popover acts as a controlled
172
- * component
173
- */
174
- static getDerivedStateFromProps(props: Props, state: State): Partial<State> | null | undefined;
175
- state: State;
176
- /**
177
- * Popover content ref
178
- */
179
- contentRef: React.RefObject<PopoverContent | PopoverContentCore>;
180
- /**
181
- * Returns focus to a given element.
182
- */
183
- maybeReturnFocus: () => void;
184
- /**
185
- * Popover dialog closed
186
- */
187
- handleClose: (shouldReturnFocus?: boolean) => void;
188
- /**
189
- * Popover dialog opened
190
- */
191
- handleOpen: () => void;
192
- updateRef: (actualRef?: HTMLElement) => void;
193
- renderContent(uniqueId: string): PopoverContents;
194
- renderPopper(uniqueId: string): React.ReactNode;
195
- getHost(): Element | null | undefined;
196
- renderPortal(uniqueId: string, opened: boolean): React.ReactNode;
197
- render(): React.ReactNode;
198
- }
199
- export {};
160
+ declare function Popover(props: Props): React.JSX.Element;
161
+ export default Popover;
package/dist/es/index.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import * as ReactDOM from 'react-dom';
4
- import { View, keys, Id, addStyle } from '@khanacademy/wonder-blocks-core';
5
- import { TooltipTail, TooltipPopper } from '@khanacademy/wonder-blocks-tooltip';
6
- import { maybeGetPortalMountedModalHostElement } from '@khanacademy/wonder-blocks-modal';
3
+ import { useMergeRefs, getElementRef, FloatingReferenceAttributeName, canAcceptRef, Floating } from '@khanacademy/wonder-blocks-floating';
4
+ import { View, addStyle } from '@khanacademy/wonder-blocks-core';
7
5
  import { StyleSheet } from 'aphrodite';
8
- import * as tokens from '@khanacademy/wonder-blocks-tokens';
9
- import { border, semanticColor, boxShadow, sizing } from '@khanacademy/wonder-blocks-tokens';
6
+ import { sizing } from '@khanacademy/wonder-blocks-tokens';
10
7
  import { Heading, BodyText } from '@khanacademy/wonder-blocks-typography';
11
8
  import { actionStyles } from '@khanacademy/wonder-blocks-styles';
12
9
  import xIcon from '@phosphor-icons/core/regular/x.svg';
@@ -14,24 +11,16 @@ import IconButton from '@khanacademy/wonder-blocks-icon-button';
14
11
 
15
12
  const defaultContext={close:undefined,placement:"top"};const PopoverContext=React.createContext(defaultContext);PopoverContext.displayName="PopoverContext";
16
13
 
17
- class PopoverAnchor extends React.Component{componentDidMount(){const anchorNode=ReactDOM.findDOMNode(this);if(anchorNode){this.props.anchorRef(anchorNode);}}render(){const{children,id,onClick,"aria-controls":ariaControls,"aria-expanded":ariaExpanded}=this.props;const sharedProps={id:id,"aria-controls":ariaControls,"aria-expanded":ariaExpanded};if(typeof children==="function"){const renderedChildren=children({open:onClick});return React.cloneElement(renderedChildren,sharedProps)}else {return React.cloneElement(children,{...children.props,...sharedProps,onClick:children.props.onClick?e=>{e.stopPropagation();children.props.onClick();onClick();}:onClick})}}}
14
+ const PopoverAnchor=React.forwardRef(function PopoverAnchor(props,ref){const{children,id,onClick,"aria-controls":ariaControls,"aria-expanded":ariaExpanded,[FloatingReferenceAttributeName]:floatingReferenceId}=props;const isFunctionChildren=typeof children==="function";const renderedChildren=isFunctionChildren?children({open:onClick}):children;const triggerRef=useMergeRefs([ref,getElementRef(renderedChildren)]);const sharedProps={id:id,"aria-controls":ariaControls,"aria-expanded":ariaExpanded,[FloatingReferenceAttributeName]:floatingReferenceId,...canAcceptRef(renderedChildren)?{ref:triggerRef}:undefined};if(isFunctionChildren){return React.cloneElement(renderedChildren,sharedProps)}return React.cloneElement(renderedChildren,{...renderedChildren.props,...sharedProps,onClick:renderedChildren.props.onClick?e=>{e.stopPropagation();renderedChildren.props.onClick();onClick();}:onClick})});PopoverAnchor.displayName="PopoverAnchor";
18
15
 
19
- class PopoverDialog extends React.Component{componentDidUpdate(prevProps){if(prevProps.placement!==this.props.placement){this.props.onUpdate(this.props.placement);}}render(){const{placement,children,id,isReferenceHidden,updateBubbleRef,updateTailRef,tailOffset,style,showTail,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,"aria-label":ariaLabel}=this.props;const contentProps=children.props;const color=contentProps.color;return jsx(React.Fragment,{children:jsxs(View,{"aria-label":ariaLabel,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,id:id,role:"dialog",ref:updateBubbleRef,"data-placement":placement,style:[isReferenceHidden&&styles$2.hide,styles$2[`content-${placement}`],style],children:[children,jsx(TooltipTail,{show:showTail,color:color,updateRef:updateTailRef,placement:placement,offset:tailOffset})]})})}}const styles$2=StyleSheet.create({hide:{pointerEvents:"none",opacity:0,backgroundColor:tokens.semanticColor.core.transparent,color:tokens.semanticColor.core.transparent},"content-top":{flexDirection:"column"},"content-right":{flexDirection:"row-reverse"},"content-bottom":{flexDirection:"column-reverse"},"content-left":{flexDirection:"row"}});
16
+ const PopoverDialog=React.forwardRef(function PopoverDialog(props,ref){const{children,id,placement,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,"aria-label":ariaLabel}=props;return jsx(View,{"aria-label":ariaLabel,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,id:id,role:"dialog",ref:ref,"data-placement":placement,children:children})});PopoverDialog.displayName="PopoverDialog";
20
17
 
21
- const FOCUSABLE_ELEMENTS='button:not([tabindex="-1"]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}function isFocusable(element){return element.matches(FOCUSABLE_ELEMENTS)}
22
-
23
- class PopoverEventListener extends React.Component{componentDidMount(){window.addEventListener("keyup",this._handleKeyup);window.addEventListener("click",this._handleClick);}componentWillUnmount(){window.removeEventListener("keyup",this._handleKeyup);window.removeEventListener("click",this._handleClick);}render(){return null}constructor(...args){super(...args),this.state={isFirstClick:true},this._handleKeyup=e=>{if(e.key==="Escape"){e.preventDefault();e.stopPropagation();this.props.onClose(true);}},this._handleClick=e=>{if(this.state.isFirstClick){this.setState({isFirstClick:false});return}const node=ReactDOM.findDOMNode(this.props.contentRef?.current);if(node&&!node.contains(e.target)){e.preventDefault();e.stopPropagation();const shouldReturnFocus=!isFocusable(e.target);this.props.onClose(shouldReturnFocus);}};}}
24
-
25
- class InitialFocus extends React.Component{componentDidMount(){const node=ReactDOM.findDOMNode(this);if(!node){return}this.setInitialFocusableElement(node);}maybeGetInitialFocusElement(node){const{initialFocusId}=this.props;if(!initialFocusId){return null}return node.querySelector(`#${initialFocusId}`)}maybeGetFirstFocusableElement(node){const focusableElements=findFocusableNodes(node);if(!focusableElements.length){return null}return focusableElements[0]}render(){return this.props.children}constructor(...args){super(...args),this.setInitialFocusableElement=node=>{const firstFocusableElement=this.maybeGetInitialFocusElement(node)||this.maybeGetFirstFocusableElement(node)||node;if(firstFocusableElement===node){node.tabIndex=-1;}setTimeout(()=>{firstFocusableElement.focus();},this.props.delay||0);};}}
26
-
27
- class FocusManager extends React.Component{componentDidMount(){this.addEventListeners();}componentDidUpdate(){this.removeEventListeners();this.addEventListeners();}componentWillUnmount(){this.changeFocusabilityInsidePopover(true);this.removeEventListeners();}removeEventListeners(){const{anchorElement}=this.props;if(anchorElement){anchorElement.removeEventListener("keydown",this.handleKeydownPreviousFocusableElement);}if(!this.nextElementAfterPopover){window.removeEventListener("blur",()=>{this.changeFocusabilityInsidePopover(true);});}if(this.firstFocusableElementInPopover){this.firstFocusableElementInPopover.removeEventListener("keydown",this.handleKeydownFirstFocusableElement);}if(this.lastFocusableElementInPopover){this.lastFocusableElementInPopover.removeEventListener("keydown",this.handleKeydownLastFocusableElement);}if(this.nextElementAfterPopover){this.nextElementAfterPopover.removeEventListener("keydown",this.handleKeydownNextFocusableElement);}}render(){const{children}=this.props;return jsx("div",{ref:this.getComponentRootNode,onClick:()=>{this.changeFocusabilityInsidePopover(true);},onFocus:()=>{this.changeFocusabilityInsidePopover(true);},onBlur:()=>{this.changeFocusabilityInsidePopover(false);},children:jsx(InitialFocus,{initialFocusId:this.props.initialFocusId,delay:this.props.initialFocusDelay,children:children})})}constructor(...args){super(...args),this.elementsThatCanBeFocusableInsidePopover=[],this.firstFocusableElementInPopover=null,this.lastFocusableElementInPopover=null,this.addEventListeners=()=>{const{anchorElement}=this.props;if(anchorElement){anchorElement.addEventListener("keydown",this.handleKeydownPreviousFocusableElement);}if(this.rootNode){this.elementsThatCanBeFocusableInsidePopover=findFocusableNodes(this.rootNode);this.firstFocusableElementInPopover=this.elementsThatCanBeFocusableInsidePopover[0];this.lastFocusableElementInPopover=this.elementsThatCanBeFocusableInsidePopover[this.elementsThatCanBeFocusableInsidePopover.length-1];}this.nextElementAfterPopover=this.getNextFocusableElement();if(!this.nextElementAfterPopover){window.addEventListener("blur",()=>{this.changeFocusabilityInsidePopover(true);});}if(this.firstFocusableElementInPopover){this.firstFocusableElementInPopover.addEventListener("keydown",this.handleKeydownFirstFocusableElement);}if(this.lastFocusableElementInPopover){this.lastFocusableElementInPopover.addEventListener("keydown",this.handleKeydownLastFocusableElement);}if(this.nextElementAfterPopover){this.nextElementAfterPopover.addEventListener("keydown",this.handleKeydownNextFocusableElement);}},this.handleKeydownFirstFocusableElement=e=>{if(e.key===keys.tab&&e.shiftKey){e.preventDefault();this.props.anchorElement?.focus();}},this.handleKeydownLastFocusableElement=e=>{if(this.nextElementAfterPopover&&e.key===keys.tab&&!e.shiftKey){e.preventDefault();this.nextElementAfterPopover?.focus();}if(e.key===keys.tab&&!e.shiftKey){this.props.onFocusOut?.();}},this.getNextFocusableElement=()=>{const{anchorElement}=this.props;if(!anchorElement){return}const focusableElements=findFocusableNodes(document);const focusableElementsOutside=focusableElements.filter(element=>{const index=this.elementsThatCanBeFocusableInsidePopover.indexOf(element);return index<0});const anchorIndex=focusableElementsOutside.indexOf(anchorElement);if(anchorIndex>=0&&anchorIndex!==focusableElementsOutside.length-1){const nextElementIndex=anchorIndex<focusableElementsOutside.length-1?anchorIndex+1:0;return focusableElementsOutside[nextElementIndex]}return},this.getComponentRootNode=node=>{if(!node){return}const rootNode=ReactDOM.findDOMNode(node);if(!rootNode){throw new Error("Assertion error: root node should exist after mount")}this.rootNode=rootNode;},this.changeFocusabilityInsidePopover=(enabled=true)=>{const tabIndex=enabled?"0":"-1";this.elementsThatCanBeFocusableInsidePopover.forEach(element=>{element.setAttribute("tabIndex",tabIndex);});},this.handleKeydownPreviousFocusableElement=e=>{if(e.key===keys.tab&&!e.shiftKey){e.preventDefault();this.firstFocusableElementInPopover?.focus();}if(e.key===keys.tab&&e.shiftKey){this.props.onFocusOut?.();}},this.handleKeydownNextFocusableElement=e=>{if(e.key===keys.tab&&e.shiftKey){e.preventDefault();this.lastFocusableElementInPopover?.focus();}};}}
28
-
29
- class Popover extends React.Component{static getDerivedStateFromProps(props,state){return {opened:typeof props.opened==="boolean"?props.opened:state.opened}}renderContent(uniqueId){const{content}=this.props;const popoverContents=typeof content==="function"?content({close:this.handleClose}):content;return React.cloneElement(popoverContents,{ref:this.contentRef,uniqueId})}renderPopper(uniqueId){const{autoUpdate,dismissEnabled,initialFocusId,placement,showTail,portal,"aria-label":ariaLabel,"aria-describedby":ariaDescribedBy,rootBoundary,viewportPadding,initialFocusDelay}=this.props;const{anchorElement}=this.state;const describedBy=ariaDescribedBy||`${uniqueId}-content`;const ariaLabelledBy=ariaLabel?undefined:`${uniqueId}-title`;const shouldAnchorExist=autoUpdate?anchorElement:true;if(!shouldAnchorExist){return null}const popperContent=jsx(TooltipPopper,{anchorElement:anchorElement,autoUpdate:autoUpdate,placement:placement,rootBoundary:rootBoundary,viewportPadding:viewportPadding,children:props=>jsx(PopoverDialog,{...props,"aria-label":ariaLabel,"aria-describedby":describedBy,"aria-labelledby":ariaLabelledBy,id:uniqueId,onUpdate:placement=>this.setState({placement}),showTail:showTail,children:this.renderContent(uniqueId)})});if(portal){return jsx(FocusManager,{anchorElement:anchorElement,initialFocusId:initialFocusId,initialFocusDelay:initialFocusDelay,onFocusOut:dismissEnabled?this.handleClose:undefined,children:popperContent})}else {return jsx(InitialFocus,{initialFocusId:initialFocusId,delay:initialFocusDelay,children:popperContent})}}getHost(){return maybeGetPortalMountedModalHostElement(this.state.anchorElement)||document.body}renderPortal(uniqueId,opened){if(!opened){return null}const{portal}=this.props;const popperHost=this.getHost();if(portal&&popperHost){return ReactDOM.createPortal(this.renderPopper(uniqueId),popperHost)}return this.renderPopper(uniqueId)}render(){const{children,dismissEnabled,id}=this.props;const{opened,placement}=this.state;return jsxs(PopoverContext.Provider,{value:{close:this.handleClose,placement:placement},children:[jsx(Id,{id:id,children:uniqueId=>jsxs(React.Fragment,{children:[jsx(PopoverAnchor,{anchorRef:this.updateRef,id:`${uniqueId}-anchor`,"aria-controls":uniqueId,"aria-expanded":opened?"true":"false",onClick:this.handleOpen,children:children}),this.renderPortal(uniqueId,opened)]})}),dismissEnabled&&opened&&jsx(PopoverEventListener,{onClose:this.handleClose,contentRef:this.contentRef})]})}constructor(...args){super(...args),this.state={opened:!!this.props.opened,placement:this.props.placement},this.contentRef=React.createRef(),this.maybeReturnFocus=()=>{const{anchorElement}=this.state;const{closedFocusId}=this.props;if(closedFocusId){const focusElement=ReactDOM.findDOMNode(document.getElementById(closedFocusId));focusElement?.focus();return}if(anchorElement){anchorElement.focus();}},this.handleClose=(shouldReturnFocus=true)=>{this.setState({opened:false},()=>{this.props.onClose?.();if(shouldReturnFocus){this.maybeReturnFocus();}});},this.handleOpen=()=>{if(this.props.dismissEnabled&&this.state.opened){this.handleClose(true);}else {this.setState({opened:true});}},this.updateRef=actualRef=>{if(actualRef&&this.state.anchorElement!==actualRef){this.setState({anchorElement:actualRef});}};}}Popover.defaultProps={placement:"top",showTail:true,portal:true,rootBoundary:"viewport"};
18
+ function Popover(props){const{children,content,placement:placementProp="top",dismissEnabled,id,closedFocusId,initialFocusId,opened:openedProp,onClose,showTail=true,portal=true,rootBoundary="viewport",viewportPadding,testId,"aria-label":ariaLabel,"aria-describedby":ariaDescribedBy}=props;const isControlled=typeof openedProp==="boolean";const[internalOpened,setInternalOpened]=React.useState(!!openedProp);const opened=isControlled?openedProp:internalOpened;const[placement,setPlacement]=React.useState(placementProp);const initialFocusRef=React.useRef(null);const returnFocusRef=React.useRef(null);const closeFiredRef=React.useRef(false);const generatedUniqueId=React.useId();const uniqueId=id??generatedUniqueId;React.useEffect(()=>{if(opened&&closedFocusId){returnFocusRef.current=document.getElementById(closedFocusId);}},[opened,closedFocusId]);const handleOpenChange=React.useCallback(nextOpened=>{if(!isControlled){setInternalOpened(nextOpened);}if(nextOpened){closeFiredRef.current=false;}else if(!closeFiredRef.current){closeFiredRef.current=true;onClose?.();}},[isControlled,onClose]);const handleClose=React.useCallback(()=>{handleOpenChange(false);},[handleOpenChange]);const handleAnchorClick=React.useCallback(()=>{if(!opened){handleOpenChange(true);}else if(dismissEnabled){handleOpenChange(false);}},[opened,dismissEnabled,handleOpenChange]);const handlePlacementChange=React.useCallback(nextPlacement=>{setPlacement(nextPlacement.split("-")[0]);},[]);const handleDialogRef=React.useCallback(node=>{if(node&&initialFocusId){initialFocusRef.current=node.querySelector(`#${initialFocusId}`);}},[initialFocusId]);const renderContent=()=>{const popoverContents=typeof content==="function"?content({close:handleClose}):content;return React.cloneElement(popoverContents,{uniqueId})};const describedBy=ariaDescribedBy||`${uniqueId}-content`;const ariaLabelledBy=ariaLabel?undefined:`${uniqueId}-title`;const dialog=jsx(PopoverDialog,{ref:handleDialogRef,id:uniqueId,placement:placement,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,"aria-describedby":describedBy,children:renderContent()});return jsx(PopoverContext.Provider,{value:{close:handleClose,placement:placement},children:jsx(Floating,{open:opened,strategy:"fixed",onOpenChange:handleOpenChange,placement:placementProp,onPlacementChange:handlePlacementChange,content:dialog,dismissEnabled:!!dismissEnabled,closeOnFocusOut:!!dismissEnabled,showArrow:showTail,portal:portal,rootBoundary:rootBoundary,shiftPadding:viewportPadding,initialFocusRef:initialFocusId?initialFocusRef:undefined,returnFocus:closedFocusId?returnFocusRef:true,testId:testId,children:jsx(PopoverAnchor,{id:`${uniqueId}-anchor`,"aria-controls":uniqueId,"aria-expanded":opened?"true":"false",onClick:handleAnchorClick,children:children})})})}
30
19
 
31
20
  class CloseButton extends React.Component{render(){const{"aria-label":ariaLabel="Close Popover",style,testId}=this.props;return jsx(PopoverContext.Consumer,{children:({close})=>{return jsx(IconButton,{icon:xIcon,"aria-label":ariaLabel,onClick:close,kind:"tertiary",actionType:"neutral",style:style,testId:testId})}})}}CloseButton.defaultProps={"aria-label":"Close Popover"};
32
21
 
33
- class PopoverContentCore extends React.Component{render(){const{"aria-label":ariaLabel,children,closeButtonLight,closeButtonLabel,closeButtonVisible,style,testId}=this.props;return jsxs(View,{testId:testId,style:[styles$1.content,style],"aria-label":ariaLabel,children:[closeButtonVisible&&jsx(CloseButton,{"aria-label":closeButtonLabel,style:[styles$1.closeButton,closeButtonLight&&actionStyles.inverse],testId:`${testId||"popover"}-close-btn`}),children]})}}PopoverContentCore.defaultProps={closeButtonLight:false,closeButtonVisible:false};const styles$1=StyleSheet.create({content:{borderRadius:border.radius.radius_040,border:`solid 1px ${semanticColor.core.border.neutral.subtle}`,backgroundColor:semanticColor.core.background.base.default,boxShadow:boxShadow.mid,margin:0,maxInlineSize:`calc(${sizing.size_160} * 18)`,padding:sizing.size_240,overflow:"hidden",justifyContent:"center"},closeButton:{margin:0,position:"absolute",insetInlineEnd:sizing.size_040,insetBlockStart:sizing.size_040,zIndex:1}});
22
+ const PopoverContentCore=React.forwardRef(function PopoverContentCore({"aria-label":ariaLabel,children,closeButtonLight=false,closeButtonLabel,closeButtonVisible=false,style,testId},ref){return jsxs(View,{testId:testId,style:[styles$1.content,style],"aria-label":ariaLabel,ref:ref,children:[closeButtonVisible&&jsx(CloseButton,{"aria-label":closeButtonLabel,style:[styles$1.closeButton,closeButtonLight&&actionStyles.inverse],testId:`${testId||"popover"}-close-btn`}),children]})});PopoverContentCore.displayName="PopoverContentCore";const styles$1=StyleSheet.create({content:{margin:0,maxInlineSize:`calc(${sizing.size_160} * 18)`,padding:sizing.size_240,overflow:"hidden",justifyContent:"center"},closeButton:{margin:0,position:"absolute",insetInlineEnd:sizing.size_040,insetBlockStart:sizing.size_040,zIndex:1}});
34
23
 
35
- const StyledImg=addStyle("img");class PopoverContent extends React.Component{componentDidMount(){const{icon,image}=this.props;if(image&&icon){throw new Error("'image' and 'icon' cannot be used at the same time. You can fix this by either removing 'image' or 'icon' from your instance.")}}validateProps({placement}){if(this.props.image&&(placement==="left"||placement==="right")){throw new Error("'image' can only be vertically placed. You can fix this by either changing `placement` to `top` or `bottom` or removing the `image` prop inside `content`.")}}render(){const{closeButtonLabel,closeButtonVisible,content,icon,image,style,title,titleHeadingTag,testId,uniqueId}=this.props;return jsx(PopoverContext.Consumer,{children:({close,placement})=>{this.validateProps({close,placement});return jsxs(PopoverContentCore,{closeButtonLight:image&&placement==="top",closeButtonLabel:closeButtonLabel,closeButtonVisible:closeButtonVisible,style:style,testId:testId,children:[jsxs(View,{style:!!icon&&styles.withIcon,children:[this.maybeRenderImage({placement}),this.maybeRenderIcon(),jsxs(View,{style:styles.text,children:[jsx(Heading,{size:"medium",tag:titleHeadingTag,id:`${uniqueId}-title`,style:styles.title,children:title}),jsx(BodyText,{tag:"span",id:`${uniqueId}-content`,children:content})]})]}),this.maybeRenderActions(close)]})}})}constructor(...args){super(...args),this.maybeRenderImage=({placement})=>{const{image}=this.props;if(!image){return null}return jsx(View,{style:[styles.image,placement==="bottom"&&styles.imageToBottom],children:image})},this.maybeRenderIcon=()=>{const{icon,iconAlt}=this.props;if(!icon){return null}return jsx(View,{style:styles.iconContainer,children:typeof icon!=="string"?icon:jsx(StyledImg,{src:icon,style:styles.icon,alt:iconAlt||""})})},this.maybeRenderActions=close=>{const{actions}=this.props;if(!actions){return null}return jsx(View,{style:styles.actions,children:typeof actions==="function"?actions({close:close}):actions})};}}PopoverContent.defaultProps={closeButtonVisible:false};const styles=StyleSheet.create({actions:{marginBlockStart:sizing.size_240,flexDirection:"row",alignItems:"center",justifyContent:"flex-end"},text:{justifyContent:"center"},title:{marginBlockEnd:sizing.size_080},iconContainer:{alignItems:"center",justifyContent:"center",blockSize:sizing.size_640,inlineSize:sizing.size_640,minInlineSize:sizing.size_640,marginInlineEnd:sizing.size_160,overflow:"hidden"},icon:{width:"100%"},withIcon:{flexDirection:"row"},image:{marginBlockEnd:sizing.size_240,marginBlockStart:`calc(-1 * ${sizing.size_240})`,marginInline:`calc(-1 * ${sizing.size_240})`,inlineSize:`calc(100% + ${sizing.size_480})`},imageToBottom:{marginBlockEnd:`calc(-1 * ${sizing.size_240})`,marginBlockStart:sizing.size_240,order:1}});
24
+ const StyledImg=addStyle("img");const PopoverContent=React.forwardRef(function PopoverContent(props,ref){const{actions,closeButtonLabel,closeButtonVisible=false,content,icon,iconAlt,image,style,title,titleHeadingTag,testId,uniqueId}=props;const{close,placement}=React.useContext(PopoverContext);React.useEffect(()=>{if(image&&icon){throw new Error("'image' and 'icon' cannot be used at the same time. You can fix this by either removing 'image' or 'icon' from your instance.")}},[image,icon]);const validateProps=React.useCallback(({placement})=>{if(image&&(placement==="left"||placement==="right")){throw new Error("'image' can only be vertically placed. You can fix this by either changing `placement` to `top` or `bottom` or removing the `image` prop inside `content`.")}},[image]);const maybeRenderImage=context=>{if(!image){return null}return jsx(View,{style:[styles.image,context.placement==="bottom"&&styles.imageToBottom],children:image})};const maybeRenderIcon=()=>{if(!icon){return null}return jsx(View,{style:styles.iconContainer,children:typeof icon!=="string"?icon:jsx(StyledImg,{src:icon,style:styles.icon,alt:iconAlt||""})})};const maybeRenderActions=close=>{if(!actions){return null}return jsx(View,{style:styles.actions,children:typeof actions==="function"?actions({close:close}):actions})};validateProps({close,placement});return jsxs(PopoverContentCore,{closeButtonLight:!!image&&placement==="top",closeButtonLabel:closeButtonLabel,closeButtonVisible:closeButtonVisible,style:style,testId:testId,ref:ref,children:[jsxs(View,{style:!!icon&&styles.withIcon,children:[maybeRenderImage({placement}),maybeRenderIcon(),jsxs(View,{style:styles.text,children:[jsx(Heading,{size:"medium",tag:titleHeadingTag,id:`${uniqueId}-title`,style:styles.title,children:title}),jsx(BodyText,{tag:"span",id:`${uniqueId}-content`,children:content})]})]}),maybeRenderActions(close)]})});PopoverContent.displayName="PopoverContent";const styles=StyleSheet.create({actions:{marginBlockStart:sizing.size_240,flexDirection:"row",alignItems:"center",justifyContent:"flex-end"},text:{justifyContent:"center"},title:{marginBlockEnd:sizing.size_080},iconContainer:{alignItems:"center",justifyContent:"center",blockSize:sizing.size_640,inlineSize:sizing.size_640,minInlineSize:sizing.size_640,marginInlineEnd:sizing.size_160,overflow:"hidden"},icon:{width:"100%"},withIcon:{flexDirection:"row"},image:{marginBlockEnd:sizing.size_240,marginBlockStart:`calc(-1 * ${sizing.size_240})`,marginInline:`calc(-1 * ${sizing.size_240})`,inlineSize:`calc(100% + ${sizing.size_480})`},imageToBottom:{marginBlockEnd:`calc(-1 * ${sizing.size_240})`,marginBlockStart:sizing.size_240,order:1}});
36
25
 
37
26
  export { Popover, PopoverContent, PopoverContentCore };
package/dist/index.js CHANGED
@@ -4,12 +4,10 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var React = require('react');
7
- var ReactDOM = require('react-dom');
7
+ var wonderBlocksFloating = require('@khanacademy/wonder-blocks-floating');
8
8
  var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
9
- var wonderBlocksTooltip = require('@khanacademy/wonder-blocks-tooltip');
10
- var wonderBlocksModal = require('@khanacademy/wonder-blocks-modal');
11
9
  var aphrodite = require('aphrodite');
12
- var tokens = require('@khanacademy/wonder-blocks-tokens');
10
+ var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
13
11
  var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
14
12
  var wonderBlocksStyles = require('@khanacademy/wonder-blocks-styles');
15
13
  var xIcon = require('@phosphor-icons/core/regular/x.svg');
@@ -36,32 +34,22 @@ function _interopNamespace(e) {
36
34
  }
37
35
 
38
36
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
39
- var ReactDOM__namespace = /*#__PURE__*/_interopNamespace(ReactDOM);
40
- var tokens__namespace = /*#__PURE__*/_interopNamespace(tokens);
41
37
  var xIcon__default = /*#__PURE__*/_interopDefaultLegacy(xIcon);
42
38
  var IconButton__default = /*#__PURE__*/_interopDefaultLegacy(IconButton);
43
39
 
44
40
  const defaultContext={close:undefined,placement:"top"};const PopoverContext=React__namespace.createContext(defaultContext);PopoverContext.displayName="PopoverContext";
45
41
 
46
- class PopoverAnchor extends React__namespace.Component{componentDidMount(){const anchorNode=ReactDOM__namespace.findDOMNode(this);if(anchorNode){this.props.anchorRef(anchorNode);}}render(){const{children,id,onClick,"aria-controls":ariaControls,"aria-expanded":ariaExpanded}=this.props;const sharedProps={id:id,"aria-controls":ariaControls,"aria-expanded":ariaExpanded};if(typeof children==="function"){const renderedChildren=children({open:onClick});return React__namespace.cloneElement(renderedChildren,sharedProps)}else {return React__namespace.cloneElement(children,{...children.props,...sharedProps,onClick:children.props.onClick?e=>{e.stopPropagation();children.props.onClick();onClick();}:onClick})}}}
42
+ const PopoverAnchor=React__namespace.forwardRef(function PopoverAnchor(props,ref){const{children,id,onClick,"aria-controls":ariaControls,"aria-expanded":ariaExpanded,[wonderBlocksFloating.FloatingReferenceAttributeName]:floatingReferenceId}=props;const isFunctionChildren=typeof children==="function";const renderedChildren=isFunctionChildren?children({open:onClick}):children;const triggerRef=wonderBlocksFloating.useMergeRefs([ref,wonderBlocksFloating.getElementRef(renderedChildren)]);const sharedProps={id:id,"aria-controls":ariaControls,"aria-expanded":ariaExpanded,[wonderBlocksFloating.FloatingReferenceAttributeName]:floatingReferenceId,...wonderBlocksFloating.canAcceptRef(renderedChildren)?{ref:triggerRef}:undefined};if(isFunctionChildren){return React__namespace.cloneElement(renderedChildren,sharedProps)}return React__namespace.cloneElement(renderedChildren,{...renderedChildren.props,...sharedProps,onClick:renderedChildren.props.onClick?e=>{e.stopPropagation();renderedChildren.props.onClick();onClick();}:onClick})});PopoverAnchor.displayName="PopoverAnchor";
47
43
 
48
- class PopoverDialog extends React__namespace.Component{componentDidUpdate(prevProps){if(prevProps.placement!==this.props.placement){this.props.onUpdate(this.props.placement);}}render(){const{placement,children,id,isReferenceHidden,updateBubbleRef,updateTailRef,tailOffset,style,showTail,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,"aria-label":ariaLabel}=this.props;const contentProps=children.props;const color=contentProps.color;return jsxRuntime.jsx(React__namespace.Fragment,{children:jsxRuntime.jsxs(wonderBlocksCore.View,{"aria-label":ariaLabel,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,id:id,role:"dialog",ref:updateBubbleRef,"data-placement":placement,style:[isReferenceHidden&&styles$2.hide,styles$2[`content-${placement}`],style],children:[children,jsxRuntime.jsx(wonderBlocksTooltip.TooltipTail,{show:showTail,color:color,updateRef:updateTailRef,placement:placement,offset:tailOffset})]})})}}const styles$2=aphrodite.StyleSheet.create({hide:{pointerEvents:"none",opacity:0,backgroundColor:tokens__namespace.semanticColor.core.transparent,color:tokens__namespace.semanticColor.core.transparent},"content-top":{flexDirection:"column"},"content-right":{flexDirection:"row-reverse"},"content-bottom":{flexDirection:"column-reverse"},"content-left":{flexDirection:"row"}});
44
+ const PopoverDialog=React__namespace.forwardRef(function PopoverDialog(props,ref){const{children,id,placement,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,"aria-label":ariaLabel}=props;return jsxRuntime.jsx(wonderBlocksCore.View,{"aria-label":ariaLabel,"aria-describedby":ariaDescribedby,"aria-labelledby":ariaLabelledBy,id:id,role:"dialog",ref:ref,"data-placement":placement,children:children})});PopoverDialog.displayName="PopoverDialog";
49
45
 
50
- const FOCUSABLE_ELEMENTS='button:not([tabindex="-1"]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}function isFocusable(element){return element.matches(FOCUSABLE_ELEMENTS)}
51
-
52
- class PopoverEventListener extends React__namespace.Component{componentDidMount(){window.addEventListener("keyup",this._handleKeyup);window.addEventListener("click",this._handleClick);}componentWillUnmount(){window.removeEventListener("keyup",this._handleKeyup);window.removeEventListener("click",this._handleClick);}render(){return null}constructor(...args){super(...args),this.state={isFirstClick:true},this._handleKeyup=e=>{if(e.key==="Escape"){e.preventDefault();e.stopPropagation();this.props.onClose(true);}},this._handleClick=e=>{if(this.state.isFirstClick){this.setState({isFirstClick:false});return}const node=ReactDOM__namespace.findDOMNode(this.props.contentRef?.current);if(node&&!node.contains(e.target)){e.preventDefault();e.stopPropagation();const shouldReturnFocus=!isFocusable(e.target);this.props.onClose(shouldReturnFocus);}};}}
53
-
54
- class InitialFocus extends React__namespace.Component{componentDidMount(){const node=ReactDOM__namespace.findDOMNode(this);if(!node){return}this.setInitialFocusableElement(node);}maybeGetInitialFocusElement(node){const{initialFocusId}=this.props;if(!initialFocusId){return null}return node.querySelector(`#${initialFocusId}`)}maybeGetFirstFocusableElement(node){const focusableElements=findFocusableNodes(node);if(!focusableElements.length){return null}return focusableElements[0]}render(){return this.props.children}constructor(...args){super(...args),this.setInitialFocusableElement=node=>{const firstFocusableElement=this.maybeGetInitialFocusElement(node)||this.maybeGetFirstFocusableElement(node)||node;if(firstFocusableElement===node){node.tabIndex=-1;}setTimeout(()=>{firstFocusableElement.focus();},this.props.delay||0);};}}
55
-
56
- class FocusManager extends React__namespace.Component{componentDidMount(){this.addEventListeners();}componentDidUpdate(){this.removeEventListeners();this.addEventListeners();}componentWillUnmount(){this.changeFocusabilityInsidePopover(true);this.removeEventListeners();}removeEventListeners(){const{anchorElement}=this.props;if(anchorElement){anchorElement.removeEventListener("keydown",this.handleKeydownPreviousFocusableElement);}if(!this.nextElementAfterPopover){window.removeEventListener("blur",()=>{this.changeFocusabilityInsidePopover(true);});}if(this.firstFocusableElementInPopover){this.firstFocusableElementInPopover.removeEventListener("keydown",this.handleKeydownFirstFocusableElement);}if(this.lastFocusableElementInPopover){this.lastFocusableElementInPopover.removeEventListener("keydown",this.handleKeydownLastFocusableElement);}if(this.nextElementAfterPopover){this.nextElementAfterPopover.removeEventListener("keydown",this.handleKeydownNextFocusableElement);}}render(){const{children}=this.props;return jsxRuntime.jsx("div",{ref:this.getComponentRootNode,onClick:()=>{this.changeFocusabilityInsidePopover(true);},onFocus:()=>{this.changeFocusabilityInsidePopover(true);},onBlur:()=>{this.changeFocusabilityInsidePopover(false);},children:jsxRuntime.jsx(InitialFocus,{initialFocusId:this.props.initialFocusId,delay:this.props.initialFocusDelay,children:children})})}constructor(...args){super(...args),this.elementsThatCanBeFocusableInsidePopover=[],this.firstFocusableElementInPopover=null,this.lastFocusableElementInPopover=null,this.addEventListeners=()=>{const{anchorElement}=this.props;if(anchorElement){anchorElement.addEventListener("keydown",this.handleKeydownPreviousFocusableElement);}if(this.rootNode){this.elementsThatCanBeFocusableInsidePopover=findFocusableNodes(this.rootNode);this.firstFocusableElementInPopover=this.elementsThatCanBeFocusableInsidePopover[0];this.lastFocusableElementInPopover=this.elementsThatCanBeFocusableInsidePopover[this.elementsThatCanBeFocusableInsidePopover.length-1];}this.nextElementAfterPopover=this.getNextFocusableElement();if(!this.nextElementAfterPopover){window.addEventListener("blur",()=>{this.changeFocusabilityInsidePopover(true);});}if(this.firstFocusableElementInPopover){this.firstFocusableElementInPopover.addEventListener("keydown",this.handleKeydownFirstFocusableElement);}if(this.lastFocusableElementInPopover){this.lastFocusableElementInPopover.addEventListener("keydown",this.handleKeydownLastFocusableElement);}if(this.nextElementAfterPopover){this.nextElementAfterPopover.addEventListener("keydown",this.handleKeydownNextFocusableElement);}},this.handleKeydownFirstFocusableElement=e=>{if(e.key===wonderBlocksCore.keys.tab&&e.shiftKey){e.preventDefault();this.props.anchorElement?.focus();}},this.handleKeydownLastFocusableElement=e=>{if(this.nextElementAfterPopover&&e.key===wonderBlocksCore.keys.tab&&!e.shiftKey){e.preventDefault();this.nextElementAfterPopover?.focus();}if(e.key===wonderBlocksCore.keys.tab&&!e.shiftKey){this.props.onFocusOut?.();}},this.getNextFocusableElement=()=>{const{anchorElement}=this.props;if(!anchorElement){return}const focusableElements=findFocusableNodes(document);const focusableElementsOutside=focusableElements.filter(element=>{const index=this.elementsThatCanBeFocusableInsidePopover.indexOf(element);return index<0});const anchorIndex=focusableElementsOutside.indexOf(anchorElement);if(anchorIndex>=0&&anchorIndex!==focusableElementsOutside.length-1){const nextElementIndex=anchorIndex<focusableElementsOutside.length-1?anchorIndex+1:0;return focusableElementsOutside[nextElementIndex]}return},this.getComponentRootNode=node=>{if(!node){return}const rootNode=ReactDOM__namespace.findDOMNode(node);if(!rootNode){throw new Error("Assertion error: root node should exist after mount")}this.rootNode=rootNode;},this.changeFocusabilityInsidePopover=(enabled=true)=>{const tabIndex=enabled?"0":"-1";this.elementsThatCanBeFocusableInsidePopover.forEach(element=>{element.setAttribute("tabIndex",tabIndex);});},this.handleKeydownPreviousFocusableElement=e=>{if(e.key===wonderBlocksCore.keys.tab&&!e.shiftKey){e.preventDefault();this.firstFocusableElementInPopover?.focus();}if(e.key===wonderBlocksCore.keys.tab&&e.shiftKey){this.props.onFocusOut?.();}},this.handleKeydownNextFocusableElement=e=>{if(e.key===wonderBlocksCore.keys.tab&&e.shiftKey){e.preventDefault();this.lastFocusableElementInPopover?.focus();}};}}
57
-
58
- class Popover extends React__namespace.Component{static getDerivedStateFromProps(props,state){return {opened:typeof props.opened==="boolean"?props.opened:state.opened}}renderContent(uniqueId){const{content}=this.props;const popoverContents=typeof content==="function"?content({close:this.handleClose}):content;return React__namespace.cloneElement(popoverContents,{ref:this.contentRef,uniqueId})}renderPopper(uniqueId){const{autoUpdate,dismissEnabled,initialFocusId,placement,showTail,portal,"aria-label":ariaLabel,"aria-describedby":ariaDescribedBy,rootBoundary,viewportPadding,initialFocusDelay}=this.props;const{anchorElement}=this.state;const describedBy=ariaDescribedBy||`${uniqueId}-content`;const ariaLabelledBy=ariaLabel?undefined:`${uniqueId}-title`;const shouldAnchorExist=autoUpdate?anchorElement:true;if(!shouldAnchorExist){return null}const popperContent=jsxRuntime.jsx(wonderBlocksTooltip.TooltipPopper,{anchorElement:anchorElement,autoUpdate:autoUpdate,placement:placement,rootBoundary:rootBoundary,viewportPadding:viewportPadding,children:props=>jsxRuntime.jsx(PopoverDialog,{...props,"aria-label":ariaLabel,"aria-describedby":describedBy,"aria-labelledby":ariaLabelledBy,id:uniqueId,onUpdate:placement=>this.setState({placement}),showTail:showTail,children:this.renderContent(uniqueId)})});if(portal){return jsxRuntime.jsx(FocusManager,{anchorElement:anchorElement,initialFocusId:initialFocusId,initialFocusDelay:initialFocusDelay,onFocusOut:dismissEnabled?this.handleClose:undefined,children:popperContent})}else {return jsxRuntime.jsx(InitialFocus,{initialFocusId:initialFocusId,delay:initialFocusDelay,children:popperContent})}}getHost(){return wonderBlocksModal.maybeGetPortalMountedModalHostElement(this.state.anchorElement)||document.body}renderPortal(uniqueId,opened){if(!opened){return null}const{portal}=this.props;const popperHost=this.getHost();if(portal&&popperHost){return ReactDOM__namespace.createPortal(this.renderPopper(uniqueId),popperHost)}return this.renderPopper(uniqueId)}render(){const{children,dismissEnabled,id}=this.props;const{opened,placement}=this.state;return jsxRuntime.jsxs(PopoverContext.Provider,{value:{close:this.handleClose,placement:placement},children:[jsxRuntime.jsx(wonderBlocksCore.Id,{id:id,children:uniqueId=>jsxRuntime.jsxs(React__namespace.Fragment,{children:[jsxRuntime.jsx(PopoverAnchor,{anchorRef:this.updateRef,id:`${uniqueId}-anchor`,"aria-controls":uniqueId,"aria-expanded":opened?"true":"false",onClick:this.handleOpen,children:children}),this.renderPortal(uniqueId,opened)]})}),dismissEnabled&&opened&&jsxRuntime.jsx(PopoverEventListener,{onClose:this.handleClose,contentRef:this.contentRef})]})}constructor(...args){super(...args),this.state={opened:!!this.props.opened,placement:this.props.placement},this.contentRef=React__namespace.createRef(),this.maybeReturnFocus=()=>{const{anchorElement}=this.state;const{closedFocusId}=this.props;if(closedFocusId){const focusElement=ReactDOM__namespace.findDOMNode(document.getElementById(closedFocusId));focusElement?.focus();return}if(anchorElement){anchorElement.focus();}},this.handleClose=(shouldReturnFocus=true)=>{this.setState({opened:false},()=>{this.props.onClose?.();if(shouldReturnFocus){this.maybeReturnFocus();}});},this.handleOpen=()=>{if(this.props.dismissEnabled&&this.state.opened){this.handleClose(true);}else {this.setState({opened:true});}},this.updateRef=actualRef=>{if(actualRef&&this.state.anchorElement!==actualRef){this.setState({anchorElement:actualRef});}};}}Popover.defaultProps={placement:"top",showTail:true,portal:true,rootBoundary:"viewport"};
46
+ function Popover(props){const{children,content,placement:placementProp="top",dismissEnabled,id,closedFocusId,initialFocusId,opened:openedProp,onClose,showTail=true,portal=true,rootBoundary="viewport",viewportPadding,testId,"aria-label":ariaLabel,"aria-describedby":ariaDescribedBy}=props;const isControlled=typeof openedProp==="boolean";const[internalOpened,setInternalOpened]=React__namespace.useState(!!openedProp);const opened=isControlled?openedProp:internalOpened;const[placement,setPlacement]=React__namespace.useState(placementProp);const initialFocusRef=React__namespace.useRef(null);const returnFocusRef=React__namespace.useRef(null);const closeFiredRef=React__namespace.useRef(false);const generatedUniqueId=React__namespace.useId();const uniqueId=id??generatedUniqueId;React__namespace.useEffect(()=>{if(opened&&closedFocusId){returnFocusRef.current=document.getElementById(closedFocusId);}},[opened,closedFocusId]);const handleOpenChange=React__namespace.useCallback(nextOpened=>{if(!isControlled){setInternalOpened(nextOpened);}if(nextOpened){closeFiredRef.current=false;}else if(!closeFiredRef.current){closeFiredRef.current=true;onClose?.();}},[isControlled,onClose]);const handleClose=React__namespace.useCallback(()=>{handleOpenChange(false);},[handleOpenChange]);const handleAnchorClick=React__namespace.useCallback(()=>{if(!opened){handleOpenChange(true);}else if(dismissEnabled){handleOpenChange(false);}},[opened,dismissEnabled,handleOpenChange]);const handlePlacementChange=React__namespace.useCallback(nextPlacement=>{setPlacement(nextPlacement.split("-")[0]);},[]);const handleDialogRef=React__namespace.useCallback(node=>{if(node&&initialFocusId){initialFocusRef.current=node.querySelector(`#${initialFocusId}`);}},[initialFocusId]);const renderContent=()=>{const popoverContents=typeof content==="function"?content({close:handleClose}):content;return React__namespace.cloneElement(popoverContents,{uniqueId})};const describedBy=ariaDescribedBy||`${uniqueId}-content`;const ariaLabelledBy=ariaLabel?undefined:`${uniqueId}-title`;const dialog=jsxRuntime.jsx(PopoverDialog,{ref:handleDialogRef,id:uniqueId,placement:placement,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,"aria-describedby":describedBy,children:renderContent()});return jsxRuntime.jsx(PopoverContext.Provider,{value:{close:handleClose,placement:placement},children:jsxRuntime.jsx(wonderBlocksFloating.Floating,{open:opened,strategy:"fixed",onOpenChange:handleOpenChange,placement:placementProp,onPlacementChange:handlePlacementChange,content:dialog,dismissEnabled:!!dismissEnabled,closeOnFocusOut:!!dismissEnabled,showArrow:showTail,portal:portal,rootBoundary:rootBoundary,shiftPadding:viewportPadding,initialFocusRef:initialFocusId?initialFocusRef:undefined,returnFocus:closedFocusId?returnFocusRef:true,testId:testId,children:jsxRuntime.jsx(PopoverAnchor,{id:`${uniqueId}-anchor`,"aria-controls":uniqueId,"aria-expanded":opened?"true":"false",onClick:handleAnchorClick,children:children})})})}
59
47
 
60
48
  class CloseButton extends React__namespace.Component{render(){const{"aria-label":ariaLabel="Close Popover",style,testId}=this.props;return jsxRuntime.jsx(PopoverContext.Consumer,{children:({close})=>{return jsxRuntime.jsx(IconButton__default["default"],{icon:xIcon__default["default"],"aria-label":ariaLabel,onClick:close,kind:"tertiary",actionType:"neutral",style:style,testId:testId})}})}}CloseButton.defaultProps={"aria-label":"Close Popover"};
61
49
 
62
- class PopoverContentCore extends React__namespace.Component{render(){const{"aria-label":ariaLabel,children,closeButtonLight,closeButtonLabel,closeButtonVisible,style,testId}=this.props;return jsxRuntime.jsxs(wonderBlocksCore.View,{testId:testId,style:[styles$1.content,style],"aria-label":ariaLabel,children:[closeButtonVisible&&jsxRuntime.jsx(CloseButton,{"aria-label":closeButtonLabel,style:[styles$1.closeButton,closeButtonLight&&wonderBlocksStyles.actionStyles.inverse],testId:`${testId||"popover"}-close-btn`}),children]})}}PopoverContentCore.defaultProps={closeButtonLight:false,closeButtonVisible:false};const styles$1=aphrodite.StyleSheet.create({content:{borderRadius:tokens.border.radius.radius_040,border:`solid 1px ${tokens.semanticColor.core.border.neutral.subtle}`,backgroundColor:tokens.semanticColor.core.background.base.default,boxShadow:tokens.boxShadow.mid,margin:0,maxInlineSize:`calc(${tokens.sizing.size_160} * 18)`,padding:tokens.sizing.size_240,overflow:"hidden",justifyContent:"center"},closeButton:{margin:0,position:"absolute",insetInlineEnd:tokens.sizing.size_040,insetBlockStart:tokens.sizing.size_040,zIndex:1}});
50
+ const PopoverContentCore=React__namespace.forwardRef(function PopoverContentCore({"aria-label":ariaLabel,children,closeButtonLight=false,closeButtonLabel,closeButtonVisible=false,style,testId},ref){return jsxRuntime.jsxs(wonderBlocksCore.View,{testId:testId,style:[styles$1.content,style],"aria-label":ariaLabel,ref:ref,children:[closeButtonVisible&&jsxRuntime.jsx(CloseButton,{"aria-label":closeButtonLabel,style:[styles$1.closeButton,closeButtonLight&&wonderBlocksStyles.actionStyles.inverse],testId:`${testId||"popover"}-close-btn`}),children]})});PopoverContentCore.displayName="PopoverContentCore";const styles$1=aphrodite.StyleSheet.create({content:{margin:0,maxInlineSize:`calc(${wonderBlocksTokens.sizing.size_160} * 18)`,padding:wonderBlocksTokens.sizing.size_240,overflow:"hidden",justifyContent:"center"},closeButton:{margin:0,position:"absolute",insetInlineEnd:wonderBlocksTokens.sizing.size_040,insetBlockStart:wonderBlocksTokens.sizing.size_040,zIndex:1}});
63
51
 
64
- const StyledImg=wonderBlocksCore.addStyle("img");class PopoverContent extends React__namespace.Component{componentDidMount(){const{icon,image}=this.props;if(image&&icon){throw new Error("'image' and 'icon' cannot be used at the same time. You can fix this by either removing 'image' or 'icon' from your instance.")}}validateProps({placement}){if(this.props.image&&(placement==="left"||placement==="right")){throw new Error("'image' can only be vertically placed. You can fix this by either changing `placement` to `top` or `bottom` or removing the `image` prop inside `content`.")}}render(){const{closeButtonLabel,closeButtonVisible,content,icon,image,style,title,titleHeadingTag,testId,uniqueId}=this.props;return jsxRuntime.jsx(PopoverContext.Consumer,{children:({close,placement})=>{this.validateProps({close,placement});return jsxRuntime.jsxs(PopoverContentCore,{closeButtonLight:image&&placement==="top",closeButtonLabel:closeButtonLabel,closeButtonVisible:closeButtonVisible,style:style,testId:testId,children:[jsxRuntime.jsxs(wonderBlocksCore.View,{style:!!icon&&styles.withIcon,children:[this.maybeRenderImage({placement}),this.maybeRenderIcon(),jsxRuntime.jsxs(wonderBlocksCore.View,{style:styles.text,children:[jsxRuntime.jsx(wonderBlocksTypography.Heading,{size:"medium",tag:titleHeadingTag,id:`${uniqueId}-title`,style:styles.title,children:title}),jsxRuntime.jsx(wonderBlocksTypography.BodyText,{tag:"span",id:`${uniqueId}-content`,children:content})]})]}),this.maybeRenderActions(close)]})}})}constructor(...args){super(...args),this.maybeRenderImage=({placement})=>{const{image}=this.props;if(!image){return null}return jsxRuntime.jsx(wonderBlocksCore.View,{style:[styles.image,placement==="bottom"&&styles.imageToBottom],children:image})},this.maybeRenderIcon=()=>{const{icon,iconAlt}=this.props;if(!icon){return null}return jsxRuntime.jsx(wonderBlocksCore.View,{style:styles.iconContainer,children:typeof icon!=="string"?icon:jsxRuntime.jsx(StyledImg,{src:icon,style:styles.icon,alt:iconAlt||""})})},this.maybeRenderActions=close=>{const{actions}=this.props;if(!actions){return null}return jsxRuntime.jsx(wonderBlocksCore.View,{style:styles.actions,children:typeof actions==="function"?actions({close:close}):actions})};}}PopoverContent.defaultProps={closeButtonVisible:false};const styles=aphrodite.StyleSheet.create({actions:{marginBlockStart:tokens.sizing.size_240,flexDirection:"row",alignItems:"center",justifyContent:"flex-end"},text:{justifyContent:"center"},title:{marginBlockEnd:tokens.sizing.size_080},iconContainer:{alignItems:"center",justifyContent:"center",blockSize:tokens.sizing.size_640,inlineSize:tokens.sizing.size_640,minInlineSize:tokens.sizing.size_640,marginInlineEnd:tokens.sizing.size_160,overflow:"hidden"},icon:{width:"100%"},withIcon:{flexDirection:"row"},image:{marginBlockEnd:tokens.sizing.size_240,marginBlockStart:`calc(-1 * ${tokens.sizing.size_240})`,marginInline:`calc(-1 * ${tokens.sizing.size_240})`,inlineSize:`calc(100% + ${tokens.sizing.size_480})`},imageToBottom:{marginBlockEnd:`calc(-1 * ${tokens.sizing.size_240})`,marginBlockStart:tokens.sizing.size_240,order:1}});
52
+ const StyledImg=wonderBlocksCore.addStyle("img");const PopoverContent=React__namespace.forwardRef(function PopoverContent(props,ref){const{actions,closeButtonLabel,closeButtonVisible=false,content,icon,iconAlt,image,style,title,titleHeadingTag,testId,uniqueId}=props;const{close,placement}=React__namespace.useContext(PopoverContext);React__namespace.useEffect(()=>{if(image&&icon){throw new Error("'image' and 'icon' cannot be used at the same time. You can fix this by either removing 'image' or 'icon' from your instance.")}},[image,icon]);const validateProps=React__namespace.useCallback(({placement})=>{if(image&&(placement==="left"||placement==="right")){throw new Error("'image' can only be vertically placed. You can fix this by either changing `placement` to `top` or `bottom` or removing the `image` prop inside `content`.")}},[image]);const maybeRenderImage=context=>{if(!image){return null}return jsxRuntime.jsx(wonderBlocksCore.View,{style:[styles.image,context.placement==="bottom"&&styles.imageToBottom],children:image})};const maybeRenderIcon=()=>{if(!icon){return null}return jsxRuntime.jsx(wonderBlocksCore.View,{style:styles.iconContainer,children:typeof icon!=="string"?icon:jsxRuntime.jsx(StyledImg,{src:icon,style:styles.icon,alt:iconAlt||""})})};const maybeRenderActions=close=>{if(!actions){return null}return jsxRuntime.jsx(wonderBlocksCore.View,{style:styles.actions,children:typeof actions==="function"?actions({close:close}):actions})};validateProps({close,placement});return jsxRuntime.jsxs(PopoverContentCore,{closeButtonLight:!!image&&placement==="top",closeButtonLabel:closeButtonLabel,closeButtonVisible:closeButtonVisible,style:style,testId:testId,ref:ref,children:[jsxRuntime.jsxs(wonderBlocksCore.View,{style:!!icon&&styles.withIcon,children:[maybeRenderImage({placement}),maybeRenderIcon(),jsxRuntime.jsxs(wonderBlocksCore.View,{style:styles.text,children:[jsxRuntime.jsx(wonderBlocksTypography.Heading,{size:"medium",tag:titleHeadingTag,id:`${uniqueId}-title`,style:styles.title,children:title}),jsxRuntime.jsx(wonderBlocksTypography.BodyText,{tag:"span",id:`${uniqueId}-content`,children:content})]})]}),maybeRenderActions(close)]})});PopoverContent.displayName="PopoverContent";const styles=aphrodite.StyleSheet.create({actions:{marginBlockStart:wonderBlocksTokens.sizing.size_240,flexDirection:"row",alignItems:"center",justifyContent:"flex-end"},text:{justifyContent:"center"},title:{marginBlockEnd:wonderBlocksTokens.sizing.size_080},iconContainer:{alignItems:"center",justifyContent:"center",blockSize:wonderBlocksTokens.sizing.size_640,inlineSize:wonderBlocksTokens.sizing.size_640,minInlineSize:wonderBlocksTokens.sizing.size_640,marginInlineEnd:wonderBlocksTokens.sizing.size_160,overflow:"hidden"},icon:{width:"100%"},withIcon:{flexDirection:"row"},image:{marginBlockEnd:wonderBlocksTokens.sizing.size_240,marginBlockStart:`calc(-1 * ${wonderBlocksTokens.sizing.size_240})`,marginInline:`calc(-1 * ${wonderBlocksTokens.sizing.size_240})`,inlineSize:`calc(100% + ${wonderBlocksTokens.sizing.size_480})`},imageToBottom:{marginBlockEnd:`calc(-1 * ${wonderBlocksTokens.sizing.size_240})`,marginBlockStart:wonderBlocksTokens.sizing.size_240,order:1}});
65
53
 
66
54
  exports.Popover = Popover;
67
55
  exports.PopoverContent = PopoverContent;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The coarse placement of the popover relative to its anchor.
3
+ *
4
+ * NOTE: Wonder Blocks Floating (floating-ui) supports more granular placements
5
+ * (e.g. `top-start`), but Popover only exposes and reasons about these four
6
+ * coarse values. The resolved floating-ui placement is normalized down to one
7
+ * of these before being shared via `PopoverContext`.
8
+ */
9
+ export type Placement = "top" | "bottom" | "left" | "right";
10
+ /**
11
+ * The root boundary that the popover is kept within when it is repositioned to
12
+ * stay in view.
13
+ *
14
+ * - `"viewport"`: keep the popover within the user's viewport.
15
+ * - `"document"`: keep the popover within the document body.
16
+ */
17
+ export type RootBoundary = "viewport" | "document";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "6.3.18",
6
+ "version": "7.0.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -21,20 +21,17 @@
21
21
  "types": "dist/index.d.ts",
22
22
  "dependencies": {
23
23
  "@khanacademy/wonder-blocks-core": "12.5.0",
24
+ "@khanacademy/wonder-blocks-floating": "0.1.0",
24
25
  "@khanacademy/wonder-blocks-icon-button": "12.0.1",
25
- "@khanacademy/wonder-blocks-modal": "8.8.4",
26
26
  "@khanacademy/wonder-blocks-styles": "0.4.0",
27
27
  "@khanacademy/wonder-blocks-tokens": "18.0.0",
28
- "@khanacademy/wonder-blocks-tooltip": "4.2.7",
29
28
  "@khanacademy/wonder-blocks-typography": "5.0.5"
30
29
  },
31
30
  "peerDependencies": {
32
31
  "@phosphor-icons/core": "^2.0.2",
33
- "@popperjs/core": "^2.10.1",
34
32
  "aphrodite": "^1.2.5",
35
33
  "react": "18.2.0",
36
- "react-dom": "18.2.0",
37
- "react-popper": "^2.3.0"
34
+ "react-dom": "18.2.0"
38
35
  },
39
36
  "devDependencies": {
40
37
  "@khanacademy/wb-dev-build-settings": "3.3.0"
@@ -1,107 +0,0 @@
1
- import * as React from "react";
2
- type Props = {
3
- /**
4
- * The popover content container
5
- */
6
- children: React.ReactElement<any>;
7
- /**
8
- * A reference to the trigger element
9
- */
10
- anchorElement: HTMLElement | null | undefined;
11
- /**
12
- * The selector for the element that will be focused when the dialog shows.
13
- * When not set, the first tabbable element within the dialog will be used.
14
- */
15
- initialFocusId?: string;
16
- /**
17
- * The delay in milliseconds before the initial focus is set.
18
- * This is to ensure that any active event listeners have time to finish.
19
- *
20
- * Defaults to 0.
21
- */
22
- initialFocusDelay?: number;
23
- /**
24
- * The callback to be called when the focus is lost (tabbing out of the
25
- * popover).
26
- */
27
- onFocusOut?: () => void;
28
- };
29
- /**
30
- * This component ensures that focus flows correctly when the popover is open.
31
- *
32
- * Inside the popover:
33
- * - `tab`: Moves focus to the next focusable element.
34
- * - `shift + tab`: Moves focus to the previous focusable element.
35
- *
36
- * After the focus reaches the start/end of the popover, then we handle two
37
- * different scenarios:
38
- *
39
- * 1. If the focus has reached the last focusable element inside the popover,
40
- * the next tab will set focus on the next focusable element that exists
41
- * after the PopoverAnchor.
42
- * 2. If the focus is set to the first focusable element inside the popover, the
43
- * next shift + tab will set focus on the PopoverAnchor element.
44
- *
45
- */
46
- export default class FocusManager extends React.Component<Props> {
47
- /**
48
- * The focusable element that is positioned after the trigger element
49
- */
50
- nextElementAfterPopover: HTMLElement | null | undefined;
51
- /**
52
- * Tabbing is restricted to descendents of this element.
53
- */
54
- rootNode: HTMLElement | null | undefined;
55
- componentDidMount(): void;
56
- componentDidUpdate(): void;
57
- /**
58
- * Remove keydown listeners
59
- */
60
- componentWillUnmount(): void;
61
- /**
62
- * List of focusable elements within the popover content
63
- */
64
- elementsThatCanBeFocusableInsidePopover: Array<HTMLElement>;
65
- /**
66
- * The first focusable element inside the popover (if it exists)
67
- */
68
- firstFocusableElementInPopover: HTMLElement | null | undefined;
69
- /**
70
- * The last focusable element inside the popover (if it exists)
71
- */
72
- lastFocusableElementInPopover: HTMLElement | null | undefined;
73
- /**
74
- * Add keydown listeners
75
- */
76
- addEventListeners: () => void;
77
- removeEventListeners(): void;
78
- handleKeydownFirstFocusableElement: (e: KeyboardEvent) => void;
79
- handleKeydownLastFocusableElement: (e: KeyboardEvent) => void;
80
- /**
81
- * Gets the next focusable element after the anchor element
82
- */
83
- getNextFocusableElement: () => HTMLElement | null | undefined;
84
- /**
85
- * Gets the list of focusable elements inside the popover
86
- */
87
- getComponentRootNode: () => void;
88
- /**
89
- * Toggle focusability for all the focusable elements inside the popover.
90
- * This is useful to prevent the user from tabbing into the popover when it
91
- * reaches to the last focusable element within the document.
92
- */
93
- changeFocusabilityInsidePopover: (enabled?: boolean) => void;
94
- /**
95
- * Triggered when the focus is leaving the previous focusable element. This
96
- * way, the focus is redirected to the first focusable element inside the
97
- * popover.
98
- */
99
- handleKeydownPreviousFocusableElement: (e: KeyboardEvent) => void;
100
- /**
101
- * Triggered when the focus is leaving the next focusable element. This way,
102
- * the focus is redirected to the last focusable element inside the popover.
103
- */
104
- handleKeydownNextFocusableElement: (e: KeyboardEvent) => void;
105
- render(): React.ReactNode;
106
- }
107
- export {};
@@ -1,40 +0,0 @@
1
- import * as React from "react";
2
- type Props = {
3
- /**
4
- * The container to apply the initial focus
5
- */
6
- children: React.ReactElement<any>;
7
- /**
8
- * The selector for the element that will be focused when the component shows.
9
- * When not set, the first tabbable element within the component will be used.
10
- */
11
- initialFocusId?: string;
12
- /**
13
- * The delay in milliseconds before the initial focus is set.
14
- * This is to ensure that any active event listeners have time to finish.
15
- *
16
- * Defaults to 0.
17
- */
18
- delay?: number;
19
- };
20
- /**
21
- * This component finds which element (from within the children) needs to
22
- * receive focus. After that, the children is rendered with the focus assigned.
23
- */
24
- export default class InitialFocus extends React.Component<Props> {
25
- componentDidMount(): void;
26
- /**
27
- * Gets the focusable element and applies focus to it
28
- */
29
- setInitialFocusableElement: (node: HTMLElement) => void;
30
- /**
31
- * Returns an element specified by the user
32
- */
33
- maybeGetInitialFocusElement(node: HTMLElement): HTMLElement | null;
34
- /**
35
- * Returns the first focusable element found inside the children
36
- */
37
- maybeGetFirstFocusableElement(node: HTMLElement): HTMLElement | null;
38
- render(): React.ReactNode;
39
- }
40
- export {};
@@ -1,34 +0,0 @@
1
- import * as React from "react";
2
- import PopoverContent from "./popover-content";
3
- import PopoverContentCore from "./popover-content-core";
4
- type Props = {
5
- /**
6
- * Called when `esc` is pressed
7
- */
8
- onClose: (shouldReturnFocus: boolean) => unknown;
9
- /**
10
- * Popover Content ref.
11
- * Will close the popover when clicking outside this element.
12
- */
13
- contentRef?: React.RefObject<PopoverContentCore | PopoverContent>;
14
- };
15
- type State = {
16
- /**
17
- * Tracks the first click triggered by the click event listener.
18
- */
19
- isFirstClick: boolean;
20
- };
21
- /**
22
- * A component that, when mounted, calls `onClose` when certain events occur.
23
- * This includes when pressing Escape or clicking outside the Popover.
24
- * @see @khanacademy/wonder-blocks-modal/components/modal-launcher.js
25
- */
26
- export default class PopoverEventListener extends React.Component<Props, State> {
27
- state: State;
28
- componentDidMount(): void;
29
- componentWillUnmount(): void;
30
- _handleKeyup: (e: KeyboardEvent) => void;
31
- _handleClick: (e: MouseEvent) => void;
32
- render(): React.ReactElement | null;
33
- }
34
- export {};