Skip to content

Commit 7833821

Browse files
committed
fix(accessibility) make tooltip component keyboard accessible
1 parent f1a0012 commit 7833821

File tree

3 files changed

+68
-14
lines changed

3 files changed

+68
-14
lines changed

react/features/base/toolbox/components/ToolboxItem.web.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
114114
text = { this.label } />
115115
);
116116
}
117-
let children = (
117+
const children = (
118118
<Fragment>
119119
{ this._renderIcon() }
120120
{ showLabel && <span>
@@ -125,13 +125,15 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
125125
);
126126

127127
if (useTooltip) {
128-
children = (
128+
const tooltip = (
129129
<Tooltip
130130
content = { this.tooltip ?? '' }
131131
position = { tooltipPosition }>
132-
{ children }
132+
{ React.createElement(elementType, props, children) }
133133
</Tooltip>
134134
);
135+
136+
return tooltip;
135137
}
136138

137139
return React.createElement(elementType, props, children);

react/features/base/tooltip/components/Tooltip.tsx

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
1+
import React, { ReactElement, cloneElement, useCallback, useEffect, useRef, useState } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
33
import { keyframes } from 'tss-react';
44
import { makeStyles } from 'tss-react/mui';
@@ -58,12 +58,14 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
5858
const dispatch = useDispatch();
5959
const [ visible, setVisible ] = useState(false);
6060
const [ isUnmounting, setIsUnmounting ] = useState(false);
61+
const [ wasOpenedWithKeyboard, setWasOpenedWithKeyboard ] = useState(false);
6162
const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer);
6263
const { classes, cx } = useStyles();
6364
const timeoutID = useRef({
6465
open: 0,
6566
close: 0
6667
});
68+
const tooltipId = useRef(`tooltip-${Math.random().toString(36).substring(2, 11)}`);
6769
const {
6870
content: storeContent,
6971
previousContent,
@@ -73,7 +75,10 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
7375
const contentComponent = (
7476
<div
7577
className = { cx(classes.container, previousContent === '' && 'mounting-animation',
76-
isUnmounting && 'unmounting') }>
78+
isUnmounting && 'unmounting') }
79+
id = { tooltipId.current }
80+
role = 'tooltip'
81+
tabIndex = { wasOpenedWithKeyboard ? 0 : -1 }>
7782
{content}
7883
</div>
7984
);
@@ -89,31 +94,37 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
8994
setIsUnmounting(false);
9095
};
9196

92-
const onPopoverOpen = useCallback(() => {
97+
const onPopoverOpen = useCallback((keyboardTriggered = false) => {
9398
if (isUnmounting) {
9499
return;
95100
}
96101

102+
setWasOpenedWithKeyboard(keyboardTriggered);
97103
clearTimeout(timeoutID.current.close);
98104
timeoutID.current.close = 0;
99105
if (!visible) {
100106
if (isVisible) {
101107
openPopover();
102108
} else {
109+
const delay = keyboardTriggered ? 0 : TOOLTIP_DELAY;
110+
103111
timeoutID.current.open = window.setTimeout(() => {
104112
openPopover();
105-
}, TOOLTIP_DELAY);
113+
}, delay);
106114
}
107115
}
108116
}, [ visible, isVisible, isUnmounting ]);
109117

110-
const onPopoverClose = useCallback(() => {
118+
const onPopoverClose = useCallback((immediate = false) => {
111119
clearTimeout(timeoutID.current.open);
112120
if (visible) {
121+
const delay = immediate ? 0 : TOOLTIP_DELAY;
122+
113123
timeoutID.current.close = window.setTimeout(() => {
114124
setIsUnmounting(true);
115-
}, TOOLTIP_DELAY);
125+
}, delay);
116126
}
127+
setWasOpenedWithKeyboard(false);
117128
}, [ visible ]);
118129

119130
useEffect(() => {
@@ -132,13 +143,50 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
132143
clearTimeout(timeoutID.current.close);
133144
timeoutID.current.close = 0;
134145
}
135-
}, [ storeContent ]);
146+
}, [ storeContent, content ]);
147+
148+
const handleFocus = useCallback(() => {
149+
onPopoverOpen(true);
150+
}, [ onPopoverOpen ]);
136151

152+
const handleBlur = useCallback(() => {
153+
onPopoverClose(true);
154+
}, [ onPopoverClose ]);
155+
156+
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
157+
if (event.key === 'Escape' && visible) {
158+
event.preventDefault();
159+
onPopoverClose(true);
160+
}
161+
}, [ visible, onPopoverClose ]);
137162

138163
if (isMobileBrowser() || overflowDrawer) {
139164
return children;
140165
}
141166

167+
const enhancedChildren = cloneElement(children, {
168+
'aria-describedby': visible ? tooltipId.current : undefined,
169+
tabIndex: children.props.tabIndex !== undefined ? children.props.tabIndex : 0,
170+
onFocus: (event: React.FocusEvent) => {
171+
handleFocus();
172+
if (children.props.onFocus) {
173+
children.props.onFocus(event);
174+
}
175+
},
176+
onBlur: (event: React.FocusEvent) => {
177+
handleBlur();
178+
if (children.props.onBlur) {
179+
children.props.onBlur(event);
180+
}
181+
},
182+
onKeyDown: (event: React.KeyboardEvent) => {
183+
handleKeyDown(event);
184+
if (children.props.onKeyDown) {
185+
children.props.onKeyDown(event);
186+
}
187+
}
188+
});
189+
142190
return (
143191
<Popover
144192
allowClick = { true }
@@ -148,8 +196,9 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
148196
onPopoverClose = { onPopoverClose }
149197
onPopoverOpen = { onPopoverOpen }
150198
position = { position }
199+
role = 'tooltip'
151200
visible = { visible }>
152-
{children}
201+
{enhancedChildren}
153202
</Popover>
154203
);
155204
};

react/features/file-sharing/components/web/FileSharing.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IReduxState } from '../../../app/types';
77
import Avatar from '../../../base/avatar/components/Avatar';
88
import Icon from '../../../base/icons/components/Icon';
99
import { IconCloudUpload, IconDownload, IconTrash } from '../../../base/icons/svg';
10+
import Tooltip from '../../../base/tooltip/components/Tooltip';
1011
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
1112
import Button from '../../../base/ui/components/web/Button';
1213
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
@@ -325,9 +326,11 @@ const FileSharing = () => {
325326
src = { getFileIcon(file.fileType) } />
326327
</div>
327328
<div className = { classes.fileItemDetails }>
328-
<div className = { classes.fileName }>
329-
{ file.fileName }
330-
</div>
329+
<Tooltip content = { file.fileName }>
330+
<div className = { classes.fileName }>
331+
{ file.fileName }
332+
</div>
333+
</Tooltip>
331334
<div className = { classes.fileSize }>
332335
{ formatFileSize(file.fileSize) }
333336
</div>

0 commit comments

Comments
 (0)