From 78b03ffad7888d65f5739599b81f96df11c975ba Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Mon, 25 Sep 2023 13:30:50 -0700 Subject: [PATCH 1/6] [setup] Mock canvas & truncation Jest logic --- scripts/jest/setup/mocks.js | 6 ++++ src/components/text_truncate/utils.testenv.ts | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/components/text_truncate/utils.testenv.ts diff --git a/scripts/jest/setup/mocks.js b/scripts/jest/setup/mocks.js index d83925fd818..1f7dc9cdb80 100644 --- a/scripts/jest/setup/mocks.js +++ b/scripts/jest/setup/mocks.js @@ -15,6 +15,12 @@ jest.mock('./../../../src/components/icon', () => { return { EuiIcon }; }); +jest.mock('./../../../src/components/text_truncate', () => { + const rest = jest.requireActual('./../../../src/components/text_truncate'); + const utils = require('./../../../src/components/text_truncate/utils.testenv'); + return { ...rest, ...utils }; +}); + jest.mock('./../../../src/services/accessibility', () => { const a11y = jest.requireActual('./../../../src/services/accessibility'); const { diff --git a/src/components/text_truncate/utils.testenv.ts b/src/components/text_truncate/utils.testenv.ts new file mode 100644 index 00000000000..9018cacc784 --- /dev/null +++ b/src/components/text_truncate/utils.testenv.ts @@ -0,0 +1,36 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TruncationUtils as _TruncationUtils } from './utils'; + +export class CanvasTextUtils { + constructor(_: any) {} + + computeFontFromElement = (_: HTMLElement) => ''; + + textWidth = 0; + + currentText = ''; + setTextToCheck = (text: string) => { + this.currentText = text; + }; +} + +export class TruncationUtils extends _TruncationUtils { + constructor(props: ConstructorParameters[0]) { + super(props); + } + + // Jest perf optimization - since there's no meaningful truncation we can make + // without meaningful width calculations, just return the full untruncated text + truncateStart = (_?: number) => this.fullText; + truncateEnd = (_?: number) => this.fullText; + truncateStartEndAtPosition = (_?: number) => this.fullText; + truncateStartEndAtMiddle = () => this.fullText; + truncateMiddle = () => this.fullText; +} From d7f30ee71fc1ebc3109c85502d4596696daa31aa Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 21 Sep 2023 14:37:04 -0700 Subject: [PATCH 2/6] [EuiComboBoxInput] Replace `AutosizeInput` with more performant canvas util/logic + minor destructuring/code cleanup in `inputOnChange` --- src/components/combo_box/_combo_box.scss | 31 ++++++-------- .../combo_box_input/combo_box_input.tsx | 42 ++++++++++++------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/components/combo_box/_combo_box.scss b/src/components/combo_box/_combo_box.scss index 671bdbdc000..6c0ecb3611a 100644 --- a/src/components/combo_box/_combo_box.scss +++ b/src/components/combo_box/_combo_box.scss @@ -55,27 +55,20 @@ /** * 1. Force field height to match other field heights. * 2. Force input height to expand to fill this element. - * 3. Reset appearance on Safari. - * 4. Fix react-input-autosize appearance. - * 5. Prevent a lot of input from causing the react-input-autosize to overflow the container. + * 3. Reset input appearance to mimic text */ .euiComboBox__input { - // stylelint-disable-next-line declaration-no-important - display: inline-flex !important; /* 1 */ - height: $euiSizeXL; /* 2 */ - overflow: hidden; /* 5 */ - - > input { - @include euiFont; - appearance: none; /* 3 */ - padding: 0; - border: none; - background: transparent; - font-size: $euiFontSizeS; - color: $euiTextColor; - margin: $euiSizeXS; - line-height: $euiLineHeight; /* 4 */ - } + block-size: $euiSizeL; /* 2 */ + min-inline-size: 2px; + max-inline-size: 100%; + margin: $euiSizeXS; + + /* 3 */ + appearance: none; + outline: none; + border: none; + background: transparent; + color: $euiTextColor; } &.euiComboBox-isOpen { diff --git a/src/components/combo_box/combo_box_input/combo_box_input.tsx b/src/components/combo_box/combo_box_input/combo_box_input.tsx index a9f8e1ddde4..4a34bd4675d 100644 --- a/src/components/combo_box/combo_box_input/combo_box_input.tsx +++ b/src/components/combo_box/combo_box_input/combo_box_input.tsx @@ -14,10 +14,10 @@ import React, { RefCallback, } from 'react'; import classNames from 'classnames'; -import AutosizeInput from 'react-input-autosize'; import { CommonProps } from '../../common'; import { htmlIdGenerator, keys } from '../../../services'; +import { CanvasTextUtils } from '../../text_truncate'; import { EuiScreenReaderOnly } from '../../accessibility'; import { EuiFormControlLayout, @@ -35,7 +35,6 @@ import { } from '../types'; export interface EuiComboBoxInputProps extends CommonProps { - autoSizeInputRef?: RefCallback; compressed: boolean; focusedOptionId?: string; fullWidth?: boolean; @@ -71,6 +70,7 @@ export interface EuiComboBoxInputProps extends CommonProps { } interface EuiComboBoxInputState { + inputWidth: number; hasFocus: boolean; } @@ -79,9 +79,28 @@ export class EuiComboBoxInput extends Component< EuiComboBoxInputState > { state: EuiComboBoxInputState = { + inputWidth: 2, hasFocus: false, }; + private widthUtils?: CanvasTextUtils; + + inputRefCallback = (el: HTMLInputElement) => { + this.widthUtils = new CanvasTextUtils({ container: el }); + this.props.inputRef?.(el); + }; + + updateInputSize = (inputValue: string) => { + if (!this.widthUtils) return; + + this.widthUtils.setTextToCheck(inputValue); + // Canvas has minute subpixel differences in rendering compared to DOM + // We'll buffer the input by ~2px just to ensure sufficient width + const inputWidth = Math.ceil(this.widthUtils.textWidth) + 2; + + this.setState({ inputWidth }); + }; + updatePosition = () => { // Wait a beat for the DOM to update, since we depend on DOM elements' bounds. requestAnimationFrame(() => { @@ -139,17 +158,10 @@ export class EuiComboBoxInput extends Component< } inputOnChange: ChangeEventHandler = (event) => { - const { onChange, searchValue } = this.props; - if (onChange) { - onChange(event.target.value as typeof searchValue); - } - }; + const { value } = event.target; - inputRefCallback = (ref: HTMLInputElement & AutosizeInput) => { - const { autoSizeInputRef } = this.props; - if (autoSizeInputRef) { - autoSizeInputRef(ref); - } + this.updateInputSize(value); + this.props.onChange?.(value); }; render() { @@ -159,7 +171,6 @@ export class EuiComboBoxInput extends Component< fullWidth, hasSelectedOptions, id, - inputRef, isDisabled, isListOpen, noIcon, @@ -327,7 +338,7 @@ export class EuiComboBoxInput extends Component< > {!singleSelection || !searchValue ? pills : null} {placeholderMessage} - extends Component< data-test-subj="comboBoxSearchInput" disabled={isDisabled} id={id} - inputRef={inputRef} onBlur={this.onBlur} onChange={this.inputOnChange} onFocus={this.onFocus} onKeyDown={this.onKeyDown} ref={this.inputRefCallback} role="combobox" - style={{ fontSize: 14 }} + style={{ inlineSize: this.state.inputWidth }} value={searchValue} autoFocus={autoFocus} /> From a9930daf77feaed231f8d13a6be7f42beec17922 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 21 Sep 2023 14:37:31 -0700 Subject: [PATCH 3/6] [EuiComboBox] Remove `AutosizeInput` references + update snapshots --- .../__snapshots__/combo_box.test.tsx.snap | 73 ++++++------------- src/components/combo_box/combo_box.tsx | 15 ---- 2 files changed, 23 insertions(+), 65 deletions(-) diff --git a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap index 747e46f60e8..6bf8b35fb61 100644 --- a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap +++ b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap @@ -16,26 +16,19 @@ exports[`EuiComboBox is rendered 1`] = ` data-test-subj="comboBoxInput" tabindex="-1" > -
-
- -
-
+ data-test-subj="comboBoxSearchInput" + id="generated-id__eui-combobox-id" + role="combobox" + style="inline-size: 2px;" + value="" + />
extends Component< }); } }; - autoSizeInputRefInstance: RefInstance = null; - autoSizeInputRefCallback: RefCallback = ( - ref - ) => { - this.autoSizeInputRefInstance = ref; - }; searchInputRefInstance: RefInstance = null; searchInputRefCallback: RefCallback = (ref) => { @@ -787,13 +780,6 @@ export class EuiComboBox extends Component< componentDidMount() { this._isMounted = true; - - // TODO: This will need to be called once the actual stylesheet loads. - setTimeout(() => { - if (this.autoSizeInputRefInstance) { - this.autoSizeInputRefInstance.copyInputStyles(); - } - }, 100); } static getDerivedStateFromProps( @@ -1044,7 +1030,6 @@ export class EuiComboBox extends Component< ref={this.comboBoxRefCallback} > Date: Thu, 21 Sep 2023 14:37:38 -0700 Subject: [PATCH 4/6] Remove `react-autosize-input` dependency --- package.json | 2 -- yarn.lock | 16 +--------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/package.json b/package.json index 30e4e4c6b41..98eed9ba2c8 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@hello-pangea/dnd": "^16.3.0", "@types/lodash": "^4.14.198", "@types/numeral": "^2.0.2", - "@types/react-input-autosize": "^2.2.1", "@types/react-window": "^1.8.5", "@types/refractor": "^3.0.2", "@types/resize-observer-browser": "^0.1.7", @@ -80,7 +79,6 @@ "react-dropzone": "^11.7.1", "react-element-to-jsx-string": "^14.3.4", "react-focus-on": "^3.9.1", - "react-input-autosize": "^3.0.0", "react-is": "^17.0.2", "react-remove-scroll-bar": "^2.3.4", "react-virtualized-auto-sizer": "^1.0.20", diff --git a/yarn.lock b/yarn.lock index 8ef2aad9d05..0e0365445af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5083,13 +5083,6 @@ dependencies: "@types/react" "*" -"@types/react-input-autosize@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/react-input-autosize/-/react-input-autosize-2.2.1.tgz#6a335212e7fce1e1a4da56ae2095c8c5c35fbfe6" - integrity sha512-RxzEjd4gbLAAdLQ92Q68/AC+TfsAKTc4evsArUH1aIShIMqQMIMjsxoSnwyjtbFTO/AGIW/RQI94XSdvOxCz/w== - dependencies: - "@types/react" "*" - "@types/react-is@^17.0.3": version "17.0.3" resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" @@ -17709,7 +17702,7 @@ prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -18147,13 +18140,6 @@ react-helmet@^6.1.0: react-fast-compare "^3.1.1" react-side-effect "^2.1.0" -react-input-autosize@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" - integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg== - dependencies: - prop-types "^15.5.8" - react-inspector@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.2.tgz#aa3028803550cb6dbd7344816d5c80bf39d07e9d" From 0c4e758920f26c07bef932c3de9c64e1df36fa4f Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 21 Sep 2023 14:56:33 -0700 Subject: [PATCH 5/6] changelog --- upcoming_changelogs/7215.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 upcoming_changelogs/7215.md diff --git a/upcoming_changelogs/7215.md b/upcoming_changelogs/7215.md new file mode 100644 index 00000000000..080bc0e5d56 --- /dev/null +++ b/upcoming_changelogs/7215.md @@ -0,0 +1 @@ +- Improved the performance of `EuiComboBox` by removing the `react-autosizer-input` dependency From adcc5010c81b64c8d44a4fe77de36b61da8da339 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 21 Sep 2023 15:15:13 -0700 Subject: [PATCH 6/6] Write E2E Cypress tests --- src/components/combo_box/combo_box.spec.tsx | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/components/combo_box/combo_box.spec.tsx b/src/components/combo_box/combo_box.spec.tsx index 4910941a1f5..e40d4ffd85d 100644 --- a/src/components/combo_box/combo_box.spec.tsx +++ b/src/components/combo_box/combo_box.spec.tsx @@ -50,6 +50,42 @@ describe('EuiComboBox', () => { }); }); + describe('input auto sizing', () => { + it('resizes the width of the input based to fit the search text', () => { + cy.realMount(); + cy.get('[data-test-subj="comboBoxSearchInput"]').should( + 'have.attr', + 'style', + 'inline-size: 2px;' + ); + + cy.get('[data-test-subj="comboBoxSearchInput"]').realClick(); + cy.realType('lorem ipsum dolor'); + cy.get('[data-test-subj="comboBoxSearchInput"]').should( + 'have.attr', + 'style', + 'inline-size: 121px;' + ); + }); + + it('does not exceed the maximum possible width of the input wrapper', () => { + cy.realMount(); + cy.get('[data-test-subj="comboBoxSearchInput"]').realClick(); + cy.realType( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit......' + ); + + cy.get('[data-test-subj="comboBoxSearchInput"]').should( + 'have.attr', + 'style', + 'inline-size: 387px;' + ); + cy.get('[data-test-subj="comboBoxSearchInput"]') + .invoke('width') + .should('be.eq', 354); + }); + }); + describe('truncation', () => { const sharedProps = { style: { width: 200 },