change(ui) - floating ui

This commit is contained in:
Shekar Siri 2022-11-09 09:59:09 +01:00
parent afe43842fc
commit 55edc0b592
13 changed files with 401 additions and 200 deletions

View file

@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import stl from './notifications.module.css';
import { connect } from 'react-redux';
import { Icon, Popup } from 'UI';
import { Icon, Popup, Tooltip } from 'UI';
import { fetchList, setViewed, clearAll } from 'Duck/notifications';
import { setLastRead } from 'Duck/announcements';
import { useModal } from 'App/components/Modal';
@ -29,7 +29,7 @@ function Notifications(props: Props) {
}, []);
return useObserver(() => (
<Popup content={`Alerts`}>
<Tooltip tooltip={`Alerts`}>
<div
className={stl.button}
onClick={() => showModal(<AlertTriggersModal />, { right: true })}
@ -39,7 +39,7 @@ function Notifications(props: Props) {
</div>
<Icon name="bell" size="18" color="gray-dark" />
</div>
</Popup>
</Tooltip>
));
}

View file

@ -1,8 +1,8 @@
import { useModal } from 'App/components/Modal';
import React from 'react';
import SessionSettings from 'Shared/SessionSettings';
import { Button } from 'UI';
import { Tooltip } from 'react-tippy';
import { Button, Tooltip } from 'UI';
// import { Tooltip } from 'react-tippy';
function SessionSettingButton(props: any) {
const { showModal } = useModal();
@ -14,7 +14,7 @@ function SessionSettingButton(props: any) {
return (
<div className="cursor-pointer ml-4" onClick={handleClick}>
{/* @ts-ignore */}
<Tooltip title="Session Settings" unmountHTMLWhenHide>
<Tooltip tooltip="Session Settings">
<Button icon="sliders" variant="text" />
</Tooltip>
</div>

View file

@ -1,21 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import { toast } from 'react-toastify';
import { connectPlayer } from 'Player';
import withRequest from 'HOCs/withRequest';
import { Icon, Button } from 'UI';
import { Icon, Button, Popover } from 'UI';
import styles from './sharePopup.module.css';
import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton';
import SessionCopyLink from './SessionCopyLink';
import Select from 'Shared/Select';
import { Tooltip } from 'react-tippy';
import cn from 'classnames';
import { fetchList, init } from 'Duck/integrations/slack';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import { fetchList } from 'Duck/integrations/slack';
// @connectPlayer((state) => ({
// time: state.time,
// }))
@connect(
(state) => ({
channels: state.getIn(['slack', 'list']),
@ -75,84 +69,67 @@ export default class SharePopup extends React.PureComponent {
const options = channels
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
.toJS();
return (
<OutsideClickDetectingDiv
className={cn('relative flex items-center')}
onClickOutside={() => {
this.setState({ isOpen: false });
}}
>
<Tooltip
open={isOpen}
theme="light"
interactive
position="bottom"
unmountHTMLWhenHide
useContext
arrow
trigger="click"
shown={this.handleOpen}
className="h-full w-full p-3"
// beforeHidden={this.handleClose}
html={
<div className={styles.wrapper}>
<div className={styles.header}>
<div className={cn(styles.title, 'text-lg')}>Share this session link to Slack</div>
</div>
{options.length === 0 ? (
<>
<div className={styles.body}>
<IntegrateSlackButton />
</div>
{showCopyLink && (
<div className={styles.footer}>
<SessionCopyLink />
</div>
)}
</>
) : (
<div>
<div className={styles.body}>
<textarea
name="message"
id="message"
cols="30"
rows="4"
resize="none"
onChange={this.editMessage}
value={comment}
placeholder="Add Message (Optional)"
className="p-4"
/>
<div className="flex items-center justify-between">
<Select
options={options}
defaultValue={channelId}
onChange={this.changeChannel}
className="mr-4"
/>
<div>
<Button onClick={this.share} variant="primary">
<div className="flex items-center">
<Icon name="integrations/slack-bw" size="18" marginRight="10" />
{loading ? 'Sending...' : 'Send'}
</div>
</Button>
</div>
</div>
</div>
return (
<Popover
render={() => (
<div className={styles.wrapper}>
<div className={styles.header}>
<div className={cn(styles.title, 'text-lg')}>Share this session link to Slack</div>
</div>
{options.length === 0 ? (
<>
<div className={styles.body}>
<IntegrateSlackButton />
</div>
{showCopyLink && (
<div className={styles.footer}>
<SessionCopyLink />
</div>
)}
</>
) : (
<div>
<div className={styles.body}>
<textarea
name="message"
id="message"
cols="30"
rows="4"
resize="none"
onChange={this.editMessage}
value={comment}
placeholder="Add Message (Optional)"
className="p-4"
/>
<div className="flex items-center justify-between">
<Select
options={options}
defaultValue={channelId}
onChange={this.changeChannel}
className="mr-4"
/>
<div>
<Button onClick={this.share} variant="primary">
<div className="flex items-center">
<Icon name="integrations/slack-bw" size="18" marginRight="10" />
{loading ? 'Sending...' : 'Send'}
</div>
</Button>
</div>
</div>
</div>
)}
</div>
}
>
<div onClick={this.onClickHandler}>{trigger}</div>
</Tooltip>
</OutsideClickDetectingDiv>
<div className={styles.footer}>
<SessionCopyLink />
</div>
</div>
)}
</div>
)}
>
<div className="p-3 w-full">{trigger}</div>
</Popover>
);
}
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Icon } from 'UI';
import { Icon, Popover } from 'UI';
import styles from './itemMenu.module.css';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import cn from 'classnames';
interface Item {
@ -58,71 +57,69 @@ export default class ItemMenu extends React.PureComponent<Props> {
const parentStyles = label ? 'rounded px-2 py-2 hover:bg-gray-light' : '';
return (
<div className={styles.wrapper}>
<OutsideClickDetectingDiv onClickOutside={this.closeMenu}>
<Popover
render={() => (
<div
onClick={this.toggleMenu}
className={cn(
'flex items-center cursor-pointer select-none hover rounded-full',
!this.props.flat ? parentStyles : '',
{ 'bg-gray-light': !this.props.flat && displayed && label }
)}
className={cn(styles.menu, { [styles.menuDim]: !bold })}
// style={{
// top: this.props.flat ? 24 : undefined,
// }}
// data-displayed={displayed}
>
{label && (
<span
className={cn(
'mr-1',
bold ? 'font-medium color-gray-darkest' : 'color-gray-medium'
)}
>
{label}
</span>
)}
{this.props.flat ? null : (
<div
ref={(ref) => {
this.menuBtnRef = ref;
}}
className={cn('rounded-full flex items-center justify-center', {
'bg-gray-light': displayed,
'w-10 h-10': !label,
})}
role="button"
>
<Icon name="ellipsis-v" size="16" />
</div>
)}
</div>
</OutsideClickDetectingDiv>
<div
className={cn(styles.menu, { [styles.menuDim]: !bold })}
style={{
top: this.props.flat ? 24 : undefined,
}}
data-displayed={displayed}
>
{items
.filter(({ hidden }) => !hidden)
.map(({ onClick, text, icon, disabled = false }) => (
<div
key={text}
onClick={!disabled ? this.onClick(onClick) : () => {}}
className={disabled ? 'cursor-not-allowed' : ''}
role="menuitem"
>
<div className={cn(styles.menuItem, 'text-neutral-700', { disabled: disabled })}>
{icon && (
<div className={styles.iconWrapper}>
{/* @ts-ignore */}
<Icon name={icon} size="13" color="gray-dark" />
</div>
)}
<div>{text}</div>
{items
.filter(({ hidden }) => !hidden)
.map(({ onClick, text, icon, disabled = false }) => (
<div
key={text}
onClick={!disabled ? this.onClick(onClick) : () => {}}
className={disabled ? 'cursor-not-allowed' : ''}
role="menuitem"
>
<div className={cn(styles.menuItem, 'text-neutral-700', { disabled: disabled })}>
{icon && (
<div className={styles.iconWrapper}>
{/* @ts-ignore */}
<Icon name={icon} size="13" color="gray-dark" />
</div>
)}
<div>{text}</div>
</div>
</div>
</div>
))}
))}
</div>
)}
>
<div
// onClick={this.toggleMenu}
className={cn(
'flex items-center cursor-pointer select-none hover rounded-full',
!this.props.flat ? parentStyles : '',
{ 'bg-gray-light': !this.props.flat && displayed && label }
)}
>
{label && (
<span
className={cn('mr-1', bold ? 'font-medium color-gray-darkest' : 'color-gray-medium')}
>
{label}
</span>
)}
{this.props.flat ? null : (
<div
ref={(ref) => {
this.menuBtnRef = ref;
}}
className={cn('rounded-full flex items-center justify-center', {
'bg-gray-light': displayed,
'w-10 h-10': !label,
})}
role="button"
>
<Icon name="ellipsis-v" size="16" />
</div>
)}
</div>
</div>
</Popover>
);
}
}

View file

@ -40,9 +40,9 @@
white-space: nowrap;
z-index: 20;
position: absolute;
right: 0px;
top: 37px;
/* position: absolute; */
/* right: 0px; */
/* top: 37px; */
min-width: 150px;
background-color: $white;
border-radius: 3px;

View file

@ -0,0 +1,84 @@
import React, { cloneElement, useMemo, useState } from 'react';
import {
Placement,
offset,
flip,
shift,
autoUpdate,
useFloating,
useInteractions,
useRole,
useDismiss,
useId,
useClick,
FloatingFocusManager,
} from '@floating-ui/react-dom-interactions';
import { mergeRefs } from 'react-merge-refs';
interface Props {
render: (data: { close: () => void; labelId: string; descriptionId: string }) => React.ReactNode;
placement?: Placement;
children: JSX.Element;
}
const Popover = ({ children, render, placement }: Props) => {
const [open, setOpen] = useState(false);
const { x, y, reference, floating, strategy, context } = useFloating({
open,
onOpenChange: setOpen,
middleware: [offset(5), flip(), shift()],
placement,
whileElementsMounted: autoUpdate,
});
const id = useId();
const labelId = `${id}-label`;
const descriptionId = `${id}-description`;
const { getReferenceProps, getFloatingProps } = useInteractions([
useClick(context),
useRole(context),
useDismiss(context),
]);
// Preserve the consumer's ref
const ref = useMemo(() => mergeRefs([reference, (children as any).ref]), [reference, children]);
return (
<>
{cloneElement(children, getReferenceProps({ ref, ...children.props }))}
{open && (
<FloatingFocusManager
context={context}
modal={false}
order={['reference', 'content']}
returnFocus={false}
>
<div
ref={floating}
className="rounded border shadow"
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
}}
aria-labelledby={labelId}
aria-describedby={descriptionId}
{...getFloatingProps()}
>
{render({
labelId,
descriptionId,
close: () => {
setOpen(false);
},
})}
</div>
</FloatingFocusManager>
)}
</>
);
};
export default Popover;

View file

@ -0,0 +1 @@
export { default } from './Popover';

View file

@ -0,0 +1,109 @@
import * as React from 'react';
import { mergeRefs } from 'react-merge-refs';
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useHover,
useFocus,
useDismiss,
useRole,
useInteractions,
FloatingPortal,
} from '@floating-ui/react-dom-interactions';
import type { Placement } from '@floating-ui/react-dom-interactions';
export function useTooltipState({
initialOpen = false,
placement = 'top',
}: {
initialOpen?: boolean;
placement?: Placement;
} = {}) {
const [open, setOpen] = React.useState(initialOpen);
const data = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [offset(5), flip(), shift()],
});
const context = data.context;
const hover = useHover(context, { move: false, restMs: 500 });
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'tooltip' });
const interactions = useInteractions([hover, focus, dismiss, role]);
return React.useMemo(
() => ({
open,
setOpen,
...interactions,
...data,
}),
[open, setOpen, interactions, data]
);
}
type TooltipState = ReturnType<typeof useTooltipState>;
export const TooltipAnchor = React.forwardRef<
HTMLElement,
React.HTMLProps<HTMLElement> & {
state: TooltipState;
asChild?: boolean;
}
>(function TooltipAnchor({ children, state, asChild = false, ...props }, propRef) {
const childrenRef = (children as any).ref;
const ref = React.useMemo(
() => mergeRefs([state.reference, propRef, childrenRef]),
[state.reference, propRef, childrenRef]
);
// `asChild` allows the user to pass any element as the anchor
if (asChild && React.isValidElement(children)) {
return React.cloneElement(
children,
state.getReferenceProps({ ref, ...props, ...children.props })
);
}
return (
<button ref={ref} {...state.getReferenceProps(props)}>
{children}
</button>
);
});
export const FloatingTooltip = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement> & { state: TooltipState }
>(function Tooltip({ state, ...props }, propRef) {
const ref = React.useMemo(() => mergeRefs([state.floating, propRef]), [state.floating, propRef]);
return (
<FloatingPortal>
{state.open && (
<div
ref={ref}
style={{
position: state.strategy,
top: state.y ?? 0,
left: state.x ?? 0,
visibility: state.x == null ? 'hidden' : 'visible',
transition: 'opacity 1s',
...props.style,
}}
{...state.getFloatingProps(props)}
/>
)}
</FloatingPortal>
);
});

