Skip to content

Commit

Permalink
fix(Tooltip): prevent focus hijacking by other tooltips with focus tr…
Browse files Browse the repository at this point in the history
…ap (#8713)

* fix(Tooltip): prevent focus hijacking with focusable children

* fix(Tooltip): restore focus to trigger element

only occurs if focus is not applied to another element after blur

* docs(Tooltip): add temporary test story

Co-authored-by: TJ Egan <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored May 31, 2021
1 parent 9541752 commit 3b4b69a
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 15 deletions.
116 changes: 116 additions & 0 deletions packages/react/src/components/Tooltip/Tooltip-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import Tooltip from '../Tooltip';
import { Tooltip as OGTooltip } from './Tooltip';
import Button from '../Button';
import TextInput from '../TextInput';
import { OverflowMenuVertical16 } from '@carbon/icons-react';
import mdx from './Tooltip.mdx';

Expand Down Expand Up @@ -271,3 +272,118 @@ OnlyCustomIcon.parameters = {
export const UncontrolledTooltip = () => <UncontrolledTooltipExample />;

UncontrolledTooltip.storyName = 'uncontrolled tooltip';

export const App = () => (
<div>
<Tooltip
className="view-example-tooltip"
triggerClassName="view-example-tooltip-trigger"
triggerText={
<div className="view-example-text">
<span>example1</span>
</div>
}
align="start"
direction="bottom"
showIcon={false}>
<div>
<div className="bx--snippet">
<div className="bx--snippet-container">
<code>
<pre>
<span>This is the first message.</span>
</pre>
</code>
</div>
<Button />
</div>
</div>
</Tooltip>
<Tooltip
className="view-example-tooltip2"
triggerClassName="view-example-tooltip-trigger2"
triggerText={
<div className="view-example-text2">
<span>example2</span>
</div>
}
align="start"
direction="bottom"
showIcon={false}>
<div className="bx--snippet">
<div className="bx--snippet-container">
<code>
<pre>
<span>This is the second message.</span>
</pre>
</code>
</div>
<Button />
</div>
</Tooltip>
</div>
);

export const App2 = () => {
const getTooltip = (id, label, includeId, withButton) => (
<Tooltip
tooltipId={(id && `tooltip-${id}`) || undefined}
triggerText={label}>
<p>
This is some tooltip text. This box shows the maximum amount of text
that should appear inside. If more room is needed please use a modal
instead.
</p>
<div className={`${prefix}--tooltip__footer`}>
{withButton && (
<a href="/" className={`${prefix}--link`}>
Learn More
</a>
)}
{withButton && <Button size="small">Create</Button>}
</div>
</Tooltip>
);
const getTextInput = (id, label, includeId, withButton) => (
<TextInput
style={{ marginBottom: 20 }}
id={id}
labelText={getTooltip(`tooltip-${id}`, label, includeId, withButton)}
/>
);

return (
<div className="app">
{getTextInput('bad-text-input-1', 'Text input (tooltip with id) 1', true)}
{getTextInput(
'bad-text-input-2',
'Text input (tooltip with id) 2',
true,
true
)}
{getTextInput(
'bad-text-input-3',
'Text input (tooltip with id) 3',
true,
true
)}
{getTextInput(
'good-text-input-1',
'Text input (tooltip with no id) 1',
false
)}
{getTextInput(
'good-text-input-2',
'Text input (tooltip with no id) 2',
false,
true
)}
{getTextInput(
'good-text-input-3',
'Text input (tooltip with no id) 3',
false,
true
)}
</div>
);
};
17 changes: 2 additions & 15 deletions packages/react/src/components/Tooltip/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ import FloatingMenu, {
import ClickListener from '../../internal/ClickListener';
import mergeRefs from '../../tools/mergeRefs';
import { keys, matches as keyDownMatch } from '../../internal/keyboard';
import {
selectorFocusable,
selectorTabbable,
} from '../../internal/keyboard/navigation';
import isRequiredOneOf from '../../prop-types/isRequiredOneOf';
import requiredIfValueExists from '../../prop-types/requiredIfValueExists';
import { useControlledStateWithValue } from '../../internal/FeatureFlags';
Expand Down Expand Up @@ -303,17 +299,8 @@ class Tooltip extends Component {
}
if (!open && tooltipBody && tooltipBody.id === this._tooltipId) {
this._tooltipDismissed = true;
const primaryFocusNode = tooltipBody.querySelector(
this.props.selectorPrimaryFocus || null
);
const tabbableNode = tooltipBody.querySelector(selectorTabbable);
const focusableNode = tooltipBody.querySelector(selectorFocusable);
const focusTarget =
primaryFocusNode || // User defined focusable node
tabbableNode || // First sequentially focusable node
focusableNode || // First programmatic focusable node
tooltipBody;
if (focusTarget !== tooltipBody) {
const currentActiveNode = event?.relatedTarget;
if (!currentActiveNode && document.activeElement === document.body) {
this._triggerRef?.current.focus();
}
}
Expand Down

0 comments on commit 3b4b69a

Please sign in to comment.