From 154be22161bd5d3c707e2aa1d25ef7c88b426ac1 Mon Sep 17 00:00:00 2001 From: Kevin Burnett <18027+burnettk@users.noreply.github.com> Date: Wed, 22 May 2024 15:07:23 +0000 Subject: [PATCH] fixed the select widget and text fields so they are no longer so slow when typing (#1573) * fixed the select widget and text fields so they are no longer so slow when typing * attempt to wait a little after typing in cypress tests for the debounce w/ burnettk * fixed onChangeOverride issue w/ burnettk --------- Co-authored-by: jasquat --- .../cypress/e2e/tasks.cy.js | 2 + .../BaseInputTemplate/BaseInputTemplate.tsx | 12 +- .../SelectWidget/SelectWidget.tsx | 167 +++--------------- .../TextareaWidget/TextareaWidget.tsx | 16 +- 4 files changed, 48 insertions(+), 149 deletions(-) diff --git a/spiffworkflow-frontend/cypress/e2e/tasks.cy.js b/spiffworkflow-frontend/cypress/e2e/tasks.cy.js index f0ece168..a0761c4a 100644 --- a/spiffworkflow-frontend/cypress/e2e/tasks.cy.js +++ b/spiffworkflow-frontend/cypress/e2e/tasks.cy.js @@ -2,6 +2,8 @@ const submitInputIntoFormField = (taskName, fieldKey, fieldValue) => { cy.contains(`Task: ${taskName}`, { timeout: 10000 }); cy.get(fieldKey).clear(); cy.get(fieldKey).type(fieldValue); + // wait a little bit after typing for the debounce to take effect + cy.wait(100); cy.contains('Submit').click(); }; diff --git a/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx b/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx index 49e312e0..2cc721b1 100644 --- a/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx +++ b/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx @@ -87,6 +87,14 @@ export default function BaseInputTemplate< 100 ); + const addDebouncedOnChangeText = useDebouncedCallback( + (fullObject: React.ChangeEvent) => { + (onChangeOverride || _onChange)(fullObject); + }, + // delay in ms + 100 + ); + let enableCounter = false; let maxCount = undefined; if (options && options.counter) { @@ -180,9 +188,9 @@ export default function BaseInputTemplate< invalid={commonAttributes.invalid} invalidText={commonAttributes.errorMessageForField} autoFocus={autofocus} - onChange={onChangeOverride || _onChange} disabled={disabled || readonly} - value={value || value === 0 ? value : ''} + defaultValue={value || value === 0 ? value : ''} + onChange={addDebouncedOnChangeText} onBlur={_onBlur} onFocus={_onFocus} enableCounter={enableCounter} diff --git a/spiffworkflow-frontend/src/rjsf/carbon_theme/SelectWidget/SelectWidget.tsx b/spiffworkflow-frontend/src/rjsf/carbon_theme/SelectWidget/SelectWidget.tsx index c06a8cb8..a9181139 100644 --- a/spiffworkflow-frontend/src/rjsf/carbon_theme/SelectWidget/SelectWidget.tsx +++ b/spiffworkflow-frontend/src/rjsf/carbon_theme/SelectWidget/SelectWidget.tsx @@ -1,140 +1,18 @@ import { Select, SelectItem } from '@carbon/react'; -import { WidgetProps } from '@rjsf/utils'; +import { + FormContextType, + RJSFSchema, + StrictRJSFSchema, + WidgetProps, +} from '@rjsf/utils'; +import { ChangeEvent, FocusEvent } from 'react'; import { getCommonAttributes } from '../../helpers'; -// this guessType, asNumber, and processSelectValue code is pulled from rjsf/utils version 5.0.0-beta.20 -// the function was removed. -/** Given a specific `value` attempts to guess the type of a schema element. In the case where we have to implicitly - * create a schema, it is useful to know what type to use based on the data we are defining. - * - * @param value - The value from which to guess the type - * @returns - The best guess for the object type - */ -function guessType(value) { - if (Array.isArray(value)) { - return 'array'; - } - if (typeof value === 'string') { - return 'string'; - } - if (value == null) { - return 'null'; - } - if (typeof value === 'boolean') { - return 'boolean'; - } - if (!isNaN(value)) { - return 'number'; - } - if (typeof value === 'object') { - return 'object'; - } - // Default to string if we can't figure it out - return 'string'; -} - -/** Attempts to convert the string into a number. If an empty string is provided, then `undefined` is returned. If a - * `null` is provided, it is returned. If the string ends in a `.` then the string is returned because the user may be - * in the middle of typing a float number. If a number ends in a pattern like `.0`, `.20`, `.030`, string is returned - * because the user may be typing number that will end in a non-zero digit. Otherwise, the string is wrapped by - * `Number()` and if that result is not `NaN`, that number will be returned, otherwise the string `value` will be. - * - * @param value - The string or null value to convert to a number - * @returns - The `value` converted to a number when appropriate, otherwise the `value` - */ -function asNumber(value) { - if (value === '') { - return undefined; - } - if (value === null) { - return null; - } - if (/\.$/.test(value)) { - // '3.' can't really be considered a number even if it parses in js. The - // user is most likely entering a float. - return value; - } - if (/\.0$/.test(value)) { - // we need to return this as a string here, to allow for input like 3.07 - return value; - } - if (/\.\d*0$/.test(value)) { - // It's a number, that's cool - but we need it as a string so it doesn't screw - // with the user when entering dollar amounts or other values (such as those with - // specific precision or number of significant digits) - return value; - } - var n = Number(value); - var valid = typeof n === 'number' && !Number.isNaN(n); - return valid ? n : value; -} - -function get(object, path) { - // Split the path into an array of keys - const keys = Array.isArray(path) ? path : path.split('.'); - - // Traverse the object along the keys - let result = object; - for (let key of keys) { - if (result == null) { - return undefined; // If the path does not exist, return undefined - } - result = result[key]; - } - - return result; // Return the found value or undefined if not found -} - -var nums = /*#__PURE__*/ new Set(['number', 'integer']); -/** Returns the real value for a select widget due to a silly limitation in the DOM which causes option change event - * values to always be retrieved as strings. Uses the `schema` to help determine the value's true type. If the value is - * an empty string, then the `emptyValue` from the `options` is returned, falling back to undefined. - * - * @param schema - The schema to used to determine the value's true type - * @param [value] - The value to convert - * @param [options] - The UIOptionsType from which to potentially extract the emptyValue - * @returns - The `value` converted to the proper type - */ -function processSelectValue(schema, value, options) { - var schemaEnum = schema['enum'], - type = schema.type, - items = schema.items; - if (value === '') { - return options && options.emptyValue !== undefined - ? options.emptyValue - : undefined; - } - if (type === 'array' && items && nums.has(get(items, 'type'))) { - return value.map(asNumber); - } - if (type === 'boolean') { - return value === 'true'; - } - if (nums.has(type)) { - return asNumber(value); - } - // If type is undefined, but an enum is present, try and infer the type from - // the enum values - if (Array.isArray(schemaEnum)) { - if ( - schemaEnum.every(function (x) { - return nums.has(guessType(x)); - }) - ) { - return asNumber(value); - } - if ( - schemaEnum.every(function (x) { - return guessType(x) === 'boolean'; - }) - ) { - return value === 'true'; - } - } - return value; -} - -function SelectWidget({ +function SelectWidget< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>({ schema, id, options, @@ -151,22 +29,21 @@ function SelectWidget({ uiSchema, placeholder, rawErrors = [], -}: WidgetProps) { +}: WidgetProps) { const { enumOptions } = options; let { enumDisabled } = options; const emptyValue = multiple ? [] : ''; - const _onChange = ({ - target: { value }, - }: React.ChangeEvent<{ name?: string; value: unknown }>) => - onChange(processSelectValue(schema, value, options)); - const _onBlur = ({ target: { value } }: React.FocusEvent) => - onBlur(id, processSelectValue(schema, value, options)); - const _onFocus = ({ - target: { value }, - }: React.FocusEvent) => - onFocus(id, processSelectValue(schema, value, options)); + const _onChange = ({ target: { value } }: ChangeEvent<{ value: string }>) => { + onChange(value); + }; + + const _onBlur = ({ target: { value } }: FocusEvent) => { + onBlur(id, value); + }; + const _onFocus = ({ target: { value } }: FocusEvent) => + onFocus(id, value); const commonAttributes = getCommonAttributes( label, diff --git a/spiffworkflow-frontend/src/rjsf/carbon_theme/TextareaWidget/TextareaWidget.tsx b/spiffworkflow-frontend/src/rjsf/carbon_theme/TextareaWidget/TextareaWidget.tsx index 9cdd58d5..a153de12 100644 --- a/spiffworkflow-frontend/src/rjsf/carbon_theme/TextareaWidget/TextareaWidget.tsx +++ b/spiffworkflow-frontend/src/rjsf/carbon_theme/TextareaWidget/TextareaWidget.tsx @@ -1,6 +1,7 @@ import React, { FocusEvent, useCallback } from 'react'; // @ts-ignore import { TextArea } from '@carbon/react'; +import { useDebouncedCallback } from 'use-debounce'; import { FormContextType, RJSFSchema, @@ -52,6 +53,17 @@ function TextareaWidget< [id, onFocus] ); + // this helps with performance for the select widget with rsjf 5.1+. + // otherwise if the form has an enum with a corresponding oneOf, after choosing + // an option in the dropdown, the text area slows way down. + const addDebouncedOnChangeText = useDebouncedCallback( + (fullObject: React.ChangeEvent) => { + handleChange(fullObject); + }, + // delay in ms + 100 + ); + const commonAttributes = getCommonAttributes( label, schema, @@ -78,7 +90,7 @@ function TextareaWidget< name={id} className="text-input" helperText={commonAttributes.helperText} - value={value || ''} + defaultValue={value || ''} labelText="" placeholder={placeholder} required={required} @@ -88,7 +100,7 @@ function TextareaWidget< rows={options.rows} onBlur={handleBlur} onFocus={handleFocus} - onChange={handleChange} + onChange={addDebouncedOnChangeText} invalid={commonAttributes.invalid} invalidText={commonAttributes.errorMessageForField} enableCounter={enableCounter}