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