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 <jasquat@users.noreply.github.com>
This commit is contained in:
Kevin Burnett 2024-05-22 15:07:23 +00:00 committed by GitHub
parent e5d51907fe
commit 154be22161
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 48 additions and 149 deletions

View File

@ -2,6 +2,8 @@ const submitInputIntoFormField = (taskName, fieldKey, fieldValue) => {
cy.contains(`Task: ${taskName}`, { timeout: 10000 }); cy.contains(`Task: ${taskName}`, { timeout: 10000 });
cy.get(fieldKey).clear(); cy.get(fieldKey).clear();
cy.get(fieldKey).type(fieldValue); cy.get(fieldKey).type(fieldValue);
// wait a little bit after typing for the debounce to take effect
cy.wait(100);
cy.contains('Submit').click(); cy.contains('Submit').click();
}; };

View File

@ -87,6 +87,14 @@ export default function BaseInputTemplate<
100 100
); );
const addDebouncedOnChangeText = useDebouncedCallback(
(fullObject: React.ChangeEvent<HTMLInputElement>) => {
(onChangeOverride || _onChange)(fullObject);
},
// delay in ms
100
);
let enableCounter = false; let enableCounter = false;
let maxCount = undefined; let maxCount = undefined;
if (options && options.counter) { if (options && options.counter) {
@ -180,9 +188,9 @@ export default function BaseInputTemplate<
invalid={commonAttributes.invalid} invalid={commonAttributes.invalid}
invalidText={commonAttributes.errorMessageForField} invalidText={commonAttributes.errorMessageForField}
autoFocus={autofocus} autoFocus={autofocus}
onChange={onChangeOverride || _onChange}
disabled={disabled || readonly} disabled={disabled || readonly}
value={value || value === 0 ? value : ''} defaultValue={value || value === 0 ? value : ''}
onChange={addDebouncedOnChangeText}
onBlur={_onBlur} onBlur={_onBlur}
onFocus={_onFocus} onFocus={_onFocus}
enableCounter={enableCounter} enableCounter={enableCounter}

View File

@ -1,140 +1,18 @@
import { Select, SelectItem } from '@carbon/react'; 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'; import { getCommonAttributes } from '../../helpers';
// this guessType, asNumber, and processSelectValue code is pulled from rjsf/utils version 5.0.0-beta.20 function SelectWidget<
// the function was removed. T = any,
/** Given a specific `value` attempts to guess the type of a schema element. In the case where we have to implicitly S extends StrictRJSFSchema = RJSFSchema,
* create a schema, it is useful to know what type to use based on the data we are defining. F extends FormContextType = any
* >({
* @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({
schema, schema,
id, id,
options, options,
@ -151,22 +29,21 @@ function SelectWidget({
uiSchema, uiSchema,
placeholder, placeholder,
rawErrors = [], rawErrors = [],
}: WidgetProps) { }: WidgetProps<T, S, F>) {
const { enumOptions } = options; const { enumOptions } = options;
let { enumDisabled } = options; let { enumDisabled } = options;
const emptyValue = multiple ? [] : ''; const emptyValue = multiple ? [] : '';
const _onChange = ({ const _onChange = ({ target: { value } }: ChangeEvent<{ value: string }>) => {
target: { value }, onChange(value);
}: React.ChangeEvent<{ name?: string; value: unknown }>) => };
onChange(processSelectValue(schema, value, options));
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) => const _onBlur = ({ target: { value } }: FocusEvent<HTMLInputElement>) => {
onBlur(id, processSelectValue(schema, value, options)); onBlur(id, value);
const _onFocus = ({ };
target: { value }, const _onFocus = ({ target: { value } }: FocusEvent<HTMLInputElement>) =>
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
onFocus(id, processSelectValue(schema, value, options));
const commonAttributes = getCommonAttributes( const commonAttributes = getCommonAttributes(
label, label,

View File

@ -1,6 +1,7 @@
import React, { FocusEvent, useCallback } from 'react'; import React, { FocusEvent, useCallback } from 'react';
// @ts-ignore // @ts-ignore
import { TextArea } from '@carbon/react'; import { TextArea } from '@carbon/react';
import { useDebouncedCallback } from 'use-debounce';
import { import {
FormContextType, FormContextType,
RJSFSchema, RJSFSchema,
@ -52,6 +53,17 @@ function TextareaWidget<
[id, onFocus] [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<HTMLTextAreaElement>) => {
handleChange(fullObject);
},
// delay in ms
100
);
const commonAttributes = getCommonAttributes( const commonAttributes = getCommonAttributes(
label, label,
schema, schema,
@ -78,7 +90,7 @@ function TextareaWidget<
name={id} name={id}
className="text-input" className="text-input"
helperText={commonAttributes.helperText} helperText={commonAttributes.helperText}
value={value || ''} defaultValue={value || ''}
labelText="" labelText=""
placeholder={placeholder} placeholder={placeholder}
required={required} required={required}
@ -88,7 +100,7 @@ function TextareaWidget<
rows={options.rows} rows={options.rows}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
onChange={handleChange} onChange={addDebouncedOnChangeText}
invalid={commonAttributes.invalid} invalid={commonAttributes.invalid}
invalidText={commonAttributes.errorMessageForField} invalidText={commonAttributes.errorMessageForField}
enableCounter={enableCounter} enableCounter={enableCounter}