Fix button tooltips

This commit is contained in:
Robert Long 2022-01-20 13:03:54 -08:00
parent abae58489c
commit d7d38c1ba9
8 changed files with 105 additions and 174 deletions

View file

@ -1,32 +1,46 @@
import React, { forwardRef } from "react"; import React, { forwardRef, useRef } from "react";
import { useTooltipTriggerState } from "@react-stately/tooltip"; import { useTooltipTriggerState } from "@react-stately/tooltip";
import { FocusableProvider } from "@react-aria/focus";
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip"; import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
import { mergeProps, useObjectRef } from "@react-aria/utils"; import { mergeProps, useObjectRef } from "@react-aria/utils";
import styles from "./Tooltip.module.css"; import styles from "./Tooltip.module.css";
import classNames from "classnames"; import classNames from "classnames";
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
export function Tooltip({ position, state, ...props }) { export const Tooltip = forwardRef(
let { tooltipProps } = useTooltip(props, state); ({ position, state, className, ...props }, ref) => {
let { tooltipProps } = useTooltip(props, state);
return ( return (
<div <div
className={classNames(styles.tooltip, styles[position || "bottom"])} className={classNames(styles.tooltip, className)}
{...mergeProps(props, tooltipProps)} {...mergeProps(props, tooltipProps)}
> ref={ref}
{props.children} >
</div> {props.children}
); </div>
} );
}
);
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => { export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
const tooltipState = useTooltipTriggerState(rest); const tooltipState = useTooltipTriggerState(rest);
const triggerRef = useObjectRef(ref); const triggerRef = useObjectRef(ref);
const overlayRef = useRef();
const { triggerProps, tooltipProps } = useTooltipTrigger( const { triggerProps, tooltipProps } = useTooltipTrigger(
rest, rest,
tooltipState, tooltipState,
triggerRef triggerRef
); );
const { overlayProps } = useOverlayPosition({
placement: rest.placement || "top",
targetRef: triggerRef,
overlayRef,
isOpen: tooltipState.isOpen,
offset: 5,
});
if ( if (
!Array.isArray(children) || !Array.isArray(children) ||
children.length > 2 || children.length > 2 ||
@ -40,13 +54,20 @@ export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
const [tooltipTrigger, tooltip] = children; const [tooltipTrigger, tooltip] = children;
return ( return (
<div className={styles.tooltipContainer}> <FocusableProvider ref={triggerRef} {...triggerProps}>
<tooltipTrigger.type {<tooltipTrigger.type {...mergeProps(tooltipTrigger.props, rest)} />}
{...mergeProps(triggerProps, tooltipTrigger.props, rest)} {tooltipState.isOpen && (
ref={triggerRef} <OverlayContainer>
/> <Tooltip
{tooltipState.isOpen && tooltip({ state: tooltipState, ...tooltipProps })} state={tooltipState}
</div> {...mergeProps(tooltipProps, overlayProps)}
ref={overlayRef}
>
{tooltip()}
</Tooltip>
</OverlayContainer>
)}
</FocusableProvider>
); );
}); });

View file

@ -1,6 +1,5 @@
.tooltip { .tooltip {
background-color: var(--bgColor2); background-color: var(--bgColor2);
position: absolute;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -9,25 +8,5 @@
border-radius: 8px; border-radius: 8px;
max-width: 135px; max-width: 135px;
width: max-content; width: max-content;
z-index: 1;
left: 50%;
transform: translateX(-50%);
text-align: center; text-align: center;
} }
.tooltip.top {
bottom: calc(100% + 6px);
}
.tooltip.bottom {
top: calc(100% + 6px);
}
.tooltip.bottomLeft {
top: calc(100% + 6px);
left: -25%;
}
.tooltipContainer {
position: relative;
}

View file

