@khanacademy/wonder-blocks-popover 3.2.15 → 3.2.16
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 +13 -0
- package/package.json +7 -7
- package/src/components/__tests__/focus-manager.test.tsx +0 -180
- package/src/components/__tests__/initial-focus.test.tsx +0 -73
- package/src/components/__tests__/popover-anchor.test.tsx +0 -61
- package/src/components/__tests__/popover-content.test.tsx +0 -76
- package/src/components/__tests__/popover-content.typestest.tsx +0 -38
- package/src/components/__tests__/popover-dialog.test.tsx +0 -98
- package/src/components/__tests__/popover-event-listener.test.tsx +0 -98
- package/src/components/__tests__/popover.test.tsx +0 -932
- package/src/components/close-button.tsx +0 -61
- package/src/components/focus-manager.tsx +0 -344
- package/src/components/initial-focus.ts +0 -87
- package/src/components/popover-anchor.ts +0 -93
- package/src/components/popover-content-core.tsx +0 -143
- package/src/components/popover-content.tsx +0 -319
- package/src/components/popover-context.ts +0 -40
- package/src/components/popover-dialog.tsx +0 -150
- package/src/components/popover-event-listener.ts +0 -96
- package/src/components/popover.tsx +0 -410
- package/src/index.ts +0 -5
- package/src/util/__tests__/util.test.tsx +0 -38
- package/src/util/util.ts +0 -20
- package/tsconfig-build.json +0 -17
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as ReactDOM from "react-dom";
|
|
3
|
-
import {isFocusable} from "../util/util";
|
|
4
|
-
|
|
5
|
-
import PopoverContent from "./popover-content";
|
|
6
|
-
import PopoverContentCore from "./popover-content-core";
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
/**
|
|
10
|
-
* Called when `esc` is pressed
|
|
11
|
-
*/
|
|
12
|
-
onClose: (shouldReturnFocus: boolean) => unknown;
|
|
13
|
-
/**
|
|
14
|
-
* Popover Content ref.
|
|
15
|
-
* Will close the popover when clicking outside this element.
|
|
16
|
-
*/
|
|
17
|
-
contentRef?: React.RefObject<PopoverContentCore | PopoverContent>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type State = {
|
|
21
|
-
/**
|
|
22
|
-
* Tracks the first click triggered by the click event listener.
|
|
23
|
-
*/
|
|
24
|
-
isFirstClick: boolean;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* A component that, when mounted, calls `onClose` when certain events occur.
|
|
29
|
-
* This includes when pressing Escape or clicking outside the Popover.
|
|
30
|
-
* @see @khanacademy/wonder-blocks-modal/components/modal-launcher.js
|
|
31
|
-
*/
|
|
32
|
-
export default class PopoverEventListener extends React.Component<
|
|
33
|
-
Props,
|
|
34
|
-
State
|
|
35
|
-
> {
|
|
36
|
-
state: State = {
|
|
37
|
-
isFirstClick: true,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
componentDidMount() {
|
|
41
|
-
window.addEventListener("keyup", this._handleKeyup);
|
|
42
|
-
window.addEventListener("click", this._handleClick);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
componentWillUnmount() {
|
|
46
|
-
window.removeEventListener("keyup", this._handleKeyup);
|
|
47
|
-
window.removeEventListener("click", this._handleClick);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
_handleKeyup: (e: KeyboardEvent) => void = (e) => {
|
|
51
|
-
// We check the key as that's keyboard layout agnostic and also avoids
|
|
52
|
-
// the minefield of deprecated number type properties like keyCode and
|
|
53
|
-
// which, with the replacement code, which uses a string instead.
|
|
54
|
-
if (e.key === "Escape") {
|
|
55
|
-
// Stop the event going any further.
|
|
56
|
-
// For cancellation events, like the Escape key, we generally should
|
|
57
|
-
// air on the side of caution and only allow it to cancel one thing.
|
|
58
|
-
// So, it's polite for us to stop propagation of the event.
|
|
59
|
-
// Otherwise, we end up with UX where one Escape key press
|
|
60
|
-
// unexpectedly cancels multiple things.
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
e.stopPropagation();
|
|
63
|
-
// In the case of the Escape key, we should return focus to the
|
|
64
|
-
// trigger button.
|
|
65
|
-
this.props.onClose(true);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
_handleClick: (e: MouseEvent) => void = (e) => {
|
|
70
|
-
// Prevents the problem where clicking the trigger button
|
|
71
|
-
// triggers a click event and immediately closes the popover.
|
|
72
|
-
if (this.state.isFirstClick) {
|
|
73
|
-
this.setState({isFirstClick: false});
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const node = ReactDOM.findDOMNode(this.props.contentRef?.current);
|
|
78
|
-
if (node && !node.contains(e.target as any)) {
|
|
79
|
-
// Stop the event going any further.
|
|
80
|
-
// Only allow click to cancel one thing at a time.
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
e.stopPropagation();
|
|
83
|
-
|
|
84
|
-
// Determine if the focus must go to a focusable/interactive
|
|
85
|
-
// element.
|
|
86
|
-
const shouldReturnFocus = !isFocusable(e.target as any);
|
|
87
|
-
// If that's the case, we need to prevent the default behavior of
|
|
88
|
-
// returning the focus to the trigger button.
|
|
89
|
-
this.props.onClose(shouldReturnFocus);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
render(): React.ReactElement | null {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as ReactDOM from "react-dom";
|
|
3
|
-
|
|
4
|
-
import {IDProvider} from "@khanacademy/wonder-blocks-core";
|
|
5
|
-
import {TooltipPopper} from "@khanacademy/wonder-blocks-tooltip";
|
|
6
|
-
import {maybeGetPortalMountedModalHostElement} from "@khanacademy/wonder-blocks-modal";
|
|
7
|
-
|
|
8
|
-
import type {AriaProps} from "@khanacademy/wonder-blocks-core";
|
|
9
|
-
import type {
|
|
10
|
-
Placement,
|
|
11
|
-
PopperElementProps,
|
|
12
|
-
} from "@khanacademy/wonder-blocks-tooltip";
|
|
13
|
-
import type {RootBoundary} from "@popperjs/core";
|
|
14
|
-
|
|
15
|
-
import PopoverContent from "./popover-content";
|
|
16
|
-
import PopoverContentCore from "./popover-content-core";
|
|
17
|
-
import PopoverContext from "./popover-context";
|
|
18
|
-
import PopoverAnchor from "./popover-anchor";
|
|
19
|
-
import PopoverDialog from "./popover-dialog";
|
|
20
|
-
import PopoverEventListener from "./popover-event-listener";
|
|
21
|
-
import InitialFocus from "./initial-focus";
|
|
22
|
-
import FocusManager from "./focus-manager";
|
|
23
|
-
|
|
24
|
-
type PopoverContents =
|
|
25
|
-
| React.ReactElement<React.ComponentProps<typeof PopoverContent>>
|
|
26
|
-
| React.ReactElement<React.ComponentProps<typeof PopoverContentCore>>;
|
|
27
|
-
|
|
28
|
-
type Props = AriaProps &
|
|
29
|
-
Readonly<{
|
|
30
|
-
/**
|
|
31
|
-
* The element that triggers the popover. This element will be used to
|
|
32
|
-
* position the popover. It can be either a Node or a function using the
|
|
33
|
-
* children-as-function pattern to pass an open function for use anywhere
|
|
34
|
-
* within children. The latter provides a lot of flexibility in terms of
|
|
35
|
-
* what actions may trigger the `Popover` to launch the popover dialog.
|
|
36
|
-
*/
|
|
37
|
-
children:
|
|
38
|
-
| React.ReactElement<any>
|
|
39
|
-
| ((arg1: {open: () => void}) => React.ReactElement<any>);
|
|
40
|
-
/**
|
|
41
|
-
* The content of the popover. You can either use
|
|
42
|
-
* [PopoverContent](#PopoverContent) with one of the pre-defined variants,
|
|
43
|
-
* or include your own custom content using
|
|
44
|
-
* [PopoverContentCore](#PopoverContentCore directly.
|
|
45
|
-
*
|
|
46
|
-
* If the popover needs to close itself, the close function provided to this
|
|
47
|
-
* callback can be called to close the popover.
|
|
48
|
-
*/
|
|
49
|
-
content:
|
|
50
|
-
| PopoverContents
|
|
51
|
-
| ((arg1: {close: () => void}) => PopoverContents);
|
|
52
|
-
/**
|
|
53
|
-
* Where the popover should try to appear in relation to the trigger element.
|
|
54
|
-
*/
|
|
55
|
-
placement: Placement;
|
|
56
|
-
/**
|
|
57
|
-
* When enabled, user can hide the popover content by pressing the `esc` key
|
|
58
|
-
* or clicking/tapping outside of it.
|
|
59
|
-
*/
|
|
60
|
-
dismissEnabled?: boolean;
|
|
61
|
-
/**
|
|
62
|
-
* The unique identifier to give to the popover. Provide this in cases
|
|
63
|
-
* where you want to override the default accessibility solution. This
|
|
64
|
-
* identifier will be applied to the popover title and content.
|
|
65
|
-
*
|
|
66
|
-
* This is also used as a prefix to the IDs of the popover's elements.
|
|
67
|
-
*
|
|
68
|
-
* For example, if you pass `"my-popover"` as the ID, the popover title
|
|
69
|
-
* will have the ID `"my-popover-title"` and the popover content will
|
|
70
|
-
* have the ID `"my-popover-content"`.
|
|
71
|
-
*
|
|
72
|
-
*/
|
|
73
|
-
id?: string;
|
|
74
|
-
/**
|
|
75
|
-
* The selector for the element that will be focused after the popover
|
|
76
|
-
* dialog closes. When not set, the element that triggered the popover
|
|
77
|
-
* will be used.
|
|
78
|
-
*/
|
|
79
|
-
closedFocusId?: string;
|
|
80
|
-
/**
|
|
81
|
-
* The selector for the element that will be focused when the popover
|
|
82
|
-
* content shows. When not set, the first focusable element within the
|
|
83
|
-
* popover content will be used.
|
|
84
|
-
*/
|
|
85
|
-
initialFocusId?: string;
|
|
86
|
-
/**
|
|
87
|
-
* Renders the popover when true, renders nothing when false.
|
|
88
|
-
*
|
|
89
|
-
* Using this prop makes the component behave as a controlled component. The
|
|
90
|
-
* parent is responsible for managing the opening/closing of the popover
|
|
91
|
-
* when using this prop.
|
|
92
|
-
*/
|
|
93
|
-
opened?: boolean;
|
|
94
|
-
/**
|
|
95
|
-
* Called when the popover closes
|
|
96
|
-
*/
|
|
97
|
-
onClose?: () => unknown;
|
|
98
|
-
/**
|
|
99
|
-
* Test ID used for e2e testing.
|
|
100
|
-
*/
|
|
101
|
-
testId?: string;
|
|
102
|
-
/**
|
|
103
|
-
* Whether to show the popover tail or not. Defaults to true.
|
|
104
|
-
*/
|
|
105
|
-
showTail: boolean;
|
|
106
|
-
/**
|
|
107
|
-
* Optional property to enable the portal functionality of popover.
|
|
108
|
-
* This is very handy in cases where the Popover can't be easily
|
|
109
|
-
* injected into the DOM structure and requires portaling to
|
|
110
|
-
* the trigger location.
|
|
111
|
-
*
|
|
112
|
-
* Set to "true" by default.
|
|
113
|
-
*
|
|
114
|
-
* CAUTION: Turning off portal could cause some clipping issues
|
|
115
|
-
* especially around legacy code with usage of z-indexing,
|
|
116
|
-
* Use caution when turning this functionality off and ensure
|
|
117
|
-
* your content does not get clipped or hidden.
|
|
118
|
-
*/
|
|
119
|
-
portal?: boolean;
|
|
120
|
-
/**
|
|
121
|
-
* Optional property to set what the root boundary is for the popper behavior.
|
|
122
|
-
* This is set to "viewport" by default, causing the popper to be positioned based
|
|
123
|
-
* on the user's viewport. If set to "document", it will position itself based
|
|
124
|
-
* on where there is available room within the document body.
|
|
125
|
-
*/
|
|
126
|
-
rootBoundary?: RootBoundary;
|
|
127
|
-
}>;
|
|
128
|
-
|
|
129
|
-
type State = Readonly<{
|
|
130
|
-
/**
|
|
131
|
-
* Keeps a reference of the dialog state
|
|
132
|
-
*/
|
|
133
|
-
opened: boolean;
|
|
134
|
-
/**
|
|
135
|
-
* Anchor element DOM reference
|
|
136
|
-
*/
|
|
137
|
-
anchorElement?: HTMLElement;
|
|
138
|
-
/**
|
|
139
|
-
* Current popper placement
|
|
140
|
-
*/
|
|
141
|
-
placement: Placement;
|
|
142
|
-
}>;
|
|
143
|
-
|
|
144
|
-
type DefaultProps = Readonly<{
|
|
145
|
-
placement: Props["placement"];
|
|
146
|
-
showTail: Props["showTail"];
|
|
147
|
-
portal: Props["portal"];
|
|
148
|
-
rootBoundary: Props["rootBoundary"];
|
|
149
|
-
}>;
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Popovers provide additional information that is related to a particular
|
|
153
|
-
* element and/or content. They can include text, links, icons and
|
|
154
|
-
* illustrations. The main difference with `Tooltip` is that they must be
|
|
155
|
-
* dismissed by clicking an element.
|
|
156
|
-
*
|
|
157
|
-
* This component uses the `PopoverPopper` component to position the
|
|
158
|
-
* `PopoverContentCore` component according to the children it is wrapping.
|
|
159
|
-
*
|
|
160
|
-
* ### Usage
|
|
161
|
-
*
|
|
162
|
-
* ```jsx
|
|
163
|
-
* import {Popover, PopoverContent} from "@khanacademy/wonder-blocks-popover";
|
|
164
|
-
*
|
|
165
|
-
* <Popover
|
|
166
|
-
* onClose={() => {}}
|
|
167
|
-
* content={
|
|
168
|
-
* <PopoverContent title="Title" content="Some content" closeButtonVisible />
|
|
169
|
-
* }>
|
|
170
|
-
* {({ open }) => <Button onClick={open}>Open popover</Button>}
|
|
171
|
-
* </Popover>
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
export default class Popover extends React.Component<Props, State> {
|
|
175
|
-
static defaultProps: DefaultProps = {
|
|
176
|
-
placement: "top",
|
|
177
|
-
showTail: true,
|
|
178
|
-
portal: true,
|
|
179
|
-
rootBoundary: "viewport",
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Used to sync the `opened` state when Popover acts as a controlled
|
|
184
|
-
* component
|
|
185
|
-
*/
|
|
186
|
-
static getDerivedStateFromProps(
|
|
187
|
-
props: Props,
|
|
188
|
-
state: State,
|
|
189
|
-
): Partial<State> | null | undefined {
|
|
190
|
-
return {
|
|
191
|
-
opened:
|
|
192
|
-
typeof props.opened === "boolean" ? props.opened : state.opened,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
state: State = {
|
|
197
|
-
opened: !!this.props.opened,
|
|
198
|
-
placement: this.props.placement,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Popover content ref
|
|
203
|
-
*/
|
|
204
|
-
contentRef: React.RefObject<PopoverContent | PopoverContentCore> =
|
|
205
|
-
React.createRef();
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Returns focus to a given element.
|
|
209
|
-
*/
|
|
210
|
-
maybeReturnFocus = () => {
|
|
211
|
-
const {anchorElement} = this.state;
|
|
212
|
-
const {closedFocusId} = this.props;
|
|
213
|
-
|
|
214
|
-
// Focus on the specified element after dismissing the popover.
|
|
215
|
-
if (closedFocusId) {
|
|
216
|
-
const focusElement = ReactDOM.findDOMNode(
|
|
217
|
-
document.getElementById(closedFocusId),
|
|
218
|
-
) as any;
|
|
219
|
-
|
|
220
|
-
focusElement?.focus();
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// If no element is specified, focus on the element that triggered the
|
|
225
|
-
// popover.
|
|
226
|
-
if (anchorElement) {
|
|
227
|
-
anchorElement.focus();
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Popover dialog closed
|
|
233
|
-
*/
|
|
234
|
-
handleClose: (shouldReturnFocus?: boolean) => void = (
|
|
235
|
-
shouldReturnFocus = true,
|
|
236
|
-
) => {
|
|
237
|
-
this.setState({opened: false}, () => {
|
|
238
|
-
this.props.onClose?.();
|
|
239
|
-
|
|
240
|
-
if (shouldReturnFocus) {
|
|
241
|
-
this.maybeReturnFocus();
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Popover dialog opened
|
|
248
|
-
*/
|
|
249
|
-
handleOpen: () => void = () => {
|
|
250
|
-
if (this.props.dismissEnabled && this.state.opened) {
|
|
251
|
-
this.handleClose(true);
|
|
252
|
-
} else {
|
|
253
|
-
this.setState({opened: true});
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
updateRef = (actualRef?: HTMLElement) => {
|
|
258
|
-
if (actualRef && this.state.anchorElement !== actualRef) {
|
|
259
|
-
this.setState({
|
|
260
|
-
anchorElement: actualRef,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
renderContent(uniqueId: string): PopoverContents {
|
|
266
|
-
const {content} = this.props;
|
|
267
|
-
|
|
268
|
-
const popoverContents: PopoverContents =
|
|
269
|
-
typeof content === "function"
|
|
270
|
-
? content({
|
|
271
|
-
close: this.handleClose,
|
|
272
|
-
})
|
|
273
|
-
: content;
|
|
274
|
-
|
|
275
|
-
// @ts-expect-error: TS2769 - No overload matches this call.
|
|
276
|
-
return React.cloneElement(popoverContents, {
|
|
277
|
-
ref: this.contentRef,
|
|
278
|
-
// internal prop: only injected by Popover
|
|
279
|
-
// This allows us to announce the popover content when it is opened.
|
|
280
|
-
uniqueId,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
renderPopper(uniqueId: string): React.ReactNode {
|
|
285
|
-
const {
|
|
286
|
-
initialFocusId,
|
|
287
|
-
placement,
|
|
288
|
-
showTail,
|
|
289
|
-
portal,
|
|
290
|
-
"aria-label": ariaLabel,
|
|
291
|
-
"aria-describedby": ariaDescribedBy,
|
|
292
|
-
rootBoundary,
|
|
293
|
-
} = this.props;
|
|
294
|
-
const {anchorElement} = this.state;
|
|
295
|
-
|
|
296
|
-
const describedBy = ariaDescribedBy || `${uniqueId}-content`;
|
|
297
|
-
|
|
298
|
-
const ariaLabelledBy = ariaLabel ? undefined : `${uniqueId}-title`;
|
|
299
|
-
|
|
300
|
-
const popperContent = (
|
|
301
|
-
<TooltipPopper
|
|
302
|
-
anchorElement={anchorElement}
|
|
303
|
-
placement={placement}
|
|
304
|
-
rootBoundary={rootBoundary}
|
|
305
|
-
>
|
|
306
|
-
{(props: PopperElementProps) => (
|
|
307
|
-
<PopoverDialog
|
|
308
|
-
{...props}
|
|
309
|
-
aria-label={ariaLabel}
|
|
310
|
-
aria-describedby={describedBy}
|
|
311
|
-
aria-labelledby={ariaLabelledBy}
|
|
312
|
-
id={uniqueId}
|
|
313
|
-
onUpdate={(placement) => this.setState({placement})}
|
|
314
|
-
showTail={showTail}
|
|
315
|
-
>
|
|
316
|
-
{this.renderContent(uniqueId)}
|
|
317
|
-
</PopoverDialog>
|
|
318
|
-
)}
|
|
319
|
-
</TooltipPopper>
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
if (portal) {
|
|
323
|
-
return (
|
|
324
|
-
<FocusManager
|
|
325
|
-
anchorElement={anchorElement}
|
|
326
|
-
initialFocusId={initialFocusId}
|
|
327
|
-
>
|
|
328
|
-
{popperContent}
|
|
329
|
-
</FocusManager>
|
|
330
|
-
);
|
|
331
|
-
} else {
|
|
332
|
-
return (
|
|
333
|
-
// Ensures the user is focused on the first available element
|
|
334
|
-
// when popover is rendered without the focus manager.
|
|
335
|
-
<InitialFocus initialFocusId={initialFocusId}>
|
|
336
|
-
{popperContent}
|
|
337
|
-
</InitialFocus>
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
getHost(): Element | null | undefined {
|
|
343
|
-
// If we are in a modal, we find where we should be portalling the
|
|
344
|
-
// popover by using the helper function from the modal package on the
|
|
345
|
-
// trigger element. If we are not in a modal, we use body as the
|
|
346
|
-
// location to portal to.
|
|
347
|
-
return (
|
|
348
|
-
maybeGetPortalMountedModalHostElement(this.state.anchorElement) ||
|
|
349
|
-
document.body
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
renderPortal(uniqueId: string, opened: boolean) {
|
|
354
|
-
if (!opened) {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const {portal} = this.props;
|
|
359
|
-
const popperHost = this.getHost();
|
|
360
|
-
|
|
361
|
-
// Attach the popover to a Portal
|
|
362
|
-
if (portal && popperHost) {
|
|
363
|
-
return ReactDOM.createPortal(
|
|
364
|
-
this.renderPopper(uniqueId),
|
|
365
|
-
popperHost,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Otherwise, append the dialog next to the trigger element
|
|
370
|
-
return this.renderPopper(uniqueId);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
render(): React.ReactNode {
|
|
374
|
-
const {children, dismissEnabled, id} = this.props;
|
|
375
|
-
const {opened, placement} = this.state;
|
|
376
|
-
|
|
377
|
-
return (
|
|
378
|
-
<PopoverContext.Provider
|
|
379
|
-
value={{
|
|
380
|
-
close: this.handleClose,
|
|
381
|
-
placement: placement,
|
|
382
|
-
}}
|
|
383
|
-
>
|
|
384
|
-
<IDProvider id={id} scope="popover">
|
|
385
|
-
{(uniqueId) => (
|
|
386
|
-
<React.Fragment>
|
|
387
|
-
<PopoverAnchor
|
|
388
|
-
anchorRef={this.updateRef}
|
|
389
|
-
id={`${uniqueId}-anchor`}
|
|
390
|
-
aria-controls={uniqueId}
|
|
391
|
-
aria-expanded={opened ? "true" : "false"}
|
|
392
|
-
onClick={this.handleOpen}
|
|
393
|
-
>
|
|
394
|
-
{children}
|
|
395
|
-
</PopoverAnchor>
|
|
396
|
-
{this.renderPortal(uniqueId, opened)}
|
|
397
|
-
</React.Fragment>
|
|
398
|
-
)}
|
|
399
|
-
</IDProvider>
|
|
400
|
-
|
|
401
|
-
{dismissEnabled && opened && (
|
|
402
|
-
<PopoverEventListener
|
|
403
|
-
onClose={this.handleClose}
|
|
404
|
-
contentRef={this.contentRef}
|
|
405
|
-
/>
|
|
406
|
-
)}
|
|
407
|
-
</PopoverContext.Provider>
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {render, screen} from "@testing-library/react";
|
|
3
|
-
import {isFocusable} from "../util";
|
|
4
|
-
|
|
5
|
-
describe("isFocusable", () => {
|
|
6
|
-
it("should mark a button as focusable", () => {
|
|
7
|
-
// Arrange
|
|
8
|
-
render(<button>Open popover</button>);
|
|
9
|
-
|
|
10
|
-
// Act
|
|
11
|
-
const result = isFocusable(screen.getByRole("button"));
|
|
12
|
-
|
|
13
|
-
// Assert
|
|
14
|
-
expect(result).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should mark a div as non-focusable", () => {
|
|
18
|
-
// Arrange
|
|
19
|
-
render(<div>placeholder</div>);
|
|
20
|
-
|
|
21
|
-
// Act
|
|
22
|
-
const result = isFocusable(screen.getByText("placeholder"));
|
|
23
|
-
|
|
24
|
-
// Assert
|
|
25
|
-
expect(result).toBe(false);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("should mark a div with tabIndex greater than -1 as focusable", () => {
|
|
29
|
-
// Arrange
|
|
30
|
-
render(<div tabIndex={0}>placeholder</div>);
|
|
31
|
-
|
|
32
|
-
// Act
|
|
33
|
-
const result = isFocusable(screen.getByText("placeholder"));
|
|
34
|
-
|
|
35
|
-
// Assert
|
|
36
|
-
expect(result).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
});
|
package/src/util/util.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* List of elements that can be focused
|
|
3
|
-
* @see https://www.w3.org/TR/html5/editing.html#can-be-focused
|
|
4
|
-
*/
|
|
5
|
-
const FOCUSABLE_ELEMENTS =
|
|
6
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
7
|
-
|
|
8
|
-
export function findFocusableNodes(
|
|
9
|
-
root: HTMLElement | Document,
|
|
10
|
-
): Array<HTMLElement> {
|
|
11
|
-
return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Checks if an element is focusable
|
|
16
|
-
* @see https://html.spec.whatwg.org/multipage/interaction.html#focusable-area
|
|
17
|
-
*/
|
|
18
|
-
export function isFocusable(element: HTMLElement): boolean {
|
|
19
|
-
return element.matches(FOCUSABLE_ELEMENTS);
|
|
20
|
-
}
|
package/tsconfig-build.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"exclude": ["dist"],
|
|
3
|
-
"extends": "../tsconfig-shared.json",
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"outDir": "./dist",
|
|
6
|
-
"rootDir": "src",
|
|
7
|
-
},
|
|
8
|
-
"references": [
|
|
9
|
-
{"path": "../wonder-blocks-core/tsconfig-build.json"},
|
|
10
|
-
{"path": "../wonder-blocks-icon/tsconfig-build.json"},
|
|
11
|
-
{"path": "../wonder-blocks-icon-button/tsconfig-build.json"},
|
|
12
|
-
{"path": "../wonder-blocks-modal/tsconfig-build.json"},
|
|
13
|
-
{"path": "../wonder-blocks-tokens/tsconfig-build.json"},
|
|
14
|
-
{"path": "../wonder-blocks-tooltip/tsconfig-build.json"},
|
|
15
|
-
{"path": "../wonder-blocks-typography/tsconfig-build.json"},
|
|
16
|
-
]
|
|
17
|
-
}
|