diff --git a/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx b/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx
index fad2600de3e..148f35c6af3 100644
--- a/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx
+++ b/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx
@@ -1,6 +1,4 @@
import { screen, render, fireEvent, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { useState } from 'react';
import { measureText } from '../../utils/measureText';
@@ -15,214 +13,15 @@ jest.mock('../../utils/measureText', () => {
return { measureText };
});
-// The length calculation should be (text.length * 14) + 24
-const FIXTURES = {
- 'Initial value': '206px',
- 'Initial value with more text': '416px',
- 'A new value': '178px',
- 'Placeholder text': '248px',
- _emptyString: '80px', // min width
- foo: '80px', // min width
-} as const;
-
describe('AutoSizeInput', () => {
- it('all the test fixture strings should be a different length', () => {
- const lengths = Object.keys(FIXTURES).map((key) => key.length);
+ it('should support default Input API', () => {
+ const onChange = jest.fn();
+ render();
- // The unique number of lengths should be the same as the number of items in the object
- const uniqueLengths = new Set(lengths);
- expect(uniqueLengths.size).toBe(lengths.length);
- });
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+ fireEvent.change(input, { target: { value: 'foo' } });
- describe('as an uncontrolled component', () => {
- it('renders an initial value prop with correct width', async () => {
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- expect(input.value).toBe('Initial value');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Initial value']);
- });
-
- it('renders an updated value prop with correct width', () => {
- const { rerender } = render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- rerender();
-
- expect(input.value).toBe('A new value');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['A new value']);
- });
-
- it('renders the user typing in the input with correct width', async () => {
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- await userEvent.type(input, 'Initial value with more text');
-
- expect(input.value).toBe('Initial value with more text');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Initial value with more text']);
- });
-
- it('renders correctly after the user clears the input', async () => {
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- await userEvent.clear(input);
-
- expect(input.value).toBe('');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES._emptyString);
- });
-
- it('renders correctly with a placeholder after the user clears the input', async () => {
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- await userEvent.clear(input);
-
- expect(input.value).toBe('');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Placeholder text']);
- });
-
- it('emits onCommitChange when you blur the input', () => {
- const onCommitChange = jest.fn();
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
-
- fireEvent.blur(input);
-
- expect(onCommitChange).toHaveBeenCalledTimes(1);
- });
-
- it('emits onBlur instead of onCommitChange when you blur the input', () => {
- const onCommitChange = jest.fn();
- const onBlur = jest.fn();
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
-
- fireEvent.blur(input);
-
- expect(onCommitChange).not.toHaveBeenCalled();
- expect(onBlur).toHaveBeenCalledTimes(1);
- });
-
- it('emits the value when you press enter', async () => {
- const onCommitChange = jest.fn();
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
-
- await userEvent.type(input, '{enter}');
-
- expect(onCommitChange).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('as a controlled component', () => {
- function ControlledAutoSizeInputExample() {
- const [value, setValue] = useState('');
- return setValue(event.currentTarget.value)} />;
- }
-
- // AutoSizeInput is considered controlled when it has both value and onChange props
-
- it('renders a value prop with correct width', () => {
- const onChange = jest.fn();
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- expect(input.value).toBe('Initial value');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Initial value']);
- });
-
- it('renders an updated value prop with correct width', () => {
- const onChange = jest.fn();
- const { rerender } = render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- rerender();
-
- expect(input.value).toBe('A new value');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['A new value']);
- });
-
- it('as a user types, the value is not updated because it is controlled', async () => {
- const onChange = jest.fn();
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- await userEvent.type(input, ' and more text');
-
- expect(input.value).toBe('Initial value');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Initial value']);
- });
-
- it('functions correctly as a controlled input', async () => {
- render();
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
- const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
-
- await userEvent.type(input, 'Initial value with more text');
- expect(input.value).toBe('Initial value with more text');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Initial value with more text']);
- });
-
- it('emits onCommitChange when you blur the input', () => {
- const onCommitChange = jest.fn();
- const onChange = jest.fn();
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
-
- fireEvent.blur(input);
-
- expect(onCommitChange).toHaveBeenCalledTimes(1);
- });
-
- it('emits onBlur instead of onCommitChange when you blur the input', () => {
- const onCommitChange = jest.fn();
- const onBlur = jest.fn();
- const onChange = jest.fn();
- render(
-
- );
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
-
- fireEvent.blur(input);
-
- expect(onCommitChange).not.toHaveBeenCalled();
- expect(onBlur).toHaveBeenCalledTimes(1);
- });
-
- it('emits the value when you press enter', async () => {
- const onCommitChange = jest.fn();
- const onChange = jest.fn();
- render();
-
- const input: HTMLInputElement = screen.getByTestId('autosize-input');
-
- await userEvent.type(input, '{enter}');
-
- expect(onCommitChange).toHaveBeenCalledTimes(1);
- });
+ expect(onChange).toHaveBeenCalled();
});
it('should have default minWidth when empty', () => {
@@ -231,38 +30,104 @@ describe('AutoSizeInput', () => {
const input: HTMLInputElement = screen.getByTestId('autosize-input');
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
+ fireEvent.change(input, { target: { value: '' } });
+
expect(input.value).toBe('');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES._emptyString);
+ expect(getComputedStyle(inputWrapper).width).toBe('80px');
});
it('should have default minWidth for short content', () => {
- render();
+ render();
const input: HTMLInputElement = screen.getByTestId('autosize-input');
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
+ fireEvent.change(input, { target: { value: 'foo' } });
+
expect(input.value).toBe('foo');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['foo']);
+ expect(getComputedStyle(inputWrapper).width).toBe('80px');
+ });
+
+ it('should change width for long content', () => {
+ render();
+
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+ const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
+
+ fireEvent.change(input, { target: { value: 'very very long value' } });
+ expect(getComputedStyle(inputWrapper).width).toBe('304px');
});
it('should use placeholder for width if input is empty', () => {
- render();
+ render();
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Placeholder text']);
+ expect(getComputedStyle(inputWrapper).width).toBe('304px');
});
it('should use value for width even with a placeholder', () => {
- render();
+ render();
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
- expect(getComputedStyle(inputWrapper).width).toBe(FIXTURES['Initial value']);
+ fireEvent.change(input, { target: { value: 'very very long value' } });
+ expect(getComputedStyle(inputWrapper).width).toBe('304px');
+ });
+
+ it('should call onBlur if set when blurring', () => {
+ const onBlur = jest.fn();
+ const onCommitChange = jest.fn();
+ render();
+
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+
+ fireEvent.blur(input);
+
+ expect(onBlur).toHaveBeenCalled();
+ expect(onCommitChange).not.toHaveBeenCalled();
+ });
+
+ it('should call onCommitChange if not set when blurring', () => {
+ const onCommitChange = jest.fn();
+ render();
+
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+
+ fireEvent.blur(input);
+
+ expect(onCommitChange).toHaveBeenCalled();
+ });
+
+ it('should call onKeyDown if set when keydown', () => {
+ const onKeyDown = jest.fn();
+ const onCommitChange = jest.fn();
+ render();
+
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+
+ fireEvent.keyDown(input, { key: 'Enter' });
+
+ expect(onKeyDown).toHaveBeenCalled();
+ expect(onCommitChange).not.toHaveBeenCalled();
+ });
+
+ it('should call onCommitChange if not set when keydown', () => {
+ const onCommitChange = jest.fn();
+ render();
+
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+
+ fireEvent.keyDown(input, { key: 'Enter' });
+
+ expect(onCommitChange).toHaveBeenCalled();
});
it('should respect min width', async () => {
render();
+ await waitFor(() => expect(measureText).toHaveBeenCalled());
+
expect(getComputedStyle(screen.getByTestId('input-wrapper')).width).toBe('32px');
});
@@ -280,6 +145,32 @@ describe('AutoSizeInput', () => {
expect(getComputedStyle(screen.getByTestId('input-wrapper')).width).toBe('32px');
});
+ it('should update the input value if the value prop changes', () => {
+ const { rerender } = render();
+
+ // Get a handle on the original input element (to catch if it's unmounted)
+ // And check the initial value
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+ expect(input.value).toBe('Initial');
+
+ // Rerender and make sure it clears the input
+ rerender();
+ expect(input.value).toBe('Updated');
+ });
+
+ it('should clear the input when the value is changed to an empty string', () => {
+ const { rerender } = render();
+
+ // Get a handle on the original input element (to catch if it's unmounted)
+ // And check the initial value
+ const input: HTMLInputElement = screen.getByTestId('autosize-input');
+ expect(input.value).toBe('Initial');
+
+ // Rerender and make sure it clears the input
+ rerender();
+ expect(input.value).toBe('');
+ });
+
it('should render string values as expected', () => {
render();
diff --git a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx
index e7a89ab4fc8..9e7b275c120 100644
--- a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx
+++ b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { useEffect, useMemo } from 'react';
import * as React from 'react';
import { measureText } from '../../utils/measureText';
@@ -28,32 +28,37 @@ export const AutoSizeInput = React.forwardRef((props, r
placeholder,
...restProps
} = props;
- const [inputState, setInputValue] = useControlledState(controlledValue, onChange);
- const inputValue = inputState || defaultValue;
+ // Initialize internal state
+ const [value, setValue] = React.useState(controlledValue ?? defaultValue);
+
+ // Update internal state when controlled `value` prop changes
+ useEffect(() => {
+ setValue(controlledValue ?? defaultValue);
+ }, [controlledValue, defaultValue]);
// Update input width when `value`, `minWidth`, or `maxWidth` change
const inputWidth = useMemo(() => {
- const displayValue = inputValue || placeholder || '';
+ const displayValue = value || placeholder || '';
const valueString = typeof displayValue === 'string' ? displayValue : displayValue.toString();
return getWidthFor(valueString, minWidth, maxWidth);
- }, [placeholder, inputValue, minWidth, maxWidth]);
+ }, [placeholder, value, minWidth, maxWidth]);
return (
+ // Used to tell Input to increase the width properly of the input to fit the text.
+ // See comment in Input.tsx for more details
{
if (onChange) {
onChange(event);
}
-
- setInputValue(event.currentTarget.value);
+ setValue(event.currentTarget.value);
}}
- width={inputWidth}
onBlur={(event) => {
if (onBlur) {
onBlur(event);
@@ -68,7 +73,8 @@ export const AutoSizeInput = React.forwardRef((props, r
onCommitChange(event);
}
}}
- data-testid={'autosize-input'}
+ width={inputWidth}
+ data-testid="autosize-input"
/>
);
@@ -94,39 +100,3 @@ function getWidthFor(value: string, minWidth: number, maxWidth: number | undefin
}
AutoSizeInput.displayName = 'AutoSizeInput';
-
-/**
- * Hook to abstract away state management for controlled and uncontrolled inputs.
- * If the initial value is not undefined, then the value will be controlled by the parent
- * for the lifetime of the component and calls to setState will be ignored.
- */
-function useControlledState(controlledValue: T, onChange: Function | undefined): [T, (newValue: T) => void] {
- const isControlledNow = controlledValue !== undefined && onChange !== undefined;
- const isControlledRef = useRef(isControlledNow); // set the initial value - we never change this
-
- const hasLoggedControlledWarning = useRef(false);
- if (isControlledNow !== isControlledRef.current && !hasLoggedControlledWarning.current) {
- console.warn(
- 'An AutoSizeInput is changing from an uncontrolled to a controlled input. If you want to control the input, the empty value should be an empty string.'
- );
- hasLoggedControlledWarning.current = true;
- }
-
- const [internalValue, setInternalValue] = React.useState(controlledValue);
-
- useEffect(() => {
- if (!isControlledRef.current) {
- setInternalValue(controlledValue);
- }
- }, [controlledValue]);
-
- const handleChange = useCallback((newValue: T) => {
- if (!isControlledRef.current) {
- setInternalValue(newValue);
- }
- }, []);
-
- const value = isControlledRef.current ? controlledValue : internalValue;
-
- return [value, handleChange];
-}