pl-fe: replace react-overlays with @floating-ui/react
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -3,12 +3,14 @@ import {
|
||||
autoPlacement,
|
||||
FloatingArrow,
|
||||
offset,
|
||||
shift,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useHover,
|
||||
useInteractions,
|
||||
useTransitionStyles,
|
||||
type OffsetOptions,
|
||||
} from '@floating-ui/react';
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef, useState } from 'react';
|
||||
@ -25,6 +27,7 @@ interface IPopover {
|
||||
interaction?: 'click' | 'hover';
|
||||
/** Add a class to the reference (trigger) element */
|
||||
referenceElementClassName?: string;
|
||||
offsetOptions?: OffsetOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,14 +36,12 @@ interface IPopover {
|
||||
* Similar to tooltip, but requires a click and is used for larger blocks
|
||||
* of information.
|
||||
*/
|
||||
const Popover: React.FC<IPopover> = (props) => {
|
||||
const { children, content, referenceElementClassName, interaction = 'hover', isFlush = false } = props;
|
||||
|
||||
const Popover: React.FC<IPopover> = ({ children, content, referenceElementClassName, interaction = 'hover', isFlush = false, offsetOptions = 10 }) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const arrowRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
const { x, y, strategy, refs, context } = useFloating({
|
||||
const { x, y, strategy, refs, context, placement } = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
placement: 'top',
|
||||
@ -48,7 +49,10 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||
autoPlacement({
|
||||
allowedPlacements: ['top', 'bottom'],
|
||||
}),
|
||||
offset(10),
|
||||
offset(offsetOptions),
|
||||
shift({
|
||||
padding: 8,
|
||||
}),
|
||||
arrow({
|
||||
element: arrowRef,
|
||||
}),
|
||||
@ -59,10 +63,11 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
transform: 'scale(0.8)',
|
||||
transformOrigin: placement === 'bottom' ? 'top' : 'bottom',
|
||||
},
|
||||
duration: {
|
||||
open: 200,
|
||||
close: 200,
|
||||
open: 100,
|
||||
close: 100,
|
||||
},
|
||||
});
|
||||
|
||||
@ -82,6 +87,7 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||
ref: refs.setReference,
|
||||
...getReferenceProps(),
|
||||
className: clsx(children.props.className, referenceElementClassName),
|
||||
'aria-expanded': isOpen,
|
||||
})}
|
||||
|
||||
{(isMounted) && (
|
||||
@ -94,20 +100,23 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||
left: x ?? 0,
|
||||
...styles,
|
||||
}}
|
||||
className={
|
||||
clsx({
|
||||
'z-40 rounded-lg bg-white shadow-2xl dark:bg-gray-900 dark:ring-2 dark:ring-primary-700': true,
|
||||
'p-6': !isFlush,
|
||||
})
|
||||
}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{content}
|
||||
<div
|
||||
className={
|
||||
clsx(
|
||||
'z-40 overflow-hidden rounded-lg bg-white shadow-2xl dark:bg-gray-900 dark:ring-2 dark:ring-primary-700',
|
||||
{ 'p-6': !isFlush },
|
||||
)
|
||||
}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{content}
|
||||
|
||||
</div>
|
||||
<FloatingArrow
|
||||
ref={arrowRef}
|
||||
context={context}
|
||||
className='-ml-2 fill-white dark:hidden' /** -ml-2 to fix offcenter arrow */
|
||||
className='fill-white dark:fill-primary-700'
|
||||
tipRadius={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,49 +1,31 @@
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { SketchPicker, ColorChangeHandler } from 'react-color';
|
||||
import React from 'react';
|
||||
import { SketchPicker, type ColorChangeHandler } from 'react-color';
|
||||
|
||||
import { isMobile } from 'pl-fe/is-mobile';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
import { Popover } from 'pl-fe/components/ui';
|
||||
|
||||
interface IColorPicker {
|
||||
style?: React.CSSProperties;
|
||||
value: string;
|
||||
onChange: ColorChangeHandler;
|
||||
onClose: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ColorPicker: React.FC<IColorPicker> = ({ style, value, onClose, onChange }) => {
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleDocumentClick = (e: MouseEvent | TouchEvent) => {
|
||||
if (node.current && !node.current.contains(e.target as HTMLElement)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleDocumentClick, false);
|
||||
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', handleDocumentClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const pickerStyle: React.CSSProperties = {
|
||||
...style,
|
||||
marginLeft: isMobile(window.innerWidth) ? '20px' : '12px',
|
||||
position: 'absolute',
|
||||
zIndex: 1000,
|
||||
};
|
||||
|
||||
return (
|
||||
<div id='SketchPickerContainer' ref={node} style={pickerStyle}>
|
||||
<SketchPicker color={value} disableAlpha onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ColorPicker: React.FC<IColorPicker> = ({ value, onChange, className }) => (
|
||||
<div className={className}>
|
||||
<Popover
|
||||
interaction='click'
|
||||
content={
|
||||
<SketchPicker color={value} disableAlpha onChange={onChange} />
|
||||
}
|
||||
isFlush
|
||||
>
|
||||
<div
|
||||
className='size-full'
|
||||
role='presentation'
|
||||
style={{ background: value }}
|
||||
title={value}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
export { ColorPicker as default };
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
// @ts-ignore: TODO: upgrade react-overlays. v3.1 and above have TS definitions
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
|
||||
import { isMobile } from 'pl-fe/is-mobile';
|
||||
|
||||
import ColorPicker from './color-picker';
|
||||
|
||||
import type { ColorChangeHandler } from 'react-color';
|
||||
|
||||
interface IColorWithPicker {
|
||||
value: string;
|
||||
onChange: ColorChangeHandler;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ColorWithPicker: React.FC<IColorWithPicker> = ({ value, onChange, className }) => {
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const [active, setActive] = useState(false);
|
||||
const [placement, setPlacement] = useState<string | null>(null);
|
||||
|
||||
const hidePicker = () => {
|
||||
setActive(false);
|
||||
};
|
||||
|
||||
const showPicker = () => {
|
||||
setActive(true);
|
||||
setPlacement(isMobile(window.innerWidth) ? 'bottom' : 'right');
|
||||
};
|
||||
|
||||
const onToggle: React.MouseEventHandler = (e) => {
|
||||
if (active) {
|
||||
hidePicker();
|
||||
} else {
|
||||
showPicker();
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
ref={node}
|
||||
className='size-full'
|
||||
role='presentation'
|
||||
style={{ background: value }}
|
||||
title={value}
|
||||
onClick={onToggle}
|
||||
/>
|
||||
|
||||
<Overlay show={active} placement={placement} target={node.current}>
|
||||
<ColorPicker value={value} onChange={onChange} onClose={hidePicker} />
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ColorWithPicker as default };
|
||||
@ -1,9 +1,8 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
// @ts-ignore
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
|
||||
import ForkAwesomeIcon from 'pl-fe/components/fork-awesome-icon';
|
||||
import { Popover } from 'pl-fe/components/ui';
|
||||
|
||||
import IconPickerMenu from './icon-picker-menu';
|
||||
|
||||
@ -19,65 +18,32 @@ interface IIconPickerDropdown {
|
||||
const IconPickerDropdown: React.FC<IIconPickerDropdown> = ({ value, onPickIcon }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [active, setActive] = useState(false);
|
||||
const [placement, setPlacement] = useState<'bottom' | 'top'>();
|
||||
|
||||
const target = useRef(null);
|
||||
|
||||
const onShowDropdown: React.KeyboardEventHandler<HTMLDivElement> = ({ target }) => {
|
||||
setActive(true);
|
||||
|
||||
const { top } = (target as any).getBoundingClientRect();
|
||||
setPlacement(top * 2 < innerHeight ? 'bottom' : 'top');
|
||||
};
|
||||
|
||||
const onHideDropdown = () => {
|
||||
setActive(false);
|
||||
};
|
||||
|
||||
const onToggle: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
if (!e.key || e.key === 'Enter') {
|
||||
if (active) {
|
||||
onHideDropdown();
|
||||
} else {
|
||||
onShowDropdown(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onHideDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
const forkAwesomeIcons = require('../forkawesome.json');
|
||||
|
||||
return (
|
||||
<div onKeyDown={handleKeyDown}>
|
||||
<div
|
||||
ref={target}
|
||||
className='flex size-[38px] cursor-pointer items-center justify-center text-lg'
|
||||
title={title}
|
||||
aria-label={title}
|
||||
aria-expanded={active}
|
||||
role='button'
|
||||
onClick={onToggle as any as React.MouseEventHandler<HTMLDivElement>}
|
||||
onKeyDown={onToggle}
|
||||
tabIndex={0}
|
||||
<div>
|
||||
<Popover
|
||||
interaction='click'
|
||||
content={
|
||||
<IconPickerMenu
|
||||
icons={forkAwesomeIcons}
|
||||
onPick={onPickIcon}
|
||||
/>
|
||||
}
|
||||
isFlush
|
||||
>
|
||||
<ForkAwesomeIcon id={value} />
|
||||
</div>
|
||||
<div
|
||||
className='flex size-[38px] cursor-pointer items-center justify-center text-lg'
|
||||
title={title}
|
||||
aria-label={title}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForkAwesomeIcon id={value} />
|
||||
</div>
|
||||
|
||||
<Overlay show={active} placement={placement} target={target.current}>
|
||||
<IconPickerMenu
|
||||
icons={forkAwesomeIcons}
|
||||
onClose={onHideDropdown}
|
||||
onPick={onPickIcon}
|
||||
/>
|
||||
</Overlay>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Text } from 'pl-fe/components/ui';
|
||||
@ -9,40 +8,16 @@ const messages = defineMessages({
|
||||
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
||||
});
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
interface IIconPickerMenu {
|
||||
icons: Record<string, Array<string>>;
|
||||
onClose: () => void;
|
||||
onPick: (icon: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onClose, onPick, style }) => {
|
||||
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onPick, style }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const node = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleDocumentClick = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
if (node.current && !node.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleDocumentClick, false);
|
||||
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', handleDocumentClick, listenerOptions as any);
|
||||
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setRef = (c: HTMLDivElement) => {
|
||||
node.current = c;
|
||||
|
||||
if (!c) return;
|
||||
|
||||
// Nice and dirty hack to display the icons
|
||||
@ -54,7 +29,6 @@ const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onClose, onPick, sty
|
||||
};
|
||||
|
||||
const handleClick = (icon: string) => {
|
||||
onClose();
|
||||
onPick(icon);
|
||||
};
|
||||
|
||||
@ -79,16 +53,14 @@ const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onClose, onPick, sty
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-[101] -my-0.5'
|
||||
style={{ transform: 'translateX(calc(-1 * env(safe-area-inset-right)))', ...style }}
|
||||
className='h-[270px] overflow-x-hidden overflow-y-scroll rounded bg-white p-1.5 text-gray-900 dark:bg-primary-900 dark:text-gray-100'
|
||||
aria-label={title}
|
||||
ref={setRef}
|
||||
>
|
||||
<div className='h-[270px] overflow-x-hidden overflow-y-scroll rounded bg-white p-1.5 text-gray-900 dark:bg-primary-900 dark:text-gray-100' aria-label={title}>
|
||||
<Text className='px-1.5 py-1'><FormattedMessage id='icon_button.icons' defaultMessage='Icons' /></Text>
|
||||
<ul className='grid grid-cols-8'>
|
||||
{Object.values(icons).flat().map(icon => renderIcon(icon))}
|
||||
</ul>
|
||||
</div>
|
||||
<Text className='px-1.5 py-1'><FormattedMessage id='icon_button.icons' defaultMessage='Icons' /></Text>
|
||||
<ul className='grid grid-cols-8'>
|
||||
{Object.values(icons).flat().map(icon => renderIcon(icon))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import ColorWithPicker from 'pl-fe/features/pl-fe-config/components/color-with-picker';
|
||||
import ColorPicker from 'pl-fe/features/pl-fe-config/components/color-picker';
|
||||
|
||||
import type { ColorChangeHandler } from 'react-color';
|
||||
|
||||
@ -17,7 +17,7 @@ const Color: React.FC<IColor> = ({ color, onChange }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ColorWithPicker
|
||||
<ColorPicker
|
||||
className='size-full'
|
||||
value={color}
|
||||
onChange={handleChange}
|
||||
|
||||
@ -8,7 +8,7 @@ import { fetchPlFeConfig } from 'pl-fe/actions/pl-fe';
|
||||
import DropdownMenu from 'pl-fe/components/dropdown-menu';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
import { Button, Column, Form, FormActions } from 'pl-fe/components/ui';
|
||||
import ColorWithPicker from 'pl-fe/features/pl-fe-config/components/color-with-picker';
|
||||
import ColorPicker from 'pl-fe/features/pl-fe-config/components/color-picker';
|
||||
import { useAppDispatch, useAppSelector, usePlFeConfig } from 'pl-fe/hooks';
|
||||
import { normalizePlFeConfig } from 'pl-fe/normalizers';
|
||||
import toast from 'pl-fe/toast';
|
||||
@ -265,7 +265,7 @@ const ColorListItem: React.FC<IColorListItem> = ({ label, value, onChange }) =>
|
||||
|
||||
return (
|
||||
<ListItem label={label}>
|
||||
<ColorWithPicker
|
||||
<ColorPicker
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className='h-8 w-10 overflow-hidden rounded-md'
|
||||
|
||||
Reference in New Issue
Block a user