@ -62,7 +62,7 @@ export function UserMenu({
return ( return (
<PopoverMenuTrigger placement="bottom right"> <PopoverMenuTrigger placement="bottom right">
<TooltipTrigger> <TooltipTrigger placement="bottom left">
<Button variant="icon" className={styles.userButton}> <Button variant="icon" className={styles.userButton}>
{isAuthenticated && !isPasswordlessUser ? ( {isAuthenticated && !isPasswordlessUser ? (
<Avatar <Avatar
@ -75,11 +75,7 @@ export function UserMenu({
<UserIcon /> <UserIcon />
)} )}
</Button> </Button>
{(props) => ( {() => "Profile"}
<Tooltip position="bottomLeft" {...props}>
Profile
</Tooltip>
)}
</TooltipTrigger> </TooltipTrigger>
{(props) => ( {(props) => (
<Menu {...props} label="User menu" onAction={onAction}> <Menu {...props} label="User menu" onAction={onAction}>

View file

@ -76,25 +76,13 @@ export const Button = forwardRef(
} }
); );
export function ButtonTooltip({ className, children }) {
return (
<div className={classNames(styles.buttonTooltip, className)}>
{children}
</div>
);
}
export function MicButton({ muted, ...rest }) { export function MicButton({ muted, ...rest }) {
return ( return (
<TooltipTrigger> <TooltipTrigger>
<Button variant="toolbar" {...rest} off={muted}> <Button variant="toolbar" {...rest} off={muted}>
{muted ? <MuteMicIcon /> : <MicIcon />} {muted ? <MuteMicIcon /> : <MicIcon />}
</Button> </Button>
{(props) => ( {() => (muted ? "Unmute microphone" : "Mute microphone")}
<Tooltip position="top" {...props}>
{muted ? "Unmute microphone" : "Mute microphone"}
</Tooltip>
)}
</TooltipTrigger> </TooltipTrigger>
); );
} }
@ -105,11 +93,7 @@ export function VideoButton({ muted, ...rest }) {
<Button variant="toolbar" {...rest} off={muted}> <Button variant="toolbar" {...rest} off={muted}>
{muted ? <DisableVideoIcon /> : <VideoIcon />} {muted ? <DisableVideoIcon /> : <VideoIcon />}
</Button> </Button>
{(props) => ( {() => (muted ? "Turn on camera" : "Turn off camera")}
<Tooltip position="top" {...props}>
{muted ? "Turn on camera" : "Turn off camera"}
</Tooltip>
)}
</TooltipTrigger> </TooltipTrigger>
); );
} }
@ -120,11 +104,7 @@ export function ScreenshareButton({ enabled, className, ...rest }) {
<Button variant="toolbar" {...rest} on={enabled}> <Button variant="toolbar" {...rest} on={enabled}>
<ScreenshareIcon /> <ScreenshareIcon />
</Button> </Button>
{(props) => ( {() => (enabled ? "Stop sharing screen" : "Share screen")}
<Tooltip position="top" {...props}>
{enabled ? "Stop sharing screen" : "Share screen"}
</Tooltip>
)}
</TooltipTrigger> </TooltipTrigger>
); );
} }
@ -139,11 +119,7 @@ export function HangupButton({ className, ...rest }) {
> >
<HangupIcon /> <HangupIcon />
</Button> </Button>
{(props) => ( {() => "Leave"}
<Tooltip position="top" {...props}>
Leave
</Tooltip>
)}
</TooltipTrigger> </TooltipTrigger>
); );
} }

View file

