typescript
This commit is contained in:
+79
-29
@@ -1,11 +1,51 @@
|
||||
import React, {
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import { CheckIcon, ChevronDownIcon } from '../../ui/icons';
|
||||
|
||||
type CapabilityValue = string | number;
|
||||
|
||||
export interface CapabilityDropdownOption {
|
||||
value?: CapabilityValue | null;
|
||||
id?: CapabilityValue | null;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface CapabilityDropdownProps {
|
||||
id?: string;
|
||||
options?: Array<CapabilityDropdownOption | CapabilityValue>;
|
||||
selectedValues?: CapabilityValue[];
|
||||
onSelect?: (value: CapabilityValue) => void;
|
||||
onDeselect?: (value: CapabilityValue) => void;
|
||||
formatLabel?: (value: CapabilityValue) => string;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
summaryLabel?: string;
|
||||
}
|
||||
|
||||
const resolveCapabilityValue = (
|
||||
option: CapabilityDropdownOption | CapabilityValue | null | undefined,
|
||||
): CapabilityValue | null => {
|
||||
if (option == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof option === 'string' || typeof option === 'number') {
|
||||
return option;
|
||||
}
|
||||
if (option.value != null) {
|
||||
return option.value;
|
||||
}
|
||||
if (option.id != null) {
|
||||
return option.id;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CapabilityDropdown = ({
|
||||
id,
|
||||
options = [],
|
||||
@@ -16,9 +56,9 @@ const CapabilityDropdown = ({
|
||||
disabled = false,
|
||||
loading = false,
|
||||
summaryLabel = 'capabilities',
|
||||
}) => {
|
||||
const anchorRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
}: CapabilityDropdownProps): JSX.Element => {
|
||||
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const close = useCallback(() => {
|
||||
@@ -37,14 +77,17 @@ const CapabilityDropdown = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handlePointerEvent = (event) => {
|
||||
if (anchorRef.current?.contains(event.target) || menuRef.current?.contains(event.target)) {
|
||||
const handlePointerEvent = (event: MouseEvent | TouchEvent) => {
|
||||
if (anchorRef.current?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
if (menuRef.current?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
close();
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
@@ -61,7 +104,10 @@ const CapabilityDropdown = ({
|
||||
};
|
||||
}, [close, isOpen]);
|
||||
|
||||
const handleOptionClick = useCallback((value) => {
|
||||
const handleOptionClick = useCallback((value: CapabilityValue | null) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (selectedValues.includes(value)) {
|
||||
onDeselect?.(value);
|
||||
} else {
|
||||
@@ -69,26 +115,27 @@ const CapabilityDropdown = ({
|
||||
}
|
||||
}, [onDeselect, onSelect, selectedValues]);
|
||||
|
||||
const total = options.length;
|
||||
const derivedOptions = useMemo(() => options.filter(Boolean), [options]);
|
||||
const total = derivedOptions.length;
|
||||
const selectedCount = selectedValues.length;
|
||||
|
||||
const summaryText = total
|
||||
? `${selectedCount}/${total} ${summaryLabel} enabled`
|
||||
: selectedCount
|
||||
? `${selectedCount} ${summaryLabel} selected`
|
||||
: loading
|
||||
? `Loading ${summaryLabel}…`
|
||||
: `No ${summaryLabel}`;
|
||||
|
||||
const buttonText = selectedCount || loading || total
|
||||
? summaryText
|
||||
: `Select ${summaryLabel}`;
|
||||
|
||||
const emptyMessage = loading
|
||||
? `Loading ${summaryLabel}…`
|
||||
: `No ${summaryLabel} available.`;
|
||||
const summaryText = useMemo(() => {
|
||||
if (total) {
|
||||
return `${selectedCount}/${total} ${summaryLabel} enabled`;
|
||||
}
|
||||
if (selectedCount) {
|
||||
return `${selectedCount} ${summaryLabel} selected`;
|
||||
}
|
||||
if (loading) {
|
||||
return `Loading ${summaryLabel}…`;
|
||||
}
|
||||
return `No ${summaryLabel}`;
|
||||
}, [loading, selectedCount, summaryLabel, total]);
|
||||
|
||||
const buttonText = total || selectedCount || loading ? summaryText : `Select ${summaryLabel}`;
|
||||
const emptyMessage = loading ? `Loading ${summaryLabel}…` : `No ${summaryLabel} available.`;
|
||||
const isDisabled = disabled || (total === 0 && !selectedCount) || loading;
|
||||
const menuId = id ? `${id}-menu` : undefined;
|
||||
|
||||
return (
|
||||
<div className="capability-dropdown">
|
||||
@@ -97,7 +144,7 @@ const CapabilityDropdown = ({
|
||||
className="capability-dropdown__trigger"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={id ? `${id}-menu` : undefined}
|
||||
aria-controls={menuId}
|
||||
onClick={toggle}
|
||||
disabled={isDisabled}
|
||||
ref={anchorRef}
|
||||
@@ -107,18 +154,21 @@ const CapabilityDropdown = ({
|
||||
</button>
|
||||
{isOpen ? (
|
||||
<div
|
||||
id={id ? `${id}-menu` : undefined}
|
||||
id={menuId}
|
||||
role="menu"
|
||||
ref={menuRef}
|
||||
className="menu menu--floating capability-dropdown__menu"
|
||||
data-floating-position
|
||||
>
|
||||
{total ? (
|
||||
options.map((option) => {
|
||||
const value = option?.value ?? option?.id ?? option;
|
||||
derivedOptions.map((option) => {
|
||||
const value = resolveCapabilityValue(option);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
const label = typeof formatLabel === 'function'
|
||||
? formatLabel(value)
|
||||
: option?.label || String(value);
|
||||
: (typeof option === 'object' && option?.label) || String(value);
|
||||
const selected = selectedValues.includes(value);
|
||||
return (
|
||||
<button
|
||||
Reference in New Issue
Block a user