@khanacademy/perseus 0.4.1 → 0.4.2
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 +9 -0
- package/dist/es/index.js +613 -5189
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +607 -5183
- package/dist/index.js.map +1 -1
- package/dist/strings.js +0 -2
- package/package.json +6 -3
- package/src/components/highlighting/ui/highlight-tooltip.jsx +0 -16
- package/src/components/new-tooltip/README.md +1 -108
- package/src/components/new-tooltip/new-tooltip.jsx +47 -291
- package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +0 -437
- package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +0 -70
- package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +0 -98
- package/src/components/new-tooltip/__tests__/util_test.js +0 -47
- package/src/components/new-tooltip/flexible-position.jsx +0 -182
- package/src/components/new-tooltip/tooltip-arrow.jsx +0 -184
- package/src/components/new-tooltip/tooltip-bubble.jsx +0 -223
- package/src/components/new-tooltip/tooltip-portal.jsx +0 -101
- package/src/components/new-tooltip/tooltip-positioner.jsx +0 -529
- package/src/components/new-tooltip/util.js +0 -27
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
/**
|
|
3
|
-
* This is FlexiblePosition, part of NewTooltip.
|
|
4
|
-
*
|
|
5
|
-
* This generalizable component attempts to center its children relative to a
|
|
6
|
-
* given point... but refuses to allow the children to overflow their
|
|
7
|
-
* container. If the children *would* overflow, we first use the Flexbox
|
|
8
|
-
* algorithm to shift the element off-center. If that still doesn't work (i.e.,
|
|
9
|
-
* the children are larger than the container), then we shrink the children.
|
|
10
|
-
*
|
|
11
|
-
* This layout algorithm is entirely implemented via Flexbox. We have three
|
|
12
|
-
* elements: the pre-content spacer, the content, and the post-content spacer.
|
|
13
|
-
* The pre-content spacer and post-content spacer use their basis sizes to
|
|
14
|
-
* position the content at the target point, then shrink by equal amounts in
|
|
15
|
-
* order to *center* the content at the target point.
|
|
16
|
-
*
|
|
17
|
-
* To understand the algorithm, first consider *just* the spacer elements.
|
|
18
|
-
* If the container is 80px wide, and we're targeting a point 60px from the
|
|
19
|
-
* right and 20px from the left, then the spacer elements will use those values
|
|
20
|
-
* as their flex-basis:
|
|
21
|
-
*
|
|
22
|
-
* [ pre-content ][ post-content ]
|
|
23
|
-
*
|
|
24
|
-
* They take up 100% of the container by default. But, if the content element
|
|
25
|
-
* _also_ wants space, then the pre-content and post-content spacers need to
|
|
26
|
-
* shrink. We've configured them to shrink by equal amounts, which therefore
|
|
27
|
-
* center-aligns the target element at the target point.
|
|
28
|
-
*
|
|
29
|
-
* [ pre-content ][content ][post-content ]
|
|
30
|
-
*
|
|
31
|
-
* (Incidentally, the flex-shrink property is surprisingly complicated: it's
|
|
32
|
-
* defined relative to the element's own flex-basis. So, to get the spacers to
|
|
33
|
-
* shrink at equal rates, we define each spacer's flex-shrink to match the
|
|
34
|
-
* _other_ element's flex-basis. Fun! ^_^`)
|
|
35
|
-
*
|
|
36
|
-
* TODO(mdr): This component currently only supports horizontal positioning,
|
|
37
|
-
* but we'll probably extend it later to support vertical positioning, too.
|
|
38
|
-
*/
|
|
39
|
-
import {StyleSheet, css} from "aphrodite";
|
|
40
|
-
import * as React from "react";
|
|
41
|
-
|
|
42
|
-
type FlexiblePositionProps = {
|
|
43
|
-
children: React.Node,
|
|
44
|
-
// The direction along which to position the element.
|
|
45
|
-
//
|
|
46
|
-
// If "horizontal", this will be a full-width element that horizontally
|
|
47
|
-
// positions its children near a target x-coordinate.
|
|
48
|
-
//
|
|
49
|
-
// If "vertical", this will be a full-height element that vertically
|
|
50
|
-
// positions its children near a target y-coordinate.
|
|
51
|
-
direction: "horizontal" | "vertical",
|
|
52
|
-
// Specify the target point, by providing its distance from the flex-start
|
|
53
|
-
// and its distance from the flex-end. (These, together, must sum to the
|
|
54
|
-
// total container size.)
|
|
55
|
-
//
|
|
56
|
-
// If `direction` is "horizontal", then `pxFromStart` is the target
|
|
57
|
-
// position's offset from the left of the container, and `pxFromEnd` is the
|
|
58
|
-
// offset from the right.
|
|
59
|
-
//
|
|
60
|
-
// If `direction` is "vertical", then `pxFromStart` is the target
|
|
61
|
-
// position's offset from the top of the container, and `pxFromEnd` is the
|
|
62
|
-
// offset from the bottom.
|
|
63
|
-
pxFromStart: number,
|
|
64
|
-
pxFromEnd: number,
|
|
65
|
-
...
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export default class FlexiblePosition extends React.PureComponent<FlexiblePositionProps> {
|
|
69
|
-
render(): React.Element<"div"> {
|
|
70
|
-
const {direction, pxFromStart, pxFromEnd} = this.props;
|
|
71
|
-
|
|
72
|
-
const wrapperStyles = [
|
|
73
|
-
styles.wrapper,
|
|
74
|
-
direction === "horizontal" && styles.wrapperHorizontal,
|
|
75
|
-
direction === "vertical" && styles.wrapperVertical,
|
|
76
|
-
];
|
|
77
|
-
const contentStyles = [
|
|
78
|
-
styles.content,
|
|
79
|
-
direction === "horizontal" && styles.contentHorizontal,
|
|
80
|
-
direction === "vertical" && styles.contentVertical,
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
// We need to handle the zero cases specially, because otherwise we get
|
|
84
|
-
// spacer elements with `flex-shrink: 0` - that is, they don't shrink
|
|
85
|
-
// for the content at all!
|
|
86
|
-
if (pxFromStart === 0) {
|
|
87
|
-
return (
|
|
88
|
-
<div className={css(...wrapperStyles)}>
|
|
89
|
-
<div className={css(...contentStyles)}>
|
|
90
|
-
{this.props.children}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
if (pxFromEnd === 0) {
|
|
96
|
-
return (
|
|
97
|
-
<div
|
|
98
|
-
className={css(...wrapperStyles, styles.wrapperAlignToEnd)}
|
|
99
|
-
>
|
|
100
|
-
<div className={css(...contentStyles)}>
|
|
101
|
-
{this.props.children}
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className={css(...wrapperStyles)}>
|
|
109
|
-
<div
|
|
110
|
-
style={{
|
|
111
|
-
flexShrink: pxFromEnd,
|
|
112
|
-
flexBasis: pxFromStart,
|
|
113
|
-
}}
|
|
114
|
-
/>
|
|
115
|
-
<div className={css(...contentStyles)}>
|
|
116
|
-
{this.props.children}
|
|
117
|
-
</div>
|
|
118
|
-
<div
|
|
119
|
-
style={{
|
|
120
|
-
flexShrink: pxFromStart,
|
|
121
|
-
flexBasis: pxFromEnd,
|
|
122
|
-
}}
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const styles = StyleSheet.create({
|
|
130
|
-
wrapper: {
|
|
131
|
-
display: "flex",
|
|
132
|
-
overflow: "hidden",
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
wrapperHorizontal: {
|
|
136
|
-
flexDirection: "row",
|
|
137
|
-
|
|
138
|
-
// The FlexiblePosition container takes the full width of its parent,
|
|
139
|
-
// and will not allow overflow (see `styles.wrapper`). This ensures
|
|
140
|
-
// that we prefer to shrink our contents, rather than overflow our
|
|
141
|
-
// parent.
|
|
142
|
-
width: "100%",
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
wrapperVertical: {
|
|
146
|
-
flexDirection: "column",
|
|
147
|
-
|
|
148
|
-
// The FlexiblePosition container takes the full height of its parent,
|
|
149
|
-
// and will not allow overflow (see `styles.wrapper`). This ensures
|
|
150
|
-
// that we prefer to shrink our contents, rather than overflow our
|
|
151
|
-
// parent.
|
|
152
|
-
height: "100%",
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
wrapperAlignToEnd: {
|
|
156
|
-
justifyContent: "flex-end",
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
content: {
|
|
160
|
-
// The content element must not grow or shrink, and instead always use
|
|
161
|
-
// its basis value.
|
|
162
|
-
flex: "0 0 auto",
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
contentHorizontal: {
|
|
166
|
-
// The only case in which the content element should "shrink" is when
|
|
167
|
-
// the content itself is too large for the container. In this case, we
|
|
168
|
-
// constrain the size with maximum width. (We can't use flex-shrink for
|
|
169
|
-
// this behavior, because we need the spacer elements to be prioritized
|
|
170
|
-
// as the only flex-shrinkable elements.)
|
|
171
|
-
maxWidth: "100%",
|
|
172
|
-
},
|
|
173
|
-
|
|
174
|
-
contentVertical: {
|
|
175
|
-
// The only case in which the content element should "shrink" is when
|
|
176
|
-
// the content itself is too large for the container. In this case, we
|
|
177
|
-
// constrain the size with maximum height. (We can't use flex-shrink
|
|
178
|
-
// for this behavior, because we need the spacer elements to be
|
|
179
|
-
// prioritized as the only flex-shrinkable elements.)
|
|
180
|
-
maxHeight: "100%",
|
|
181
|
-
},
|
|
182
|
-
});
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
/**
|
|
3
|
-
* This is TooltipArrow, part of NewTooltip.
|
|
4
|
-
*
|
|
5
|
-
* This component manages the visuals for the tooltip "arrow" UI element, which
|
|
6
|
-
* is visually attached to the TooltipBubble, and points to the target element.
|
|
7
|
-
*
|
|
8
|
-
* This component and TooltipBubble both count as part of the tooltip, and
|
|
9
|
-
* trigger mouse events like `onClick`, `onMouseEnter`, and `onMouseLeave`.
|
|
10
|
-
*/
|
|
11
|
-
import {StyleSheet, css} from "aphrodite";
|
|
12
|
-
import * as React from "react";
|
|
13
|
-
|
|
14
|
-
import {Errors} from "../../logging/log.js";
|
|
15
|
-
import {PerseusError} from "../../perseus-error.js";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
arrowHeight,
|
|
19
|
-
arrowWidth,
|
|
20
|
-
dropShadowOpacity,
|
|
21
|
-
dropShadowRadius,
|
|
22
|
-
dropShadowXOffset,
|
|
23
|
-
dropShadowYOffset,
|
|
24
|
-
} from "./style-constants.js";
|
|
25
|
-
import {getTooltipColors} from "./util.js";
|
|
26
|
-
|
|
27
|
-
import type {NewTooltipProps} from "./types.js";
|
|
28
|
-
|
|
29
|
-
type TooltipArrowProps = {|
|
|
30
|
-
tooltipProps: {
|
|
31
|
-
side: NewTooltipProps["side"],
|
|
32
|
-
color: NewTooltipProps["color"],
|
|
33
|
-
inverted: NewTooltipProps["inverted"],
|
|
34
|
-
|
|
35
|
-
onClick?: NewTooltipProps["onClick"],
|
|
36
|
-
onMouseEnter?: NewTooltipProps["onMouseEnter"],
|
|
37
|
-
onMouseLeave?: NewTooltipProps["onMouseLeave"],
|
|
38
|
-
...
|
|
39
|
-
},
|
|
40
|
-
|};
|
|
41
|
-
|
|
42
|
-
export default class TooltipArrow extends React.PureComponent<TooltipArrowProps> {
|
|
43
|
-
render(): React.Element<"svg"> {
|
|
44
|
-
const {onClick, onMouseEnter, onMouseLeave, side} =
|
|
45
|
-
this.props.tooltipProps;
|
|
46
|
-
const {backgroundColor} = getTooltipColors(this.props.tooltipProps);
|
|
47
|
-
|
|
48
|
-
// Draw the three points of the arrow. Depending on the arrow's
|
|
49
|
-
// direction (i.e., the tooltip's "side"), we choose different points,
|
|
50
|
-
// and set our SVG's bounds differently.
|
|
51
|
-
//
|
|
52
|
-
// Note that `arrowWidth` and `arrowHeight` refer to the
|
|
53
|
-
// downward-pointing arrow (i.e. side="top"). When the arrow points to
|
|
54
|
-
// the left or right instead, the width/height are inverted.
|
|
55
|
-
let points;
|
|
56
|
-
let svgWidth;
|
|
57
|
-
let svgHeight;
|
|
58
|
-
if (side === "top") {
|
|
59
|
-
points = [
|
|
60
|
-
"0,0",
|
|
61
|
-
`${arrowWidth / 2},${arrowHeight}`,
|
|
62
|
-
`${arrowWidth},0`,
|
|
63
|
-
];
|
|
64
|
-
svgWidth = arrowWidth;
|
|
65
|
-
svgHeight = arrowHeight;
|
|
66
|
-
} else if (side === "right") {
|
|
67
|
-
points = [
|
|
68
|
-
`${arrowHeight},0`,
|
|
69
|
-
`0,${arrowWidth / 2}`,
|
|
70
|
-
`${arrowHeight},${arrowWidth}`,
|
|
71
|
-
];
|
|
72
|
-
svgWidth = arrowHeight;
|
|
73
|
-
svgHeight = arrowWidth;
|
|
74
|
-
} else if (side === "bottom") {
|
|
75
|
-
points = [
|
|
76
|
-
`0,${arrowHeight}`,
|
|
77
|
-
`${arrowWidth / 2},0`,
|
|
78
|
-
`${arrowWidth},${arrowHeight}`,
|
|
79
|
-
];
|
|
80
|
-
svgWidth = arrowWidth;
|
|
81
|
-
svgHeight = arrowHeight;
|
|
82
|
-
} else if (side === "left") {
|
|
83
|
-
points = [
|
|
84
|
-
"0,0",
|
|
85
|
-
`${arrowHeight},${arrowWidth / 2}`,
|
|
86
|
-
`0,${arrowWidth}`,
|
|
87
|
-
];
|
|
88
|
-
svgWidth = arrowHeight;
|
|
89
|
-
svgHeight = arrowWidth;
|
|
90
|
-
} else {
|
|
91
|
-
throw new PerseusError(
|
|
92
|
-
`unexpected side ${side}`,
|
|
93
|
-
Errors.InvalidInput,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// This SVG is copied from tooltip-package/tooltip.jsx - with some minor
|
|
98
|
-
// modifications, because styling is handled differently.
|
|
99
|
-
return (
|
|
100
|
-
<svg
|
|
101
|
-
width={svgWidth}
|
|
102
|
-
height={svgHeight}
|
|
103
|
-
className={css(styles.tooltipArrow)}
|
|
104
|
-
>
|
|
105
|
-
{/* Create an SVG filter that applies a blur to an element.
|
|
106
|
-
* We'll apply it to a dark shape outlining the tooltip, which
|
|
107
|
-
* will produce the overall effect of a drop-shadow.
|
|
108
|
-
*
|
|
109
|
-
* Also, scope its ID by side, so that tooltips with other
|
|
110
|
-
* "side" values don't end up using the wrong filter from
|
|
111
|
-
* elsewhere in the document. (The `height` value depends on
|
|
112
|
-
* which way the arrow is turned!)
|
|
113
|
-
*
|
|
114
|
-
* In general, it's not *great* that multiple tooltips of the
|
|
115
|
-
* same side will yield ID conflicts... but at least they'll
|
|
116
|
-
* be the same filter, so it doesn't matter which one we
|
|
117
|
-
* reference, so long as it's for the correct side. */}
|
|
118
|
-
<filter id={`tooltip-dropshadow-${side}`} height="150%">
|
|
119
|
-
<feOffset
|
|
120
|
-
dx={dropShadowXOffset}
|
|
121
|
-
dy={dropShadowYOffset}
|
|
122
|
-
result="offsetblur"
|
|
123
|
-
/>
|
|
124
|
-
<feGaussianBlur
|
|
125
|
-
in="SourceAlpha"
|
|
126
|
-
stdDeviation={dropShadowRadius / 2}
|
|
127
|
-
/>
|
|
128
|
-
<feComponentTransfer>
|
|
129
|
-
<feFuncA type="linear" slope={dropShadowOpacity} />
|
|
130
|
-
</feComponentTransfer>
|
|
131
|
-
<feMerge>
|
|
132
|
-
<feMergeNode />
|
|
133
|
-
<feMergeNode in="SourceGraphic" />
|
|
134
|
-
</feMerge>
|
|
135
|
-
</filter>
|
|
136
|
-
|
|
137
|
-
{/* Draw the background of the tooltip arrow. */}
|
|
138
|
-
<polyline
|
|
139
|
-
fill={backgroundColor}
|
|
140
|
-
stroke={backgroundColor}
|
|
141
|
-
points={points.join(" ")}
|
|
142
|
-
// We attach our mouse event styles and handlers to this
|
|
143
|
-
// element, instead of the root SVG element. That way, our
|
|
144
|
-
// target area will only include the actual _arrow_ shape,
|
|
145
|
-
// rather than the entire bounding rectangle.
|
|
146
|
-
className={css(
|
|
147
|
-
onClick && styles.tooltipArrowShapeWithOnClick,
|
|
148
|
-
)}
|
|
149
|
-
onClick={onClick}
|
|
150
|
-
onMouseEnter={onMouseEnter}
|
|
151
|
-
onMouseLeave={onMouseLeave}
|
|
152
|
-
/>
|
|
153
|
-
|
|
154
|
-
{/* Draw an outline around the tooltip arrow, and apply the
|
|
155
|
-
* blur filter we created above, to produce a drop shadow
|
|
156
|
-
* effect. */}
|
|
157
|
-
<polyline
|
|
158
|
-
// Redraw the stroke on top of the background color,
|
|
159
|
-
// so that the ends aren't extra dark where they meet
|
|
160
|
-
// the border of the tooltip.
|
|
161
|
-
fill={backgroundColor}
|
|
162
|
-
points={points.join(" ")}
|
|
163
|
-
stroke="rgba(0, 0, 0, 0.1)"
|
|
164
|
-
filter={`url(#tooltip-dropshadow-${side})`}
|
|
165
|
-
/>
|
|
166
|
-
</svg>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const styles = StyleSheet.create({
|
|
172
|
-
tooltipArrow: {
|
|
173
|
-
display: "block",
|
|
174
|
-
|
|
175
|
-
// Ensure that clicking on the tooltip's empty space doesn't clear the
|
|
176
|
-
// user's text selection elsewhere in the page.
|
|
177
|
-
userSelect: "none",
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
tooltipArrowShapeWithOnClick: {
|
|
181
|
-
pointerEvents: "auto",
|
|
182
|
-
cursor: "pointer",
|
|
183
|
-
},
|
|
184
|
-
});
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
/**
|
|
3
|
-
* This is TooltipBubble, part of NewTooltip.
|
|
4
|
-
*
|
|
5
|
-
* This component manages the visuals for the tooltip "bubble" UI element,
|
|
6
|
-
* which contains the tooltip content.
|
|
7
|
-
*
|
|
8
|
-
* This component and TooltipArrow both count as part of the tooltip, and
|
|
9
|
-
* trigger mouse events like `onClick`, `onMouseEnter`, and `onMouseLeave`.
|
|
10
|
-
*/
|
|
11
|
-
import {UniqueIDProvider} from "@khanacademy/wonder-blocks-core";
|
|
12
|
-
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
13
|
-
import {StyleSheet, css} from "aphrodite";
|
|
14
|
-
import * as React from "react";
|
|
15
|
-
|
|
16
|
-
import {typography} from "../../styles/global-styles.js";
|
|
17
|
-
import a11yStyles from "../../util/a11y.js";
|
|
18
|
-
import Icon from "../icon.jsx";
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
bubbleBorderRadius,
|
|
22
|
-
dropShadowOpacity,
|
|
23
|
-
dropShadowRadius,
|
|
24
|
-
dropShadowXOffset,
|
|
25
|
-
dropShadowYOffset,
|
|
26
|
-
} from "./style-constants.js";
|
|
27
|
-
import {getTooltipColors} from "./util.js";
|
|
28
|
-
|
|
29
|
-
import type {NewTooltipProps} from "./types.js";
|
|
30
|
-
import type {IIdentifierFactory} from "@khanacademy/wonder-blocks-core";
|
|
31
|
-
|
|
32
|
-
const closeIconPath =
|
|
33
|
-
"M6.26353762,4.99851587 L9.73097464,1.53107884 C10.0836373,1.17841618 10.0842213,0.612127047 9.73530496,0.263210718 C9.38395604,-0.0881381913 8.81874474,-0.0837668714 8.46743686,0.267541014 L4.99999981,3.73497806 L1.5325628,0.267541051 C1.1812549,-0.0837668481 0.616043606, -0.0881381955 0.264694717,0.263210694 C-0.0842215912,0.612127004 -0.0836375768,1.17841613 0.269025093,1.5310788 L3.73646206,4.9985158 L0.269025109,8.46595276 C-0.083637537,8.81861541 -0.0842215923, 9.38490462 0.264694642,9.73382106 C0.616043456,10.0851701 1.18125469, 10.0807988 1.53256259,9.72949093 L4.99999988,6.26205363 L8.46743739, 9.72949117 C8.8187453,10.0807991 9.38395655,10.0851704 9.73530537, 9.73382138 C10.0842216,9.38490498 10.0836375,8.81861579 9.73097488, 8.46595313 L6.26353762,4.99851587 Z";
|
|
34
|
-
|
|
35
|
-
type TooltipBubbleProps = {|
|
|
36
|
-
tooltipProps: NewTooltipProps,
|
|
37
|
-
dismiss: () => void,
|
|
38
|
-
|};
|
|
39
|
-
|
|
40
|
-
export default class TooltipBubble extends React.PureComponent<TooltipBubbleProps> {
|
|
41
|
-
_handleClickClose: () => void = () => this.props.dismiss();
|
|
42
|
-
|
|
43
|
-
_renderCloseButton(
|
|
44
|
-
ids: IIdentifierFactory,
|
|
45
|
-
): null | React.Element<"button"> {
|
|
46
|
-
const {inverted, dismissOnClickClose} = this.props.tooltipProps;
|
|
47
|
-
|
|
48
|
-
if (!dismissOnClickClose) {
|
|
49
|
-
// When `dismissOnClickClose` is not set, don't render a close
|
|
50
|
-
// button at all.
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const buttonDescriptionId = ids.get("button-description");
|
|
55
|
-
const headerId = this.getA11yHeaderId(ids);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<button
|
|
59
|
-
className={css(styles.closeIcon)}
|
|
60
|
-
onClick={this._handleClickClose}
|
|
61
|
-
aria-describedby={`${buttonDescriptionId} ${headerId}`}
|
|
62
|
-
>
|
|
63
|
-
<Icon
|
|
64
|
-
title={i18n._("Close")}
|
|
65
|
-
icon={closeIconPath}
|
|
66
|
-
size={10}
|
|
67
|
-
color={
|
|
68
|
-
// Use a light icon if we're inverted, or dark
|
|
69
|
-
// otherwise.
|
|
70
|
-
inverted
|
|
71
|
-
? "rgba(255, 255, 255, 0.7)"
|
|
72
|
-
: "rgba(0, 0, 0, 0.15)"
|
|
73
|
-
}
|
|
74
|
-
/>
|
|
75
|
-
<div
|
|
76
|
-
id={buttonDescriptionId}
|
|
77
|
-
className={css(a11yStyles.srOnly)}
|
|
78
|
-
>
|
|
79
|
-
{i18n._("Closes the tooltip: ")}
|
|
80
|
-
</div>
|
|
81
|
-
</button>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
getA11yHeaderId(ids: IIdentifierFactory): string {
|
|
86
|
-
const {
|
|
87
|
-
tooltipProps: {a11y},
|
|
88
|
-
} = this.props;
|
|
89
|
-
if (!a11y || !a11y.title) {
|
|
90
|
-
// No title specified so assume our content labels us.
|
|
91
|
-
return this.getA11yContentId(ids);
|
|
92
|
-
}
|
|
93
|
-
if (typeof a11y.title === "string") {
|
|
94
|
-
// We have a string title, so we'll generate our own label.
|
|
95
|
-
return ids.get("header");
|
|
96
|
-
}
|
|
97
|
-
// We've been given a specific identifier, so use that.
|
|
98
|
-
return a11y.title.id;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getA11yContentId(ids: IIdentifierFactory): string {
|
|
102
|
-
return ids.get("content");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
renderTooltip(ids: IIdentifierFactory): React.Element<"div"> {
|
|
106
|
-
const {
|
|
107
|
-
content,
|
|
108
|
-
dismissOnClickClose,
|
|
109
|
-
noPadding,
|
|
110
|
-
onClick,
|
|
111
|
-
onMouseEnter,
|
|
112
|
-
onMouseLeave,
|
|
113
|
-
a11y,
|
|
114
|
-
testId,
|
|
115
|
-
} = this.props.tooltipProps;
|
|
116
|
-
const a11yTitle = a11y && a11y.title;
|
|
117
|
-
const a11yAssertiveness = a11y && a11y.assertiveness;
|
|
118
|
-
const {backgroundColor, textColor} = getTooltipColors(
|
|
119
|
-
this.props.tooltipProps,
|
|
120
|
-
);
|
|
121
|
-
const headerId = this.getA11yHeaderId(ids);
|
|
122
|
-
const contentId = this.getA11yContentId(ids);
|
|
123
|
-
return (
|
|
124
|
-
<div
|
|
125
|
-
className={css(
|
|
126
|
-
styles.tooltipBubble,
|
|
127
|
-
!noPadding && styles.tooltipBubbleWithPadding,
|
|
128
|
-
dismissOnClickClose && styles.tooltipBubbleWithCloseIcon,
|
|
129
|
-
onClick && styles.tooltipBubbleWithOnClick,
|
|
130
|
-
)}
|
|
131
|
-
onClick={onClick}
|
|
132
|
-
onMouseEnter={onMouseEnter}
|
|
133
|
-
onMouseLeave={onMouseLeave}
|
|
134
|
-
style={{backgroundColor, color: textColor}}
|
|
135
|
-
role="region"
|
|
136
|
-
aria-labelledby={headerId}
|
|
137
|
-
aria-describedby={contentId === headerId ? null : contentId}
|
|
138
|
-
data-test-id={testId}
|
|
139
|
-
>
|
|
140
|
-
{this._renderCloseButton(ids)}
|
|
141
|
-
<div aria-live={a11yAssertiveness}>
|
|
142
|
-
{typeof a11yTitle === "string" ? (
|
|
143
|
-
<div className={css(a11yStyles.srOnly)} id={headerId}>
|
|
144
|
-
{a11yTitle}
|
|
145
|
-
</div>
|
|
146
|
-
) : null}
|
|
147
|
-
<div id={contentId} className={css(styles.tooltipContent)}>
|
|
148
|
-
{content}
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
render(): React.Node {
|
|
156
|
-
return (
|
|
157
|
-
<UniqueIDProvider mockOnFirstRender={false} scope="tooltip-bubble">
|
|
158
|
-
{(ids) => this.renderTooltip(ids)}
|
|
159
|
-
</UniqueIDProvider>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// These styles are copied from tooltip-package/tooltip.jsx - with some minor
|
|
165
|
-
// modifications, because positioning is handled differently.
|
|
166
|
-
//
|
|
167
|
-
// TODO(mdr): Also, the standard tooltip seemed to have `transition` styles
|
|
168
|
-
// designed to fade-in the tooltip... but they don't seem to work? I've
|
|
169
|
-
// removed them for now. We'd probably have to use an
|
|
170
|
-
// AphroditeCSSTransitionGroup, in order to initialize the opacity to 0,
|
|
171
|
-
// then wait a tick before setting the opacity to 1.
|
|
172
|
-
const styles = StyleSheet.create({
|
|
173
|
-
tooltipBubble: {
|
|
174
|
-
// layout styles
|
|
175
|
-
boxSizing: "border-box",
|
|
176
|
-
position: "relative", // for the close icon
|
|
177
|
-
|
|
178
|
-
// content styles
|
|
179
|
-
...typography.labelMedium,
|
|
180
|
-
|
|
181
|
-
// border and box shadow styles
|
|
182
|
-
borderRadius: bubbleBorderRadius,
|
|
183
|
-
border: "1px solid rgba(0, 0, 0, 0.1)",
|
|
184
|
-
boxShadow:
|
|
185
|
-
`${dropShadowXOffset}px ${dropShadowYOffset}px ` +
|
|
186
|
-
`${dropShadowRadius}px 0px ` +
|
|
187
|
-
`rgba(0, 0, 0, ${dropShadowOpacity})`,
|
|
188
|
-
|
|
189
|
-
// Ensure that clicking on the tooltip's empty space doesn't clear the
|
|
190
|
-
// user's text selection elsewhere in the page. (We reset this rule in
|
|
191
|
-
// `tooltipContent`, so that the user _can_ select text _inside_ the
|
|
192
|
-
// tooltip.)
|
|
193
|
-
userSelect: "none",
|
|
194
|
-
},
|
|
195
|
-
|
|
196
|
-
tooltipBubbleWithPadding: {
|
|
197
|
-
padding: "10px 23px",
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
tooltipBubbleWithCloseIcon: {
|
|
201
|
-
paddingRight: 34,
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
tooltipBubbleWithOnClick: {
|
|
205
|
-
cursor: "pointer",
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
tooltipContent: {
|
|
209
|
-
// Reset the `userSelect: none` rule in `tooltipBubble`, to enable the
|
|
210
|
-
// user to select the tooltip's content.
|
|
211
|
-
userSelect: "auto",
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
closeIcon: {
|
|
215
|
-
display: "block",
|
|
216
|
-
position: "absolute",
|
|
217
|
-
right: 10,
|
|
218
|
-
top: 10,
|
|
219
|
-
padding: 0,
|
|
220
|
-
background: "inherit",
|
|
221
|
-
border: "none",
|
|
222
|
-
},
|
|
223
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
/**
|
|
3
|
-
* This is TooltipPortal, part of NewTooltip.
|
|
4
|
-
*
|
|
5
|
-
* This component renders nothing inline - but it mounts a TooltipPositioner in
|
|
6
|
-
* `#outer-wrapper`, and unmounts it when this unmounts.
|
|
7
|
-
*
|
|
8
|
-
* This is a lot like a React 16 "portal", but we're in React 15, so we sorta
|
|
9
|
-
* have to reimplement that behavior ourselves ^_^`
|
|
10
|
-
* https://reactjs.org/docs/portals.html
|
|
11
|
-
*
|
|
12
|
-
* This component has a public method `remeasure`, to delegate `remeasure`
|
|
13
|
-
* calls down from NewTooltip to TooltipPositioner.
|
|
14
|
-
*
|
|
15
|
-
* TODO(mdr): Once we upgrade to React 16, replace some of this logic with a
|
|
16
|
-
* built-in React portal.
|
|
17
|
-
*/
|
|
18
|
-
import * as React from "react";
|
|
19
|
-
import ReactDOM from "react-dom";
|
|
20
|
-
|
|
21
|
-
import TooltipPositioner from "./tooltip-positioner.jsx";
|
|
22
|
-
|
|
23
|
-
import type {NewTooltipProps} from "./types.js";
|
|
24
|
-
|
|
25
|
-
type TooltipPortalProps = {|
|
|
26
|
-
tooltipProps: NewTooltipProps,
|
|
27
|
-
dismiss: () => void,
|
|
28
|
-
rootElement: HTMLElement,
|
|
29
|
-
targetElement: HTMLElement,
|
|
30
|
-
isAboveModal?: boolean,
|
|
31
|
-
|};
|
|
32
|
-
export default class TooltipPortal extends React.Component<TooltipPortalProps> {
|
|
33
|
-
_mountNode: ?HTMLElement;
|
|
34
|
-
_tooltipPositioner: ?TooltipPositioner;
|
|
35
|
-
|
|
36
|
-
// We mounted, so we should mount the TooltipPositioner! Create a node for
|
|
37
|
-
// it, then mount it there.
|
|
38
|
-
componentDidMount() {
|
|
39
|
-
const mountNode = document.createElement("div");
|
|
40
|
-
this._mountNode = mountNode;
|
|
41
|
-
this.props.rootElement.appendChild(mountNode);
|
|
42
|
-
|
|
43
|
-
this._renderInMountNode(mountNode);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// We updated, so we should update to the TooltipPositioner! Re-call
|
|
47
|
-
// ReactDOM.render, which will trigger an update rather than a mount,
|
|
48
|
-
// because the component is already mounted in that location.
|
|
49
|
-
componentDidUpdate() {
|
|
50
|
-
const mountNode = this._mountNode;
|
|
51
|
-
if (!mountNode) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
this._renderInMountNode(mountNode);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// We unmounted, so we should unmount the TooltipPositioner! Ask React to
|
|
59
|
-
// unmount it, then destroy the node we created.
|
|
60
|
-
componentWillUnmount() {
|
|
61
|
-
const mountNode = this._mountNode;
|
|
62
|
-
if (!mountNode) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
ReactDOM.unmountComponentAtNode(mountNode);
|
|
67
|
-
this.props.rootElement.removeChild(mountNode);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
remeasure() {
|
|
71
|
-
this._tooltipPositioner && this._tooltipPositioner.remeasure();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
_renderInMountNode(mountNode: HTMLElement) {
|
|
75
|
-
const {
|
|
76
|
-
tooltipProps,
|
|
77
|
-
dismiss,
|
|
78
|
-
rootElement,
|
|
79
|
-
targetElement,
|
|
80
|
-
isAboveModal,
|
|
81
|
-
} = this.props;
|
|
82
|
-
|
|
83
|
-
// TODO(LP-11406): Replace with React Portal
|
|
84
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
85
|
-
ReactDOM.render(
|
|
86
|
-
<TooltipPositioner
|
|
87
|
-
tooltipProps={tooltipProps}
|
|
88
|
-
dismiss={dismiss}
|
|
89
|
-
rootElement={rootElement}
|
|
90
|
-
targetElement={targetElement}
|
|
91
|
-
isAboveModal={isAboveModal}
|
|
92
|
-
ref={(node) => (this._tooltipPositioner = node)}
|
|
93
|
-
/>,
|
|
94
|
-
mountNode,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
render(): null {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}
|