Skip to content

Commit 05c0c43

Browse files
Tooltip Refactoring (#155)
* builds * builds again * linter is happy and tests passed * created react types package * saving progress * attempt at proper aria labeling * swap naming in the story * clean up * more clean up * name flip * add some tests
1 parent 5d803d7 commit 05c0c43

File tree

17 files changed

+358
-47
lines changed

17 files changed

+358
-47
lines changed

packages/@react-aria/tooltip/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
},
1717
"dependencies": {
1818
"@babel/runtime": "^7.6.2",
19-
"@react-aria/utils": "^3.0.0-alpha.2"
19+
"@react-aria/utils": "^3.0.0-alpha.2",
20+
"@react-aria/overlays": "^3.0.0-alpha.2",
21+
"@react-aria/interactions": "^3.0.0-alpha.2",
22+
"@react-stately/tooltip": "^3.0.0-alpha.2"
2023
},
2124
"peerDependencies": {
2225
"react": "^16.8.0"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './useTooltip';
2+
export * from './useTooltipTrigger';

packages/@react-aria/tooltip/src/useTooltip.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ interface TooltipAria {
1212

1313
export function useTooltip(props: TooltipProps): TooltipAria {
1414
let tooltipId = useId(props.id);
15+
1516
let {
1617
role = 'tooltip'
1718
} = props;
19+
20+
let tooltipProps = {
21+
role,
22+
id: tooltipId
23+
};
24+
1825
return {
19-
tooltipProps: {
20-
role,
21-
id: tooltipId
22-
}
26+
tooltipProps
2327
};
2428
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {chain} from '@react-aria/utils';
2+
import {DOMProps} from '@react-types/shared';
3+
import {HTMLAttributes, RefObject} from 'react';
4+
import {PressProps} from '@react-aria/interactions';
5+
import {TooltipProps} from '@react-types/tooltip';
6+
import {TooltipTriggerState} from '@react-stately/tooltip';
7+
import {useId} from '@react-aria/utils';
8+
import {useOverlay} from '@react-aria/overlays';
9+
10+
interface TriggerRefProps extends DOMProps, HTMLAttributes<HTMLElement> {
11+
ref: RefObject<HTMLElement | null>,
12+
}
13+
14+
interface TooltipTriggerProps {
15+
tooltipProps: TooltipProps,
16+
triggerProps: TriggerRefProps,
17+
state: TooltipTriggerState,
18+
type: string
19+
}
20+
21+
interface TooltipTriggerAria {
22+
triggerProps: HTMLAttributes<HTMLElement> & PressProps
23+
tooltipProps: HTMLAttributes<HTMLElement>
24+
}
25+
26+
export function useTooltipTrigger(props: TooltipTriggerProps): TooltipTriggerAria {
27+
let tooltipId = useId();
28+
let {
29+
tooltipProps,
30+
triggerProps,
31+
state,
32+
type
33+
} = props;
34+
35+
let onClose = () => {
36+
state.setOpen(false);
37+
};
38+
39+
let {overlayProps} = useOverlay({
40+
ref: triggerProps.ref,
41+
onClose: onClose,
42+
isOpen: state.open
43+
});
44+
45+
let onKeyDownTrigger = (e) => {
46+
if (triggerProps.ref && triggerProps.ref.current) {
47+
// dismiss tooltip on esc key press
48+
if (e.key === 'Escape') {
49+
e.preventDefault();
50+
e.stopPropagation();
51+
state.setOpen(false);
52+
}
53+
}
54+
};
55+
56+
let onPress = () => {
57+
state.setOpen(!state.open);
58+
};
59+
60+
let triggerType = type;
61+
62+
return {
63+
triggerProps: {
64+
...tooltipProps,
65+
...overlayProps,
66+
'aria-describedby': tooltipId,
67+
onKeyDown: chain(triggerProps.onKeyDown, onKeyDownTrigger),
68+
onPress: triggerType === 'click' ? onPress : undefined
69+
},
70+
tooltipProps: {
71+
id: tooltipId
72+
}
73+
};
74+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {cleanup} from '@testing-library/react';
2+
import React from 'react';
3+
import {renderHook} from 'react-hooks-testing-library';
4+
import {useTooltip} from '../';
5+
6+
describe('useTooltip', function () {
7+
afterEach(cleanup);
8+
9+
let renderTooltipHook = (props) => {
10+
let {result} = renderHook(() => useTooltip(props));
11+
return result.current;
12+
};
13+
14+
it('handles defaults', function () {
15+
let {tooltipProps} = renderTooltipHook({children: 'Test Tooltip'});
16+
expect(tooltipProps.role).toBe('tooltip');
17+
expect(tooltipProps.id).toBeTruthy();
18+
});
19+
});

packages/@react-spectrum/tooltip/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@react-spectrum/overlays": "^3.0.0-alpha.2",
3535
"@react-spectrum/utils": "^3.0.0-alpha.2",
3636
"@react-stately/utils": "^3.0.0-alpha.2",
37+
"@react-stately/tooltip": "^3.0.0-alpha.2",
3738
"@react-types/shared": "^3.0.0-rc.1"
3839
},
3940
"devDependencies": {

packages/@react-spectrum/tooltip/src/Tooltip.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import {classNames, filterDOMProps, useStyleProps} from '@react-spectrum/utils';
2-
import {DOMProps, StyleProps} from '@react-types/shared';
3-
import React, {ReactNode, RefObject, useRef} from 'react';
2+
import React, {RefObject, useRef} from 'react';
3+
import {SpectrumTooltipProps} from '@react-types/tooltip';
44
import styles from '@adobe/spectrum-css-temp/components/tooltip/vars.css';
5+
import {useTooltip} from '@react-aria/tooltip';
56

6-
interface TooltipProps extends DOMProps, StyleProps {
7-
children: ReactNode,
8-
variant?: 'neutral' | 'positive' | 'negative' | 'info',
9-
placement?: 'right' | 'left' | 'top' | 'bottom',
10-
isOpen?: boolean
11-
}
12-
13-
export const Tooltip = React.forwardRef((props: TooltipProps, ref: RefObject<HTMLDivElement>) => {
7+
export const Tooltip = React.forwardRef((props: SpectrumTooltipProps, ref: RefObject<HTMLDivElement>) => {
148
ref = ref || useRef();
159
let {
1610
variant = 'neutral',
@@ -19,11 +13,13 @@ export const Tooltip = React.forwardRef((props: TooltipProps, ref: RefObject<HTM
1913
...otherProps
2014
} = props;
2115
let {styleProps} = useStyleProps(otherProps);
16+
let {tooltipProps} = useTooltip(props);
2217

2318
return (
2419
<div
2520
{...filterDOMProps(otherProps)}
2621
{...styleProps}
22+
{...tooltipProps}
2723
className={classNames(
2824
styles,
2925
'spectrum-Tooltip',

packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,40 @@
11
import {DOMRefValue} from '@react-types/shared';
22
import {Overlay} from '@react-spectrum/overlays';
3-
import {PositionProps, useOverlayPosition} from '@react-aria/overlays';
43
import {PressResponder} from '@react-aria/interactions';
5-
import React, {Fragment, ReactElement, RefObject, useRef} from 'react';
4+
import React, {Fragment, useRef} from 'react';
5+
import {TooltipTriggerProps} from '@react-types/tooltip';
66
import {unwrapDOMRef} from '@react-spectrum/utils';
7-
import {useControlledState} from '@react-stately/utils';
8-
9-
interface TooltipTriggerProps extends PositionProps {
10-
children: ReactElement[],
11-
type?: 'click',
12-
targetRef?: RefObject<HTMLElement>,
13-
isOpen?: boolean,
14-
defaultOpen?: boolean,
15-
onOpenChange?: (isOpen: boolean) => void
16-
}
7+
import {useOverlayPosition} from '@react-aria/overlays';
8+
import {useTooltipTrigger} from '@react-aria/tooltip';
9+
import {useTooltipTriggerState} from '@react-stately/tooltip';
1710

1811
export function TooltipTrigger(props: TooltipTriggerProps) {
1912
let {
2013
children,
2114
type,
2215
targetRef,
2316
isOpen,
24-
defaultOpen,
25-
onOpenChange
17+
isDisabled
2618
} = props;
2719

2820
let [trigger, content] = React.Children.toArray(children);
2921

30-
let [open, setOpen] = useControlledState(isOpen, defaultOpen || false, onOpenChange);
31-
32-
let onInteraction = () => {
33-
setOpen(!open);
34-
};
22+
let state = useTooltipTriggerState(props);
3523

3624
let containerRef = useRef<DOMRefValue<HTMLDivElement>>();
3725
let triggerRef = useRef<HTMLElement>();
3826
let overlayRef = useRef<HTMLDivElement>();
3927

28+
let {triggerProps, tooltipProps} = useTooltipTrigger({
29+
tooltipProps: content.props,
30+
triggerProps: {
31+
...trigger.props,
32+
ref: triggerRef
33+
},
34+
state,
35+
type
36+
});
37+
4038
let {overlayProps, placement, arrowProps} = useOverlayPosition({
4139
placement: props.placement,
4240
containerRef: unwrapDOMRef(containerRef),
@@ -47,23 +45,20 @@ export function TooltipTrigger(props: TooltipTriggerProps) {
4745

4846
delete overlayProps.style.position;
4947

50-
let triggerPropsWithRef = {
51-
ref: triggerRef
52-
};
53-
5448
let overlay = (
55-
<Overlay isOpen={open} ref={containerRef}>
56-
{React.cloneElement(content, {placement: placement, arrowProps: arrowProps, ref: overlayRef, ...overlayProps, isOpen: open})}
49+
<Overlay isOpen={state.open} ref={containerRef}>
50+
{React.cloneElement(content, {placement: placement, arrowProps: arrowProps, ref: overlayRef, UNSAFE_style: overlayProps.style, isOpen: open, ...tooltipProps})}
5751
</Overlay>
5852
);
5953

6054
if (type === 'click') {
6155
return (
6256
<Fragment>
6357
<PressResponder
64-
{...triggerPropsWithRef}
58+
{...triggerProps}
59+
ref={triggerRef}
6560
isPressed={isOpen}
66-
onPress={onInteraction}>
61+
isDisabled={isDisabled}>
6762
{trigger}
6863
</PressResponder>
6964
{overlay}

packages/@react-spectrum/tooltip/stories/Tooltip.stories.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,20 @@ storiesOf('Tooltip', module)
4141
() => render(longMarkup)
4242
)
4343
.add(
44-
'triggered by click, placement: right',
45-
() => renderWithTrigger('This is a tooltip.', {placement: 'right', type: 'click'})
46-
).add(
4744
'triggered by click, placement: left',
48-
() => renderWithTrigger('This is a tooltip.', {placement: 'left', type: 'click'})
45+
() => renderWithTrigger('This is a tooltip.', {placement: 'start', type: 'click'})
46+
).add(
47+
'triggered by click, placement: right',
48+
() => renderWithTrigger('This is a tooltip.', {placement: 'end', type: 'click'})
4949
).add(
5050
'triggered by click, placement: top',
5151
() => renderWithTrigger('This is a tooltip.', {placement: 'top', type: 'click'})
5252
).add(
5353
'triggered by click, placement: bottom',
5454
() => renderWithTrigger('This is a tooltip.', {placement: 'bottom', type: 'click'})
55+
).add(
56+
'isDisabled: true',
57+
() => renderWithTrigger('This is a tooltip.', {placement: 'left', type: 'click', isDisabled: true})
5558
);
5659

5760
function render(content, props = {}) {
@@ -69,7 +72,7 @@ function render(content, props = {}) {
6972
function renderWithTrigger(content, props = {}) {
7073
return (
7174
<TooltipTrigger {...props}>
72-
<ActionButton>Click Me</ActionButton>
75+
<ActionButton>Trigger Tooltip</ActionButton>
7376
<Tooltip>
7477
{content}
7578
</Tooltip>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {cleanup, render} from '@testing-library/react';
2+
import React from 'react';
3+
import {Tooltip} from '../';
4+
import V2Tooltip from '@react/react-spectrum/Tooltip';
5+
6+
describe('Tooltip', function () {
7+
afterEach(() => {
8+
cleanup();
9+
});
10+
11+
it.each`
12+
Name | Component
13+
${'Tooltip'} | ${Tooltip}
14+
${'V2Tooltip'} | ${V2Tooltip}
15+
`('$Name supports children', ({Component}) => {
16+
let {getByText} = render(<Component>This is a tooltip</Component>);
17+
expect(getByText('This is a tooltip')).toBeTruthy();
18+
});
19+
});

0 commit comments

Comments
 (0)