diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx index 2c9099510..fdf4bb48b 100644 --- a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx +++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx @@ -2,7 +2,7 @@ import dcmjs from 'dcmjs'; import moment from 'moment'; import React, { useState, useMemo, useCallback } from 'react'; 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 DicomTagTable from './DicomTagTable'; @@ -173,11 +173,17 @@ const DicomTagBrowser = ({ Search metadata - + + + + + diff --git a/platform/ui-next/src/components/InputFilter/InputFilter.tsx b/platform/ui-next/src/components/InputFilter/InputFilter.tsx new file mode 100644 index 000000000..f53505550 --- /dev/null +++ b/platform/ui-next/src/components/InputFilter/InputFilter.tsx @@ -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) => void; + clearValue: () => void; + inputRef: React.RefObject; +}; + +const InputFilterContext = createContext(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(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) => { + setValue(event.target.value); + }, + [setValue] + ); + + const clearValue = useCallback(() => { + setValue(''); + inputRef.current?.focus(); + }, [setValue]); + + return ( + +
{children}
+
+ ); +} + +// Input component +interface InputProps extends Omit, 'onChange'> { + className?: string; +} + +function InputComponent({ className, placeholder, ...props }: InputProps) { + const { value, handleChange, inputRef } = useInputFilterContext(); + + return ( + + ); +} + +// Search icon component +interface SearchIconProps { + className?: string; +} + +function SearchIcon({ className }: SearchIconProps) { + return ( + + + + ); +} + +// Clear button component +interface ClearButtonProps { + className?: string; +} + +function ClearButton({ className }: ClearButtonProps) { + const { value, clearValue } = useInputFilterContext(); + + if (!value) { + return null; + } + + return ( + + + + ); +} + +// 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 ( + + {children} + + ); + } + + // Otherwise act as the complete pre-composed component + return ( + + + + + + ); +} + +// Attach subcomponents as static properties +InputFilter.Input = InputComponent; +InputFilter.SearchIcon = SearchIcon; +InputFilter.ClearButton = ClearButton; + +export { InputFilter, type InputFilterProps }; diff --git a/platform/ui-next/src/components/InputFilter/index.ts b/platform/ui-next/src/components/InputFilter/index.ts new file mode 100644 index 000000000..6d20f6e69 --- /dev/null +++ b/platform/ui-next/src/components/InputFilter/index.ts @@ -0,0 +1,2 @@ +export { InputFilter } from './InputFilter'; +export type { InputFilterProps } from './InputFilter'; diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index be9145409..84310f7e7 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -58,6 +58,7 @@ import { InputDialog, PresetDialog } from './OHIFDialogs'; import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals'; import Modal from './Modal/Modal'; import { FooterAction } from './FooterAction'; +import { InputFilter } from './InputFilter'; import { DropdownMenu, @@ -237,4 +238,5 @@ export { UserPreferencesModal, FooterAction, ToolSettings, + InputFilter, }; diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index f4869cfaa..37388deb0 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -107,6 +107,7 @@ import { ImageModal, UserPreferencesModal, FooterAction, + InputFilter, } from './components'; import { DataRow } from './components/DataRow'; @@ -245,4 +246,5 @@ export { useDialog, ManagedDialog, ToolSettings, -}; + InputFilter, +}; \ No newline at end of file