typescript

This commit is contained in:
2025-11-13 01:07:55 +01:00
parent b812d748ea
commit ada089c05b
147 changed files with 7427 additions and 2534 deletions
@@ -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