updated checkbox widget to use carbon and added tooltip options w/ burnettk (#466)

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-09-07 13:41:06 -04:00 committed by GitHub
parent f9e4cf577a
commit 69e2fef0b1
7 changed files with 149 additions and 168 deletions

View File

@ -1,4 +1,3 @@
// @ts-ignore
import { DatePicker, DatePickerInput, TextInput } from '@carbon/react'; import { DatePicker, DatePickerInput, TextInput } from '@carbon/react';
import { import {
getInputProps, getInputProps,
@ -11,6 +10,7 @@ import {
import { useCallback } from 'react'; import { useCallback } from 'react';
import { DATE_FORMAT_CARBON, DATE_FORMAT_FOR_DISPLAY } from '../../../config'; import { DATE_FORMAT_CARBON, DATE_FORMAT_FOR_DISPLAY } from '../../../config';
import { ymdDateStringToConfiguredFormat } from '../../../helpers'; import { ymdDateStringToConfiguredFormat } from '../../../helpers';
import { getCommonAttributes } from '../../helpers';
/** The `BaseInputTemplate` is the template to use to render the basic `<input>` component for the `core` theme. /** The `BaseInputTemplate` is the template to use to render the basic `<input>` component for the `core` theme.
* It is used as the template for rendering many of the <input> based widgets that differ by `type` and callbacks only. * It is used as the template for rendering many of the <input> based widgets that differ by `type` and callbacks only.
@ -70,28 +70,12 @@ export default function BaseInputTemplate<
[onFocus, id] [onFocus, id]
); );
let labelToUse = label; const commonAttributes = getCommonAttributes(
if (uiSchema && uiSchema['ui:title']) { label,
labelToUse = uiSchema['ui:title']; schema,
} else if (schema && schema.title) { uiSchema,
labelToUse = schema.title; rawErrors
} );
let helperText = null;
if (uiSchema && uiSchema['ui:help']) {
helperText = uiSchema['ui:help'];
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
if ('validationErrorMessage' in schema) {
errorMessageForField = (schema as any).validationErrorMessage;
} else {
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
}
}
let component = null; let component = null;
if (type === 'date') { if (type === 'date') {
@ -118,15 +102,15 @@ export default function BaseInputTemplate<
<DatePickerInput <DatePickerInput
id={id} id={id}
placeholder={DATE_FORMAT_FOR_DISPLAY} placeholder={DATE_FORMAT_FOR_DISPLAY}
helperText={helperText} helperText={commonAttributes.helperText}
type="text" type="text"
size="md" size="md"
value={dateValue} value={dateValue}
autocomplete="off" autocomplete="off"
allowInput={false} allowInput={false}
onChange={_onChange} onChange={_onChange}
invalid={invalid} invalid={commonAttributes.invalid}
invalidText={errorMessageForField} invalidText={commonAttributes.errorMessageForField}
autoFocus={autofocus} autoFocus={autofocus}
disabled={disabled || readonly} disabled={disabled || readonly}
onBlur={_onBlur} onBlur={_onBlur}
@ -141,9 +125,9 @@ export default function BaseInputTemplate<
<TextInput <TextInput
id={id} id={id}
className="text-input" className="text-input"
helperText={helperText} helperText={commonAttributes.helperText}
invalid={invalid} invalid={commonAttributes.invalid}
invalidText={errorMessageForField} invalidText={commonAttributes.errorMessageForField}
autoFocus={autofocus} autoFocus={autofocus}
disabled={disabled || readonly} disabled={disabled || readonly}
value={value || value === 0 ? value : ''} value={value || value === 0 ? value : ''}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import Checkbox from '@mui/material/Checkbox'; import { Checkbox } from '@carbon/react';
import FormControlLabel from '@mui/material/FormControlLabel'; import { WidgetProps } from '@rjsf/utils';
import { schemaRequiresTrueValue, WidgetProps } from '@rjsf/utils'; import { getCommonAttributes } from '../../helpers';
function CheckboxWidget(props: WidgetProps) { function CheckboxWidget(props: WidgetProps) {
const { const {
@ -15,13 +15,19 @@ function CheckboxWidget(props: WidgetProps) {
onChange, onChange,
onBlur, onBlur,
onFocus, onFocus,
uiSchema,
rawErrors,
required,
} = props; } = props;
// Because an unchecked checkbox will cause html5 validation to fail, only add const _onChange = (_: any, newValue: any) => {
// the "required" attribute if the field value must be "true", due to the // if this field is required and it is not checked then change the value to undefined
// "const" or "enum" keywords // otherwise rjsf will not flag this field as invalid
const required = schemaRequiresTrueValue(schema); if (required && !newValue.checked) {
onChange(undefined);
const _onChange = (_: any, checked: boolean) => onChange(checked); } else {
onChange(newValue.checked);
}
};
const _onBlur = ({ const _onBlur = ({
target: { value }, target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onBlur(id, value); }: React.FocusEvent<HTMLButtonElement>) => onBlur(id, value);
@ -29,22 +35,32 @@ function CheckboxWidget(props: WidgetProps) {
target: { value }, target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onFocus(id, value); }: React.FocusEvent<HTMLButtonElement>) => onFocus(id, value);
const commonAttributes = getCommonAttributes(
label,
schema,
uiSchema,
rawErrors
);
return ( return (
<FormControlLabel <Checkbox
control={ id={id}
<Checkbox name={id}
id={id} checked={typeof value === 'undefined' ? false : Boolean(value)}
name={id} disabled={disabled || readonly}
checked={typeof value === 'undefined' ? false : Boolean(value)} title={commonAttributes.tooltipText}
required={required} autoFocus={autofocus}
disabled={disabled || readonly} invalid={commonAttributes.invalid}
autoFocus={autofocus} invalidText={commonAttributes.errorMessageForField}
onChange={_onChange} helperText={commonAttributes.helperText}
onBlur={_onBlur} labelText={
onFocus={_onFocus} required
/> ? commonAttributes.labelWithRequiredIndicator
: commonAttributes.label
} }
label={label || ''} onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
/> />
); );
} }

View File

@ -1,5 +1,6 @@
import { Select, SelectItem } from '@carbon/react'; import { Select, SelectItem } from '@carbon/react';
import { WidgetProps, processSelectValue } from '@rjsf/utils'; import { WidgetProps, processSelectValue } from '@rjsf/utils';
import { getCommonAttributes } from '../../helpers';
function SelectWidget({ function SelectWidget({
schema, schema,
@ -35,30 +36,12 @@ function SelectWidget({
}: React.FocusEvent<HTMLInputElement>) => }: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, processSelectValue(schema, value, options)); onFocus(id, processSelectValue(schema, value, options));
let labelToUse = label; const commonAttributes = getCommonAttributes(
if (uiSchema && uiSchema['ui:title']) { label,
labelToUse = uiSchema['ui:title']; schema,
} else if (schema && schema.title) { uiSchema,
labelToUse = schema.title; rawErrors
} );
let helperText = null;
if (uiSchema && uiSchema['ui:help']) {
helperText = uiSchema['ui:help'];
}
if (required) {
labelToUse = `${labelToUse}*`;
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
if ('validationErrorMessage' in schema) {
errorMessageForField = (schema as any).validationErrorMessage;
} else {
errorMessageForField = rawErrors[0];
}
}
// ok. so in safari, the select widget showed the first option, whereas in chrome it forced you to select an option. // ok. so in safari, the select widget showed the first option, whereas in chrome it forced you to select an option.
// this change causes causes safari to act a little bit more like chrome, but it's different because we are actually adding // this change causes causes safari to act a little bit more like chrome, but it's different because we are actually adding
@ -99,7 +82,7 @@ function SelectWidget({
name={id} name={id}
labelText="" labelText=""
select select
helperText={helperText} helperText={commonAttributes.helperText}
value={typeof value === 'undefined' ? emptyValue : value} value={typeof value === 'undefined' ? emptyValue : value}
disabled={disabled || readonly} disabled={disabled || readonly}
autoFocus={autofocus} autoFocus={autofocus}
@ -107,8 +90,8 @@ function SelectWidget({
onChange={_onChange} onChange={_onChange}
onBlur={_onBlur} onBlur={_onBlur}
onFocus={_onFocus} onFocus={_onFocus}
invalid={invalid} invalid={commonAttributes.invalid}
invalidText={errorMessageForField} invalidText={commonAttributes.errorMessageForField}
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
}} }}
@ -116,7 +99,7 @@ function SelectWidget({
multiple: typeof multiple === 'undefined' ? false : multiple, multiple: typeof multiple === 'undefined' ? false : multiple,
}} }}
> >
{(enumOptions as any).map(({ value, label }: any, i: number) => { {(enumOptions as any).map(({ value, label }: any, _i: number) => {
const disabled: any = const disabled: any =
enumDisabled && (enumDisabled as any).indexOf(value) != -1; enumDisabled && (enumDisabled as any).indexOf(value) != -1;
return <SelectItem text={label} value={value} disabled={disabled} />; return <SelectItem text={label} value={value} disabled={disabled} />;

View File

@ -7,6 +7,7 @@ import {
StrictRJSFSchema, StrictRJSFSchema,
WidgetProps, WidgetProps,
} from '@rjsf/utils'; } from '@rjsf/utils';
import { getCommonAttributes } from '../../helpers';
/** The `TextareaWidget` is a widget for rendering input fields as textarea. /** The `TextareaWidget` is a widget for rendering input fields as textarea.
* *
@ -51,34 +52,19 @@ function TextareaWidget<
[id, onFocus] [id, onFocus]
); );
let labelToUse = label; const commonAttributes = getCommonAttributes(
if (uiSchema && uiSchema['ui:title']) { label,
labelToUse = uiSchema['ui:title']; schema,
} else if (schema && schema.title) { uiSchema,
labelToUse = schema.title; rawErrors
} );
if (required) {
labelToUse = `${labelToUse}*`;
}
let helperText = null;
if (uiSchema && uiSchema['ui:help']) {
helperText = uiSchema['ui:help'];
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
errorMessageForField = rawErrors[0];
}
return ( return (
<TextArea <TextArea
id={id} id={id}
name={id} name={id}
className="text-input" className="text-input"
helperText={helperText} helperText={commonAttributes.helperText}
value={value || ''} value={value || ''}
labelText="" labelText=""
placeholder={placeholder} placeholder={placeholder}
@ -90,8 +76,8 @@ function TextareaWidget<
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
onChange={handleChange} onChange={handleChange}
invalid={invalid} invalid={commonAttributes.invalid}
invalidText={errorMessageForField} invalidText={commonAttributes.errorMessageForField}
/> />
); );
} }

View File

@ -10,10 +10,12 @@ import {
convertStringToDate, convertStringToDate,
dateStringToYMDFormat, dateStringToYMDFormat,
} from '../../../helpers'; } from '../../../helpers';
import { getCommonAttributes } from '../../helpers';
interface widgetArgs { interface widgetArgs {
id: string; id: string;
value: any; value: any;
label: string;
schema?: any; schema?: any;
uiSchema?: any; uiSchema?: any;
disabled?: boolean; disabled?: boolean;
@ -21,7 +23,6 @@ interface widgetArgs {
rawErrors?: any; rawErrors?: any;
onChange?: any; onChange?: any;
autofocus?: any; autofocus?: any;
label?: string;
} }
// NOTE: To properly validate that both start and end dates are specified // NOTE: To properly validate that both start and end dates are specified
@ -41,15 +42,12 @@ export default function DateRangePickerWidget({
label, label,
rawErrors = [], rawErrors = [],
}: widgetArgs) { }: widgetArgs) {
let invalid = false; const commonAttributes = getCommonAttributes(
let errorMessageForField = null; label,
schema,
let labelToUse = label; uiSchema,
if (uiSchema && uiSchema['ui:title']) { rawErrors
labelToUse = uiSchema['ui:title']; );
} else if (schema && schema.title) {
labelToUse = schema.title;
}
const onChangeLocal = useCallback( const onChangeLocal = useCallback(
(dateRange: Date[]) => { (dateRange: Date[]) => {
@ -69,22 +67,6 @@ export default function DateRangePickerWidget({
[onChange] [onChange]
); );
let helperText = null;
if (uiSchema && uiSchema['ui:help']) {
helperText = uiSchema['ui:help'];
}
if (!invalid && rawErrors && rawErrors.length > 0) {
invalid = true;
if ('validationErrorMessage' in schema) {
errorMessageForField = (schema as any).validationErrorMessage;
} else {
errorMessageForField = `${(labelToUse || '').replace(/\*$/, '')} ${
rawErrors[0]
}`;
}
}
let dateValue: (Date | null)[] | null = value; let dateValue: (Date | null)[] | null = value;
if (value) { if (value) {
const [startDateString, endDateString] = value.split(DATE_RANGE_DELIMITER); const [startDateString, endDateString] = value.split(DATE_RANGE_DELIMITER);
@ -113,12 +95,12 @@ export default function DateRangePickerWidget({
<DatePickerInput <DatePickerInput
id={`${id}-start`} id={`${id}-start`}
placeholder={DATE_FORMAT_FOR_DISPLAY} placeholder={DATE_FORMAT_FOR_DISPLAY}
helperText={helperText} helperText={commonAttributes.helperText}
type="text" type="text"
size="md" size="md"
disabled={disabled || readonly} disabled={disabled || readonly}
invalid={invalid} invalid={commonAttributes.invalid}
invalidText={errorMessageForField} invalidText={commonAttributes.errorMessageForField}
autoFocus={autofocus} autoFocus={autofocus}
pattern={null} pattern={null}
/> />
@ -128,7 +110,7 @@ export default function DateRangePickerWidget({
type="text" type="text"
size="md" size="md"
disabled={disabled || readonly} disabled={disabled || readonly}
invalid={invalid} invalid={commonAttributes.invalid}
autoFocus={autofocus} autoFocus={autofocus}
pattern={null} pattern={null}
/> />

View File

@ -1,19 +1,20 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { ComboBox } from '@carbon/react'; import { ComboBox } from '@carbon/react';
import HttpService from '../../../services/HttpService'; import HttpService from '../../../services/HttpService';
import { getCommonAttributes } from '../../helpers';
interface typeaheadArgs { interface typeaheadArgs {
id: string; id: string;
onChange: any; onChange: any;
options: any; options: any;
value: any; value: any;
label: string;
schema?: any; schema?: any;
uiSchema?: any; uiSchema?: any;
disabled?: boolean; disabled?: boolean;
readonly?: boolean; readonly?: boolean;
rawErrors?: any; rawErrors?: any;
placeholder?: string; placeholder?: string;
label?: string;
} }
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
@ -36,30 +37,23 @@ export default function TypeaheadWidget({
const itemFormatRegex = /[^{}]+(?=})/g; const itemFormatRegex = /[^{}]+(?=})/g;
let itemFormatSubstitutions: string[] | null = null; let itemFormatSubstitutions: string[] | null = null;
let invalid = false;
let errorMessageForField = null; const commonAttributes = getCommonAttributes(
label,
schema,
uiSchema,
rawErrors
);
if (itemFormat) { if (itemFormat) {
try { try {
itemFormatSubstitutions = itemFormat.match(itemFormatRegex); itemFormatSubstitutions = itemFormat.match(itemFormatRegex);
} catch (e) { } catch (e) {
errorMessageForField = `itemFormat does not contain replacement keys in curly braces. It should be like: "{key1} ({key2} - {key3})"`; commonAttributes.errorMessageForField = `itemFormat does not contain replacement keys in curly braces. It should be like: "{key1} ({key2} - {key3})"`;
invalid = true; commonAttributes.invalid = true;
} }
} }
let labelToUse = label;
if (uiSchema && uiSchema['ui:title']) {
labelToUse = uiSchema['ui:title'];
} else if (schema && schema.title) {
labelToUse = schema.title;
}
if (!category) {
errorMessageForField = `category is not set in the ui:options for this field: ${labelToUse}`;
invalid = true;
}
const typeaheadSearch = useCallback( const typeaheadSearch = useCallback(
(inputText: string) => { (inputText: string) => {
const pathForCategory = (text: string) => { const pathForCategory = (text: string) => {
@ -109,20 +103,9 @@ export default function TypeaheadWidget({
placeholderText = placeholder; placeholderText = placeholder;
} }
let helperText = null; if (!category) {
if (uiSchema && uiSchema['ui:help']) { commonAttributes.errorMessageForField = `category is not set in the ui:options for this field: ${commonAttributes.label}`;
helperText = uiSchema['ui:help']; commonAttributes.invalid = true;
}
if (!invalid && rawErrors && rawErrors.length > 0) {
invalid = true;
if ('validationErrorMessage' in schema) {
errorMessageForField = (schema as any).validationErrorMessage;
} else {
errorMessageForField = `${(labelToUse || '').replace(/\*$/, '')} ${
rawErrors[0]
}`;
}
} }
return ( return (
@ -145,11 +128,11 @@ export default function TypeaheadWidget({
itemToString={itemToString} itemToString={itemToString}
placeholder={placeholderText} placeholder={placeholderText}
selectedItem={selectedItem} selectedItem={selectedItem}
helperText={helperText} helperText={commonAttributes.helperText}
disabled={disabled} disabled={disabled}
readOnly={readonly} readOnly={readonly}
invalid={invalid} invalid={commonAttributes.invalid}
invalidText={errorMessageForField} invalidText={commonAttributes.errorMessageForField}
/> />
); );
} }

View File

@ -0,0 +1,47 @@
const REQUIRED_FIELD_SYMBOL = '*';
export const getCommonAttributes = (
label: string,
schema: any,
uiSchema: any,
rawErrors: any
) => {
let labelToUse = label;
if (uiSchema && uiSchema['ui:title']) {
labelToUse = uiSchema['ui:title'];
} else if (schema && schema.title) {
labelToUse = schema.title;
}
const labelWithRequiredIndicator = `${labelToUse}${REQUIRED_FIELD_SYMBOL}`;
let helperText = null;
let tooltipText = null;
if (uiSchema) {
if (uiSchema['ui:help']) {
helperText = uiSchema['ui:help'];
}
if (uiSchema['ui:tooltip']) {
tooltipText = uiSchema['ui:tooltip'];
}
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
if ('validationErrorMessage' in schema) {
errorMessageForField = (schema as any).validationErrorMessage;
} else {
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
}
}
return {
helperText,
label: labelToUse,
invalid,
errorMessageForField,
labelWithRequiredIndicator,
tooltipText,
};
};