View file

@ -1,51 +1,21 @@
import React from 'react';
import { Popup } from 'UI';
import { useTooltipState, TooltipAnchor, FloatingTooltip } from './FloatingTooltip';
interface Props {
timeout: number
position: string
tooltip: string
trigger: React.ReactNode
// position: string;
tooltip: string;
children: any;
}
function Tooltip(props: Props) {
const state = useTooltipState();
return (
<>
<TooltipAnchor state={state}>{props.children}</TooltipAnchor>
<FloatingTooltip state={state} className="bg-gray-darkest color-white rounded py-1 px-2 animate-fade">
{props.tooltip}
</FloatingTooltip>
</>
);
}
export default class Tooltip extends React.PureComponent<Props> {
static defaultProps = {
timeout: 500,
}
state = {
open: false,
}
mouseOver = false
onMouseEnter = () => {
this.mouseOver = true;
setTimeout(() => {
if (this.mouseOver) this.setState({ open: true });
}, this.props.timeout)
}
onMouseLeave = () => {
this.mouseOver = false;
this.setState({
open: false,
});
}
render() {
const { trigger, tooltip, position } = this.props;
const { open } = this.state;
return (
<Popup
open={open}
content={tooltip}
disabled={!tooltip}
position={position}
>
<span //TODO: no wrap component around
onMouseEnter={ this.onMouseEnter }
onMouseLeave={ this.onMouseLeave }
>
{ trigger }
</span>
</Popup>
);
}
}
export default Tooltip;

