@automattic/jetpack-components 3.1.1 → 3.2.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 +15 -0
- package/build/components/diff-viewer/parse-patch.js +3 -3
- package/build/components/icon-tooltip/index.js +128 -10
- package/build/components/icon-tooltip/style.scss +32 -0
- package/build/components/icon-tooltip/types.d.ts +15 -1
- package/build/components/jetpack-footer/index.js +3 -1
- package/components/diff-viewer/parse-patch.ts +3 -3
- package/components/icon-tooltip/index.tsx +188 -23
- package/components/icon-tooltip/style.scss +32 -0
- package/components/icon-tooltip/types.ts +18 -1
- package/components/jetpack-footer/README.md +2 -0
- package/components/jetpack-footer/index.tsx +5 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
### This is a list detailing changes for the Jetpack RNA Components package releases.
|
|
4
4
|
|
|
5
|
+
## [3.2.0] - 2026-09-23
|
|
6
|
+
### Added
|
|
7
|
+
- IconTooltip: Add a `trigger` prop that renders a text trigger in place of the icon, and a `closeOnClickOutside` prop to keep a tooltip open while the visitor works elsewhere on the page. [#52704]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Tooltips: Open info tooltips with Enter or Space, close them with Escape, and keep hover tooltips from taking focus. [#52661]
|
|
11
|
+
- Tooltips: Resume tab order from the tooltip's trigger when focus leaves it. [#52661]
|
|
12
|
+
|
|
13
|
+
## [3.1.2] - 2026-09-23
|
|
14
|
+
### Fixed
|
|
15
|
+
- DiffViewer: Fix some inefficient regular expressions. [#52582]
|
|
16
|
+
- Footer: Hide Products and Help links when My Jetpack is unavailable. [#52557]
|
|
17
|
+
|
|
5
18
|
## [3.1.1] - 2026-09-21
|
|
6
19
|
### Changed
|
|
7
20
|
- Update dependencies.
|
|
@@ -1950,6 +1963,8 @@
|
|
|
1950
1963
|
### Changed
|
|
1951
1964
|
- Update node version requirement to 14.16.1
|
|
1952
1965
|
|
|
1966
|
+
[3.2.0]: https://github.com/Automattic/jetpack-components/compare/3.1.2...3.2.0
|
|
1967
|
+
[3.1.2]: https://github.com/Automattic/jetpack-components/compare/3.1.1...3.1.2
|
|
1953
1968
|
[3.1.1]: https://github.com/Automattic/jetpack-components/compare/3.1.0...3.1.1
|
|
1954
1969
|
[3.1.0]: https://github.com/Automattic/jetpack-components/compare/3.0.3...3.1.0
|
|
1955
1970
|
[3.0.3]: https://github.com/Automattic/jetpack-components/compare/3.0.2...3.0.3
|
|
@@ -24,9 +24,9 @@ export default function parsePatch(uniDiff) {
|
|
|
24
24
|
break;
|
|
25
25
|
}
|
|
26
26
|
// Diff index
|
|
27
|
-
const header = /^(?:Index:|diff(?: -r \w+)+)\s
|
|
27
|
+
const header = /^(?:Index:|diff(?: -r \w+)+)\s+/.exec(line);
|
|
28
28
|
if (header) {
|
|
29
|
-
index.index = header[
|
|
29
|
+
index.index = line.substring(header[0].length).trim();
|
|
30
30
|
}
|
|
31
31
|
i++;
|
|
32
32
|
}
|
|
@@ -63,7 +63,7 @@ export default function parsePatch(uniDiff) {
|
|
|
63
63
|
* @param {object[]} index.hunks - hunks
|
|
64
64
|
*/
|
|
65
65
|
function parseFileHeader(index) {
|
|
66
|
-
const fileHeader = /^(---|\+\+\+)\s+(
|
|
66
|
+
const fileHeader = /^(---|\+\+\+)\s+(\S.*|)\r?$/.exec(diffstr[i]);
|
|
67
67
|
if (fileHeader) {
|
|
68
68
|
const keyPrefix = fileHeader[1] === '---' ? 'old' : 'new';
|
|
69
69
|
const data = fileHeader[2].split('\t', 2);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Popover } from '@wordpress/components';
|
|
3
|
+
import { focus } from '@wordpress/dom';
|
|
3
4
|
import { Icon, info } from '@wordpress/icons';
|
|
4
5
|
import clsx from 'clsx';
|
|
5
|
-
import { useCallback, useState } from 'react';
|
|
6
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
6
7
|
import Button from '../button/index.js';
|
|
7
8
|
import './style.scss';
|
|
8
9
|
const placementsToPositions = (placement) => {
|
|
@@ -22,15 +23,90 @@ const placementsToPositions = (placement) => {
|
|
|
22
23
|
* @param {IconTooltipProps} props - Props
|
|
23
24
|
* @return {ReactElement} - JSX element
|
|
24
25
|
*/
|
|
25
|
-
const IconTooltip = ({ className = '', popoverClassName, iconClassName = '', placement = 'bottom-end', animate = true, iconCode = info, iconSize = 18, offset = 10, title, children, popoverAnchorStyle = 'icon', forceShow = false, hoverShow = false, wide = false, inline = true, shift = false, }) => {
|
|
26
|
+
const IconTooltip = ({ className = '', popoverClassName, iconClassName = '', placement = 'bottom-end', animate = true, iconCode = info, iconSize = 18, offset = 10, title, children, popoverAnchorStyle = 'icon', trigger, onTriggerClick, closeOnClickOutside = true, forceShow = false, hoverShow = false, wide = false, inline = true, shift = false, }) => {
|
|
26
27
|
const POPOVER_HELPER_WIDTH = 124;
|
|
27
28
|
const [isVisible, setIsVisible] = useState(false);
|
|
28
29
|
const [hoverTimeout, setHoverTimeout] = useState(null);
|
|
30
|
+
const wrapperRef = useRef(null);
|
|
31
|
+
const popoverRef = useRef(null);
|
|
32
|
+
const triggerRef = useRef(null);
|
|
33
|
+
const hasTextTrigger = trigger !== undefined;
|
|
34
|
+
// Where focus should land after Tab leaves the tooltip. The effect below applies it, rather
|
|
35
|
+
// than the handler, because Popover puts focus back on the trigger as it unmounts.
|
|
36
|
+
const focusAfterClose = useRef(null);
|
|
37
|
+
// Opening on hover must not pull focus off whatever the visitor is using.
|
|
38
|
+
const openedByHover = useRef(false);
|
|
29
39
|
const hideTooltip = useCallback(() => setIsVisible(false), [setIsVisible]);
|
|
30
40
|
const toggleTooltip = useCallback(e => {
|
|
31
41
|
e.preventDefault();
|
|
42
|
+
onTriggerClick?.();
|
|
43
|
+
openedByHover.current = false;
|
|
32
44
|
setIsVisible(!isVisible);
|
|
33
|
-
}, [isVisible, setIsVisible]);
|
|
45
|
+
}, [isVisible, setIsVisible, onTriggerClick]);
|
|
46
|
+
// Focus can stay on a text trigger while its tooltip is open, so it handles the dialog keys too.
|
|
47
|
+
const handleTriggerKeyDown = useCallback((event) => {
|
|
48
|
+
// A link only activates on Enter; a button also activates on Space.
|
|
49
|
+
if (event.key === ' ') {
|
|
50
|
+
if (!event.repeat) {
|
|
51
|
+
toggleTooltip(event);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isVisible) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (event.key === 'Escape') {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
hideTooltip();
|
|
61
|
+
}
|
|
62
|
+
else if (event.key === 'Tab') {
|
|
63
|
+
// Step into the popover even when it is portaled away from the trigger.
|
|
64
|
+
const first = event.shiftKey ? null : focus.tabbable.find(popoverRef.current)[0];
|
|
65
|
+
if (first) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
first.focus();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
hideTooltip();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}, [isVisible, hideTooltip, toggleTooltip]);
|
|
74
|
+
const isAnchorWrapper = popoverAnchorStyle === 'wrapper';
|
|
75
|
+
const isForcedToShow = isAnchorWrapper && forceShow;
|
|
76
|
+
const handlePopoverKeyDown = useCallback((event) => {
|
|
77
|
+
const wrapper = wrapperRef.current;
|
|
78
|
+
const popover = popoverRef.current;
|
|
79
|
+
if (event.key !== 'Tab' || !wrapper || !popover) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const tabbables = focus.tabbable.find(popover);
|
|
83
|
+
const boundary = event.shiftKey ? tabbables[0] : tabbables[tabbables.length - 1];
|
|
84
|
+
const leaving = !tabbables.length ||
|
|
85
|
+
event.target === boundary ||
|
|
86
|
+
(event.shiftKey && event.target === popover);
|
|
87
|
+
if (!leaving) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event.shiftKey && triggerRef.current) {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
triggerRef.current.focus();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// A popover rendered in a portal sits at the end of the document, so Tab out of it has
|
|
96
|
+
// to resume from the trigger's place in the page instead of the popover's.
|
|
97
|
+
const step = event.shiftKey ? focus.tabbable.findPrevious : focus.tabbable.findNext;
|
|
98
|
+
let destination = step(wrapper);
|
|
99
|
+
while (destination &&
|
|
100
|
+
(wrapper.contains(destination) || popover.contains(destination))) {
|
|
101
|
+
destination = step(destination);
|
|
102
|
+
}
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
focusAfterClose.current = destination ?? null;
|
|
105
|
+
hideTooltip();
|
|
106
|
+
}, [hideTooltip]);
|
|
107
|
+
const focusOnOpen = isForcedToShow || (hasTextTrigger && !openedByHover.current)
|
|
108
|
+
? 'firstElement'
|
|
109
|
+
: !openedByHover.current;
|
|
34
110
|
const args = {
|
|
35
111
|
// To be compatible with deprecating prop `position`.
|
|
36
112
|
position: placementsToPositions(placement),
|
|
@@ -40,24 +116,65 @@ const IconTooltip = ({ className = '', popoverClassName, iconClassName = '', pla
|
|
|
40
116
|
resize: false,
|
|
41
117
|
flip: false,
|
|
42
118
|
offset, // The distance (in px) between the anchor and the popover.
|
|
43
|
-
|
|
44
|
-
|
|
119
|
+
// Focusing the popover itself puts Escape in reach even with nothing tabbable inside. A text
|
|
120
|
+
// trigger or caller-controlled popover moves focus to its first link, if it has one.
|
|
121
|
+
focusOnMount: focusOnOpen,
|
|
122
|
+
// Tab moves through the popover in document order rather than cycling inside it, and
|
|
123
|
+
// handlePopoverKeyDown decides where it lands on the way out.
|
|
124
|
+
constrainTabbing: false,
|
|
125
|
+
onKeyDownCapture: handlePopoverKeyDown,
|
|
126
|
+
ref: popoverRef,
|
|
127
|
+
onClose: () => {
|
|
128
|
+
const popover = popoverRef.current;
|
|
129
|
+
if (hasTextTrigger && popover?.contains(popover.ownerDocument.activeElement)) {
|
|
130
|
+
focusAfterClose.current = triggerRef.current;
|
|
131
|
+
}
|
|
132
|
+
hideTooltip();
|
|
133
|
+
},
|
|
134
|
+
onFocusOutside: (event) => {
|
|
135
|
+
// A pointer press on our own trigger dismisses through that trigger instead.
|
|
136
|
+
if (closeOnClickOutside && !wrapperRef.current?.contains(event.relatedTarget)) {
|
|
137
|
+
hideTooltip();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
45
140
|
className: clsx('icon-tooltip-container', popoverClassName),
|
|
46
141
|
inline,
|
|
47
142
|
shift,
|
|
48
143
|
};
|
|
49
|
-
const
|
|
50
|
-
const wrapperClassNames = clsx('icon-tooltip-wrapper', className);
|
|
144
|
+
const wrapperClassNames = clsx('icon-tooltip-wrapper', { 'has-text-trigger': hasTextTrigger }, className);
|
|
51
145
|
const iconShiftBySize = {
|
|
52
146
|
left: isAnchorWrapper ? 0 : -(POPOVER_HELPER_WIDTH / 2 - iconSize / 2) + 'px',
|
|
53
147
|
};
|
|
54
|
-
|
|
148
|
+
// Focus may never enter the popover, so outside presses are watched directly.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!isVisible || isForcedToShow || !closeOnClickOutside) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const doc = wrapperRef.current?.ownerDocument;
|
|
154
|
+
const handlePointerDown = (event) => {
|
|
155
|
+
const target = event.target;
|
|
156
|
+
if (!wrapperRef.current?.contains(target) && !popoverRef.current?.contains(target)) {
|
|
157
|
+
hideTooltip();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
doc?.addEventListener('pointerdown', handlePointerDown);
|
|
161
|
+
return () => doc?.removeEventListener('pointerdown', handlePointerDown);
|
|
162
|
+
}, [isVisible, isForcedToShow, closeOnClickOutside, hideTooltip]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (isForcedToShow || isVisible) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const destination = focusAfterClose.current;
|
|
168
|
+
focusAfterClose.current = null;
|
|
169
|
+
destination?.focus();
|
|
170
|
+
}, [isForcedToShow, isVisible]);
|
|
55
171
|
const handleMouseEnter = useCallback(() => {
|
|
56
172
|
if (hoverShow) {
|
|
57
173
|
if (hoverTimeout) {
|
|
58
174
|
clearTimeout(hoverTimeout);
|
|
59
175
|
setHoverTimeout(null);
|
|
60
176
|
}
|
|
177
|
+
openedByHover.current = true;
|
|
61
178
|
setIsVisible(true);
|
|
62
179
|
}
|
|
63
180
|
}, [hoverShow, hoverTimeout]);
|
|
@@ -70,6 +187,7 @@ const IconTooltip = ({ className = '', popoverClassName, iconClassName = '', pla
|
|
|
70
187
|
setHoverTimeout(id);
|
|
71
188
|
}
|
|
72
189
|
}, [hoverShow]);
|
|
73
|
-
|
|
190
|
+
const helper = (_jsx("div", { className: clsx('icon-tooltip-helper', { 'is-wide': wide }), style: iconShiftBySize, children: (isForcedToShow || isVisible) && (_jsx(Popover, { ...args, children: _jsxs("div", { children: [title && _jsx("div", { className: "icon-tooltip-title", children: title }), _jsx("div", { className: "icon-tooltip-content", children: children })] }) })) }));
|
|
191
|
+
return (_jsxs("div", { ref: wrapperRef, className: wrapperClassNames, "data-testid": "icon-tooltip_wrapper", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [hasTextTrigger && (_jsxs(_Fragment, { children: [_jsx("a", { ref: triggerRef, href: "#", role: "button", className: "icon-tooltip-trigger", "aria-expanded": isVisible, onClick: toggleTooltip, onKeyDown: handleTriggerKeyDown, children: trigger }), _jsx("span", { className: "icon-tooltip-anchor", children: _jsx("span", { children: helper }) })] })), !hasTextTrigger && !isAnchorWrapper && (_jsx(Button, { variant: "link", "aria-expanded": isVisible, onClick: toggleTooltip, children: _jsx(Icon, { className: iconClassName, icon: iconCode, size: iconSize }) })), !hasTextTrigger && helper] }));
|
|
74
192
|
};
|
|
75
193
|
export default IconTooltip;
|
|
@@ -79,9 +79,41 @@ $arrow-color: var(--jp-gray);
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// A text trigger flows with the sentence around it.
|
|
83
|
+
.icon-tooltip-wrapper.has-text-trigger {
|
|
84
|
+
display: inline;
|
|
85
|
+
color: inherit;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The popover anchors at the trigger's baseline and paints under it.
|
|
89
|
+
.icon-tooltip-trigger {
|
|
90
|
+
position: relative;
|
|
91
|
+
z-index: 10;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.icon-tooltip-anchor {
|
|
95
|
+
position: absolute;
|
|
96
|
+
inset-block-start: 0;
|
|
97
|
+
inset-inline-start: 0;
|
|
98
|
+
z-index: 9;
|
|
99
|
+
|
|
100
|
+
> span {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
position: relative;
|
|
103
|
+
color: var(--jp-black);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
82
107
|
.icon-tooltip-container {
|
|
83
108
|
// Recover events
|
|
84
109
|
pointer-events: all;
|
|
110
|
+
// Takes focus on open, but its box is wider than the visible content,
|
|
111
|
+
// so the ring goes on the content instead.
|
|
112
|
+
outline: none;
|
|
113
|
+
|
|
114
|
+
&:focus-visible .components-popover__content {
|
|
115
|
+
outline: var(--wp-admin-border-width-focus, 2px) solid var(--wp-admin-theme-color, #3858e9);
|
|
116
|
+
}
|
|
85
117
|
|
|
86
118
|
// __content styles here covers both the inline and portal modes
|
|
87
119
|
.components-popover__content {
|
|
@@ -52,7 +52,21 @@ export type IconTooltipProps = {
|
|
|
52
52
|
*/
|
|
53
53
|
popoverAnchorStyle?: 'icon' | 'wrapper';
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* Text to render as the trigger in place of the icon. The component owns the open state.
|
|
56
|
+
*/
|
|
57
|
+
trigger?: ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* Called on every press of the trigger, e.g. to record analytics.
|
|
60
|
+
*/
|
|
61
|
+
onTriggerClick?: () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Whether a press outside the trigger and popover closes the tooltip. Turn it off for a
|
|
64
|
+
* reference the visitor keeps open while working elsewhere on the page.
|
|
65
|
+
*/
|
|
66
|
+
closeOnClickOutside?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Force the Popover to show without an event trigger. Only for a wrapper-anchored
|
|
69
|
+
* tooltip with no `trigger`, such as a programmatic tour step.
|
|
56
70
|
*/
|
|
57
71
|
forceShow?: boolean;
|
|
58
72
|
/**
|
|
@@ -15,7 +15,9 @@ import JetpackLogo from '../jetpack-logo/index.js';
|
|
|
15
15
|
*/
|
|
16
16
|
const JetpackFooter = ({ className, menu, ...otherProps }) => {
|
|
17
17
|
let items = [];
|
|
18
|
-
if (
|
|
18
|
+
if (getScriptData()?.myJetpack?.isAvailable === true &&
|
|
19
|
+
!isWpcomPlatformSite() &&
|
|
20
|
+
!window?.JetpackNetworkAdminData) {
|
|
19
21
|
// Published by My Jetpack, whose products tab can be renamed Features.
|
|
20
22
|
const productsSection = getScriptData()?.myJetpack?.productsSection;
|
|
21
23
|
items = [
|
|
@@ -46,10 +46,10 @@ export default function parsePatch( uniDiff: string ) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Diff index
|
|
49
|
-
const header = /^(?:Index:|diff(?: -r \w+)+)\s
|
|
49
|
+
const header = /^(?:Index:|diff(?: -r \w+)+)\s+/.exec( line );
|
|
50
50
|
|
|
51
51
|
if ( header ) {
|
|
52
|
-
index.index = header[
|
|
52
|
+
index.index = line.substring( header[ 0 ].length ).trim();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
i++;
|
|
@@ -93,7 +93,7 @@ export default function parsePatch( uniDiff: string ) {
|
|
|
93
93
|
* @param {object[]} index.hunks - hunks
|
|
94
94
|
*/
|
|
95
95
|
function parseFileHeader( index: Index ) {
|
|
96
|
-
const fileHeader = /^(---|\+\+\+)\s+(
|
|
96
|
+
const fileHeader = /^(---|\+\+\+)\s+(\S.*|)\r?$/.exec( diffstr[ i ] );
|
|
97
97
|
|
|
98
98
|
if ( fileHeader ) {
|
|
99
99
|
const keyPrefix = fileHeader[ 1 ] === '---' ? 'old' : 'new';
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { Popover } from '@wordpress/components';
|
|
2
|
+
import { focus } from '@wordpress/dom';
|
|
2
3
|
import { Icon, info } from '@wordpress/icons';
|
|
3
4
|
import clsx from 'clsx';
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
ReactElement,
|
|
11
|
+
FC,
|
|
12
|
+
FocusEvent,
|
|
13
|
+
KeyboardEvent,
|
|
14
|
+
} from 'react';
|
|
5
15
|
import Button from '../button/index.tsx';
|
|
6
16
|
import { IconTooltipProps, Placement, Position } from './types.ts';
|
|
7
17
|
|
|
@@ -38,6 +48,9 @@ const IconTooltip: FC< IconTooltipProps > = ( {
|
|
|
38
48
|
title,
|
|
39
49
|
children,
|
|
40
50
|
popoverAnchorStyle = 'icon',
|
|
51
|
+
trigger,
|
|
52
|
+
onTriggerClick,
|
|
53
|
+
closeOnClickOutside = true,
|
|
41
54
|
forceShow = false,
|
|
42
55
|
hoverShow = false,
|
|
43
56
|
wide = false,
|
|
@@ -47,15 +60,101 @@ const IconTooltip: FC< IconTooltipProps > = ( {
|
|
|
47
60
|
const POPOVER_HELPER_WIDTH = 124;
|
|
48
61
|
const [ isVisible, setIsVisible ] = useState( false );
|
|
49
62
|
const [ hoverTimeout, setHoverTimeout ] = useState( null );
|
|
63
|
+
const wrapperRef = useRef< HTMLDivElement >( null );
|
|
64
|
+
const popoverRef = useRef< HTMLDivElement >( null );
|
|
65
|
+
const triggerRef = useRef< HTMLAnchorElement >( null );
|
|
66
|
+
const hasTextTrigger = trigger !== undefined;
|
|
67
|
+
// Where focus should land after Tab leaves the tooltip. The effect below applies it, rather
|
|
68
|
+
// than the handler, because Popover puts focus back on the trigger as it unmounts.
|
|
69
|
+
const focusAfterClose = useRef< HTMLElement | null >( null );
|
|
70
|
+
// Opening on hover must not pull focus off whatever the visitor is using.
|
|
71
|
+
const openedByHover = useRef( false );
|
|
50
72
|
const hideTooltip = useCallback( () => setIsVisible( false ), [ setIsVisible ] );
|
|
51
73
|
const toggleTooltip = useCallback(
|
|
52
74
|
e => {
|
|
53
75
|
e.preventDefault();
|
|
76
|
+
onTriggerClick?.();
|
|
77
|
+
openedByHover.current = false;
|
|
54
78
|
setIsVisible( ! isVisible );
|
|
55
79
|
},
|
|
56
|
-
[ isVisible, setIsVisible ]
|
|
80
|
+
[ isVisible, setIsVisible, onTriggerClick ]
|
|
57
81
|
);
|
|
58
82
|
|
|
83
|
+
// Focus can stay on a text trigger while its tooltip is open, so it handles the dialog keys too.
|
|
84
|
+
const handleTriggerKeyDown = useCallback(
|
|
85
|
+
( event: KeyboardEvent< HTMLAnchorElement > ) => {
|
|
86
|
+
// A link only activates on Enter; a button also activates on Space.
|
|
87
|
+
if ( event.key === ' ' ) {
|
|
88
|
+
if ( ! event.repeat ) {
|
|
89
|
+
toggleTooltip( event );
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if ( ! isVisible ) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if ( event.key === 'Escape' ) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
hideTooltip();
|
|
99
|
+
} else if ( event.key === 'Tab' ) {
|
|
100
|
+
// Step into the popover even when it is portaled away from the trigger.
|
|
101
|
+
const first = event.shiftKey ? null : focus.tabbable.find( popoverRef.current )[ 0 ];
|
|
102
|
+
if ( first ) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
first.focus();
|
|
105
|
+
} else {
|
|
106
|
+
hideTooltip();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[ isVisible, hideTooltip, toggleTooltip ]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const isAnchorWrapper = popoverAnchorStyle === 'wrapper';
|
|
114
|
+
const isForcedToShow = isAnchorWrapper && forceShow;
|
|
115
|
+
|
|
116
|
+
const handlePopoverKeyDown = useCallback(
|
|
117
|
+
( event: KeyboardEvent< HTMLDivElement > ) => {
|
|
118
|
+
const wrapper = wrapperRef.current;
|
|
119
|
+
const popover = popoverRef.current;
|
|
120
|
+
if ( event.key !== 'Tab' || ! wrapper || ! popover ) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const tabbables = focus.tabbable.find( popover );
|
|
124
|
+
const boundary = event.shiftKey ? tabbables[ 0 ] : tabbables[ tabbables.length - 1 ];
|
|
125
|
+
const leaving =
|
|
126
|
+
! tabbables.length ||
|
|
127
|
+
event.target === boundary ||
|
|
128
|
+
( event.shiftKey && event.target === popover );
|
|
129
|
+
if ( ! leaving ) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if ( event.shiftKey && triggerRef.current ) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
triggerRef.current.focus();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// A popover rendered in a portal sits at the end of the document, so Tab out of it has
|
|
138
|
+
// to resume from the trigger's place in the page instead of the popover's.
|
|
139
|
+
const step = event.shiftKey ? focus.tabbable.findPrevious : focus.tabbable.findNext;
|
|
140
|
+
let destination = step( wrapper );
|
|
141
|
+
while (
|
|
142
|
+
destination &&
|
|
143
|
+
( wrapper.contains( destination ) || popover.contains( destination ) )
|
|
144
|
+
) {
|
|
145
|
+
destination = step( destination );
|
|
146
|
+
}
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
focusAfterClose.current = destination ?? null;
|
|
149
|
+
hideTooltip();
|
|
150
|
+
},
|
|
151
|
+
[ hideTooltip ]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const focusOnOpen =
|
|
155
|
+
isForcedToShow || ( hasTextTrigger && ! openedByHover.current )
|
|
156
|
+
? 'firstElement'
|
|
157
|
+
: ! openedByHover.current;
|
|
59
158
|
const args = {
|
|
60
159
|
// To be compatible with deprecating prop `position`.
|
|
61
160
|
position: placementsToPositions( placement ),
|
|
@@ -65,21 +164,65 @@ const IconTooltip: FC< IconTooltipProps > = ( {
|
|
|
65
164
|
resize: false,
|
|
66
165
|
flip: false,
|
|
67
166
|
offset, // The distance (in px) between the anchor and the popover.
|
|
68
|
-
|
|
69
|
-
|
|
167
|
+
// Focusing the popover itself puts Escape in reach even with nothing tabbable inside. A text
|
|
168
|
+
// trigger or caller-controlled popover moves focus to its first link, if it has one.
|
|
169
|
+
focusOnMount: focusOnOpen,
|
|
170
|
+
// Tab moves through the popover in document order rather than cycling inside it, and
|
|
171
|
+
// handlePopoverKeyDown decides where it lands on the way out.
|
|
172
|
+
constrainTabbing: false,
|
|
173
|
+
onKeyDownCapture: handlePopoverKeyDown,
|
|
174
|
+
ref: popoverRef,
|
|
175
|
+
onClose: () => {
|
|
176
|
+
const popover = popoverRef.current;
|
|
177
|
+
if ( hasTextTrigger && popover?.contains( popover.ownerDocument.activeElement ) ) {
|
|
178
|
+
focusAfterClose.current = triggerRef.current;
|
|
179
|
+
}
|
|
180
|
+
hideTooltip();
|
|
181
|
+
},
|
|
182
|
+
onFocusOutside: ( event: FocusEvent ) => {
|
|
183
|
+
// A pointer press on our own trigger dismisses through that trigger instead.
|
|
184
|
+
if ( closeOnClickOutside && ! wrapperRef.current?.contains( event.relatedTarget as Node ) ) {
|
|
185
|
+
hideTooltip();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
70
188
|
className: clsx( 'icon-tooltip-container', popoverClassName ),
|
|
71
189
|
inline,
|
|
72
190
|
shift,
|
|
73
191
|
} satisfies Omit< React.ComponentProps< typeof Popover >, 'children' >;
|
|
74
192
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
193
|
+
const wrapperClassNames = clsx(
|
|
194
|
+
'icon-tooltip-wrapper',
|
|
195
|
+
{ 'has-text-trigger': hasTextTrigger },
|
|
196
|
+
className
|
|
197
|
+
);
|
|
78
198
|
const iconShiftBySize = {
|
|
79
199
|
left: isAnchorWrapper ? 0 : -( POPOVER_HELPER_WIDTH / 2 - iconSize / 2 ) + 'px',
|
|
80
200
|
};
|
|
81
201
|
|
|
82
|
-
|
|
202
|
+
// Focus may never enter the popover, so outside presses are watched directly.
|
|
203
|
+
useEffect( () => {
|
|
204
|
+
if ( ! isVisible || isForcedToShow || ! closeOnClickOutside ) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const doc = wrapperRef.current?.ownerDocument;
|
|
208
|
+
const handlePointerDown = ( event: PointerEvent ) => {
|
|
209
|
+
const target = event.target as Node;
|
|
210
|
+
if ( ! wrapperRef.current?.contains( target ) && ! popoverRef.current?.contains( target ) ) {
|
|
211
|
+
hideTooltip();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
doc?.addEventListener( 'pointerdown', handlePointerDown );
|
|
215
|
+
return () => doc?.removeEventListener( 'pointerdown', handlePointerDown );
|
|
216
|
+
}, [ isVisible, isForcedToShow, closeOnClickOutside, hideTooltip ] );
|
|
217
|
+
|
|
218
|
+
useEffect( () => {
|
|
219
|
+
if ( isForcedToShow || isVisible ) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const destination = focusAfterClose.current;
|
|
223
|
+
focusAfterClose.current = null;
|
|
224
|
+
destination?.focus();
|
|
225
|
+
}, [ isForcedToShow, isVisible ] );
|
|
83
226
|
|
|
84
227
|
const handleMouseEnter = useCallback( () => {
|
|
85
228
|
if ( hoverShow ) {
|
|
@@ -87,6 +230,7 @@ const IconTooltip: FC< IconTooltipProps > = ( {
|
|
|
87
230
|
clearTimeout( hoverTimeout );
|
|
88
231
|
setHoverTimeout( null );
|
|
89
232
|
}
|
|
233
|
+
openedByHover.current = true;
|
|
90
234
|
setIsVisible( true );
|
|
91
235
|
}
|
|
92
236
|
}, [ hoverShow, hoverTimeout ] );
|
|
@@ -101,31 +245,52 @@ const IconTooltip: FC< IconTooltipProps > = ( {
|
|
|
101
245
|
}
|
|
102
246
|
}, [ hoverShow ] );
|
|
103
247
|
|
|
248
|
+
const helper = (
|
|
249
|
+
<div className={ clsx( 'icon-tooltip-helper', { 'is-wide': wide } ) } style={ iconShiftBySize }>
|
|
250
|
+
{ ( isForcedToShow || isVisible ) && (
|
|
251
|
+
<Popover { ...args }>
|
|
252
|
+
<div>
|
|
253
|
+
{ title && <div className="icon-tooltip-title">{ title }</div> }
|
|
254
|
+
<div className="icon-tooltip-content">{ children }</div>
|
|
255
|
+
</div>
|
|
256
|
+
</Popover>
|
|
257
|
+
) }
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
|
|
104
261
|
return (
|
|
105
262
|
<div
|
|
263
|
+
ref={ wrapperRef }
|
|
106
264
|
className={ wrapperClassNames }
|
|
107
265
|
data-testid="icon-tooltip_wrapper"
|
|
108
266
|
onMouseEnter={ handleMouseEnter }
|
|
109
267
|
onMouseLeave={ handleMouseLeave }
|
|
110
268
|
>
|
|
111
|
-
{
|
|
112
|
-
|
|
269
|
+
{ hasTextTrigger && (
|
|
270
|
+
<>
|
|
271
|
+
{ /* A link, not a button, so it keeps the host page's link styling. */ }
|
|
272
|
+
<a
|
|
273
|
+
ref={ triggerRef }
|
|
274
|
+
href="#"
|
|
275
|
+
role="button"
|
|
276
|
+
className="icon-tooltip-trigger"
|
|
277
|
+
aria-expanded={ isVisible }
|
|
278
|
+
onClick={ toggleTooltip }
|
|
279
|
+
onKeyDown={ handleTriggerKeyDown }
|
|
280
|
+
>
|
|
281
|
+
{ trigger }
|
|
282
|
+
</a>
|
|
283
|
+
<span className="icon-tooltip-anchor">
|
|
284
|
+
<span>{ helper }</span>
|
|
285
|
+
</span>
|
|
286
|
+
</>
|
|
287
|
+
) }
|
|
288
|
+
{ ! hasTextTrigger && ! isAnchorWrapper && (
|
|
289
|
+
<Button variant="link" aria-expanded={ isVisible } onClick={ toggleTooltip }>
|
|
113
290
|
<Icon className={ iconClassName } icon={ iconCode } size={ iconSize } />
|
|
114
291
|
</Button>
|
|
115
292
|
) }
|
|
116
|
-
|
|
117
|
-
className={ clsx( 'icon-tooltip-helper', { 'is-wide': wide } ) }
|
|
118
|
-
style={ iconShiftBySize }
|
|
119
|
-
>
|
|
120
|
-
{ ( isForcedToShow || isVisible ) && (
|
|
121
|
-
<Popover { ...args }>
|
|
122
|
-
<div>
|
|
123
|
-
{ title && <div className="icon-tooltip-title">{ title }</div> }
|
|
124
|
-
<div className="icon-tooltip-content">{ children }</div>
|
|
125
|
-
</div>
|
|
126
|
-
</Popover>
|
|
127
|
-
) }
|
|
128
|
-
</div>
|
|
293
|
+
{ ! hasTextTrigger && helper }
|
|
129
294
|
</div>
|
|
130
295
|
);
|
|
131
296
|
};
|
|
@@ -79,9 +79,41 @@ $arrow-color: var(--jp-gray);
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// A text trigger flows with the sentence around it.
|
|
83
|
+
.icon-tooltip-wrapper.has-text-trigger {
|
|
84
|
+
display: inline;
|
|
85
|
+
color: inherit;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The popover anchors at the trigger's baseline and paints under it.
|
|
89
|
+
.icon-tooltip-trigger {
|
|
90
|
+
position: relative;
|
|
91
|
+
z-index: 10;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.icon-tooltip-anchor {
|
|
95
|
+
position: absolute;
|
|
96
|
+
inset-block-start: 0;
|
|
97
|
+
inset-inline-start: 0;
|
|
98
|
+
z-index: 9;
|
|
99
|
+
|
|
100
|
+
> span {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
position: relative;
|
|
103
|
+
color: var(--jp-black);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
82
107
|
.icon-tooltip-container {
|
|
83
108
|
// Recover events
|
|
84
109
|
pointer-events: all;
|
|
110
|
+
// Takes focus on open, but its box is wider than the visible content,
|
|
111
|
+
// so the ring goes on the content instead.
|
|
112
|
+
outline: none;
|
|
113
|
+
|
|
114
|
+
&:focus-visible .components-popover__content {
|
|
115
|
+
outline: var(--wp-admin-border-width-focus, 2px) solid var(--wp-admin-theme-color, #3858e9);
|
|
116
|
+
}
|
|
85
117
|
|
|
86
118
|
// __content styles here covers both the inline and portal modes
|
|
87
119
|
.components-popover__content {
|
|
@@ -67,7 +67,24 @@ export type IconTooltipProps = {
|
|
|
67
67
|
popoverAnchorStyle?: 'icon' | 'wrapper';
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
70
|
+
* Text to render as the trigger in place of the icon. The component owns the open state.
|
|
71
|
+
*/
|
|
72
|
+
trigger?: ReactNode;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Called on every press of the trigger, e.g. to record analytics.
|
|
76
|
+
*/
|
|
77
|
+
onTriggerClick?: () => void;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether a press outside the trigger and popover closes the tooltip. Turn it off for a
|
|
81
|
+
* reference the visitor keeps open while working elsewhere on the page.
|
|
82
|
+
*/
|
|
83
|
+
closeOnClickOutside?: boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Force the Popover to show without an event trigger. Only for a wrapper-anchored
|
|
87
|
+
* tooltip with no `trigger`, such as a programmatic tour step.
|
|
71
88
|
*/
|
|
72
89
|
forceShow?: boolean;
|
|
73
90
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Component that renders Jetpack Admin Footer.
|
|
4
4
|
|
|
5
|
+
The default Products (or Features) and Help links appear only when My Jetpack reports that its admin page is available to the current user. They are also hidden on WordPress.com platform sites and in network admin. Additional links supplied through `menu` are still rendered when the default links are hidden.
|
|
6
|
+
|
|
5
7
|
## How to use
|
|
6
8
|
|
|
7
9
|
Note that most of the time you would just use `admin-page` component, which includes the footer. If you must use footer independently, basic usage is:
|
|
@@ -27,7 +27,11 @@ declare global {
|
|
|
27
27
|
const JetpackFooter: FC< JetpackFooterProps > = ( { className, menu, ...otherProps } ) => {
|
|
28
28
|
let items: JetpackFooterMenuItem[] = [];
|
|
29
29
|
|
|
30
|
-
if (
|
|
30
|
+
if (
|
|
31
|
+
getScriptData()?.myJetpack?.isAvailable === true &&
|
|
32
|
+
! isWpcomPlatformSite() &&
|
|
33
|
+
! window?.JetpackNetworkAdminData
|
|
34
|
+
) {
|
|
31
35
|
// Published by My Jetpack, whose products tab can be renamed Features.
|
|
32
36
|
const productsSection = getScriptData()?.myJetpack?.productsSection;
|
|
33
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/jetpack-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Jetpack Components Package",
|
|
5
5
|
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/components/#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -72,10 +72,9 @@
|
|
|
72
72
|
"extends @wordpress/browserslist-config"
|
|
73
73
|
],
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@automattic/
|
|
76
|
-
"@automattic/jetpack-api": "^1.0
|
|
77
|
-
"@automattic/jetpack-
|
|
78
|
-
"@automattic/jetpack-script-data": "^0.6.7",
|
|
75
|
+
"@automattic/jetpack-api": "^2.0.0",
|
|
76
|
+
"@automattic/jetpack-boost-score-api": "^1.1.0",
|
|
77
|
+
"@automattic/jetpack-script-data": "^0.6.8",
|
|
79
78
|
"@automattic/number-formatters": "^1.2.9",
|
|
80
79
|
"@babel/runtime": "^7",
|
|
81
80
|
"@gravatar-com/hovercards": "0.16.0",
|
|
@@ -85,6 +84,7 @@
|
|
|
85
84
|
"@wordpress/compose": "8.8.0",
|
|
86
85
|
"@wordpress/data": "10.55.0",
|
|
87
86
|
"@wordpress/date": "5.55.0",
|
|
87
|
+
"@wordpress/dom": "4.55.0",
|
|
88
88
|
"@wordpress/element": "8.7.0",
|
|
89
89
|
"@wordpress/i18n": "6.28.0",
|
|
90
90
|
"@wordpress/icons": "16.0.0",
|