ui(components): Adds InputFilter component to ui-next (#4848)
This commit is contained in:
parent
841320adfb
commit
f916b2f40e
@ -2,7 +2,7 @@ import dcmjs from 'dcmjs';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { classes, Types } from '@ohif/core';
|
import { classes, Types } from '@ohif/core';
|
||||||
import { InputFilterText } from '@ohif/ui';
|
import { InputFilter } from '@ohif/ui-next';
|
||||||
import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next';
|
import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next';
|
||||||
|
|
||||||
import DicomTagTable from './DicomTagTable';
|
import DicomTagTable from './DicomTagTable';
|
||||||
@ -173,11 +173,17 @@ const DicomTagBrowser = ({
|
|||||||
<span className="text-muted-foreground flex h-6 items-center text-xs">
|
<span className="text-muted-foreground flex h-6 items-center text-xs">
|
||||||
Search metadata
|
Search metadata
|
||||||
</span>
|
</span>
|
||||||
<InputFilterText
|
<InputFilter
|
||||||
placeholder="Search metadata..."
|
className="text-muted-foreground"
|
||||||
onDebounceChange={setFilterValue}
|
onChange={setFilterValue}
|
||||||
className="text-foreground"
|
>
|
||||||
/>
|
<InputFilter.SearchIcon />
|
||||||
|
<InputFilter.Input
|
||||||
|
placeholder="Search metadata"
|
||||||
|
className="pl-9 pr-9"
|
||||||
|
/>
|
||||||
|
<InputFilter.ClearButton />
|
||||||
|
</InputFilter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
206
platform/ui-next/src/components/InputFilter/InputFilter.tsx
Normal file
206
platform/ui-next/src/components/InputFilter/InputFilter.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Input } from '../Input';
|
||||||
|
import { Icons } from '../Icons';
|
||||||
|
|
||||||
|
// Context to share state between compound components
|
||||||
|
type InputFilterContextType = {
|
||||||
|
value: string;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
clearValue: () => void;
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputFilterContext = createContext<InputFilterContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
function useInputFilterContext() {
|
||||||
|
const context = useContext(InputFilterContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('InputFilter compound components must be used within InputFilter');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root component
|
||||||
|
interface RootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
debounceTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Root({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
defaultValue = '',
|
||||||
|
value: controlledValue,
|
||||||
|
onChange,
|
||||||
|
debounceTime = 200,
|
||||||
|
}: RootProps) {
|
||||||
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Determine if this is a controlled or uncontrolled component
|
||||||
|
const isControlled = controlledValue !== undefined;
|
||||||
|
const value = isControlled ? controlledValue : uncontrolledValue;
|
||||||
|
|
||||||
|
const debouncedOnChange = useMemo(() => {
|
||||||
|
return onChange ? debounce(onChange, debounceTime) : undefined;
|
||||||
|
}, [onChange, debounceTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => debouncedOnChange?.cancel();
|
||||||
|
}, [debouncedOnChange]);
|
||||||
|
|
||||||
|
const setValue = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debouncedOnChange) {
|
||||||
|
debouncedOnChange(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isControlled, debouncedOnChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setValue(event.target.value);
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearValue = useCallback(() => {
|
||||||
|
setValue('');
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, [setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputFilterContext.Provider value={{ value, setValue, handleChange, clearValue, inputRef }}>
|
||||||
|
<div className={cn('relative', className)}>{children}</div>
|
||||||
|
</InputFilterContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input component
|
||||||
|
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputComponent({ className, placeholder, ...props }: InputProps) {
|
||||||
|
const { value, handleChange, inputRef } = useInputFilterContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
className={cn('w-full', className)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search icon component
|
||||||
|
interface SearchIconProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchIcon({ className }: SearchIconProps) {
|
||||||
|
return (
|
||||||
|
<span className={cn('absolute inset-y-0 left-0 flex items-center pl-2', className)}>
|
||||||
|
<Icons.Search className="text-muted-foreground" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear button component
|
||||||
|
interface ClearButtonProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClearButton({ className }: ClearButtonProps) {
|
||||||
|
const { value, clearValue } = useInputFilterContext();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cn('absolute inset-y-0 right-0 flex items-center pr-2', className)}>
|
||||||
|
<Icons.Clear
|
||||||
|
className={cn('cursor-pointer', className)}
|
||||||
|
onClick={clearValue}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main InputFilter component
|
||||||
|
interface InputFilterProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
value?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
debounceTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputFilter({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
placeholder = 'Search...',
|
||||||
|
value,
|
||||||
|
defaultValue = '',
|
||||||
|
onChange,
|
||||||
|
debounceTime = 200,
|
||||||
|
}: InputFilterProps) {
|
||||||
|
// If children are provided, act as a container (former Root)
|
||||||
|
if (children) {
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
className={className}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={onChange}
|
||||||
|
debounceTime={debounceTime}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise act as the complete pre-composed component
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
className={className}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={onChange}
|
||||||
|
debounceTime={debounceTime}
|
||||||
|
>
|
||||||
|
<SearchIcon />
|
||||||
|
<InputComponent
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="pl-9 pr-9"
|
||||||
|
/>
|
||||||
|
<ClearButton />
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach subcomponents as static properties
|
||||||
|
InputFilter.Input = InputComponent;
|
||||||
|
InputFilter.SearchIcon = SearchIcon;
|
||||||
|
InputFilter.ClearButton = ClearButton;
|
||||||
|
|
||||||
|
export { InputFilter, type InputFilterProps };
|
||||||
2
platform/ui-next/src/components/InputFilter/index.ts
Normal file
2
platform/ui-next/src/components/InputFilter/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { InputFilter } from './InputFilter';
|
||||||
|
export type { InputFilterProps } from './InputFilter';
|
||||||
@ -58,6 +58,7 @@ import { InputDialog, PresetDialog } from './OHIFDialogs';
|
|||||||
import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
|
import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
|
||||||
import Modal from './Modal/Modal';
|
import Modal from './Modal/Modal';
|
||||||
import { FooterAction } from './FooterAction';
|
import { FooterAction } from './FooterAction';
|
||||||
|
import { InputFilter } from './InputFilter';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -237,4 +238,5 @@ export {
|
|||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
FooterAction,
|
FooterAction,
|
||||||
ToolSettings,
|
ToolSettings,
|
||||||
|
InputFilter,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -107,6 +107,7 @@ import {
|
|||||||
ImageModal,
|
ImageModal,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
FooterAction,
|
FooterAction,
|
||||||
|
InputFilter,
|
||||||
} from './components';
|
} from './components';
|
||||||
import { DataRow } from './components/DataRow';
|
import { DataRow } from './components/DataRow';
|
||||||
|
|
||||||
@ -245,4 +246,5 @@ export {
|
|||||||
useDialog,
|
useDialog,
|
||||||
ManagedDialog,
|
ManagedDialog,
|
||||||
ToolSettings,
|
ToolSettings,
|
||||||
|
InputFilter,
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user