From d22b6931e16c52992d202f3b040bd5deba8cb6c5 Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Thu, 29 Jul 2021 17:21:53 +0200 Subject: [PATCH] Improve Material Time Pickers Reworks date, time and date-time support in the React Material renderer set. The controls now support many customization options and can be invoked separately via the ui schema. We now use the much smaller 'dayjs' library (compared to 'moment') for Javascript date support. It can be configured globally and is therefore easily customizable on client side for their locales. The date, time and date-time controls now use a similar layout structure as normal controls, leading to a unified layout. Also streamlines the label prop which was only supporting string for a long time now. --- package-lock.json | 31 +- packages/angular/src/abstract-control.ts | 3 +- packages/core/src/testers/testers.ts | 32 +- packages/core/src/util/renderer.ts | 11 +- packages/example/src/index.tsx | 5 + packages/examples/src/dates.ts | 98 ++++- packages/material/package.json | 11 +- .../MaterialListWithDetailRenderer.tsx | 3 +- .../src/complex/MaterialObjectRenderer.tsx | 3 +- .../material/src/complex/TableToolbar.tsx | 3 +- .../src/controls/MaterialDateControl.tsx | 102 ++--- .../src/controls/MaterialDateTimeControl.tsx | 75 ++-- .../src/controls/MaterialInputControl.tsx | 3 +- .../src/controls/MaterialNativeControl.tsx | 5 +- .../src/controls/MaterialRadioGroup.tsx | 3 +- .../src/controls/MaterialSliderControl.tsx | 3 +- .../src/controls/MaterialTimeControl.tsx | 131 ++++++ packages/material/src/controls/index.ts | 7 + packages/material/src/index.ts | 3 + .../src/layouts/ExpandPanelRenderer.tsx | 16 +- .../src/layouts/MaterialArrayLayout.tsx | 3 +- packages/material/src/util/datejs.ts | 32 ++ packages/material/src/util/index.ts | 1 + .../renderers/MaterialDateControl.test.tsx | 42 ++ .../MaterialDateTimeControl.test.tsx | 65 ++- .../renderers/MaterialTimeControl.test.tsx | 378 ++++++++++++++++++ .../vanilla/src/complex/TableArrayControl.tsx | 4 +- .../vanilla/src/controls/InputControl.tsx | 4 +- .../src/controls/RadioGroupControl.tsx | 3 +- 29 files changed, 882 insertions(+), 198 deletions(-) create mode 100644 packages/material/src/controls/MaterialTimeControl.tsx create mode 100644 packages/material/src/util/datejs.ts create mode 100644 packages/material/test/renderers/MaterialTimeControl.test.tsx diff --git a/package-lock.json b/package-lock.json index d98bb0a3c..cbff4f7c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1475,12 +1475,12 @@ "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" }, - "@date-io/moment": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.11.tgz", - "integrity": "sha512-pLEkqp8+P1DfC+QU8StaIANXoiadjJjoImLQCy0rhFAo0RVcJB9cM7mWr7fVgM49EjCwpA8a1JekNhuRLIJVwQ==", + "@date-io/dayjs": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-1.3.13.tgz", + "integrity": "sha512-nD39xWYwQjDMIdpUzHIcADHxY9m1hm1DpOaRn3bc2rBdgmwQC0PfW0WYaHyGGP/6LEzEguINRbHuotMhf+T9Sg==", "requires": { - "@date-io/core": "^1.3.11" + "@date-io/core": "^1.3.13" } }, "@emotion/hash": { @@ -4438,11 +4438,6 @@ } } }, - "@types/uuid": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.9.tgz", - "integrity": "sha512-XDwyIlt/47l2kWLTzw/mtrpLdB+GPSskR2n/PIcPn+VYhVO77rGhRncIR5GPU0KRzXuqkDO+J5qqrG0Y8P6jzQ==" - }, "@types/webpack": { "version": "4.41.27", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.27.tgz", @@ -5620,9 +5615,9 @@ } }, "vue-loader-v16": { - "version": "npm:vue-loader@16.2.0", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.2.0.tgz", - "integrity": "sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q==", + "version": "npm:vue-loader@16.3.3", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.3.3.tgz", + "integrity": "sha512-/1GzCuQ6MRORbC+leKTKoTGtpQt60bYe0gDGEextSteA2OM+v201FPha5jzmjQzVhRcwieZeUvezAtG5a/e5cw==", "optional": true, "requires": { "chalk": "^4.1.0", @@ -10559,6 +10554,11 @@ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", "dev": true }, + "dayjs": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" + }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -18398,11 +18398,6 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" - }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", diff --git a/packages/angular/src/abstract-control.ts b/packages/angular/src/abstract-control.ts index e4eac9ec4..98652f370 100644 --- a/packages/angular/src/abstract-control.ts +++ b/packages/angular/src/abstract-control.ts @@ -26,7 +26,6 @@ import { Actions, computeLabel, ControlElement, - isPlainLabel, JsonFormsState, JsonSchema, OwnPropsOfControl, @@ -110,7 +109,7 @@ export abstract class JsonFormsAbstractControl< config } = props; this.label = computeLabel( - isPlainLabel(label) ? label : label.default, + label, required, config ? config.hideRequiredAsterisk : false ); diff --git a/packages/core/src/testers/testers.ts b/packages/core/src/testers/testers.ts index 3cd9c157e..6790b6193 100644 --- a/packages/core/src/testers/testers.ts +++ b/packages/core/src/testers/testers.ts @@ -284,13 +284,6 @@ export const isOneOfControl = and( schemaMatches(schema => schema.hasOwnProperty('oneOf')) ); -/** - * Tests whether the given UI schema is of type Control and if the schema - * has a 'date' format. - * @type {Tester} - */ -export const isDateControl = and(uiTypeIs('Control'), formatIs('date')); - /** * Tests whether the given UI schema is of type Control and if the schema * has an enum. @@ -352,20 +345,33 @@ export const isMultiLineControl = and( ); /** - * Tests whether the given UI schema is of type Control and if the schema - * has a 'time' format. + * Tests whether the given UI schema is of type Control and whether the schema + * or uischema options has a 'date' format. * @type {Tester} */ -export const isTimeControl = and(uiTypeIs('Control'), formatIs('time')); +export const isDateControl = and( + uiTypeIs('Control'), + or(formatIs('date'), optionIs('format', 'date')) +); /** - * Tests whether the given UI schema is of type Control and if the schema - * has a 'date-time' format. + * Tests whether the given UI schema is of type Control and whether the schema + * or the uischema options has a 'time' format. + * @type {Tester} + */ +export const isTimeControl = and( + uiTypeIs('Control'), + or(formatIs('time'), optionIs('format', 'time')) +); + +/** + * Tests whether the given UI schema is of type Control and whether the schema + * or the uischema options has a 'date-time' format. * @type {Tester} */ export const isDateTimeControl = and( uiTypeIs('Control'), - formatIs('date-time') + or(formatIs('date-time'), optionIs('format', 'date-time')) ); /** diff --git a/packages/core/src/util/renderer.ts b/packages/core/src/util/renderer.ts index cd43d3ee5..3b584845a 100644 --- a/packages/core/src/util/renderer.ts +++ b/packages/core/src/util/renderer.ts @@ -57,15 +57,6 @@ import { JsonFormsState } from '../store'; export { JsonFormsRendererRegistryEntry, JsonFormsCellRendererRegistryEntry }; -export interface Labels { - default: string; - [additionalLabels: string]: string; -} - -export const isPlainLabel = (label: string | Labels): label is string => { - return typeof label === 'string'; -}; - const isRequired = ( schema: JsonSchema, schemaPath: string, @@ -317,7 +308,7 @@ export interface StatePropsOfControl extends StatePropsOfScopedRenderer { /** * The label for the rendered element. */ - label: string | Labels; + label: string; /** * Description of input cell diff --git a/packages/example/src/index.tsx b/packages/example/src/index.tsx index 10f598607..527bd62a7 100644 --- a/packages/example/src/index.tsx +++ b/packages/example/src/index.tsx @@ -41,6 +41,11 @@ import { getExamples } from '@jsonforms/examples'; import { AdditionalStoreParams, exampleReducer } from './reduxUtil'; import { enhanceExample, ReactExampleDescription } from './util'; +import dayjs from 'dayjs'; +import 'dayjs/locale/de'; +import 'dayjs/locale/en'; +dayjs.locale('de'); + const setupStore = ( exampleData: ReactExampleDescription[], cells: JsonFormsCellRendererRegistryEntry[], diff --git a/packages/examples/src/dates.ts b/packages/examples/src/dates.ts index 02e484cfd..3207a3009 100644 --- a/packages/examples/src/dates.ts +++ b/packages/examples/src/dates.ts @@ -23,22 +23,46 @@ THE SOFTWARE. */ import { registerExamples } from './register'; -import moment from 'moment'; const schema = { type: 'object', properties: { - date: { - type: 'string', - format: 'date' - }, - time: { - type: 'string', - format: 'time' + schemaBased: { + type: 'object', + properties: { + date: { + type: 'string', + format: 'date', + description: 'schema-based date picker' + }, + time: { + type: 'string', + format: 'time', + description: 'schema-based time picker' + }, + datetime: { + type: 'string', + format: 'date-time', + description: 'schema-based datetime picker' + } + } }, - datetime: { - type: 'string', - format: 'date-time' + uiSchemaBased: { + type: 'object', + properties: { + date: { + type: 'string', + description: 'does not allow to select days' + }, + time: { + type: 'string', + description: '24 hour format' + }, + datetime: { + type: 'string', + description: 'uischema-based datetime picker' + } + } } } }; @@ -50,11 +74,15 @@ const uischema = { elements: [ { type: 'Control', - scope: '#/properties/date' + scope: '#/properties/schemaBased/properties/date' + }, + { + type: 'Control', + scope: '#/properties/schemaBased/properties/time' }, { type: 'Control', - scope: '#/properties/time' + scope: '#/properties/schemaBased/properties/datetime' } ] }, @@ -63,16 +91,52 @@ const uischema = { elements: [ { type: 'Control', - scope: '#/properties/datetime' + scope: '#/properties/uiSchemaBased/properties/date', + label: 'Year Month Picker', + options: { + format: 'date', + clearLabel: 'Clear it!', + cancelLabel: 'Abort', + okLabel: 'Do it', + views: ['year', 'month'], + dateFormat: 'YYYY.MM', + dateSaveFormat: 'YYYY-MM' + }, + }, + { + type: 'Control', + scope: '#/properties/uiSchemaBased/properties/time', + options: { + format: 'time', + ampm: true + } + }, + { + type: 'Control', + scope: '#/properties/uiSchemaBased/properties/datetime', + options: { + format: 'date-time', + dateTimeFormat: 'DD-MM-YY hh:mm:a', + dateTimeSaveFormat: 'YYYY/MM/DD h:mm a', + ampm: true + } } ] } ] }; + const data = { - date: new Date().toISOString().substr(0, 10), - time: '13:37', - datetime: moment().format() + schemaBased: { + date: new Date().toISOString().substr(0, 10), + time: '13:37', + datetime: new Date().toISOString() + }, + uiSchemaBased: { + date: new Date().toISOString().substr(0, 10), + time: '13:37', + datetime: '1999/12/11 10:05 am' + } }; registerExamples([ { diff --git a/packages/material/package.json b/packages/material/package.json index c51379465..07ac11af7 100644 --- a/packages/material/package.json +++ b/packages/material/package.json @@ -65,17 +65,15 @@ ] }, "dependencies": { - "@date-io/moment": "1.3.11", - "@material-ui/pickers": "^3.2.8", - "@types/uuid": "^3.4.6", - "moment": "^2.24.0", - "uuid": "^3.3.3" + "@date-io/dayjs": "1.3.13", + "dayjs": "1.10.6" }, "peerDependencies": { "@jsonforms/core": "^3.0.0-alpha.0", "@jsonforms/react": "^3.0.0-alpha.0", "@material-ui/core": "^4.7.0", - "@material-ui/icons": "^4.5.1" + "@material-ui/icons": "^4.5.1", + "@material-ui/pickers": "^3.3.10" }, "optionalPeerDependencies": { "@material-ui/lab": "^4.0.0-alpha.56" @@ -85,6 +83,7 @@ "@jsonforms/react": "^3.0.0-alpha.0", "@material-ui/core": "^4.7.0", "@material-ui/icons": "^4.5.1", + "@material-ui/pickers": "^3.2.8", "@material-ui/lab": "^4.0.0-alpha.56", "@types/enzyme": "^3.10.3", "@types/enzyme-adapter-react-16": "^1.0.5", diff --git a/packages/material/src/additional/MaterialListWithDetailRenderer.tsx b/packages/material/src/additional/MaterialListWithDetailRenderer.tsx index ecccf94bb..a88a4593d 100644 --- a/packages/material/src/additional/MaterialListWithDetailRenderer.tsx +++ b/packages/material/src/additional/MaterialListWithDetailRenderer.tsx @@ -30,7 +30,6 @@ import { createDefaultValue, findUISchema, isObjectArray, - isPlainLabel, RankedTester, rankWith, uiTypeIs @@ -101,7 +100,7 @@ export const MaterialListWithDetailRenderer = ({ diff --git a/packages/material/src/complex/TableToolbar.tsx b/packages/material/src/complex/TableToolbar.tsx index 878f6862c..017f01349 100644 --- a/packages/material/src/complex/TableToolbar.tsx +++ b/packages/material/src/complex/TableToolbar.tsx @@ -27,7 +27,6 @@ import { ControlElement, createDefaultValue, JsonSchema, - Labels } from '@jsonforms/core'; import IconButton from '@material-ui/core/IconButton'; import { Grid, Hidden, Typography } from '@material-ui/core'; @@ -40,7 +39,7 @@ import NoBorderTableCell from './NoBorderTableCell'; export interface MaterialTableToolbarProps { numColumns: number; errors: string; - label: string | Labels; + label: string; path: string; uischema: ControlElement; schema: JsonSchema; diff --git a/packages/material/src/controls/MaterialDateControl.tsx b/packages/material/src/controls/MaterialDateControl.tsx index cf82647a5..c5a9d67b0 100644 --- a/packages/material/src/controls/MaterialDateControl.tsx +++ b/packages/material/src/controls/MaterialDateControl.tsx @@ -22,44 +22,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import startsWith from 'lodash/startsWith'; import merge from 'lodash/merge'; import React from 'react'; import { - computeLabel, ControlState, DispatchPropsOfControl, isDateControl, isDescriptionHidden, - isPlainLabel, RankedTester, rankWith, StatePropsOfControl } from '@jsonforms/core'; import { Control, withJsonFormsControlProps } from '@jsonforms/react'; -import { Hidden } from '@material-ui/core'; +import { FormHelperText, Hidden } from '@material-ui/core'; import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; import EventIcon from '@material-ui/icons/Event'; -import moment from 'moment'; -import { Moment } from 'moment'; import { KeyboardDatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; -import MomentUtils from '@date-io/moment'; - -export interface DateControl { - momentLocale?: Moment; -} - -// Workaround typing problems in @material-ui/pickers@3.2.3 -const AnyPropsKeyboardDatePicker: React.FunctionComponent< - any -> = KeyboardDatePicker; +import DayJsUtils from '@date-io/dayjs'; +import { createOnChangeHandler, getData } from '../util'; export class MaterialDateControl extends Control< - StatePropsOfDateControl & DispatchPropsOfControl & DateControl, + StatePropsOfControl & DispatchPropsOfControl, ControlState > { render() { @@ -75,12 +62,8 @@ export class MaterialDateControl extends Control< path, handleChange, data, - momentLocale, config } = this.props; - const defaultLabel = label as string; - const cancelLabel = '%cancel'; - const clearLabel = '%clear'; const isValid = errors.length === 0; const appliedUiSchemaOptions = merge({}, config, uischema.options); const showDescription = !isDescriptionHidden( @@ -89,71 +72,62 @@ export class MaterialDateControl extends Control< this.state.isFocused, appliedUiSchemaOptions.showUnfocusedDescription ); - const inputProps = {}; - const localeDateTimeFormat = momentLocale - ? `${momentLocale.localeData().longDateFormat('L')}` - : 'YYYY-MM-DD'; - let labelText; - let labelCancel; - let labelClear; + const format = appliedUiSchemaOptions.dateFormat ?? 'YYYY-MM-DD'; + const saveFormat = appliedUiSchemaOptions.dateSaveFormat ?? 'YYYY-MM-DD'; - if (isPlainLabel(label)) { - labelText = label; - labelCancel = 'Cancel'; - labelClear = 'Clear'; - } else { - labelText = defaultLabel; - labelCancel = startsWith(cancelLabel, '%') ? 'Cancel' : cancelLabel; - labelClear = startsWith(clearLabel, '%') ? 'Clear' : clearLabel; - } + const firstFormHelperText = showDescription + ? description + : !isValid + ? errors + : null; + const secondFormHelperText = showDescription && !isValid ? errors : null; return ( - - + - handleChange( - path, - datetime ? moment(datetime).format('YYYY-MM-DD') : '' - ) - } - format={localeDateTimeFormat} - clearable={true} + InputLabelProps={data ? { shrink: true } : undefined} + value={getData(data, saveFormat)} + clearable + onChange={createOnChangeHandler( + path, + handleChange, + saveFormat + )} + format={format} + views={appliedUiSchemaOptions.views} disabled={!enabled} autoFocus={appliedUiSchemaOptions.focus} onFocus={this.onFocus} onBlur={this.onBlur} - cancelLabel={labelCancel} - clearLabel={labelClear} + cancelLabel={appliedUiSchemaOptions.cancelLabel} + clearLabel={appliedUiSchemaOptions.clearLabel} + okLabel={appliedUiSchemaOptions.okLabel} leftArrowIcon={} rightArrowIcon={} keyboardIcon={} - InputProps={inputProps} + invalidDateMessage={null} + maxDateMessage={null} + minDateMessage={null} /> + + {firstFormHelperText} + + + {secondFormHelperText} + ); } } -export interface StatePropsOfDateControl extends StatePropsOfControl { - defaultLabel: string; - cancelLabel: string; - clearLabel: string; -} - export const materialDateControlTester: RankedTester = rankWith( 4, isDateControl diff --git a/packages/material/src/controls/MaterialDateTimeControl.tsx b/packages/material/src/controls/MaterialDateTimeControl.tsx index 5021c4590..3251c682a 100644 --- a/packages/material/src/controls/MaterialDateTimeControl.tsx +++ b/packages/material/src/controls/MaterialDateTimeControl.tsx @@ -25,17 +25,15 @@ import React from 'react'; import merge from 'lodash/merge'; import { - computeLabel, ControlProps, ControlState, isDateTimeControl, - isPlainLabel, + isDescriptionHidden, RankedTester, rankWith } from '@jsonforms/core'; import { Control, withJsonFormsControlProps } from '@jsonforms/react'; -import moment from 'moment'; -import { Hidden } from '@material-ui/core'; +import { FormHelperText, Hidden } from '@material-ui/core'; import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; import DateRangeIcon from '@material-ui/icons/DateRange'; @@ -45,12 +43,8 @@ import { KeyboardDateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; -import MomentUtils from '@date-io/moment'; - -// Workaround typing problems in @material-ui/pickers@3.2.3 -const AnyPropsKeyboardDateTimepicker: React.FunctionComponent< - any -> = KeyboardDateTimePicker; +import DayjsUtils from '@date-io/dayjs'; +import { createOnChangeHandler, getData } from '../util'; export class MaterialDateTimeControl extends Control< ControlProps, @@ -73,39 +67,66 @@ export class MaterialDateTimeControl extends Control< } = this.props; const appliedUiSchemaOptions = merge({}, config, uischema.options); const isValid = errors.length === 0; - const inputProps = {}; + + const showDescription = !isDescriptionHidden( + visible, + description, + this.state.isFocused, + appliedUiSchemaOptions.showUnfocusedDescription + ); + + const format = appliedUiSchemaOptions.dateTimeFormat ?? 'YYYY-MM-DD HH:mm'; + const saveFormat = appliedUiSchemaOptions.dateTimeSaveFormat ?? undefined; + + const firstFormHelperText = showDescription + ? description + : !isValid + ? errors + : null; + const secondFormHelperText = showDescription && !isValid ? errors : null; return ( - - + - handleChange(path, datetime ? moment(datetime).format() : '') - } - format='MM/DD/YYYY h:mm a' - clearable={true} + InputLabelProps={data ? { shrink: true } : undefined} + value={getData(data, saveFormat)} + clearable + onChange={createOnChangeHandler( + path, + handleChange, + saveFormat + )} + format={format} + ampm={!!appliedUiSchemaOptions.ampm} + views={appliedUiSchemaOptions.views} disabled={!enabled} autoFocus={appliedUiSchemaOptions.focus} + cancelLabel={appliedUiSchemaOptions.cancelLabel} + clearLabel={appliedUiSchemaOptions.clearLabel} + okLabel={appliedUiSchemaOptions.okLabel} leftArrowIcon={} rightArrowIcon={} dateRangeIcon={} keyboardIcon={} timeIcon={} - InputProps={inputProps} + invalidDateMessage={null} + maxDateMessage={null} + minDateMessage={null} /> + + {firstFormHelperText} + + + {secondFormHelperText} + ); diff --git a/packages/material/src/controls/MaterialInputControl.tsx b/packages/material/src/controls/MaterialInputControl.tsx index f061f5a43..59061dd66 100644 --- a/packages/material/src/controls/MaterialInputControl.tsx +++ b/packages/material/src/controls/MaterialInputControl.tsx @@ -28,7 +28,6 @@ import { ControlProps, ControlState, isDescriptionHidden, - isPlainLabel } from '@jsonforms/core'; import { Control } from '@jsonforms/react'; @@ -87,7 +86,7 @@ export abstract class MaterialInputControl extends Control< error={!isValid} > {computeLabel( - isPlainLabel(label) ? label : label.default, + label, required, appliedUiSchemaOptions.hideRequiredAsterisk )} diff --git a/packages/material/src/controls/MaterialNativeControl.tsx b/packages/material/src/controls/MaterialNativeControl.tsx index bbf2e9da2..6f763eb51 100644 --- a/packages/material/src/controls/MaterialNativeControl.tsx +++ b/packages/material/src/controls/MaterialNativeControl.tsx @@ -29,7 +29,6 @@ import { ControlState, isDateControl, isDescriptionHidden, - isPlainLabel, isTimeControl, or, RankedTester, @@ -63,7 +62,7 @@ export class MaterialNativeControl extends Control { this.props.uischema.options ); const onChange = (ev: any) => handleChange(path, ev.target.value); - const fieldType = schema.format; + const fieldType = appliedUiSchemaOptions.format ?? schema.format; const showDescription = !isDescriptionHidden( visible, description, @@ -76,7 +75,7 @@ export class MaterialNativeControl extends Control { {computeLabel( - isPlainLabel(label) ? label : label.default, + label, required, appliedUiSchemaOptions.hideRequiredAsterisk )} diff --git a/packages/material/src/controls/MaterialSliderControl.tsx b/packages/material/src/controls/MaterialSliderControl.tsx index 7633bbaf2..aba083f4e 100644 --- a/packages/material/src/controls/MaterialSliderControl.tsx +++ b/packages/material/src/controls/MaterialSliderControl.tsx @@ -28,7 +28,6 @@ import { ControlProps, ControlState, isDescriptionHidden, - isPlainLabel, isRangeControl, RankedTester, rankWith @@ -98,7 +97,7 @@ export class MaterialSliderControl extends Control { > {computeLabel( - isPlainLabel(label) ? label : label.default, + label, required, appliedUiSchemaOptions.hideRequiredAsterisk )} diff --git a/packages/material/src/controls/MaterialTimeControl.tsx b/packages/material/src/controls/MaterialTimeControl.tsx new file mode 100644 index 000000000..5c9cff4a5 --- /dev/null +++ b/packages/material/src/controls/MaterialTimeControl.tsx @@ -0,0 +1,131 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import React from 'react'; +import merge from 'lodash/merge'; +import { + ControlProps, + ControlState, + isTimeControl, + isDescriptionHidden, + RankedTester, + rankWith +} from '@jsonforms/core'; +import { Control, withJsonFormsControlProps } from '@jsonforms/react'; +import { FormHelperText, Hidden } from '@material-ui/core'; +import { + KeyboardTimePicker, + MuiPickersUtilsProvider +} from '@material-ui/pickers'; +import DayjsUtils from '@date-io/dayjs'; +import { createOnChangeHandler, getData } from '../util'; + +export class MaterialTimeControl extends Control< + ControlProps, + ControlState +> { + render() { + const { + id, + description, + errors, + label, + uischema, + visible, + enabled, + required, + path, + handleChange, + data, + config + } = this.props; + const appliedUiSchemaOptions = merge({}, config, uischema.options); + const isValid = errors.length === 0; + + const showDescription = !isDescriptionHidden( + visible, + description, + this.state.isFocused, + appliedUiSchemaOptions.showUnfocusedDescription + ); + + const format = appliedUiSchemaOptions.timeFormat ?? 'HH:mm'; + const saveFormat = appliedUiSchemaOptions.timeSaveFormat ?? 'HH:mm'; + + const firstFormHelperText = showDescription + ? description + : !isValid + ? errors + : null; + const secondFormHelperText = showDescription && !isValid ? errors : null; + + return ( + + + + + {firstFormHelperText} + + + {secondFormHelperText} + + + + ); + } +} + +export const materialTimeControlTester: RankedTester = rankWith( + 4, + isTimeControl +); + +export default withJsonFormsControlProps(MaterialTimeControl); diff --git a/packages/material/src/controls/index.ts b/packages/material/src/controls/index.ts index 5bdc3da9c..4f3fb6d9c 100644 --- a/packages/material/src/controls/index.ts +++ b/packages/material/src/controls/index.ts @@ -46,6 +46,10 @@ import MaterialDateTimeControl, { materialDateTimeControlTester, MaterialDateTimeControl as MaterialDateTimeControlUnwrapped } from './MaterialDateTimeControl'; +import MaterialTimeControl, { + materialTimeControlTester, + MaterialTimeControl as MaterialTimeControlUnwrapped +} from './MaterialTimeControl'; import MaterialSliderControl, { materialSliderControlTester, MaterialSliderControl as MaterialSliderControlUnwrapped @@ -89,6 +93,7 @@ export const Unwrapped = { MaterialNativeControl: MaterialNativeControlUnwrapped, MaterialDateControl: MaterialDateControlUnwrapped, MaterialDateTimeControl: MaterialDateTimeControlUnwrapped, + MaterialTimeControl: MaterialTimeControlUnwrapped, MaterialSliderControl: MaterialSliderControlUnwrapped, MaterialRadioGroupControl: MaterialRadioGroupControlUnwrapped, MaterialIntegerControl: MaterialIntegerControlUnwrapped, @@ -112,6 +117,8 @@ export { materialDateControlTester, MaterialDateTimeControl, materialDateTimeControlTester, + MaterialTimeControl, + materialTimeControlTester, MaterialSliderControl, materialSliderControlTester, MaterialRadioGroupControl, diff --git a/packages/material/src/index.ts b/packages/material/src/index.ts index bb361fec9..83e197fbf 100644 --- a/packages/material/src/index.ts +++ b/packages/material/src/index.ts @@ -57,6 +57,8 @@ import { materialDateControlTester, MaterialDateTimeControl, materialDateTimeControlTester, + MaterialTimeControl, + materialTimeControlTester, MaterialEnumControl, materialEnumControlTester, MaterialIntegerControl, @@ -136,6 +138,7 @@ export const materialRenderers: JsonFormsRendererRegistryEntry[] = [ { tester: materialTextControlTester, renderer: MaterialTextControl }, { tester: materialDateTimeControlTester, renderer: MaterialDateTimeControl }, { tester: materialDateControlTester, renderer: MaterialDateControl }, + { tester: materialTimeControlTester, renderer: MaterialTimeControl }, { tester: materialSliderControlTester, renderer: MaterialSliderControl }, { tester: materialObjectControlTester, renderer: MaterialObjectRenderer }, { tester: materialAllOfControlTester, renderer: MaterialAllOfRenderer }, diff --git a/packages/material/src/layouts/ExpandPanelRenderer.tsx b/packages/material/src/layouts/ExpandPanelRenderer.tsx index f9151b67e..4535d48af 100644 --- a/packages/material/src/layouts/ExpandPanelRenderer.tsx +++ b/packages/material/src/layouts/ExpandPanelRenderer.tsx @@ -1,7 +1,6 @@ import merge from 'lodash/merge'; import get from 'lodash/get'; -import React, { Dispatch, Fragment, ReducerAction, useMemo, useState } from 'react'; -import { ComponentType } from 'enzyme'; +import React, { ComponentType, Dispatch, Fragment, ReducerAction, useMemo, useState, useEffect } from 'react'; import { areEqual, JsonFormsDispatch, @@ -20,7 +19,9 @@ import { update, JsonFormsCellRendererRegistryEntry, JsonFormsUISchemaRegistryEntry, - getFirstPrimitiveProp + getFirstPrimitiveProp, + createId, + removeId } from '@jsonforms/core'; import IconButton from '@material-ui/core/IconButton'; import Accordion from '@material-ui/core/Accordion'; @@ -32,7 +33,6 @@ import Avatar from '@material-ui/core/Avatar'; import DeleteIcon from '@material-ui/icons/Delete'; import ArrowUpward from '@material-ui/icons/ArrowUpward'; import ArrowDownward from '@material-ui/icons/ArrowDownward'; -import uuid from 'uuid/v1'; const iconStyle: any = { float: 'right' }; @@ -74,7 +74,13 @@ export interface ExpandPanelProps DispatchPropsOfExpandPanel {} const ExpandPanelRenderer = (props: ExpandPanelProps) => { - const [labelHtmlId] = useState(`id${uuid()}`); + const [labelHtmlId] = useState(createId('expand-panel')); + + useEffect(() => { + return () => { + removeId(labelHtmlId); + }; + }, [labelHtmlId]); const { childLabel, diff --git a/packages/material/src/layouts/MaterialArrayLayout.tsx b/packages/material/src/layouts/MaterialArrayLayout.tsx index 96f8bdb79..48a471863 100644 --- a/packages/material/src/layouts/MaterialArrayLayout.tsx +++ b/packages/material/src/layouts/MaterialArrayLayout.tsx @@ -29,7 +29,6 @@ import { composePaths, computeLabel, createDefaultValue, - isPlainLabel } from '@jsonforms/core'; import map from 'lodash/map'; import { ArrayLayoutToolbar } from './ArrayToolbar'; @@ -80,7 +79,7 @@ export class MaterialArrayLayout extends React.PureComponent<
void, + saveFormat: string | undefined +) => (time: dayjs.Dayjs) => { + if (!time) { + handleChange(path, undefined); + return; + } + const result = dayjs(time).format(saveFormat); + handleChange(path, result === 'Invalid Date' ? undefined : result); +}; + +export const getData = ( + data: any, + saveFormat: string | undefined +): dayjs.Dayjs | null => { + if (!data) { + return null; + } + const dayjsData = dayjs(data, saveFormat); + if (dayjsData.toString() === 'Invalid Date') { + return null; + } + return dayjsData; +}; diff --git a/packages/material/src/util/index.ts b/packages/material/src/util/index.ts index 59a973018..cdf369598 100644 --- a/packages/material/src/util/index.ts +++ b/packages/material/src/util/index.ts @@ -22,5 +22,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +export * from './datejs'; export * from './layout'; export * from './theme'; diff --git a/packages/material/test/renderers/MaterialDateControl.test.tsx b/packages/material/test/renderers/MaterialDateControl.test.tsx index 22375d7be..541d82efd 100644 --- a/packages/material/test/renderers/MaterialDateControl.test.tsx +++ b/packages/material/test/renderers/MaterialDateControl.test.tsx @@ -101,6 +101,19 @@ describe('Material date control tester', () => { } }) ).toBe(4); + expect( + materialDateControlTester( + { ...uischema, options: { format: 'date' } }, + { + type: 'object', + properties: { + foo: { + type: 'string' + } + } + } + ) + ).toBe(4); }); }); @@ -333,4 +346,33 @@ describe('Material date control', () => { const inputs = wrapper.find('input'); expect(inputs.length).toBe(0); }); + + it('should support format customizations', () => { + const core = initCore(schema, uischema, {foo: '06---1980'}); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + + const input = wrapper.find('input').first(); + expect(input.props().value).toBe('1980/06'); + + input.simulate('change', { target: { value: '1961/04' } }); + expect(onChangeData.data.foo).toBe('04---1961'); + }); }); diff --git a/packages/material/test/renderers/MaterialDateTimeControl.test.tsx b/packages/material/test/renderers/MaterialDateTimeControl.test.tsx index dec313e2a..edfdc6dce 100644 --- a/packages/material/test/renderers/MaterialDateTimeControl.test.tsx +++ b/packages/material/test/renderers/MaterialDateTimeControl.test.tsx @@ -31,7 +31,7 @@ import { import MaterialDateTimeControl, { materialDateTimeControlTester } from '../../src/controls/MaterialDateTimeControl'; -import moment from 'moment'; +import dayjs from 'dayjs'; import { materialRenderers } from '../../src'; import Enzyme, { mount, ReactWrapper } from 'enzyme'; @@ -41,7 +41,7 @@ import { initCore, TestEmitter } from './util'; Enzyme.configure({ adapter: new Adapter() }); -const data = { foo: moment('1980-04-04 13:37').format() }; +const data = { foo: dayjs('1980-04-04 13:37').format() }; const schema = { type: 'object', properties: { @@ -102,6 +102,19 @@ describe('Material date time control tester', () => { } }) ).toBe(2); + expect( + materialDateTimeControlTester( + { ...uischema, options: { format: 'date-time' } }, + { + type: 'object', + properties: { + foo: { + type: 'string' + } + } + } + ) + ).toBe(2); }); }); @@ -173,7 +186,7 @@ describe('Material date time control', () => { const input = wrapper.find('input').first(); expect(input.props().type).toBe('text'); - expect(input.props().value).toBe('04/04/1980 1:37 pm'); + expect(input.props().value).toBe('1980-04-04 13:37'); }); it('should update via event', () => { @@ -192,9 +205,9 @@ describe('Material date time control', () => { ); const input = wrapper.find('input').first(); - input.simulate('change', { target: { value: '04/12/1961 8:15 pm' } }); + input.simulate('change', { target: { value: '1961-12-94 20:15' } }); expect(onChangeData.data.foo).toBe( - moment('1961-04-12 20:15').format() + dayjs('1961-12-94 20:15').format() ); }); @@ -205,11 +218,11 @@ describe('Material date time control', () => { ); - core.data = { ...core.data, foo: moment('1961-04-12 20:15').format() }; + core.data = { ...core.data, foo: dayjs('1961-12-04 20:15').format() }; wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); wrapper.update(); const input = wrapper.find('input').first(); - expect(input.props().value).toBe('04/12/1961 8:15 pm'); + expect(input.props().value).toBe('1961-12-04 20:15'); }); it('should update with null value', () => { @@ -251,7 +264,7 @@ describe('Material date time control', () => { wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); wrapper.update(); const input = wrapper.find('input').first(); - expect(input.props().value).toBe('04/04/1980 1:37 pm'); + expect(input.props().value).toBe('1980-04-04 13:37'); }); it('should not update with null ref', () => { @@ -265,7 +278,7 @@ describe('Material date time control', () => { wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); wrapper.update(); const input = wrapper.find('input').first(); - expect(input.props().value).toBe('04/04/1980 1:37 pm'); + expect(input.props().value).toBe('1980-04-04 13:37'); }); it('should not update with undefined ref', () => { @@ -279,7 +292,7 @@ describe('Material date time control', () => { wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); wrapper.update(); const input = wrapper.find('input').first(); - expect(input.props().value).toBe('04/04/1980 1:37 pm'); + expect(input.props().value).toBe('1980-04-04 13:37'); }); it('can be disabled', () => { @@ -320,7 +333,7 @@ describe('Material date time control', () => { ); const input = wrapper.find('input').first(); - // there is only input id at the moment + // there is only input id at the dayjs expect(input.props().id).toBe('#/properties/foo-input'); }); @@ -338,4 +351,34 @@ describe('Material date time control', () => { const inputs = wrapper.find('input'); expect(inputs.length).toBe(0); }); + + it('should support format customizations', () => { + const core = initCore(schema, uischema, {foo: dayjs('1980-04-23 13:37').format('YYYY/MM/DD h:mm a')}); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + + const input = wrapper.find('input').first(); + expect(input.props().value).toBe('23-04-80 01:37:pm'); + + input.simulate('change', { target: { value: '10-12-05 11:22:am' } }); + expect(onChangeData.data.foo).toBe('2005/12/10 11:22 am'); + }); }); diff --git a/packages/material/test/renderers/MaterialTimeControl.test.tsx b/packages/material/test/renderers/MaterialTimeControl.test.tsx new file mode 100644 index 000000000..50ce9b8b3 --- /dev/null +++ b/packages/material/test/renderers/MaterialTimeControl.test.tsx @@ -0,0 +1,378 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import './MatchMediaMock'; +import { + ControlElement, + NOT_APPLICABLE, +} from '@jsonforms/core'; +import MaterialTimeControl, { + materialTimeControlTester +} from '../../src/controls/MaterialTimeControl'; +import * as React from 'react'; +import { materialRenderers } from '../../src'; + +import Enzyme, { mount, ReactWrapper } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { JsonFormsStateProvider } from '@jsonforms/react'; +import { initCore, TestEmitter } from './util'; + +Enzyme.configure({ adapter: new Adapter() }); + +const data = { foo: '13:37' }; +const schema = { + type: 'object', + properties: { + foo: { + type: 'string', + format: 'time' + } + } +}; +const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/foo' +}; + +describe('Material time control tester', () => { + test('should fail', () => { + expect(materialTimeControlTester(undefined, undefined)).toBe( + NOT_APPLICABLE + ); + expect(materialTimeControlTester(null, undefined)).toBe(NOT_APPLICABLE); + expect(materialTimeControlTester({ type: 'Foo' }, undefined)).toBe( + NOT_APPLICABLE + ); + expect(materialTimeControlTester({ type: 'Control' }, undefined)).toBe( + NOT_APPLICABLE + ); + expect( + materialTimeControlTester(uischema, { + type: 'object', + properties: { + foo: { type: 'string' } + } + }) + ).toBe(NOT_APPLICABLE); + expect( + materialTimeControlTester(uischema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { + type: 'string', + format: 'time' + } + } + }) + ).toBe(NOT_APPLICABLE); + }); + + it('should succeed', () => { + expect( + materialTimeControlTester(uischema, { + type: 'object', + properties: { + foo: { + type: 'string', + format: 'time' + } + } + }) + ).toBe(4); + expect( + materialTimeControlTester( + { ...uischema, options: { format: 'time' } }, + { + type: 'object', + properties: { + foo: { + type: 'string' + } + } + } + ) + ).toBe(4); + }); +}); + +describe('Material time control', () => { + let wrapper: ReactWrapper; + + afterEach(() => wrapper.unmount()); + + it('should autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: true + } + }; + const core = initCore(schema, control, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().autoFocus).toBe(true); + }); + + it('should not autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: false + } + }; + const core = initCore(schema, control, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().autoFocus).toBeFalsy(); + }); + + it('should not autofocus by default', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo' + }; + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().autoFocus).toBeFalsy(); + }); + + it('should render', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + + const input = wrapper.find('input').first(); + expect(input.props().type).toBe('text'); + expect(input.props().value).toBe('13:37'); + }); + + it('should update via event', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + const input = wrapper.find('input').first(); + input.simulate('change', { target: { value: '08:40' } }); + expect(onChangeData.data.foo).toBe('08:40'); + }); + + it('should update via action', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + core.data = { ...core.data, foo: '08:40' }; + wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().value).toBe('08:40'); + }); + + it('should update with null value', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + core.data = { ...core.data, foo: null }; + wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().value).toBe(''); + }); + + it('should update with undefined value', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + core.data = { ...core.data, foo: undefined }; + wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().value).toBe(''); + }); + + it('should not update with wrong ref', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + core.data = { ...core.data, bar: '08:40' }; + wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); + wrapper.update(); + const input = wrapper.find('input'); + expect(input.props().value).toBe('13:37'); + }); + + it('should not update with null ref', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + core.data = { ...core.data, null: '08:40' }; + wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().value).toBe('13:37'); + }); + + it('should not update with undefined ref', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + core.data = { ...core.data, undefined: '08:40' }; + wrapper.setProps({ initState: { renderers: materialRenderers, core }} ); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().value).toBe('13:37'); + }); + + it('can be disabled', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().disabled).toBeTruthy(); + }); + + it('should be enabled by default', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().disabled).toBeFalsy(); + }); + + it('should render input id', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + // there is only input id at the moment + expect(input.props().id).toBe('#/properties/foo-input'); + }); + + it('should be hideable', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const inputs = wrapper.find('input'); + expect(inputs.length).toBe(0); + }); + + it('should support format customizations', () => { + const core = initCore(schema, uischema, {foo: '1//2 pm'}); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + + const input = wrapper.find('input').first(); + expect(input.props().value).toBe('02-13'); + + input.simulate('change', { target: { value: '12-01' } }); + expect(onChangeData.data.foo).toBe('1//12 am'); + }); +}); diff --git a/packages/vanilla/src/complex/TableArrayControl.tsx b/packages/vanilla/src/complex/TableArrayControl.tsx index 565d941f9..556a876c4 100644 --- a/packages/vanilla/src/complex/TableArrayControl.tsx +++ b/packages/vanilla/src/complex/TableArrayControl.tsx @@ -35,7 +35,6 @@ import { ControlElement, createDefaultValue, Helpers, - isPlainLabel, Paths, RankedTester, Resolve, @@ -100,12 +99,11 @@ class TableArrayControl extends React.Component