View file

@ -0,0 +1,47 @@
import React from 'react';
import { Popup } from 'UI';
import { useTooltipState, TooltipAnchor, FloatingTooltip } from './FloatingTooltip';
interface Props {
timeout: number;
position: string;
tooltip: string;
trigger: React.ReactNode;
}
export default class Tooltip extends React.PureComponent<Props> {
static defaultProps = {
timeout: 500,
};
state = {
open: false,
};
mouseOver = false;
onMouseEnter = () => {
this.mouseOver = true;
setTimeout(() => {
if (this.mouseOver) this.setState({ open: true });
}, this.props.timeout);
};
onMouseLeave = () => {
this.mouseOver = false;
this.setState({
open: false,
});
};
render() {
const { trigger, tooltip, position } = this.props;
const { open } = this.state;
return (
<Popup open={open} content={tooltip} disabled={!tooltip} position={position}>
<span //TODO: no wrap component around
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{trigger}
</span>
</Popup>
);
}
}

View file

@ -56,4 +56,5 @@ export { default as Toggler } from './Toggler';
export { default as Input } from './Input';
export { default as Form } from './Form';
export { default as Modal } from './Modal';
export { default as Message } from './Message';
export { default as Message } from './Message';
export { default as Popover } from './Popover';

View file

@ -334,4 +334,17 @@ p {
#ccc 2px,
#ccc 4px
);
}
.animate-fade {
animation: fade 0.1s cubic-bezier(0.4, 0, 0.6, 1);
}
@keyframes fade {
100% {
opacity: 1;
}
0% {
opacity: 0;
}
}

View file

@ -17,6 +17,7 @@
"postinstall": "yarn gen:icons && yarn gen:colors"
},
"dependencies": {
"@floating-ui/react-dom-interactions": "^0.10.3",
"@sentry/browser": "^5.21.1",
"@svg-maps/world": "^1.0.1",
"@svgr/webpack": "^6.2.1",
@ -51,6 +52,7 @@
"react-highlight": "^0.14.0",
"react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0",
"react-merge-refs": "^2.0.1",
"react-redux": "^5.1.2",
"react-router": "^5.3.3",
"react-router-dom": "^5.3.3",