typescript
This commit is contained in:
@@ -1,17 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { ElementType, ReactNode } from 'react';
|
||||
import composeClassName from './classNames';
|
||||
|
||||
const PanelHeader = ({
|
||||
type TitleProps = Record<string, unknown>;
|
||||
|
||||
interface PanelHeaderProps {
|
||||
className?: string;
|
||||
leading?: ReactNode;
|
||||
title?: ReactNode;
|
||||
titleTag?: ElementType;
|
||||
titleProps?: TitleProps;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
const PanelHeader: React.FC<PanelHeaderProps> = ({
|
||||
className = '',
|
||||
leading = null,
|
||||
title = null,
|
||||
titleTag = 'h2',
|
||||
titleTag: HeadingTag = 'h2',
|
||||
titleProps = {},
|
||||
actions = null,
|
||||
}) => {
|
||||
const headerClassName = composeClassName('panel-header', className);
|
||||
|
||||
const renderTitle = () => {
|
||||
const renderTitle = (): ReactNode => {
|
||||
if (title == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -25,14 +36,14 @@ const PanelHeader = ({
|
||||
);
|
||||
}
|
||||
|
||||
const mergedClassName = composeClassName('panel-header__title', title.props.className || '');
|
||||
const existingClassName = (title.props as { className?: string })?.className ?? '';
|
||||
const mergedClassName = composeClassName('panel-header__title', existingClassName);
|
||||
return React.cloneElement(title, {
|
||||
...titleProps,
|
||||
className: mergedClassName,
|
||||
});
|
||||
}
|
||||
|
||||
const HeadingTag = titleTag;
|
||||
return (
|
||||
<HeadingTag className="panel-header__title" {...titleProps}>
|
||||
{title}
|
||||
@@ -1,12 +1,22 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { CSSProperties, ReactNode, FormEvent, MutableRefObject } from 'react';
|
||||
import { PlusIcon } from './icons';
|
||||
import useFloatingMenu from './useFloatingMenu';
|
||||
|
||||
const normalizeOption = (option, index) => {
|
||||
type QuickAddOption = string | number | { id?: string | number; label?: string; name?: string; [key: string]: unknown };
|
||||
|
||||
interface NormalizedOption {
|
||||
id?: string | number;
|
||||
label: string;
|
||||
original: QuickAddOption;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const normalizeOption = (option: QuickAddOption | null | undefined, index: number): NormalizedOption | null => {
|
||||
if (option == null) {
|
||||
return null;
|
||||
}
|
||||
if (option && typeof option === 'object') {
|
||||
if (typeof option === 'object') {
|
||||
const label = option.label ?? option.name;
|
||||
if (label == null) {
|
||||
return null;
|
||||
@@ -27,6 +37,36 @@ const normalizeOption = (option, index) => {
|
||||
};
|
||||
};
|
||||
|
||||
interface FloatingMenuState {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
close: () => void;
|
||||
menuRef: MutableRefObject<HTMLDivElement | null>;
|
||||
menuStyle: CSSProperties | null;
|
||||
updatePosition: () => void;
|
||||
}
|
||||
|
||||
type CSSVarStyle = CSSProperties & Record<string, string | number>;
|
||||
|
||||
export interface QuickAddMenuProps {
|
||||
onSelectOption?: (value: QuickAddOption, normalized: NormalizedOption) => Promise<void> | void;
|
||||
onCreate?: (value: string) => Promise<void> | void;
|
||||
options?: QuickAddOption[];
|
||||
placeholder?: string;
|
||||
createLabel?: string;
|
||||
emptyMessage?: string;
|
||||
className?: string;
|
||||
triggerAriaLabel?: string;
|
||||
triggerTitle?: string;
|
||||
renderOption?: (value: QuickAddOption, normalized: NormalizedOption) => ReactNode;
|
||||
menuMinWidth?: number;
|
||||
triggerClassName?: string;
|
||||
triggerContent?: ReactNode;
|
||||
disabled?: boolean;
|
||||
align?: 'start' | 'center' | 'end' | (string & {});
|
||||
positionStrategy?: 'fixed' | 'absolute' | (string & {});
|
||||
}
|
||||
|
||||
const QuickAddMenu = ({
|
||||
onSelectOption,
|
||||
onCreate,
|
||||
@@ -44,7 +84,7 @@ const QuickAddMenu = ({
|
||||
disabled = false,
|
||||
align = 'start',
|
||||
positionStrategy = 'fixed',
|
||||
}) => {
|
||||
}: QuickAddMenuProps) => {
|
||||
const anchorRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const [query, setQuery] = useState('');
|
||||
@@ -63,7 +103,7 @@ const QuickAddMenu = ({
|
||||
matchAnchorWidth: false,
|
||||
align,
|
||||
positionStrategy,
|
||||
});
|
||||
}) as FloatingMenuState;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled && isOpen) {
|
||||
@@ -87,11 +127,11 @@ const QuickAddMenu = ({
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [isOpen, updatePosition]);
|
||||
|
||||
const normalizedOptions = useMemo(
|
||||
const normalizedOptions = useMemo<NormalizedOption[]>(
|
||||
() =>
|
||||
options
|
||||
.map((option, index) => normalizeOption(option, index))
|
||||
.filter(Boolean),
|
||||
.filter((option): option is NormalizedOption => Boolean(option)),
|
||||
[options],
|
||||
);
|
||||
|
||||
@@ -104,7 +144,7 @@ const QuickAddMenu = ({
|
||||
}, [normalizedOptions, query]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
async (option) => {
|
||||
async (option: NormalizedOption) => {
|
||||
if (!option || !onSelectOption) {
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +162,7 @@ const QuickAddMenu = ({
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
async (event) => {
|
||||
async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!onCreate) {
|
||||
return;
|
||||
@@ -154,9 +194,9 @@ const QuickAddMenu = ({
|
||||
...(menuStyle.width ? { width: menuStyle.width } : null),
|
||||
}
|
||||
: undefined;
|
||||
const menuInlineStyle = isAnchoredMenu ? anchoredMenuStyle : menuStyle || undefined;
|
||||
const menuInlineStyle = (isAnchoredMenu ? anchoredMenuStyle : menuStyle || undefined) as CSSVarStyle | undefined;
|
||||
const hasFloatingWidthVar = Boolean(menuInlineStyle && Object.prototype.hasOwnProperty.call(menuInlineStyle, '--floating-min-width'));
|
||||
const menuStyleWithVar = hasFloatingWidthVar
|
||||
const menuStyleWithVar: CSSVarStyle | undefined = hasFloatingWidthVar
|
||||
? menuInlineStyle
|
||||
: {
|
||||
...(menuInlineStyle || {}),
|
||||
@@ -1,3 +0,0 @@
|
||||
export const composeClassName = (base, extra) => (extra ? `${base} ${extra}` : base);
|
||||
|
||||
export default composeClassName;
|
||||
@@ -0,0 +1,4 @@
|
||||
export const composeClassName = (base: string, extra?: string | null): string =>
|
||||
extra ? `${base} ${extra}` : base;
|
||||
|
||||
export default composeClassName;
|
||||
@@ -1,3 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import type { IconProps as TablerIconProps } from '@tabler/icons-react';
|
||||
import {
|
||||
IconChevronRight as TablerChevronRight,
|
||||
IconDownload as TablerDownload,
|
||||
@@ -47,9 +50,12 @@ import {
|
||||
IconAlertTriangle,
|
||||
} from '@tabler/icons-react';
|
||||
import FolderSvg from '../assets/folder.svg';
|
||||
import LogoSvg from '../assets/logo.svg';
|
||||
import composeClassName from './classNames';
|
||||
|
||||
export const ChevronIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
type TablerIconComponent = (props: TablerIconProps) => JSX.Element;
|
||||
|
||||
export const ChevronIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<TablerChevronRight
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -58,7 +64,7 @@ export const ChevronIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const TrashIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const TrashIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconTrash
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -67,7 +73,7 @@ export const TrashIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const EditIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const EditIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconPencil
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -76,7 +82,7 @@ export const EditIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const FolderIcon = ({ className, size = 16, title, ...rest }) => {
|
||||
export const FolderIcon: TablerIconComponent = ({ className, size = 16, title, ...rest }) => {
|
||||
const dimensionProps = typeof size === 'number' ? { width: size, height: size } : {};
|
||||
|
||||
return (
|
||||
@@ -92,7 +98,17 @@ export const FolderIcon = ({ className, size = 16, title, ...rest }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const TagIcon = ({ className, size = '1em', stroke = 0, ...rest }) => (
|
||||
export const LogoIcon: React.FC<{ className?: string; width?: number; height?: number }> = ({ className, width = 24, height = 24 }) => (
|
||||
<LogoSvg
|
||||
className={composeClassName('logo-icon', className)}
|
||||
width={width}
|
||||
height={height}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
/>
|
||||
);
|
||||
|
||||
export const TagIcon: TablerIconComponent = ({ className, size = '1em', stroke = 0, ...rest }) => (
|
||||
<IconTagFilled
|
||||
className={composeClassName('icon icon--fill', className)}
|
||||
size={size}
|
||||
@@ -101,7 +117,7 @@ export const TagIcon = ({ className, size = '1em', stroke = 0, ...rest }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const CorrespondentIcon = ({ className, size = '1em', stroke = 0, ...rest }) => (
|
||||
export const CorrespondentIcon: TablerIconComponent = ({ className, size = '1em', stroke = 0, ...rest }) => (
|
||||
<IconUserFilled
|
||||
className={composeClassName('icon icon--fill', className)}
|
||||
size={size}
|
||||
@@ -110,7 +126,7 @@ export const CorrespondentIcon = ({ className, size = '1em', stroke = 0, ...rest
|
||||
/>
|
||||
);
|
||||
|
||||
export const DownloadIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const DownloadIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<TablerDownload
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -119,7 +135,7 @@ export const DownloadIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const IconZoomInArea = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const IconZoomInArea: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<TablerZoomInArea
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -128,7 +144,7 @@ export const IconZoomInArea = ({ className, size = '1em', stroke = 1.6, ...rest
|
||||
/>
|
||||
);
|
||||
|
||||
export const ViewListIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const ViewListIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutList
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -137,7 +153,7 @@ export const ViewListIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const ViewGridIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const ViewGridIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutGrid
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -146,7 +162,7 @@ export const ViewGridIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const UploadIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const UploadIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconUpload
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -155,7 +171,7 @@ export const UploadIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =
|
||||
/>
|
||||
);
|
||||
|
||||
export const ArrowLeftIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const ArrowLeftIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconArrowLeft
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -164,7 +180,7 @@ export const ArrowLeftIcon = ({ className, size = '1em', stroke = 1.6, ...rest }
|
||||
/>
|
||||
);
|
||||
|
||||
export const ArrowRightIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const ArrowRightIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconArrowRight
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -173,7 +189,7 @@ export const ArrowRightIcon = ({ className, size = '1em', stroke = 1.6, ...rest
|
||||
/>
|
||||
);
|
||||
|
||||
export const SidebarCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const SidebarCollapseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutSidebarLeftCollapse
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -182,7 +198,7 @@ export const SidebarCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...
|
||||
/>
|
||||
);
|
||||
|
||||
export const SidebarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const SidebarExpandIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutSidebarLeftExpand
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -191,7 +207,7 @@ export const SidebarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...re
|
||||
/>
|
||||
);
|
||||
|
||||
export const InfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const InfoIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconInfoCircle
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -200,7 +216,7 @@ export const InfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const FileInfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const FileInfoIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFileInfo
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -209,7 +225,7 @@ export const FileInfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const DetailPanelCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const DetailPanelCollapseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutSidebarRightCollapse
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -218,7 +234,7 @@ export const DetailPanelCollapseIcon = ({ className, size = '1em', stroke = 1.6,
|
||||
/>
|
||||
);
|
||||
|
||||
export const BottombarCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const BottombarCollapseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutBottombarCollapse
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -227,7 +243,7 @@ export const BottombarCollapseIcon = ({ className, size = '1em', stroke = 1.6, .
|
||||
/>
|
||||
);
|
||||
|
||||
export const BottombarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const BottombarExpandIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutBottombarExpand
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -236,7 +252,7 @@ export const BottombarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...
|
||||
/>
|
||||
);
|
||||
|
||||
export const ClearAllIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const ClearAllIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconClearAll
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -245,7 +261,7 @@ export const ClearAllIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const FolderPlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const FolderPlusIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFolderPlus
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -254,7 +270,7 @@ export const FolderPlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest
|
||||
/>
|
||||
);
|
||||
|
||||
export const FoldersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const FoldersIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFolders
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -263,7 +279,7 @@ export const FoldersIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const FoldersOffIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const FoldersOffIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFoldersOff
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -272,7 +288,7 @@ export const FoldersOffIcon = ({ className, size = '1em', stroke = 1.6, ...rest
|
||||
/>
|
||||
);
|
||||
|
||||
export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const RefreshIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconRefresh
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -281,7 +297,7 @@ export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const RestoreIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const RestoreIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconRestore
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -290,7 +306,7 @@ export const RestoreIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const ArrowUpIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconArrowUp
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -299,7 +315,7 @@ export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const MinusVerticalIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconMinusVertical
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -308,7 +324,7 @@ export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...re
|
||||
/>
|
||||
);
|
||||
|
||||
export const IconFileStack = ({ className, size = 24, stroke = 160, ...rest }) => {
|
||||
export const IconFileStack: TablerIconComponent = ({ className, size = 24, stroke = 160, ...rest }) => {
|
||||
return (
|
||||
<svg
|
||||
className={composeClassName('icon', className)}
|
||||
@@ -337,7 +353,7 @@ export const IconFileStack = ({ className, size = 24, stroke = 160, ...rest }) =
|
||||
);
|
||||
};
|
||||
|
||||
export const SortAscendingLettersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const SortAscendingLettersIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconSortAscendingLetters
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -346,7 +362,7 @@ export const SortAscendingLettersIcon = ({ className, size = '1em', stroke = 1.6
|
||||
/>
|
||||
);
|
||||
|
||||
export const SortDescendingLettersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const SortDescendingLettersIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconSortDescendingLetters
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -355,7 +371,7 @@ export const SortDescendingLettersIcon = ({ className, size = '1em', stroke = 1.
|
||||
/>
|
||||
);
|
||||
|
||||
export const IconX = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const IconX: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<TablerIconX
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -364,7 +380,7 @@ export const IconX = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const CloseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const CloseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconX
|
||||
className={className}
|
||||
size={size}
|
||||
@@ -373,7 +389,7 @@ export const CloseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const SettingsIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const SettingsIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconSettings
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -382,7 +398,7 @@ export const SettingsIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const PlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const PlusIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconPlus
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -391,7 +407,7 @@ export const PlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const SunIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const SunIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconSun
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -400,7 +416,7 @@ export const SunIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const MoonIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const MoonIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconMoon
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -409,7 +425,7 @@ export const MoonIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const DesktopIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const DesktopIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconDeviceLaptop
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -418,7 +434,7 @@ export const DesktopIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const CheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const CheckIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconCheck
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -427,7 +443,7 @@ export const CheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const CircleDashedCheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const CircleDashedCheckIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconCircleDashedCheck
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -436,7 +452,7 @@ export const CircleDashedCheckIcon = ({ className, size = '1em', stroke = 1.6, .
|
||||
/>
|
||||
);
|
||||
|
||||
export const FileIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const FileIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFile
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -445,7 +461,7 @@ export const FileIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
|
||||
/>
|
||||
);
|
||||
|
||||
export const FolderOutlineIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const FolderOutlineIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFolder
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -454,7 +470,7 @@ export const FolderOutlineIcon = ({ className, size = '1em', stroke = 1.6, ...re
|
||||
/>
|
||||
);
|
||||
|
||||
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const AnalyzeIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconAnalyze
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -463,7 +479,7 @@ export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const LoaderIcon = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
|
||||
export const LoaderIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
|
||||
<IconLoader
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -472,7 +488,7 @@ export const LoaderIcon = ({ className, size = '1em', stroke = 1.8, ...rest }) =
|
||||
/>
|
||||
);
|
||||
|
||||
export const WarningIcon = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
|
||||
export const WarningIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
|
||||
<IconAlertTriangle
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -481,7 +497,7 @@ export const WarningIcon = ({ className, size = '1em', stroke = 1.8, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const WindowMaximizeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const WindowMaximizeIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconWindowMaximize
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -490,7 +506,7 @@ export const WindowMaximizeIcon = ({ className, size = '1em', stroke = 1.6, ...r
|
||||
/>
|
||||
);
|
||||
|
||||
export const LogoutIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const LogoutIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLogout
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -499,7 +515,7 @@ export const LogoutIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =
|
||||
/>
|
||||
);
|
||||
|
||||
export const ChevronDownIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const ChevronDownIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconChevronDown
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -532,7 +548,7 @@ export default {
|
||||
ChevronDownIcon,
|
||||
};
|
||||
|
||||
export const TextScanIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
export const TextScanIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconTextScan2
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
@@ -1,28 +1,41 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { MutableRefObject, CSSProperties } from 'react';
|
||||
import { clamp } from '../utils/math';
|
||||
|
||||
const DEFAULT_VIEWPORT_MARGIN = 8;
|
||||
|
||||
type PositionStrategy = 'fixed' | 'absolute' | (string & {});
|
||||
|
||||
interface FloatingMenuMetrics {
|
||||
strategy: PositionStrategy;
|
||||
top: number;
|
||||
left: number;
|
||||
minWidth?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
type FloatingMenuStyle = (CSSProperties & { '--floating-min-width'?: string }) | null;
|
||||
|
||||
const resolveViewportWidth = () => window.innerWidth || document.documentElement.clientWidth || 0;
|
||||
|
||||
const computeWidth = (anchorWidth, minWidth, matchAnchorWidth) => {
|
||||
const computeWidth = (anchorWidth: number, minWidth: number, matchAnchorWidth: boolean) => {
|
||||
if (matchAnchorWidth) {
|
||||
return Math.max(anchorWidth, minWidth);
|
||||
}
|
||||
return Math.max(minWidth || 0, anchorWidth || 0);
|
||||
};
|
||||
|
||||
const formatStyle = (metrics) => {
|
||||
const formatStyle = (metrics: FloatingMenuMetrics | null): FloatingMenuStyle => {
|
||||
if (!metrics) {
|
||||
return null;
|
||||
}
|
||||
const style = {
|
||||
const style: FloatingMenuStyle = {
|
||||
position: metrics.strategy === 'absolute' ? 'absolute' : 'fixed',
|
||||
top: metrics.top,
|
||||
left: metrics.left,
|
||||
};
|
||||
if (Number.isFinite(metrics.minWidth)) {
|
||||
style['--floating-min-width'] = `${Math.max(metrics.minWidth, 0)}px`;
|
||||
style['--floating-min-width'] = `${Math.max(metrics.minWidth ?? 0, 0)}px`;
|
||||
}
|
||||
if (metrics.width) {
|
||||
style.width = metrics.width;
|
||||
@@ -30,6 +43,17 @@ const formatStyle = (metrics) => {
|
||||
return style;
|
||||
};
|
||||
|
||||
interface UseFloatingMenuOptions {
|
||||
anchorRef?: MutableRefObject<HTMLElement | null>;
|
||||
offset?: number;
|
||||
minWidth?: number;
|
||||
matchAnchorWidth?: boolean;
|
||||
align?: 'start' | 'center' | 'end' | (string & {});
|
||||
viewportMargin?: number;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
positionStrategy?: PositionStrategy;
|
||||
}
|
||||
|
||||
const useFloatingMenu = ({
|
||||
anchorRef,
|
||||
offset = 6,
|
||||
@@ -39,9 +63,9 @@ const useFloatingMenu = ({
|
||||
viewportMargin = DEFAULT_VIEWPORT_MARGIN,
|
||||
onOpenChange,
|
||||
positionStrategy = 'fixed',
|
||||
} = {}) => {
|
||||
const menuRef = useRef(null);
|
||||
const [menuMetrics, setMenuMetrics] = useState(null);
|
||||
}: UseFloatingMenuOptions = {}) => {
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const [menuMetrics, setMenuMetrics] = useState<FloatingMenuMetrics | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const onOpenChangeRef = useRef(onOpenChange);
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
const defaultFilter = (event) => {
|
||||
if (!event) {
|
||||
return false;
|
||||
}
|
||||
const { type, button, pointerType, isPrimary } = event;
|
||||
const isPointerUp = type === 'pointerup';
|
||||
const buttonValid =
|
||||
button == null || button === 0 || (isPointerUp && (button === -1 || button === 0));
|
||||
if (!buttonValid) {
|
||||
return false;
|
||||
}
|
||||
if (pointerType === 'touch' && isPrimary === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const usePointerTap = ({
|
||||
onSingle,
|
||||
onDouble,
|
||||
delay = 240,
|
||||
filter = defaultFilter,
|
||||
} = {}) => {
|
||||
const timerRef = useRef(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return useCallback(
|
||||
(event, metadata = undefined) => {
|
||||
if (!filter(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof event.persist === 'function') {
|
||||
event.persist();
|
||||
}
|
||||
|
||||
const context = {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
pointerType: event.pointerType,
|
||||
event,
|
||||
data: metadata,
|
||||
};
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
if (typeof onDouble === 'function') {
|
||||
onDouble(context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
timerRef.current = null;
|
||||
if (typeof onSingle === 'function') {
|
||||
onSingle(context);
|
||||
}
|
||||
}, delay);
|
||||
},
|
||||
[delay, filter, onDouble, onSingle],
|
||||
);
|
||||
};
|
||||
|
||||
export default usePointerTap;
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||
|
||||
export interface PointerTapContext<T = unknown> {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
pointerType: string;
|
||||
event: PointerEventWithPersist;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
type PointerEventWithPersist = (PointerEvent | ReactPointerEvent) & { persist?: () => void };
|
||||
|
||||
type PointerTapFilter = (event?: PointerEventWithPersist | null) => boolean;
|
||||
|
||||
export interface UsePointerTapOptions<T = unknown> {
|
||||
onSingle?: (context: PointerTapContext<T>) => void;
|
||||
onDouble?: (context: PointerTapContext<T>) => void;
|
||||
delay?: number;
|
||||
filter?: PointerTapFilter;
|
||||
}
|
||||
|
||||
const defaultFilter: PointerTapFilter = (event) => {
|
||||
if (!event) {
|
||||
return false;
|
||||
}
|
||||
const { type, button, pointerType, isPrimary } = event;
|
||||
const isPointerUp = type === 'pointerup';
|
||||
const buttonValid =
|
||||
button == null || button === 0 || (isPointerUp && (button === -1 || button === 0));
|
||||
if (!buttonValid) {
|
||||
return false;
|
||||
}
|
||||
if (pointerType === 'touch' && isPrimary === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const usePointerTap = <T,>({
|
||||
onSingle,
|
||||
onDouble,
|
||||
delay = 240,
|
||||
filter = defaultFilter,
|
||||
}: UsePointerTapOptions<T> = {}) => {
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
(event?: PointerEventWithPersist | null, metadata?: T) => {
|
||||
if (!filter(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.persist?.();
|
||||
|
||||
const context: PointerTapContext<T> = {
|
||||
clientX: event.clientX ?? 0,
|
||||
clientY: event.clientY ?? 0,
|
||||
pointerType: event.pointerType ?? 'mouse',
|
||||
event,
|
||||
data: metadata,
|
||||
};
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
onDouble?.(context);
|
||||
return;
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
timerRef.current = null;
|
||||
onSingle?.(context);
|
||||
}, delay);
|
||||
},
|
||||
[delay, filter, onDouble, onSingle],
|
||||
);
|
||||
};
|
||||
|
||||
export default usePointerTap;
|
||||
Reference in New Issue
Block a user