@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 +52 -0
- package/dist/components/popover-anchor.d.ts +21 -20
- package/dist/components/popover-content-core.d.ts +34 -28
- package/dist/components/popover-content.d.ts +23 -38
- package/dist/components/popover-context.d.ts +5 -5
- package/dist/components/popover-dialog.d.ts +18 -32
- package/dist/components/popover.d.ts +35 -73
- package/dist/es/index.js +8 -19
- package/dist/index.js +7 -19
- package/dist/util/types.d.ts +17 -0
- package/package.json +3 -6
- package/dist/components/focus-manager.d.ts +0 -107
- package/dist/components/initial-focus.d.ts +0 -40
- package/dist/components/popover-event-listener.d.ts +0 -34
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 {
|
|
3
|
-
|
|
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
|
-
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
11
|
-
* PopoverContent. This is needed here to reposition the
|
|
12
|
-
* start or the end of the content, in case the
|
|
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 {
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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 &
|
|
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
|
|
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
|
|
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
|
|
113
|
-
* This is set to "viewport" by default, causing the
|
|
114
|
-
* on the user's viewport. If set to "document", it will
|
|
115
|
-
* on where there is available room within the
|
|
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
|
|
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
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*
|
|
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
|
-
|
|
127
|
+
autoUpdate?: boolean;
|
|
134
128
|
/**
|
|
135
|
-
*
|
|
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
|
-
|
|
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 `
|
|
152
|
-
*
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
4
|
-
import { View,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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");
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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");
|
|
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
|
+
"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 {};
|