From e2b6911a5b6f2ce8426e5230e5510f893e886016 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Thu, 18 Feb 2021 10:18:55 +0100 Subject: [PATCH] Add React Material boolean toggle switch control and cell In addition to the default checkbox renderers for boolean controls and cells, provide a control and cell rendering a boolean as a toggle switch for the React Material renderers. --- packages/examples/src/booleanToggle.ts | 70 +++ packages/examples/src/control-options.ts | 17 +- packages/examples/src/index.ts | 6 +- .../src/cells/MaterialBooleanToggleCell.tsx | 47 ++ packages/material/src/cells/index.ts | 5 + .../controls/MaterialBooleanToggleControl.tsx | 84 ++++ packages/material/src/controls/index.ts | 7 + packages/material/src/index.ts | 6 + .../material/src/mui-controls/MuiToggle.tsx | 56 +++ .../MaterialBooleanToggleCell.test.tsx | 460 +++++++++++++++++ .../MaterialBooleanToggleControl.test.tsx | 464 ++++++++++++++++++ 11 files changed, 1218 insertions(+), 4 deletions(-) create mode 100644 packages/examples/src/booleanToggle.ts create mode 100644 packages/material/src/cells/MaterialBooleanToggleCell.tsx create mode 100644 packages/material/src/controls/MaterialBooleanToggleControl.tsx create mode 100644 packages/material/src/mui-controls/MuiToggle.tsx create mode 100644 packages/material/test/renderers/MaterialBooleanToggleCell.test.tsx create mode 100644 packages/material/test/renderers/MaterialBooleanToggleControl.test.tsx diff --git a/packages/examples/src/booleanToggle.ts b/packages/examples/src/booleanToggle.ts new file mode 100644 index 000000000..c49af107f --- /dev/null +++ b/packages/examples/src/booleanToggle.ts @@ -0,0 +1,70 @@ +/* + The MIT License + + Copyright (c) 2017-2021 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 { registerExamples } from "./register"; + +export const schema = { + type: 'object', + properties: { + checkbox: { + type: 'boolean' + }, + toggle: { + type: 'boolean' + } + } +}; + +export const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/checkbox' + }, + { + type: 'Control', + scope: '#/properties/toggle', + options: { + toggle: true + } + } + ] +}; + +export const data = { + checkbox: false, + toggle: false +}; + +export const booleanToggleExample = { + name: 'booleanToggle', + label: 'Boolean Toggle', + data, + schema, + uischema +}; + +registerExamples([booleanToggleExample]); diff --git a/packages/examples/src/control-options.ts b/packages/examples/src/control-options.ts index e1c0f996b..c6d31aa43 100644 --- a/packages/examples/src/control-options.ts +++ b/packages/examples/src/control-options.ts @@ -1,7 +1,7 @@ /* The MIT License - Copyright (c) 2017-2019 EclipseSource Munich + Copyright (c) 2017-2021 EclipseSource Munich https://github.com/eclipsesource/jsonforms Permission is hereby granted, free of charge, to any person obtaining a copy @@ -137,6 +137,10 @@ export const extendedSchema = { hideRequiredAsterisk: { type: 'string', description: 'Hides the "*" symbol, when the field is required', + }, + toggle: { + type: 'boolean', + description: 'The "toggle" option renders boolean values as a toggle.' } }, required: ['hideRequiredAsterisk', 'restrictText'] @@ -186,6 +190,14 @@ export const extendedUischema = { options: { hideRequiredAsterisk: true } + }, + { + type: 'Control', + scope: '#/properties/toggle', + label: 'Boolean as Toggle', + options: { + toggle: true + } } ] }; @@ -194,7 +206,8 @@ export const extendedData = { multilineString: 'Multi-\nline\nexample', slider: 4, trimText: 'abcdefg', - restrictText: 'abcde' + restrictText: 'abcde', + toggle: false }; const combinedSchema = { diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index da41d1afd..564970b4e 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -1,7 +1,7 @@ /* The MIT License - Copyright (c) 2017-2019 EclipseSource Munich + Copyright (c) 2017-2021 EclipseSource Munich https://github.com/eclipsesource/jsonforms Permission is hereby granted, free of charge, to any person obtaining a copy @@ -69,6 +69,7 @@ import * as defaultExample from './default'; import * as onChange from './onChange'; import * as enumExample from './enum'; import * as radioGroupExample from './radioGroup'; +import * as booleanToggle from './booleanToggle'; export * from './register'; export * from './example'; @@ -122,5 +123,6 @@ export { ifThenElse, onChange, enumExample, - radioGroupExample + radioGroupExample, + booleanToggle }; diff --git a/packages/material/src/cells/MaterialBooleanToggleCell.tsx b/packages/material/src/cells/MaterialBooleanToggleCell.tsx new file mode 100644 index 000000000..c2ef93ade --- /dev/null +++ b/packages/material/src/cells/MaterialBooleanToggleCell.tsx @@ -0,0 +1,47 @@ +/* + The MIT License + + Copyright (c) 2017-2021 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 { + and, + CellProps, + isBooleanControl, + optionIs, + RankedTester, + rankWith, + WithClassname +} from '@jsonforms/core'; +import { withJsonFormsCellProps } from '@jsonforms/react'; +import { MuiToggle } from '../mui-controls/MuiToggle'; + +export const MaterialBooleanToggleCell = (props: CellProps & WithClassname) => { + return ; +}; + +export const materialBooleanToggleCellTester: RankedTester = rankWith( + 3, + and(isBooleanControl, optionIs('toggle', true)) +);; + +export default withJsonFormsCellProps(MaterialBooleanToggleCell); diff --git a/packages/material/src/cells/index.ts b/packages/material/src/cells/index.ts index 0428b739e..207131261 100644 --- a/packages/material/src/cells/index.ts +++ b/packages/material/src/cells/index.ts @@ -25,6 +25,9 @@ import MaterialBooleanCell, { materialBooleanCellTester } from './MaterialBooleanCell'; +import MaterialBooleanToggleCell, { + materialBooleanToggleCellTester +} from './MaterialBooleanToggleCell'; import MaterialDateCell, { materialDateCellTester } from './MaterialDateCell'; import MaterialEnumCell, { materialEnumCellTester } from './MaterialEnumCell'; import MaterialIntegerCell, { @@ -42,6 +45,8 @@ import MaterialTimeCell, { materialTimeCellTester } from './MaterialTimeCell'; export { MaterialBooleanCell, materialBooleanCellTester, + MaterialBooleanToggleCell, + materialBooleanToggleCellTester, MaterialDateCell, materialDateCellTester, MaterialEnumCell, diff --git a/packages/material/src/controls/MaterialBooleanToggleControl.tsx b/packages/material/src/controls/MaterialBooleanToggleControl.tsx new file mode 100644 index 000000000..a4d762eab --- /dev/null +++ b/packages/material/src/controls/MaterialBooleanToggleControl.tsx @@ -0,0 +1,84 @@ +/* + The MIT License + + Copyright (c) 2017-2021 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 isEmpty from 'lodash/isEmpty'; +import React from 'react'; +import { + isBooleanControl, + RankedTester, + rankWith, + ControlProps, + optionIs, + and +} from '@jsonforms/core'; +import { withJsonFormsControlProps } from '@jsonforms/react'; +import { FormControlLabel, Hidden } from '@material-ui/core'; +import { MuiToggle } from '../mui-controls/MuiToggle'; + +export const MaterialBooleanToggleControl = ({ + data, + visible, + label, + id, + enabled, + uischema, + schema, + rootSchema, + handleChange, + errors, + path, + config +}: ControlProps) => { + return ( + + + } + /> + + ); +}; + +export const materialBooleanToggleControlTester: RankedTester = rankWith( + 3, + and(isBooleanControl, optionIs('toggle', true)) +); + +export default withJsonFormsControlProps(MaterialBooleanToggleControl); diff --git a/packages/material/src/controls/index.ts b/packages/material/src/controls/index.ts index 1b0848181..31007da0e 100644 --- a/packages/material/src/controls/index.ts +++ b/packages/material/src/controls/index.ts @@ -26,6 +26,10 @@ import MaterialBooleanControl, { materialBooleanControlTester, MaterialBooleanControl as MaterialBooleanControlUnwrapped } from './MaterialBooleanControl'; +import MaterialBooleanToggleControl, { + materialBooleanToggleControlTester, + MaterialBooleanToggleControl as MaterialBooleanToggleControlUnwrapped +} from './MaterialBooleanToggleControl'; import MaterialEnumControl, { materialEnumControlTester, MaterialEnumControl as MaterialEnumControlUnwrapped @@ -80,6 +84,7 @@ import MaterialOneOfRadioGroupControl, { export const Unwrapped = { MaterialBooleanControl: MaterialBooleanControlUnwrapped, + MaterialBooleanToggleControl: MaterialBooleanToggleControlUnwrapped, MaterialEnumControl: MaterialEnumControlUnwrapped, MaterialNativeControl: MaterialNativeControlUnwrapped, MaterialDateControl: MaterialDateControlUnwrapped, @@ -97,6 +102,8 @@ export const Unwrapped = { export { MaterialBooleanControl, materialBooleanControlTester, + MaterialBooleanToggleControl, + materialBooleanToggleControlTester, MaterialEnumControl, materialEnumControlTester, MaterialNativeControl, diff --git a/packages/material/src/index.ts b/packages/material/src/index.ts index 453fe406b..4b60f318b 100644 --- a/packages/material/src/index.ts +++ b/packages/material/src/index.ts @@ -49,6 +49,8 @@ import { materialAnyOfStringOrEnumControlTester, MaterialBooleanControl, materialBooleanControlTester, + MaterialBooleanToggleControl, + materialBooleanToggleControlTester, MaterialDateControl, materialDateControlTester, MaterialDateTimeControl, @@ -87,6 +89,8 @@ import { import { MaterialBooleanCell, materialBooleanCellTester, + MaterialBooleanToggleCell, + materialBooleanToggleCellTester, MaterialDateCell, materialDateCellTester, MaterialEnumCell, @@ -120,6 +124,7 @@ export const materialRenderers: JsonFormsRendererRegistryEntry[] = [ renderer: MaterialArrayControlRenderer }, { tester: materialBooleanControlTester, renderer: MaterialBooleanControl }, + { tester: materialBooleanToggleControlTester, renderer: MaterialBooleanToggleControl }, { tester: materialNativeControlTester, renderer: MaterialNativeControl }, { tester: materialEnumControlTester, renderer: MaterialEnumControl }, { tester: materialIntegerControlTester, renderer: MaterialIntegerControl }, @@ -171,6 +176,7 @@ export const materialRenderers: JsonFormsRendererRegistryEntry[] = [ export const materialCells: JsonFormsCellRendererRegistryEntry[] = [ { tester: materialBooleanCellTester, cell: MaterialBooleanCell }, + { tester: materialBooleanToggleCellTester, cell: MaterialBooleanToggleCell }, { tester: materialDateCellTester, cell: MaterialDateCell }, { tester: materialEnumCellTester, cell: MaterialEnumCell }, { tester: materialIntegerCellTester, cell: MaterialIntegerCell }, diff --git a/packages/material/src/mui-controls/MuiToggle.tsx b/packages/material/src/mui-controls/MuiToggle.tsx new file mode 100644 index 000000000..1000c0e52 --- /dev/null +++ b/packages/material/src/mui-controls/MuiToggle.tsx @@ -0,0 +1,56 @@ +/* + The MIT License + + Copyright (c) 2017-2021 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 { CellProps, WithClassname } from '@jsonforms/core'; +import Switch from '@material-ui/core/Switch'; +import { areEqual } from '@jsonforms/react'; +import merge from 'lodash/merge'; + +export const MuiToggle = React.memo((props: CellProps & WithClassname) => { + const { + data, + className, + id, + enabled, + uischema, + path, + handleChange, + config + } = props; + const appliedUiSchemaOptions = merge({}, config, uischema.options); + const inputProps = { autoFocus: !!appliedUiSchemaOptions.focus }; + const checked = !!data; + + return ( + handleChange(path, isChecked)} + className={className} + id={id} + disabled={!enabled} + inputProps={inputProps} + /> + ); +}, areEqual); diff --git a/packages/material/test/renderers/MaterialBooleanToggleCell.test.tsx b/packages/material/test/renderers/MaterialBooleanToggleCell.test.tsx new file mode 100644 index 000000000..d6ab76ca7 --- /dev/null +++ b/packages/material/test/renderers/MaterialBooleanToggleCell.test.tsx @@ -0,0 +1,460 @@ +/* + The MIT License + + Copyright (c) 2017-2021 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 * as React from 'react'; +import { + ControlElement, + NOT_APPLICABLE, + UISchemaElement +} from '@jsonforms/core'; +import BooleanToggleCell, { + materialBooleanToggleCellTester +} from '../../src/cells/MaterialBooleanToggleCell'; +import * as ReactDOM from 'react-dom'; +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: true }; +const schema = { + type: 'boolean' +}; +const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: true + } +}; + +describe('Material boolean toggle cell tester', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: true + } + }; + + it('should fail', () => { + expect(materialBooleanToggleCellTester(undefined, undefined)).toBe( + NOT_APPLICABLE + ); + expect(materialBooleanToggleCellTester(null, undefined)).toBe( + NOT_APPLICABLE + ); + expect(materialBooleanToggleCellTester({ type: 'Foo' }, undefined)).toBe( + NOT_APPLICABLE + ); + expect( + materialBooleanToggleCellTester({ type: 'Control' }, undefined) + ).toBe(NOT_APPLICABLE); + expect( + materialBooleanToggleCellTester(control, { + type: 'object', + properties: { foo: { type: 'string' } } + }) + ).toBe(NOT_APPLICABLE); + expect( + materialBooleanToggleCellTester(control, { + type: 'object', + properties: { + foo: { + type: 'string' + }, + bar: { + type: 'boolean' + } + } + }) + ).toBe(NOT_APPLICABLE); + + // Not applicable for boolean cell if toggle option is false + expect( + materialBooleanToggleCellTester( + { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: false + } + } as UISchemaElement, + { + type: 'object', + properties: { + foo: { + type: 'boolean' + } + } + } + ) + ).toBe(NOT_APPLICABLE); + + // Not applicable for boolean cell if toggle option is not given + expect( + materialBooleanToggleCellTester( + { + type: 'Control', + scope: '#/properties/foo', + } as UISchemaElement, + { + type: 'object', + properties: { + foo: { + type: 'boolean' + } + } + } + ) + ).toBe(NOT_APPLICABLE); + }); + + it('should succeed', () => { + expect( + materialBooleanToggleCellTester(control, { + type: 'object', + properties: { + foo: { + type: 'boolean' + } + } + }) + ).toBe(3); + }); +}); + +describe('Material boolean toggle cell', () => { + let wrapper: ReactWrapper; + + afterEach(() => wrapper.unmount()); + + /** Use this container to render components */ + const container = document.createElement('div'); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(container); + }); + + // seems to be broken in material-ui + it('should autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: true, + toggle: true, + } + }; + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().autoFocus).toBeTruthy(); + }); + + it('should not autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: false, + toggle: true, + } + }; + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().autoFocus).toBe(false); + }); + + it('should not autofocus by default', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: true, + } + }; + 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( + + + + ); + + // Make sure a toggle is rendered by checking for the thumb element + expect(wrapper.find('.MuiSwitch-thumb')).toHaveLength(1); + + const input = wrapper.find('input').first(); + expect(input.props().type).toBe('checkbox'); + expect(input.props().checked).toBeTruthy(); + }); + + it('should update via input event', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + + const input = wrapper.find('input'); + input.simulate('change', { target: { value: false } }); + expect(onChangeData.data.foo).toBeFalsy(); + }); + + it('should update via action', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, foo: false }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeFalsy(); + expect(onChangeData.data.foo).toBeFalsy(); + }); + + it('should update with undefined value', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, foo: undefined }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeFalsy(); + }); + + it('should update with null value', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, foo: null }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeFalsy(); + }); + + it('should not update with wrong ref', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, bar: 11 }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeTruthy(); + }); + + it('should not update with null ref', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, null: false }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeTruthy(); + }); + + it('should not update with an undefined ref', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, undefined: false }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeTruthy(); + }); + + 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('id should be present in output', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input'); + expect(input.props().id).toBe('myid'); + }); +}); diff --git a/packages/material/test/renderers/MaterialBooleanToggleControl.test.tsx b/packages/material/test/renderers/MaterialBooleanToggleControl.test.tsx new file mode 100644 index 000000000..b63687dbe --- /dev/null +++ b/packages/material/test/renderers/MaterialBooleanToggleControl.test.tsx @@ -0,0 +1,464 @@ +/* + The MIT License + + Copyright (c) 2017-2021 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 * as React from 'react'; +import { + ControlElement, + NOT_APPLICABLE, + UISchemaElement +} from '@jsonforms/core'; +import BooleanToggleControl, { + materialBooleanToggleControlTester +} from '../../src/controls/MaterialBooleanToggleControl'; +import * as ReactDOM from 'react-dom'; +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: true }; +const schema = { + type: 'object', + properties: { + foo: { + type: 'boolean' + } + } +}; +const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: true + } +}; + +describe('Material boolean toggle control tester', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: true + } + }; + + it('should fail', () => { + expect(materialBooleanToggleControlTester(undefined, undefined)).toBe( + NOT_APPLICABLE + ); + expect(materialBooleanToggleControlTester(null, undefined)).toBe( + NOT_APPLICABLE + ); + expect(materialBooleanToggleControlTester({ type: 'Foo' }, undefined)).toBe( + NOT_APPLICABLE + ); + expect( + materialBooleanToggleControlTester({ type: 'Control' }, undefined) + ).toBe(NOT_APPLICABLE); + expect( + materialBooleanToggleControlTester(control, { + type: 'object', + properties: { foo: { type: 'string' } } + }) + ).toBe(NOT_APPLICABLE); + expect( + materialBooleanToggleControlTester(control, { + type: 'object', + properties: { + foo: { + type: 'string' + }, + bar: { + type: 'boolean' + } + } + }) + ).toBe(NOT_APPLICABLE); + + // Not applicable for boolean control if toggle option is false + expect( + materialBooleanToggleControlTester( + { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: false + } + } as UISchemaElement, + { + type: 'object', + properties: { + foo: { + type: 'boolean' + } + } + } + ) + ).toBe(NOT_APPLICABLE); + + // Not applicable for boolean control if toggle option is not given + expect( + materialBooleanToggleControlTester( + { + type: 'Control', + scope: '#/properties/foo', + } as UISchemaElement, + { + type: 'object', + properties: { + foo: { + type: 'boolean' + } + } + } + ) + ).toBe(NOT_APPLICABLE); + }); + + it('should succeed', () => { + expect( + materialBooleanToggleControlTester(control, { + type: 'object', + properties: { + foo: { + type: 'boolean' + } + } + }) + ).toBe(3); + }); +}); + +describe('Material boolean toggle control', () => { + let wrapper: ReactWrapper; + + afterEach(() => wrapper.unmount()); + + /** Use this container to render components */ + const container = document.createElement('div'); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(container); + }); + + // seems to be broken in material-ui + it('should autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: true, + toggle: true, + } + }; + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().autoFocus).toBeTruthy(); + }); + + it('should not autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: false, + toggle: true, + } + }; + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input').first(); + expect(input.props().autoFocus).toBe(false); + }); + + it('should not autofocus by default', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + toggle: true, + } + }; + 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( + + + + ); + + // Make sure a toggle is rendered by checking for the thumb element + expect(wrapper.find('.MuiSwitch-thumb')).toHaveLength(1); + + const input = wrapper.find('input').first(); + console.log('should render props', input.props()); + expect(input.props().type).toBe('checkbox'); + expect(input.props().checked).toBeTruthy(); + }); + + it('should update via input event', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + + const input = wrapper.find('input'); + input.simulate('change', { target: { value: false } }); + expect(onChangeData.data.foo).toBeFalsy(); + }); + + it('should update via action', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, foo: false }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeFalsy(); + expect(onChangeData.data.foo).toBeFalsy(); + }); + + it('should update with undefined value', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, foo: undefined }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeFalsy(); + }); + + it('should update with null value', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, foo: null }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeFalsy(); + }); + + it('should not update with wrong ref', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, bar: 11 }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeTruthy(); + }); + + it('should not update with null ref', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, null: false }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeTruthy(); + }); + + it('should not update with an undefined ref', () => { + const core = initCore(schema, uischema, data); + const onChangeData: any = { + data: undefined + }; + wrapper = mount( + + { + onChangeData.data = data; + }} + /> + + + ); + core.data = { ...core.data, undefined: false }; + wrapper.setProps({ initState: { renderers: materialRenderers, core } }); + wrapper.update(); + const input = wrapper.find('input').first(); + expect(input.props().checked).toBeTruthy(); + }); + + 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('id should be present in output', () => { + const core = initCore(schema, uischema, data); + wrapper = mount( + + + + ); + const input = wrapper.find('input'); + expect(input.props().id).toBe('myid-input'); + }); +});