Skip to content

Commit

Permalink
[Lens] Debounce axis name inputs mob programming (#100108)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbondyra committed May 18, 2021
1 parent 6bb55f8 commit 15abf24
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import './dimension_editor.scss';
import _ from 'lodash';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import React, { useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiListGroup,
Expand Down Expand Up @@ -44,6 +44,7 @@ import { ReferenceEditor } from './reference_editor';
import { setTimeScaling, TimeScaling } from './time_scaling';
import { defaultFilter, Filtering, setFilter } from './filtering';
import { AdvancedOptions } from './advanced_options';
import { useDebouncedValue } from '../../shared_components';

const operationPanels = getOperationDisplay();

Expand All @@ -53,39 +54,8 @@ export interface DimensionEditorProps extends IndexPatternDimensionEditorProps {
currentIndexPattern: IndexPattern;
}

/**
* This component shows a debounced input for the label of a dimension. It will update on root state changes
* if no debounced changes are in flight because the user is currently typing into the input.
*/
const LabelInput = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => {
const [inputValue, setInputValue] = useState(value);
const unflushedChanges = useRef(false);

// Save the initial value
const initialValue = useRef(value);

const onChangeDebounced = useMemo(() => {
const callback = _.debounce((val: string) => {
onChange(val);
unflushedChanges.current = false;
}, 256);
return (val: string) => {
unflushedChanges.current = true;
callback(val);
};
}, [onChange]);

useEffect(() => {
if (!unflushedChanges.current && value !== inputValue) {
setInputValue(value);
}
}, [value, inputValue]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = String(e.target.value);
setInputValue(val);
onChangeDebounced(val || initialValue.current);
};
const { inputValue, handleInputChange, initialValue } = useDebouncedValue({ onChange, value });

return (
<EuiFormRow
Expand All @@ -100,8 +70,10 @@ const LabelInput = ({ value, onChange }: { value: string; onChange: (value: stri
compressed
data-test-subj="indexPattern-label-edit"
value={inputValue}
onChange={handleInputChange}
placeholder={initialValue.current}
onChange={(e) => {
handleInputChange(e.target.value);
}}
placeholder={initialValue}
/>
</EuiFormRow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ jest.mock('@elastic/eui', () => {
};
});

jest.mock('react-use/lib/useDebounce', () => (fn: () => void) => fn());

jest.mock('lodash', () => {
const original = jest.requireActual('lodash');

return {
...original,
debounce: (fn: unknown) => fn,
};
});

const dataPluginMockValue = dataPluginMock.createStartContract();
// need to overwrite the formatter field first
dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ params }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* 2.0.
*/

import React, { useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import React from 'react';
import { EuiFieldText, keys } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDebouncedValue } from '../../../../shared_components';

export const LabelInput = ({
value,
Expand All @@ -27,20 +27,13 @@ export const LabelInput = ({
dataTestSubj?: string;
compressed?: boolean;
}) => {
const [inputValue, setInputValue] = useState(value);

useDebounce(() => onChange(inputValue), 256, [inputValue]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = String(e.target.value);
setInputValue(val);
};
const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange });

return (
<EuiFieldText
data-test-subj={dataTestSubj || 'lens-labelInput'}
value={inputValue}
onChange={handleInputChange}
onChange={(e) => handleInputChange(e.target.value)}
fullWidth
placeholder={placeholder || ''}
inputRef={(node) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
* 2.0.
*/

import React, { useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { IndexPattern } from './types';
import { QueryStringInput, Query } from '../../../../../src/plugins/data/public';
import { useDebouncedValue } from '../shared_components';

export const QueryInput = ({
value,
Expand All @@ -26,13 +26,7 @@ export const QueryInput = ({
onSubmit: () => void;
disableAutoFocus?: boolean;
}) => {
const [inputValue, setInputValue] = useState(value);

useDebounce(() => onChange(inputValue), 256, [inputValue]);

const handleInputChange = (input: Query) => {
setInputValue(input);
};
const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange });

return (
<QueryStringInput
Expand Down
13 changes: 5 additions & 8 deletions x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
*/

import './toolbar.scss';
import React, { useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
Expand All @@ -21,7 +20,7 @@ import { PaletteRegistry } from 'src/plugins/charts/public';
import { DEFAULT_PERCENT_DECIMALS } from './constants';
import { PieVisualizationState, SharedPieLayerState } from './types';
import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types';
import { ToolbarPopover, LegendSettingsPopover } from '../shared_components';
import { ToolbarPopover, LegendSettingsPopover, useDebouncedValue } from '../shared_components';
import { PalettePicker } from '../shared_components';

const numberOptions: Array<{
Expand Down Expand Up @@ -233,19 +232,17 @@ const DecimalPlaceSlider = ({
value: number;
setValue: (value: number) => void;
}) => {
const [localValue, setLocalValue] = useState(value);
useDebounce(() => setValue(localValue), 256, [localValue]);

const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange: setValue });
return (
<EuiRange
data-test-subj="indexPattern-dimension-formatDecimals"
value={localValue}
value={inputValue}
min={0}
max={10}
showInput
compressed
onChange={(e) => {
setLocalValue(Number(e.currentTarget.value));
handleInputChange(Number(e.currentTarget.value));
}}
/>
);
Expand Down
52 changes: 52 additions & 0 deletions x-pack/plugins/lens/public/shared_components/debounced_value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useState, useMemo, useEffect, useRef } from 'react';
import _ from 'lodash';

/**
* Debounces value changes and updates inputValue on root state changes if no debounced changes
* are in flight because the user is currently modifying the value.
*/

export const useDebouncedValue = <T>({
onChange,
value,
}: {
onChange: (val: T) => void;
value: T;
}) => {
const [inputValue, setInputValue] = useState(value);
const unflushedChanges = useRef(false);

// Save the initial value
const initialValue = useRef(value);

const onChangeDebounced = useMemo(() => {
const callback = _.debounce((val: T) => {
onChange(val);
unflushedChanges.current = false;
}, 256);
return (val: T) => {
unflushedChanges.current = true;
callback(val);
};
}, [onChange]);

useEffect(() => {
if (!unflushedChanges.current && value !== inputValue) {
setInputValue(value);
}
}, [value, inputValue]);

const handleInputChange = (val: T) => {
setInputValue(val);
onChangeDebounced(val || initialValue.current);
};

return { inputValue, handleInputChange, initialValue: initialValue.current };
};
1 change: 1 addition & 0 deletions x-pack/plugins/lens/public/shared_components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './empty_placeholder';
export { ToolbarPopoverProps, ToolbarPopover } from './toolbar_popover';
export { LegendSettingsPopover } from './legend_settings_popover';
export { PalettePicker } from './palette_picker';
export { useDebouncedValue } from './debounced_value';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useState } from 'react';
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
Expand All @@ -17,7 +17,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { XYLayerConfig, AxesSettingsConfig } from './types';
import { ToolbarPopover } from '../shared_components';
import { ToolbarPopover, useDebouncedValue } from '../shared_components';
import { isHorizontalChart } from './state_helpers';
import { EuiIconAxisBottom } from '../assets/axis_bottom';
import { EuiIconAxisLeft } from '../assets/axis_left';
Expand Down Expand Up @@ -149,15 +149,13 @@ export const AxisSettingsPopover: React.FunctionComponent<AxisSettingsPopoverPro
setEndzoneVisibility,
endzonesVisible,
}) => {
const [title, setTitle] = useState<string | undefined>(axisTitle);

const isHorizontal = layers?.length ? isHorizontalChart(layers) : false;
const config = popoverConfig(axis, isHorizontal);

const onTitleChange = (value: string): void => {
setTitle(value);
updateTitleState(value);
};
const { inputValue: title, handleInputChange: onTitleChange } = useDebouncedValue<string>({
value: axisTitle || '',
onChange: updateTitleState,
});
return (
<ToolbarPopover
title={config.popoverTitle}
Expand Down

0 comments on commit 15abf24

Please sign in to comment.