> | ((val: string) => void);
+
function YAxisUnitSelector({
- defaultValue,
value,
onSelect,
fieldLabel,
handleClear,
}: {
- defaultValue: string;
value: string;
onSelect: OnSelectType;
fieldLabel: string;
handleClear?: () => void;
}): JSX.Element {
+ const [inputValue, setInputValue] = useState('');
+
+ // Sync input value with the actual value prop
+ useEffect(() => {
+ const category = findCategoryById(value);
+ setInputValue(category?.name || '');
+ }, [value]);
+
const onSelectHandler = (selectedValue: string): void => {
- onSelect(findCategoryByName(selectedValue)?.id || '');
+ const category = findCategoryByName(selectedValue);
+ if (category) {
+ onSelect(category.id);
+ setInputValue(selectedValue);
+ }
};
+
+ const onChangeHandler = (inputValue: string): void => {
+ setInputValue(inputValue);
+ // Clear the yAxisUnit if input is empty or doesn't match any option
+ if (!inputValue) {
+ onSelect('');
+ }
+ };
+
+ const onClearHandler = (): void => {
+ setInputValue('');
+ onSelect('');
+ if (handleClear) {
+ handleClear();
+ }
+ };
+
const options = flattenedCategories.map((options) => ({
value: options.name,
}));
+
return (
{fieldLabel}
@@ -41,9 +70,9 @@ function YAxisUnitSelector({
rootClassName="y-axis-root-popover"
options={options}
allowClear
- defaultValue={findCategoryById(defaultValue)?.name}
- value={findCategoryById(value)?.name || ''}
- onClear={handleClear}
+ value={inputValue}
+ onChange={onChangeHandler}
+ onClear={onClearHandler}
onSelect={onSelectHandler}
filterOption={(inputValue, option): boolean => {
if (option) {
diff --git a/frontend/src/container/NewWidget/RightContainer/__tests__/YAxisUnitSelector.test.tsx b/frontend/src/container/NewWidget/RightContainer/__tests__/YAxisUnitSelector.test.tsx
new file mode 100644
index 000000000000..f1cb5e1e5f98
--- /dev/null
+++ b/frontend/src/container/NewWidget/RightContainer/__tests__/YAxisUnitSelector.test.tsx
@@ -0,0 +1,240 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { act } from 'react-dom/test-utils';
+
+import YAxisUnitSelector from '../YAxisUnitSelector';
+
+// Mock the dataFormatCategories to have predictable test data
+jest.mock('../dataFormatCategories', () => ({
+ flattenedCategories: [
+ { id: 'seconds', name: 'seconds (s)' },
+ { id: 'milliseconds', name: 'milliseconds (ms)' },
+ { id: 'hours', name: 'hours (h)' },
+ { id: 'minutes', name: 'minutes (m)' },
+ ],
+}));
+
+const MOCK_SECONDS = 'seconds';
+const MOCK_MILLISECONDS = 'milliseconds';
+
+describe('YAxisUnitSelector', () => {
+ const defaultProps = {
+ value: MOCK_SECONDS,
+ onSelect: jest.fn(),
+ fieldLabel: 'Y Axis Unit',
+ handleClear: jest.fn(),
+ };
+
+ let user: ReturnType;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ user = userEvent.setup();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ describe('Rendering (Read) & (write)', () => {
+ it('renders with correct field label', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('Y Axis Unit')).toBeInTheDocument();
+ const input = screen.getByRole('combobox');
+
+ expect(input).toHaveValue('seconds (s)');
+ });
+
+ it('renders with custom field label', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('Custom Unit Label')).toBeInTheDocument();
+ });
+
+ it('displays empty input when value prop is empty', () => {
+ render(
+ ,
+ );
+ expect(screen.getByDisplayValue('')).toBeInTheDocument();
+ });
+
+ it('shows placeholder text', () => {
+ render(
+ ,
+ );
+ expect(screen.getByPlaceholderText('Unit')).toBeInTheDocument();
+ });
+
+ it('handles numeric input', async () => {
+ render(
+ ,
+ );
+ const input = screen.getByRole('combobox');
+
+ await user.clear(input);
+ await user.type(input, '12345');
+ expect(input).toHaveValue('12345');
+ });
+
+ it('handles mixed content input', async () => {
+ render(
+ ,
+ );
+ const input = screen.getByRole('combobox');
+
+ await user.clear(input);
+ await user.type(input, 'Test123!@#');
+ expect(input).toHaveValue('Test123!@#');
+ });
+ });
+
+ describe('State Management', () => {
+ it('syncs input value with value prop changes', async () => {
+ const { rerender } = render(
+ ,
+ );
+ const input = screen.getByRole('combobox');
+
+ // Initial value
+ expect(input).toHaveValue('seconds (s)');
+
+ // Change value prop
+ rerender(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(input).toHaveValue('milliseconds (ms)');
+ });
+ });
+
+ it('handles empty value prop correctly', async () => {
+ const { rerender } = render(
+ ,
+ );
+ const input = screen.getByRole('combobox');
+
+ // Change to empty value
+ rerender(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(input).toHaveValue('');
+ });
+ });
+
+ it('handles invalid value prop gracefully', async () => {
+ const { rerender } = render(
+ ,
+ );
+ const input = screen.getByRole('combobox');
+
+ // Change to invalid value
+ rerender(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(input).toHaveValue('');
+ });
+ });
+
+ it('maintains local state during typing', async () => {
+ render(
+ ,
+ );
+ const input = screen.getByRole('combobox');
+
+ // first clear then type
+ await user.clear(input);
+ await user.type(input, 'test');
+ expect(input).toHaveValue('test');
+
+ // Value prop change should not override local typing
+ await act(async () => {
+ // Simulate prop change
+ render(
+ ,
+ );
+ });
+
+ // Local typing should be preserved
+ expect(input).toHaveValue('test');
+ });
+ });
+});
diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx
index 4317f1c73ac3..64c6e91ced23 100644
--- a/frontend/src/container/NewWidget/RightContainer/index.tsx
+++ b/frontend/src/container/NewWidget/RightContainer/index.tsx
@@ -337,7 +337,6 @@ function RightContainer({
{allowYAxisUnit && (