Re-build dropdown-menu using FloatingUI
This commit is contained in:
parent
17684571af
commit
60d7ff8395
29 changed files with 500 additions and 578 deletions
|
@ -1,13 +1,8 @@
|
||||||
import type { DropdownPlacement } from 'soapbox/components/dropdown-menu';
|
|
||||||
|
|
||||||
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||||
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||||
|
|
||||||
const openDropdownMenu = (id: number, placement: DropdownPlacement, keyboard: boolean) =>
|
const openDropdownMenu = () => ({ type: DROPDOWN_MENU_OPEN });
|
||||||
({ type: DROPDOWN_MENU_OPEN, id, placement, keyboard });
|
const closeDropdownMenu = () => ({ type: DROPDOWN_MENU_CLOSE });
|
||||||
|
|
||||||
const closeDropdownMenu = (id: number) =>
|
|
||||||
({ type: DROPDOWN_MENU_CLOSE, id });
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DROPDOWN_MENU_OPEN,
|
DROPDOWN_MENU_OPEN,
|
||||||
|
|
|
@ -1,420 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
|
||||||
import React from 'react';
|
|
||||||
import { spring } from 'react-motion';
|
|
||||||
// @ts-ignore: TODO: upgrade react-overlays. v3.1 and above have TS definitions
|
|
||||||
import Overlay from 'react-overlays/lib/Overlay';
|
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Counter, IconButton } from 'soapbox/components/ui';
|
|
||||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
|
||||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
|
||||||
|
|
||||||
import type { Status } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
|
||||||
let id = 0;
|
|
||||||
|
|
||||||
export interface MenuItem {
|
|
||||||
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>,
|
|
||||||
middleClick?: React.EventHandler<React.MouseEvent>,
|
|
||||||
text: string,
|
|
||||||
href?: string,
|
|
||||||
to?: string,
|
|
||||||
newTab?: boolean,
|
|
||||||
isLogout?: boolean,
|
|
||||||
icon?: string,
|
|
||||||
count?: number,
|
|
||||||
destructive?: boolean,
|
|
||||||
meta?: string,
|
|
||||||
active?: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Menu = Array<MenuItem | null>;
|
|
||||||
|
|
||||||
interface IDropdownMenu extends RouteComponentProps {
|
|
||||||
items: Menu,
|
|
||||||
onClose: () => void,
|
|
||||||
style?: React.CSSProperties,
|
|
||||||
placement?: DropdownPlacement,
|
|
||||||
arrowOffsetLeft?: string,
|
|
||||||
arrowOffsetTop?: string,
|
|
||||||
openedViaKeyboard: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDropdownMenuState {
|
|
||||||
mounted: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState> {
|
|
||||||
|
|
||||||
static defaultProps: Partial<IDropdownMenu> = {
|
|
||||||
style: {},
|
|
||||||
placement: 'bottom',
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
mounted: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
node: HTMLDivElement | null = null;
|
|
||||||
focusedItem: HTMLAnchorElement | null = null;
|
|
||||||
|
|
||||||
handleDocumentClick = (e: Event) => {
|
|
||||||
if (this.node && !this.node.contains(e.target as Node)) {
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
|
||||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
|
||||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
|
||||||
this.focusedItem.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
this.setState({ mounted: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('click', this.handleDocumentClick);
|
|
||||||
document.removeEventListener('keydown', this.handleKeyDown);
|
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef: React.RefCallback<HTMLDivElement> = c => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
setFocusRef: React.RefCallback<HTMLAnchorElement> = c => {
|
|
||||||
this.focusedItem = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (!this.node) return;
|
|
||||||
|
|
||||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
|
||||||
const index = items.indexOf(document.activeElement as any);
|
|
||||||
let element = null;
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowDown':
|
|
||||||
element = items[index + 1] || items[0];
|
|
||||||
break;
|
|
||||||
case 'ArrowUp':
|
|
||||||
element = items[index - 1] || items[items.length - 1];
|
|
||||||
break;
|
|
||||||
case 'Tab':
|
|
||||||
if (e.shiftKey) {
|
|
||||||
element = items[index - 1] || items[items.length - 1];
|
|
||||||
} else {
|
|
||||||
element = items[index + 1] || items[0];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Home':
|
|
||||||
element = items[0];
|
|
||||||
break;
|
|
||||||
case 'End':
|
|
||||||
element = items[items.length - 1];
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
|
||||||
this.props.onClose();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
element.focus();
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = e => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
this.handleClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = e => {
|
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
||||||
const item = this.props.items[i];
|
|
||||||
if (!item) return;
|
|
||||||
const { action, to } = item;
|
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (to) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(to);
|
|
||||||
} else if (typeof action === 'function') {
|
|
||||||
e.preventDefault();
|
|
||||||
action(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMiddleClick: React.EventHandler<React.MouseEvent> = e => {
|
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
||||||
const item = this.props.items[i];
|
|
||||||
if (!item) return;
|
|
||||||
const { middleClick } = item;
|
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
|
|
||||||
if (e.button === 1 && typeof middleClick === 'function') {
|
|
||||||
e.preventDefault();
|
|
||||||
middleClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAuxClick: React.EventHandler<React.MouseEvent> = e => {
|
|
||||||
if (e.button === 1) {
|
|
||||||
this.handleMiddleClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderItem(option: MenuItem | null, i: number): JSX.Element {
|
|
||||||
if (option === null) {
|
|
||||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { text, href, to, newTab, isLogout, icon, count, destructive } = option;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={clsx('dropdown-menu__item truncate', { destructive })} key={`${text}-${i}`}>
|
|
||||||
<a
|
|
||||||
href={href || to || '#'}
|
|
||||||
role='button'
|
|
||||||
tabIndex={0}
|
|
||||||
ref={i === 0 ? this.setFocusRef : null}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onAuxClick={this.handleAuxClick}
|
|
||||||
onKeyPress={this.handleItemKeyPress}
|
|
||||||
data-index={i}
|
|
||||||
target={newTab ? '_blank' : undefined}
|
|
||||||
data-method={isLogout ? 'delete' : undefined}
|
|
||||||
title={text}
|
|
||||||
>
|
|
||||||
{icon && <SvgIcon src={icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
|
||||||
|
|
||||||
<span className='truncate'>{text}</span>
|
|
||||||
|
|
||||||
{count ? (
|
|
||||||
<span className='ml-auto h-5 w-5 flex-none'>
|
|
||||||
<Counter count={count} />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
|
||||||
const { mounted } = this.state;
|
|
||||||
return (
|
|
||||||
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
|
||||||
{({ opacity, scaleX, scaleY }) => (
|
|
||||||
// It should not be transformed when mounting because the resulting
|
|
||||||
// size will be used to determine the coordinate of the menu by
|
|
||||||
// react-overlays
|
|
||||||
<div
|
|
||||||
className={`dropdown-menu ${placement}`}
|
|
||||||
style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }}
|
|
||||||
ref={this.setRef}
|
|
||||||
data-testid='dropdown-menu'
|
|
||||||
>
|
|
||||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
|
||||||
<ul>
|
|
||||||
{items.map((option, i) => this.renderItem(option, i))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const RouterDropdownMenu = withRouter(DropdownMenu);
|
|
||||||
|
|
||||||
export interface IDropdown extends RouteComponentProps {
|
|
||||||
icon?: string,
|
|
||||||
src?: string,
|
|
||||||
items: Menu,
|
|
||||||
size?: number,
|
|
||||||
active?: boolean,
|
|
||||||
pressed?: boolean,
|
|
||||||
title?: string,
|
|
||||||
disabled?: boolean,
|
|
||||||
status?: Status,
|
|
||||||
isUserTouching?: () => boolean,
|
|
||||||
isModalOpen?: boolean,
|
|
||||||
onOpen?: (
|
|
||||||
id: number,
|
|
||||||
onItemClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
|
||||||
dropdownPlacement: DropdownPlacement,
|
|
||||||
keyboard: boolean,
|
|
||||||
) => void,
|
|
||||||
onClose?: (id: number) => void,
|
|
||||||
dropdownPlacement?: string,
|
|
||||||
openDropdownId?: number | null,
|
|
||||||
openedViaKeyboard?: boolean,
|
|
||||||
text?: string,
|
|
||||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
|
||||||
children?: JSX.Element,
|
|
||||||
dropdownMenuStyle?: React.CSSProperties,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDropdownState {
|
|
||||||
id: number,
|
|
||||||
open: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DropdownPlacement = 'top' | 'bottom';
|
|
||||||
|
|
||||||
class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|
||||||
|
|
||||||
static defaultProps: Partial<IDropdown> = {
|
|
||||||
title: 'Menu',
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
id: id++,
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
target: HTMLButtonElement | null = null;
|
|
||||||
activeElement: Element | null = null;
|
|
||||||
|
|
||||||
handleClick: React.EventHandler<React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>> = e => {
|
|
||||||
const { onOpen, onShiftClick, openDropdownId } = this.props;
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (onShiftClick && e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
onShiftClick(e);
|
|
||||||
} else if (this.state.id === openDropdownId) {
|
|
||||||
this.handleClose();
|
|
||||||
} else if (onOpen) {
|
|
||||||
const { top } = e.currentTarget.getBoundingClientRect();
|
|
||||||
const placement: DropdownPlacement = top * 2 < innerHeight ? 'bottom' : 'top';
|
|
||||||
|
|
||||||
onOpen(this.state.id, this.handleItemClick, placement, e.type !== 'click');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClose = () => {
|
|
||||||
if (this.activeElement && this.activeElement === this.target) {
|
|
||||||
(this.activeElement as HTMLButtonElement).focus();
|
|
||||||
this.activeElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onClose) {
|
|
||||||
this.props.onClose(this.state.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
|
|
||||||
if (!this.state.open) {
|
|
||||||
this.activeElement = document.activeElement;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (e) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case ' ':
|
|
||||||
case 'Enter':
|
|
||||||
this.handleMouseDown(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (e) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case ' ':
|
|
||||||
case 'Enter':
|
|
||||||
this.handleClick(e);
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleItemClick: React.EventHandler<React.MouseEvent> = e => {
|
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
||||||
const item = this.props.items[i];
|
|
||||||
if (!item) return;
|
|
||||||
|
|
||||||
const { action, to } = item;
|
|
||||||
|
|
||||||
this.handleClose();
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (typeof action === 'function') {
|
|
||||||
action(e);
|
|
||||||
} else if (to) {
|
|
||||||
this.props.history?.push(to);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTargetRef: React.RefCallback<HTMLButtonElement> = c => {
|
|
||||||
this.target = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
findTarget = () => {
|
|
||||||
return this.target;
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
if (this.state.id === this.props.openDropdownId) {
|
|
||||||
this.handleClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { src = require('@tabler/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children, dropdownMenuStyle } = this.props;
|
|
||||||
const open = this.state.id === openDropdownId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children ? (
|
|
||||||
React.cloneElement(children, {
|
|
||||||
disabled,
|
|
||||||
onClick: this.handleClick,
|
|
||||||
onMouseDown: this.handleMouseDown,
|
|
||||||
onKeyDown: this.handleButtonKeyDown,
|
|
||||||
onKeyPress: this.handleKeyPress,
|
|
||||||
ref: this.setTargetRef,
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<IconButton
|
|
||||||
disabled={disabled}
|
|
||||||
className={clsx({
|
|
||||||
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
|
||||||
'text-gray-700 dark:text-gray-500': open,
|
|
||||||
})}
|
|
||||||
title={title}
|
|
||||||
src={src}
|
|
||||||
aria-pressed={pressed}
|
|
||||||
text={text}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onMouseDown={this.handleMouseDown}
|
|
||||||
onKeyDown={this.handleButtonKeyDown}
|
|
||||||
onKeyPress={this.handleKeyPress}
|
|
||||||
ref={this.setTargetRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
|
||||||
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} style={dropdownMenuStyle} />
|
|
||||||
</Overlay>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(Dropdown);
|
|
109
app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx
Normal file
109
app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Counter, Icon } from '../ui';
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>
|
||||||
|
active?: boolean
|
||||||
|
count?: number
|
||||||
|
destructive?: boolean
|
||||||
|
href?: string
|
||||||
|
icon?: string
|
||||||
|
meta?: string
|
||||||
|
middleClick?(event: React.MouseEvent): void
|
||||||
|
target?: React.HTMLAttributeAnchorTarget
|
||||||
|
text: string
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDropdownMenuItem {
|
||||||
|
index: number
|
||||||
|
item: MenuItem | null
|
||||||
|
onClick?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const itemRef = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
|
const handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!item) return;
|
||||||
|
if (onClick) onClick();
|
||||||
|
|
||||||
|
|
||||||
|
if (item.to) {
|
||||||
|
event.preventDefault();
|
||||||
|
history.push(item.to);
|
||||||
|
} else if (typeof item.action === 'function') {
|
||||||
|
event.preventDefault();
|
||||||
|
item.action(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuxClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||||
|
if (!item) return;
|
||||||
|
if (onClick) onClick();
|
||||||
|
|
||||||
|
if (event.button === 1 && item.middleClick) {
|
||||||
|
item.middleClick(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
handleClick(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const firstItem = index === 0;
|
||||||
|
|
||||||
|
if (itemRef.current && firstItem) {
|
||||||
|
itemRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [itemRef.current, index]);
|
||||||
|
|
||||||
|
if (item === null) {
|
||||||
|
return <li className='my-2 block h-[1px] bg-gray-100 dark:bg-gray-800' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className='truncate focus-within:ring-2 focus-within:ring-primary-500'>
|
||||||
|
<a
|
||||||
|
href={item.href || item.to || '#'}
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
ref={itemRef}
|
||||||
|
data-index={index}
|
||||||
|
onClick={handleClick}
|
||||||
|
onAuxClick={handleAuxClick}
|
||||||
|
onKeyPress={handleItemKeyPress}
|
||||||
|
target={item.target}
|
||||||
|
title={item.text}
|
||||||
|
className={
|
||||||
|
clsx({
|
||||||
|
'flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 cursor-pointer': true,
|
||||||
|
'text-danger-600 dark:text-danger-400': item.destructive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
||||||
|
|
||||||
|
<span className='truncate'>{item.text}</span>
|
||||||
|
|
||||||
|
{item.count ? (
|
||||||
|
<span className='ml-auto h-5 w-5 flex-none'>
|
||||||
|
<Counter count={item.count} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuItem;
|
299
app/soapbox/components/dropdown-menu/dropdown-menu.tsx
Normal file
299
app/soapbox/components/dropdown-menu/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
import { offset, Placement, useFloating } from '@floating-ui/react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { closeDropdownMenu, openDropdownMenu } from 'soapbox/actions/dropdown-menu';
|
||||||
|
import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import { isUserTouching } from 'soapbox/is-mobile';
|
||||||
|
|
||||||
|
import { IconButton, Portal } from '../ui';
|
||||||
|
|
||||||
|
import DropdownMenuItem, { MenuItem } from './dropdown-menu-item';
|
||||||
|
|
||||||
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
export type Menu = Array<MenuItem | null>;
|
||||||
|
|
||||||
|
interface IDropdownMenu {
|
||||||
|
children?: JSX.Element,
|
||||||
|
disabled?: boolean,
|
||||||
|
items: Menu,
|
||||||
|
onClose?: () => void,
|
||||||
|
onOpen?: () => void,
|
||||||
|
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
||||||
|
placement?: Placement,
|
||||||
|
src?: string,
|
||||||
|
status?: Status,
|
||||||
|
title?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
items,
|
||||||
|
onClose,
|
||||||
|
onOpen,
|
||||||
|
onShiftClick,
|
||||||
|
placement = 'top',
|
||||||
|
src = require('@tabler/icons/dots.svg'),
|
||||||
|
title = 'Menu',
|
||||||
|
...filteredProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const activeElement = useRef<Element | null>(null);
|
||||||
|
const target = useRef<Element>(null);
|
||||||
|
|
||||||
|
const isOnMobile = isUserTouching();
|
||||||
|
|
||||||
|
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({
|
||||||
|
placement,
|
||||||
|
middleware: [offset(12)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick: React.EventHandler<
|
||||||
|
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
> = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (onShiftClick && event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
onShiftClick(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
handleOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On mobile screens, let's replace the Popper dropdown with a Modal.
|
||||||
|
*/
|
||||||
|
const handleOpen = () => {
|
||||||
|
if (isOnMobile) {
|
||||||
|
dispatch(
|
||||||
|
openModal('ACTIONS', {
|
||||||
|
status: filteredProps.status,
|
||||||
|
actions: items,
|
||||||
|
onClick: handleItemClick,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(openDropdownMenu());
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onOpen) {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (activeElement.current && activeElement.current === target.current) {
|
||||||
|
(activeElement.current as any).focus();
|
||||||
|
activeElement.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOnMobile) {
|
||||||
|
dispatch(closeModal('ACTIONS'));
|
||||||
|
} else {
|
||||||
|
dispatch(closeDropdownMenu());
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
|
||||||
|
if (!isOpen) {
|
||||||
|
activeElement.current = document.activeElement;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case ' ':
|
||||||
|
case 'Enter':
|
||||||
|
handleMouseDown(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case ' ':
|
||||||
|
case 'Enter':
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
handleClick(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const i = Number(event.currentTarget.getAttribute('data-index'));
|
||||||
|
const item = items[i];
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const { action, to } = item;
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
if (typeof action === 'function') {
|
||||||
|
action(event);
|
||||||
|
} else if (to) {
|
||||||
|
history.push(to);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDocumentClick = (event: Event) => {
|
||||||
|
if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!refs.floating.current) return;
|
||||||
|
|
||||||
|
const items = Array.from(refs.floating.current.getElementsByTagName('a'));
|
||||||
|
const index = items.indexOf(document.activeElement as any);
|
||||||
|
|
||||||
|
let element = null;
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
element = items[index + 1] || items[0];
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
element = items[index - 1] || items[items.length - 1];
|
||||||
|
break;
|
||||||
|
case 'Tab':
|
||||||
|
if (e.shiftKey) {
|
||||||
|
element = items[index - 1] || items[items.length - 1];
|
||||||
|
} else {
|
||||||
|
element = items[index + 1] || items[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
element = items[0];
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
element = items[items.length - 1];
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
handleClose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('click', handleDocumentClick, false);
|
||||||
|
document.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleDocumentClick);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
document.removeEventListener('touchend', handleDocumentClick);
|
||||||
|
};
|
||||||
|
}, [refs.floating.current]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children ? (
|
||||||
|
React.cloneElement(children, {
|
||||||
|
disabled,
|
||||||
|
onClick: handleClick,
|
||||||
|
onMouseDown: handleMouseDown,
|
||||||
|
onKeyDown: handleButtonKeyDown,
|
||||||
|
onKeyPress: handleKeyPress,
|
||||||
|
ref: refs.setReference,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
disabled={disabled}
|
||||||
|
className={clsx({
|
||||||
|
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||||
|
'text-gray-700 dark:text-gray-500': isOpen,
|
||||||
|
})}
|
||||||
|
title={title}
|
||||||
|
src={src}
|
||||||
|
onClick={handleClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onKeyDown={handleButtonKeyDown}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
ref={refs.setReference}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOpen ? (
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
data-testid='dropdown-menu'
|
||||||
|
ref={refs.setFloating}
|
||||||
|
className={
|
||||||
|
clsx('relative z-[1001] w-56 rounded-md bg-white py-1 shadow-lg transition-opacity duration-100 focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
|
||||||
|
'opacity-0 pointer-events-none': !isOpen,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
position: strategy,
|
||||||
|
top: y ?? 0,
|
||||||
|
left: x ?? 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={idx}
|
||||||
|
item={item}
|
||||||
|
index={idx}
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Arrow */}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
clsx({
|
||||||
|
'absolute w-0 h-0 border-0 border-solid border-transparent': true,
|
||||||
|
'border-t-white dark:border-t-gray-900 -bottom-[5px] -ml-[5px] left-[calc(50%-2.5px)] border-t-[5px] border-x-[5px] border-b-0': placement === 'top',
|
||||||
|
'border-b-white dark:border-b-gray-900 -top-[5px] -ml-[5px] left-[calc(50%-2.5px)] border-t-0 border-x-[5px] border-b-[5px]': placement === 'bottom',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenu;
|
3
app/soapbox/components/dropdown-menu/index.ts
Normal file
3
app/soapbox/components/dropdown-menu/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { default } from './dropdown-menu';
|
||||||
|
export type { Menu } from './dropdown-menu';
|
||||||
|
export type { MenuItem } from './dropdown-menu-item';
|
|
@ -2,15 +2,13 @@ import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Stack } from 'soapbox/components/ui';
|
import { Stack } from 'soapbox/components/ui';
|
||||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||||
import ComposeButton from 'soapbox/features/ui/components/compose-button';
|
import ComposeButton from 'soapbox/features/ui/components/compose-button';
|
||||||
import { useAppSelector, useFeatures, useOwnAccount, useSettings } from 'soapbox/hooks';
|
import { useAppSelector, useFeatures, useOwnAccount, useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import DropdownMenu, { Menu } from './dropdown-menu';
|
||||||
import SidebarNavigationLink from './sidebar-navigation-link';
|
import SidebarNavigationLink from './sidebar-navigation-link';
|
||||||
|
|
||||||
import type { Menu } from 'soapbox/components/dropdown-menu';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
@ -185,7 +183,7 @@ const SidebarNavigation = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{menu.length > 0 && (
|
{menu.length > 0 && (
|
||||||
<DropdownMenu items={menu}>
|
<DropdownMenu items={menu} placement='top'>
|
||||||
<SidebarNavigationLink
|
<SidebarNavigationLink
|
||||||
icon={require('@tabler/icons/dots-circle-horizontal.svg')}
|
icon={require('@tabler/icons/dots-circle-horizontal.svg')}
|
||||||
text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />}
|
text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />}
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions
|
||||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport } from 'soapbox/actions/reports';
|
||||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||||
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||||
import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper';
|
import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper';
|
||||||
import { HStack } from 'soapbox/components/ui';
|
import { HStack } from 'soapbox/components/ui';
|
||||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
import { isLocal, isRemote } from 'soapbox/utils/accounts';
|
import { isLocal, isRemote } from 'soapbox/utils/accounts';
|
||||||
|
@ -617,13 +617,13 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(features.quotePosts && me) ? (
|
{(features.quotePosts && me) ? (
|
||||||
<DropdownMenuContainer
|
<DropdownMenu
|
||||||
items={reblogMenu}
|
items={reblogMenu}
|
||||||
disabled={!publicStatus}
|
disabled={!publicStatus}
|
||||||
onShiftClick={handleReblogClick}
|
onShiftClick={handleReblogClick}
|
||||||
>
|
>
|
||||||
{reblogButton}
|
{reblogButton}
|
||||||
</DropdownMenuContainer>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
reblogButton
|
reblogButton
|
||||||
)}
|
)}
|
||||||
|
@ -662,12 +662,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} status={status}>
|
<DropdownMenu items={menu} status={status}>
|
||||||
<StatusActionButton
|
<StatusActionButton
|
||||||
title={intl.formatMessage(messages.more)}
|
title={intl.formatMessage(messages.more)}
|
||||||
icon={require('@tabler/icons/dots.svg')}
|
icon={require('@tabler/icons/dots.svg')}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuContainer>
|
</DropdownMenu>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { deleteStatus } from 'soapbox/actions/statuses';
|
import { deleteStatus } from 'soapbox/actions/statuses';
|
||||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import { useAppDispatch, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
import { useAppDispatch, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
||||||
|
|
||||||
|
import DropdownMenu from '../dropdown-menu';
|
||||||
import { Button, HStack, Text } from '../ui';
|
import { Button, HStack, Text } from '../ui';
|
||||||
|
|
||||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
|
|
||||||
import HStack from '../hstack/hstack';
|
import HStack from '../hstack/hstack';
|
||||||
import Icon from '../icon/icon';
|
import Icon from '../icon/icon';
|
||||||
|
|
|
@ -38,6 +38,7 @@ export {
|
||||||
} from './menu/menu';
|
} from './menu/menu';
|
||||||
export { default as Modal } from './modal/modal';
|
export { default as Modal } from './modal/modal';
|
||||||
export { default as PhoneInput } from './phone-input/phone-input';
|
export { default as PhoneInput } from './phone-input/phone-input';
|
||||||
|
export { default as Portal } from './portal/portal';
|
||||||
export { default as ProgressBar } from './progress-bar/progress-bar';
|
export { default as ProgressBar } from './progress-bar/progress-bar';
|
||||||
export { default as RadioButton } from './radio-button/radio-button';
|
export { default as RadioButton } from './radio-button/radio-button';
|
||||||
export { default as Select } from './select/select';
|
export { default as Select } from './select/select';
|
||||||
|
|
16
app/soapbox/components/ui/portal/portal.tsx
Normal file
16
app/soapbox/components/ui/portal/portal.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
interface IPortal {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal
|
||||||
|
*/
|
||||||
|
const Portal: React.FC<IPortal> = ({ children }) => ReactDOM.createPortal(
|
||||||
|
children,
|
||||||
|
document.querySelector('#soapbox') as HTMLDivElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Portal;
|
|
@ -1,46 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown-menu';
|
|
||||||
import { openModal, closeModal } from '../actions/modals';
|
|
||||||
import DropdownMenu from '../components/dropdown-menu';
|
|
||||||
import { isUserTouching } from '../is-mobile';
|
|
||||||
|
|
||||||
import type { Dispatch } from 'redux';
|
|
||||||
import type { DropdownPlacement, IDropdown } from 'soapbox/components/dropdown-menu';
|
|
||||||
import type { RootState } from 'soapbox/store';
|
|
||||||
|
|
||||||
const mapStateToProps = (state: RootState) => ({
|
|
||||||
isModalOpen: Boolean(state.modals.size && state.modals.last()!.modalType === 'ACTIONS'),
|
|
||||||
dropdownPlacement: state.dropdown_menu.placement,
|
|
||||||
openDropdownId: state.dropdown_menu.openId,
|
|
||||||
openedViaKeyboard: state.dropdown_menu.keyboard,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch, { status, items, ...filteredProps }: Partial<IDropdown>) => ({
|
|
||||||
onOpen(
|
|
||||||
id: number,
|
|
||||||
onItemClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
|
||||||
dropdownPlacement: DropdownPlacement,
|
|
||||||
keyboard: boolean,
|
|
||||||
) {
|
|
||||||
dispatch(isUserTouching() ? openModal('ACTIONS', {
|
|
||||||
status,
|
|
||||||
actions: items,
|
|
||||||
onClick: onItemClick,
|
|
||||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
|
|
||||||
|
|
||||||
if (filteredProps.onOpen) {
|
|
||||||
filteredProps.onOpen(id, onItemClick, dropdownPlacement, keyboard);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClose(id: number) {
|
|
||||||
dispatch(closeModal('ACTIONS'));
|
|
||||||
dispatch(closeDropdownMenu(id));
|
|
||||||
|
|
||||||
if (filteredProps.onClose) {
|
|
||||||
filteredProps.onClose(id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
|
|
|
@ -661,7 +661,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
return <MenuDivider key={idx} />;
|
return <MenuDivider key={idx} />;
|
||||||
} else {
|
} else {
|
||||||
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
||||||
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.newTab ? '_blank' : '_self' };
|
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.target || '_self' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp key={idx} {...itemProps} className='group'>
|
<Comp key={idx} {...itemProps} className='group'>
|
||||||
|
|
|
@ -2,10 +2,10 @@ import React from 'react';
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { deleteStatusModal } from 'soapbox/actions/moderation';
|
import { deleteStatusModal } from 'soapbox/actions/moderation';
|
||||||
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import StatusContent from 'soapbox/components/status-content';
|
import StatusContent from 'soapbox/components/status-content';
|
||||||
import StatusMedia from 'soapbox/components/status-media';
|
import StatusMedia from 'soapbox/components/status-media';
|
||||||
import { HStack, Stack } from 'soapbox/components/ui';
|
import { HStack, Stack } from 'soapbox/components/ui';
|
||||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { AdminReport, Status } from 'soapbox/types/entities';
|
import type { AdminReport, Status } from 'soapbox/types/entities';
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { closeReports } from 'soapbox/actions/admin';
|
import { closeReports } from 'soapbox/actions/admin';
|
||||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||||
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||||
import { Accordion, Avatar, Button, Stack, HStack, Text } from 'soapbox/components/ui';
|
import { Accordion, Avatar, Button, Stack, HStack, Text } from 'soapbox/components/ui';
|
||||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { makeGetReport } from 'soapbox/selectors';
|
import { makeGetReport } from 'soapbox/selectors';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
|
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
|
||||||
import { Avatar, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
import { Avatar, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
||||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
import { IChat, useChatActions } from 'soapbox/queries/chats';
|
import { IChat, useChatActions } from 'soapbox/queries/chats';
|
||||||
|
@ -115,14 +115,14 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
|
||||||
<HStack alignItems='center' space={2}>
|
<HStack alignItems='center' space={2}>
|
||||||
{features.chatsDelete && (
|
{features.chatsDelete && (
|
||||||
<div className='hidden text-gray-600 hover:text-gray-100 group-hover:block'>
|
<div className='hidden text-gray-600 hover:text-gray-100 group-hover:block'>
|
||||||
<DropdownMenuContainer items={menu}>
|
<DropdownMenu items={menu}>
|
||||||
<IconButton
|
<IconButton
|
||||||
src={require('@tabler/icons/dots.svg')}
|
src={require('@tabler/icons/dots.svg')}
|
||||||
title='Settings'
|
title='Settings'
|
||||||
className='text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
|
className='text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
|
||||||
iconClassName='w-4 h-4'
|
iconClassName='w-4 h-4'
|
||||||
/>
|
/>
|
||||||
</DropdownMenuContainer>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport } from 'soapbox/actions/reports';
|
||||||
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji/emoji';
|
||||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||||
|
@ -230,7 +230,7 @@ const ChatMessage = (props: IChatMessage) => {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{menu.length > 0 && (
|
{menu.length > 0 && (
|
||||||
<DropdownMenuContainer
|
<DropdownMenu
|
||||||
items={menu}
|
items={menu}
|
||||||
onOpen={() => setIsMenuOpen(true)}
|
onOpen={() => setIsMenuOpen(true)}
|
||||||
onClose={() => setIsMenuOpen(false)}
|
onClose={() => setIsMenuOpen(false)}
|
||||||
|
@ -248,7 +248,7 @@ const ChatMessage = (props: IChatMessage) => {
|
||||||
className='h-4 w-4'
|
className='h-4 w-4'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuContainer>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -396,7 +396,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||||
return <MenuDivider key={idx} />;
|
return <MenuDivider key={idx} />;
|
||||||
} else {
|
} else {
|
||||||
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
||||||
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.newTab ? '_blank' : '_self' };
|
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.target || '_self' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp key={idx} {...itemProps} className='group'>
|
<Comp key={idx} {...itemProps} className='group'>
|
||||||
|
|
|
@ -180,7 +180,7 @@ const GroupMember: React.FC<IGroupMember> = ({ accountId, accountRole, groupId,
|
||||||
return <MenuDivider key={idx} />;
|
return <MenuDivider key={idx} />;
|
||||||
} else {
|
} else {
|
||||||
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
||||||
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.newTab ? '_blank' : '_self' };
|
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.target || '_self' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp key={idx} {...itemProps} className='group'>
|
<Comp key={idx} {...itemProps} className='group'>
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { v4 as uuidv4 } from 'uuid';
|
||||||
import { updateSoapboxConfig } from 'soapbox/actions/admin';
|
import { updateSoapboxConfig } from 'soapbox/actions/admin';
|
||||||
import { getHost } from 'soapbox/actions/instance';
|
import { getHost } from 'soapbox/actions/instance';
|
||||||
import { fetchSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { fetchSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { Button, Column, Form, FormActions } from 'soapbox/components/ui';
|
import { Button, Column, Form, FormActions } from 'soapbox/components/ui';
|
||||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import ColorWithPicker from 'soapbox/features/soapbox-config/components/color-with-picker';
|
import ColorWithPicker from 'soapbox/features/soapbox-config/components/color-with-picker';
|
||||||
import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||||
|
@ -194,7 +194,7 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<DropdownMenuContainer
|
<DropdownMenu
|
||||||
items={[{
|
items={[{
|
||||||
text: intl.formatMessage(messages.restore),
|
text: intl.formatMessage(messages.restore),
|
||||||
action: restoreDefaultTheme,
|
action: restoreDefaultTheme,
|
||||||
|
|
|
@ -4,8 +4,8 @@ import React from 'react';
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import { Widget } from 'soapbox/components/ui';
|
import { Widget } from 'soapbox/components/ui';
|
||||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
|
||||||
import InstanceRestrictions from 'soapbox/features/federation-restrictions/components/instance-restrictions';
|
import InstanceRestrictions from 'soapbox/features/federation-restrictions/components/instance-restrictions';
|
||||||
import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||||
import { makeGetRemoteInstance } from 'soapbox/selectors';
|
import { makeGetRemoteInstance } from 'soapbox/selectors';
|
||||||
|
|
|
@ -26,7 +26,7 @@ const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClo
|
||||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { icon = null, text, meta = null, active = false, href = '#', isLogout, destructive } = action;
|
const { icon = null, text, meta = null, active = false, href = '#', destructive } = action;
|
||||||
|
|
||||||
const Comp = href === '#' ? 'button' : 'a';
|
const Comp = href === '#' ? 'button' : 'a';
|
||||||
const compProps = href === '#' ? { onClick: onClick } : { href: href, rel: 'noopener' };
|
const compProps = href === '#' ? { onClick: onClick } : { href: href, rel: 'noopener' };
|
||||||
|
@ -38,7 +38,6 @@ const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClo
|
||||||
space={2.5}
|
space={2.5}
|
||||||
data-index={i}
|
data-index={i}
|
||||||
className={clsx('w-full', { active, destructive })}
|
className={clsx('w-full', { active, destructive })}
|
||||||
data-method={isLogout ? 'delete' : null}
|
|
||||||
element={Comp}
|
element={Comp}
|
||||||
>
|
>
|
||||||
{icon && <Icon title={text} src={icon} role='presentation' tabIndex={-1} />}
|
{icon && <Icon title={text} src={icon} role='presentation' tabIndex={-1} />}
|
||||||
|
|
|
@ -357,7 +357,7 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const vapidKey = useAppSelector(state => getVapidKey(state));
|
const vapidKey = useAppSelector(state => getVapidKey(state));
|
||||||
|
|
||||||
const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.openId !== null);
|
const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen);
|
||||||
const accessToken = useAppSelector(state => getAccessToken(state));
|
const accessToken = useAppSelector(state => getAccessToken(state));
|
||||||
const streamingUrl = instance.urls.get('streaming_api');
|
const streamingUrl = instance.urls.get('streaming_api');
|
||||||
const standalone = useAppSelector(isStandalone);
|
const standalone = useAppSelector(isStandalone);
|
||||||
|
|
|
@ -6,12 +6,9 @@ import {
|
||||||
} from '../actions/dropdown-menu';
|
} from '../actions/dropdown-menu';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { DropdownPlacement } from 'soapbox/components/dropdown-menu';
|
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
const ReducerRecord = ImmutableRecord({
|
||||||
openId: null as number | null,
|
isOpen: false,
|
||||||
placement: null as any as DropdownPlacement,
|
|
||||||
keyboard: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type State = ReturnType<typeof ReducerRecord>;
|
type State = ReturnType<typeof ReducerRecord>;
|
||||||
|
@ -19,9 +16,9 @@ type State = ReturnType<typeof ReducerRecord>;
|
||||||
export default function dropdownMenu(state: State = ReducerRecord(), action: AnyAction) {
|
export default function dropdownMenu(state: State = ReducerRecord(), action: AnyAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case DROPDOWN_MENU_OPEN:
|
case DROPDOWN_MENU_OPEN:
|
||||||
return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard });
|
return state.set('isOpen', true);
|
||||||
case DROPDOWN_MENU_CLOSE:
|
case DROPDOWN_MENU_CLOSE:
|
||||||
return state.openId === action.id ? state.set('openId', null) : state;
|
return state.set('isOpen', false);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -467,7 +467,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* Whether client settings can be retrieved from the API.
|
* Whether client settings can be retrieved from the API.
|
||||||
* @see GET /api/pleroma/frontend_configurations
|
* @see GET /api/pleroma/frontend_configurations
|
||||||
*/
|
*/
|
||||||
frontendConfigurations: v.software === PLEROMA,
|
frontendConfigurations: false, // v.software === PLEROMA,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups.
|
* Groups.
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
// COMPONENTS
|
// COMPONENTS
|
||||||
@import 'components/buttons';
|
@import 'components/buttons';
|
||||||
@import 'components/dropdown-menu';
|
|
||||||
@import 'components/modal';
|
@import 'components/modal';
|
||||||
@import 'components/compose-form';
|
@import 'components/compose-form';
|
||||||
@import 'components/emoji-reacts';
|
@import 'components/emoji-reacts';
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
.dropdown-menu {
|
|
||||||
@apply absolute bg-white dark:bg-gray-900 z-[1001] rounded-md shadow-lg py-1 w-56 dark:ring-2 dark:ring-primary-700 focus:outline-none;
|
|
||||||
|
|
||||||
&.left { transform-origin: 100% 50%; }
|
|
||||||
&.top { transform-origin: 50% 100%; }
|
|
||||||
&.bottom { transform-origin: 50% 0; }
|
|
||||||
&.right { transform-origin: 0 50%; }
|
|
||||||
|
|
||||||
&__arrow {
|
|
||||||
@apply absolute w-0 h-0;
|
|
||||||
border: 0 solid transparent;
|
|
||||||
|
|
||||||
&.left {
|
|
||||||
@apply border-l-white dark:border-l-gray-900;
|
|
||||||
right: -5px;
|
|
||||||
margin-top: -5px;
|
|
||||||
border-width: 5px 0 5px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top {
|
|
||||||
@apply border-t-white dark:border-t-gray-900;
|
|
||||||
bottom: -5px;
|
|
||||||
margin-left: -5px;
|
|
||||||
border-width: 5px 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom {
|
|
||||||
@apply border-b-white dark:border-b-gray-900;
|
|
||||||
top: -5px;
|
|
||||||
margin-left: -5px;
|
|
||||||
border-width: 0 5px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.right {
|
|
||||||
@apply border-r-white dark:border-r-gray-900;
|
|
||||||
left: -5px;
|
|
||||||
margin-top: -5px;
|
|
||||||
border-width: 5px 5px 5px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
@apply focus-within:ring-primary-500 focus-within:ring-2;
|
|
||||||
|
|
||||||
a {
|
|
||||||
@apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 cursor-pointer;
|
|
||||||
|
|
||||||
> .svg-icon:first-child {
|
|
||||||
@apply h-5 w-5 mr-2.5 transition-none;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@apply stroke-[1.5px] transition-none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.destructive a {
|
|
||||||
@apply text-danger-600 dark:text-danger-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__separator {
|
|
||||||
@apply block my-2 h-[1px] bg-gray-100 dark:bg-gray-800;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -46,6 +46,7 @@
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@floating-ui/react": "^0.19.1",
|
||||||
"@fontsource/inter": "^4.5.1",
|
"@fontsource/inter": "^4.5.1",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
|
|
40
yarn.lock
40
yarn.lock
|
@ -1722,6 +1722,34 @@
|
||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
|
"@floating-ui/core@^1.2.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.2.0.tgz#ae7ae7923d41f3d84cb2fd88740a89436610bbec"
|
||||||
|
integrity sha512-GHUXPEhMEmTpnpIfesFA2KAoMJPb1SPQw964tToQwt+BbGXdhqTCWT1rOb0VURGylsxsYxiGMnseJ3IlclVpVA==
|
||||||
|
|
||||||
|
"@floating-ui/dom@^1.1.1":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.2.0.tgz#a60212069cc58961c478037c30eba4b191c75316"
|
||||||
|
integrity sha512-QXzg57o1cjLz3cGETzKXjI3kx1xyS49DW9l7kV2jw2c8Yftd434t2hllX0sVGn2Q8MtcW/4pNm8bfE1/4n6mng==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core" "^1.2.0"
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@^1.2.2":
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.2.2.tgz#ed256992fd44fcfcddc96da68b4b92f123d61871"
|
||||||
|
integrity sha512-DbmFBLwFrZhtXgCI2ra7wXYT8L2BN4/4AMQKyu05qzsVji51tXOfF36VE2gpMB6nhJGHa85PdEg75FB4+vnLFQ==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom" "^1.1.1"
|
||||||
|
|
||||||
|
"@floating-ui/react@^0.19.1":
|
||||||
|
version "0.19.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.1.tgz#bcaeaf3856dfeea388816f7e66750cab26208376"
|
||||||
|
integrity sha512-h7hr53rLp+VVvWvbu0dOBvGsLeeZwn1DTLIllIaLYjGWw20YhAgEqegHU+nc7BJ30ttxq4Sq6hqARm0ne6chXQ==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom" "^1.2.2"
|
||||||
|
aria-hidden "^1.1.3"
|
||||||
|
tabbable "^6.0.1"
|
||||||
|
|
||||||
"@fontsource/inter@^4.5.1":
|
"@fontsource/inter@^4.5.1":
|
||||||
version "4.5.1"
|
version "4.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-4.5.1.tgz#058d8a02354f3c78e369d452c15d33557ec1b705"
|
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-4.5.1.tgz#058d8a02354f3c78e369d452c15d33557ec1b705"
|
||||||
|
@ -5381,6 +5409,13 @@ argparse@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
|
aria-hidden@^1.1.3:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.2.tgz#8c4f7cc88d73ca42114106fdf6f47e68d31475b8"
|
||||||
|
integrity sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
aria-query@^4.2.2:
|
aria-query@^4.2.2:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
|
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
|
||||||
|
@ -16517,6 +16552,11 @@ tabbable@^5.3.3:
|
||||||
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf"
|
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf"
|
||||||
integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==
|
integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==
|
||||||
|
|
||||||
|
tabbable@^6.0.1:
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.0.1.tgz#427a09b13c83ae41eed3e88abb76a4af28bde1a6"
|
||||||
|
integrity sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA==
|
||||||
|
|
||||||
table@^6.8.1:
|
table@^6.8.1:
|
||||||
version "6.8.1"
|
version "6.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf"
|
resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf"
|
||||||
|
|
Loading…
Reference in a new issue