@ -100,35 +100,6 @@ limitations under the License.
fill: #21262c; fill: #21262c;
} }
.buttonTooltip {
display: none;
background-color: var(--bgColor2);
position: absolute;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 8px 10px;
color: var(--textColor1);
border-radius: 8px;
max-width: 135px;
width: max-content;
z-index: 1;
}
.buttonTooltip.bottomRight {
right: 0;
}
.toolbarButton:hover .buttonTooltip {
display: flex;
bottom: calc(100% + 6px);
}
.iconButton:hover .buttonTooltip {
display: flex;
top: calc(100% + 6px);
}
.secondary, .secondary,
.copyButton { .copyButton {
color: #0dbd8b; color: #0dbd8b;

View file

@ -1,72 +1,68 @@
import React, { useRef } from "react"; import React, { forwardRef, useRef } from "react";
import styles from "./PopoverMenu.module.css"; import styles from "./PopoverMenu.module.css";
import { useMenuTriggerState } from "@react-stately/menu"; import { useMenuTriggerState } from "@react-stately/menu";
import { useMenuTrigger } from "@react-aria/menu"; import { useMenuTrigger } from "@react-aria/menu";
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays"; import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
import { mergeProps, useObjectRef } from "@react-aria/utils";
import classNames from "classnames"; import classNames from "classnames";
import { Popover } from "./Popover"; import { Popover } from "./Popover";
export function PopoverMenuTrigger({ export const PopoverMenuTrigger = forwardRef(
children, ({ children, placement, className, disableOnState, ...rest }, ref) => {
placement, const popoverMenuState = useMenuTriggerState(rest);
className, const buttonRef = useObjectRef(ref);
disableOnState, const { menuTriggerProps, menuProps } = useMenuTrigger(
...rest {},
}) { popoverMenuState,
const popoverMenuState = useMenuTriggerState(rest); buttonRef
const buttonRef = useRef(); );
const { menuTriggerProps, menuProps } = useMenuTrigger(
{},
popoverMenuState,
buttonRef
);
const popoverRef = useRef(); const popoverRef = useRef();
const { overlayProps } = useOverlayPosition({ const { overlayProps } = useOverlayPosition({
targetRef: buttonRef, targetRef: buttonRef,
overlayRef: popoverRef, overlayRef: popoverRef,
placement: placement || "top", placement: placement || "top",
offset: 5, offset: 5,
isOpen: popoverMenuState.isOpen, isOpen: popoverMenuState.isOpen,
}); });
if ( if (
!Array.isArray(children) || !Array.isArray(children) ||
children.length > 2 || children.length > 2 ||
typeof children[1] !== "function" typeof children[1] !== "function"
) { ) {
throw new Error( throw new Error(
"PopoverMenu must have two props. The first being a button and the second being a render prop." "PopoverMenu must have two props. The first being a button and the second being a render prop."
);
}
const [popoverTrigger, popoverMenu] = children;
return (
<div className={classNames(styles.popoverMenuTrigger, className)}>
<popoverTrigger.type
{...mergeProps(popoverTrigger.props, menuTriggerProps)}
on={!disableOnState && popoverMenuState.isOpen}
ref={buttonRef}
/>
{popoverMenuState.isOpen && (
<OverlayContainer>
<Popover
{...overlayProps}
isOpen={popoverMenuState.isOpen}
onClose={popoverMenuState.close}
ref={popoverRef}
>
{popoverMenu({
...menuProps,
autoFocus: popoverMenuState.focusStrategy,
onClose: popoverMenuState.close,
})}
</Popover>
</OverlayContainer>
)}
</div>
); );
} }
);
const [popoverTrigger, popoverMenu] = children;
return (
<div className={classNames(styles.popoverMenuTrigger, className)}>
<popoverTrigger.type
{...popoverTrigger.props}
{...menuTriggerProps}
on={!disableOnState && popoverMenuState.isOpen}
ref={buttonRef}
/>
{popoverMenuState.isOpen && (
<OverlayContainer>
<Popover
{...overlayProps}
isOpen={popoverMenuState.isOpen}
onClose={popoverMenuState.close}
ref={popoverRef}
>
{popoverMenu({
...menuProps,
autoFocus: popoverMenuState.focusStrategy,
onClose: popoverMenuState.close,
})}
</Popover>
</OverlayContainer>
)}
</div>
);
}

View file

@ -16,11 +16,7 @@ export function GridLayoutMenu({ layout, setLayout }) {
<Button variant="icon"> <Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />} {layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button> </Button>
{(props) => ( {() => "Layout Type"}
<Tooltip position="bottom" {...props}>
Layout Type
</Tooltip>
)}
</TooltipTrigger> </TooltipTrigger>
{(props) => ( {(props) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}> <Menu {...props} label="Grid layout menu" onAction={setLayout}>

View file

@ -38,15 +38,11 @@ export function OverflowMenu({
return ( return (
<> <>
<PopoverMenuTrigger disableOnState> <PopoverMenuTrigger disableOnState>
<TooltipTrigger> <TooltipTrigger placement="top">
<Button variant="toolbar"> <Button variant="toolbar">
<OverflowIcon /> <OverflowIcon />
</Button> </Button>
{(props) => ( {() => "More"}
<Tooltip position="top" {...props}>
More
</Tooltip>
)}
</TooltipTrigger> </TooltipTrigger>
{(props) => ( {(props) => (
<Menu {...props} label="More menu" onAction={onAction}> <Menu {...props} label="More menu" onAction={onAction}>