-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Using FloatingUI with Tooltips #1311
Changes from 23 commits
1bfd0fa
9fa8894
0a1fd99
0e9e02c
701e11e
846ccc5
500abf5
e4526b2
6caa8e8
78349e3
5612aab
489353f
c63f4bc
6a1ff7d
15a4e1f
10ca0d5
bb3d015
57fb938
6c92a27
1d9711c
036eba8
0a4f064
2c6be71
aba9381
4a7eeac
ed9b87e
39039e1
d685395
de7cc7b
7941e3c
a968a8a
852f8f2
4067a2b
4bafd1f
78b969c
0388ed9
4218e10
ceb1fad
7eb56db
5f55605
632a611
9d49e79
f89929b
8238668
2fe2428
79b6e93
3688e31
ea653d9
06f69d0
862810a
efb03ed
11df6c4
4262507
0a8e749
7bf4cc0
b0aa00e
1eb021f
4867592
8ee3dbc
a46162e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,58 +4,192 @@ | |
*--------------------------------------------------------------------------------------------*/ | ||
import * as React from 'react'; | ||
import cx from 'classnames'; | ||
import { Popover, Box } from '../utils/index.js'; | ||
import type { CommonProps, PopoverProps } from '../utils/index.js'; | ||
import { | ||
useMergeRefs, | ||
useFloating, | ||
autoUpdate, | ||
offset, | ||
flip, | ||
shift, | ||
useHover, | ||
useFocus, | ||
useDismiss, | ||
useRole, | ||
useInteractions, | ||
useClick, | ||
FloatingPortal, | ||
} from '@floating-ui/react'; | ||
import type { Placement } from '@floating-ui/react'; | ||
import { Box, type PolymorphicForwardRefComponent } from '../utils/index.js'; | ||
import { ThemeContext } from '../ThemeProvider/ThemeContext.js'; | ||
import ReactDOM from 'react-dom'; | ||
|
||
export type TooltipProps = { | ||
type TooltipOptions = { | ||
/** | ||
* Placement of the Tooltip | ||
* @default 'top' | ||
*/ | ||
placement?: Placement; | ||
/** | ||
* Property for manual visibility control | ||
*/ | ||
visible?: boolean; | ||
/** | ||
* Function that allows users to control Tooltip visibility | ||
*/ | ||
toggleVisible?: (open: boolean) => void; | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* Use this if you want Tooltip to follow moving trigger element | ||
* @default false; | ||
*/ | ||
followTrigger?: boolean; | ||
}; | ||
|
||
type TooltipOwnProps = { | ||
/** | ||
* Content of the tooltip. | ||
*/ | ||
content: React.ReactNode; | ||
content?: React.ReactNode; | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* Element to have tooltip on. Has to be a valid JSX element and needs to forward its ref. | ||
* If not specified, the `reference` prop should be used instead. | ||
*/ | ||
children?: JSX.Element; | ||
} & Omit<PopoverProps, 'className'> & | ||
Omit<CommonProps, 'title'>; | ||
children?: React.ReactNode; | ||
/** | ||
* Element to append tooltip to. | ||
* Appends to ThemeProvider portalContainerRef by default. | ||
*/ | ||
appendTo?: HTMLElement; | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
/** | ||
* Basic tooltip component to display informative content when an element is hovered or focused. | ||
* Uses the {@link Popover} component, which is a wrapper around [tippy.js](https://atomiks.github.io/tippyjs). | ||
* @example | ||
* <Tooltip content='tooltip text' placement='top'><div>Hover here</div></Tooltip> | ||
* @example | ||
* const buttonRef = React.useRef(); | ||
* ... | ||
* <Button ref={buttonRef} /> | ||
* <Tooltip content='tooltip text' reference={buttonRef} /> | ||
*/ | ||
export const Tooltip = (props: TooltipProps) => { | ||
const { content, children, className, style, visible, ref, id, ...rest } = | ||
props; | ||
const useTooltip = ({ | ||
placement = 'top', | ||
visible: controlledOpen, | ||
toggleVisible: setControlledOpen, | ||
followTrigger = false, | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}: TooltipOptions = {}) => { | ||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false); | ||
|
||
const open = controlledOpen ?? uncontrolledOpen; | ||
const setOpen = setControlledOpen ?? setUncontrolledOpen; | ||
|
||
const data = useFloating({ | ||
placement, | ||
open, | ||
onOpenChange: setOpen, | ||
whileElementsMounted: (referenceEl, floatingEl, update) => | ||
autoUpdate(referenceEl, floatingEl, update, { | ||
animationFrame: followTrigger, | ||
}), | ||
middleware: [offset(5), flip(), shift()], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allow users to customise middleware? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. see iTwin/iTwinUI-react#783 and iTwin/iTwinUI-react#947 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe adding prop would be ok? middleware?: {
offset?: number;
flip?: boolean;
..etc;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, lets control all the names now. Other option would be to allow user to pass any and all middleware, but then they might start doing weird things I also think it should be possible for the user to toggle There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. autoUpdateOptions were added.
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
const context = data.context; | ||
|
||
const hover = useHover(context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be allow users to customise interactions? eg. disable tooltip on focus. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it would be nice to give control, but i fear that developers will start removing focus as trigger, making tooltips inaccessible There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not giving control for now. We can add that later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hover should have some delay and also use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also missing controlled There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed this one. |
||
const focus = useFocus(context, { | ||
enabled: controlledOpen == null, | ||
}); | ||
const click = useClick(context); | ||
|
||
const dismiss = useDismiss(context); | ||
const role = useRole(context, { role: 'tooltip' }); | ||
|
||
const interactions = useInteractions([hover, focus, click, dismiss, role]); | ||
|
||
return React.useMemo( | ||
() => ({ | ||
open, | ||
setOpen, | ||
...interactions, | ||
...data, | ||
}), | ||
[open, setOpen, interactions, data], | ||
); | ||
}; | ||
|
||
const TooltipContext = React.createContext< | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(ReturnType<typeof useTooltip> & TooltipOwnProps) | null | ||
>(null); | ||
|
||
const useTooltipContext = () => { | ||
const context = React.useContext(TooltipContext); | ||
|
||
if (context == null) { | ||
throw new Error('Tooltip components must be wrapped in <Tooltip />'); | ||
} | ||
|
||
return context; | ||
}; | ||
|
||
const TooltipComponent = ({ | ||
content, | ||
children, | ||
appendTo, | ||
...options | ||
}: TooltipOwnProps & TooltipOptions) => { | ||
const tooltip = useTooltip(options); | ||
return ( | ||
<Popover | ||
visible={visible} | ||
interactive={false} | ||
content={ | ||
<Box | ||
className={cx('iui-tooltip', className)} | ||
style={style} | ||
role='tooltip' | ||
id={id} | ||
> | ||
{content} | ||
</Box> | ||
} | ||
offset={[0, 4]} | ||
<TooltipContext.Provider value={{ ...tooltip, appendTo }}> | ||
<TooltipTrigger>{children}</TooltipTrigger> | ||
<TooltipContent>{content}</TooltipContent> | ||
</TooltipContext.Provider> | ||
); | ||
}; | ||
|
||
const TooltipTrigger = React.forwardRef((props, propRef) => { | ||
const { children, ...rest } = props; | ||
const context = useTooltipContext(); | ||
const ref = useMergeRefs([context.refs.setReference, propRef]); | ||
|
||
return React.isValidElement(children) | ||
? React.cloneElement( | ||
children, | ||
context.getReferenceProps({ | ||
ref, | ||
...rest, | ||
...children.props, | ||
}), | ||
) | ||
: null; | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
}) as PolymorphicForwardRefComponent<'div', {}>; | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const TooltipContent = React.forwardRef((props, propRef) => { | ||
const { children, className, ...rest } = props; | ||
const context = useTooltipContext(); | ||
const themeInfo = React.useContext(ThemeContext); | ||
const ref = useMergeRefs([context.refs.setFloating, propRef]); | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const contentBox = ( | ||
<Box | ||
className={cx('iui-tooltip', className)} | ||
ref={ref} | ||
{...rest} | ||
style={context.floatingStyles} | ||
{...context.getFloatingProps(rest)} | ||
> | ||
{children && React.cloneElement(children, { title: undefined })} | ||
</Popover> | ||
{children} | ||
</Box> | ||
); | ||
}; | ||
|
||
export default Tooltip; | ||
if (!context.open) { | ||
return null; | ||
} | ||
|
||
const portalRoot = context.appendTo ?? themeInfo?.portalContainerRef?.current; | ||
|
||
return portalRoot ? ( | ||
ReactDOM.createPortal(contentBox, portalRoot) | ||
) : ( | ||
<FloatingPortal>{contentBox}</FloatingPortal> | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
}) as PolymorphicForwardRefComponent<'div', {}>; | ||
|
||
/** | ||
* Basic tooltip component to display informative content when an element is hovered or focused. | ||
* Uses [FloatingUI](https://floating-ui.com/). | ||
* @example | ||
* <Tooltip content='tooltip text' placement='top'>Hover here</Tooltip> | ||
*/ | ||
export const Tooltip = TooltipComponent; | ||
mayank99 marked this conversation as resolved.
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one is broken. We rely on
reference
prop from tippy to do this, so need an equivalent hereThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the reference!