From b2c9ef10f81717b8ec6bfc1a2e4d2dfbc00c4498 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 4 Aug 2022 15:53:51 -0400 Subject: [PATCH] feat(textInput,formHelpers): discovery-151 pf4 textInput (#149) * textInput, pf4 textInput wrapper * formHelpers, mock event, future validation --- .../__snapshots__/formHelpers.test.js.snap | 39 +++++ .../__snapshots__/textInput.test.js.snap | 135 +++++++++++++++ .../form/__tests__/formHelpers.test.js | 23 +++ .../form/__tests__/textInput.test.js | 93 ++++++++++ src/components/form/formHelpers.js | 43 +++++ src/components/form/textInput.js | 163 ++++++++++++++++++ 6 files changed, 496 insertions(+) create mode 100644 src/components/form/__tests__/__snapshots__/formHelpers.test.js.snap create mode 100644 src/components/form/__tests__/__snapshots__/textInput.test.js.snap create mode 100644 src/components/form/__tests__/formHelpers.test.js create mode 100644 src/components/form/__tests__/textInput.test.js create mode 100644 src/components/form/formHelpers.js create mode 100644 src/components/form/textInput.js diff --git a/src/components/form/__tests__/__snapshots__/formHelpers.test.js.snap b/src/components/form/__tests__/__snapshots__/formHelpers.test.js.snap new file mode 100644 index 00000000..223c1311 --- /dev/null +++ b/src/components/form/__tests__/__snapshots__/formHelpers.test.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormHelpers should have specific helpers: helpers 1`] = ` +Object { + "createMockEvent": [Function], + "doesNotHaveMinimumCharacters": [Function], +} +`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: array 1`] = `true`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: null 1`] = `true`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: plain object 1`] = `true`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 1 chars expect 1 1`] = `false`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 2 chars expect 2 1`] = `false`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 3 chars expect 2 1`] = `false`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 4 chars expect 5 1`] = `true`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 5 chars expect 5 1`] = `false`; + +exports[`FormHelpers should return a boolean for not having the correct number of characters: undefined 1`] = `true`; + +exports[`FormHelpers should return a mocked event object: mock event 1`] = ` +Object { + "checked": undefined, + "currentTarget": Object {}, + "id": undefined, + "keyCode": undefined, + "name": undefined, + "persist": [Function], + "target": Object {}, + "value": undefined, +} +`; diff --git a/src/components/form/__tests__/__snapshots__/textInput.test.js.snap b/src/components/form/__tests__/__snapshots__/textInput.test.js.snap new file mode 100644 index 00000000..a6ce8ce8 --- /dev/null +++ b/src/components/form/__tests__/__snapshots__/textInput.test.js.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TextInput Component should handle readOnly, disabled: active 1`] = ` + +`; + +exports[`TextInput Component should handle readOnly, disabled: disabled 1`] = ` + +`; + +exports[`TextInput Component should handle readOnly, disabled: readOnly 1`] = ` + +`; + +exports[`TextInput Component should render a basic component: basic component 1`] = ` + +`; + +exports[`TextInput Component should return a mouseup event on text clear: emulated event, mouseup 1`] = ` +Array [ + Array [ + Object { + "checked": undefined, + "currentTarget": Object { + "value": "", + }, + "id": undefined, + "keyCode": undefined, + "name": undefined, + "persist": [Function], + "target": Object {}, + "value": "", + }, + ], +] +`; + +exports[`TextInput Component should return an emulated onChange event: emulated event, change 1`] = ` +Array [ + Array [ + Object { + "checked": undefined, + "currentTarget": Object { + "value": "dolor sit", + }, + "id": undefined, + "keyCode": undefined, + "name": undefined, + "persist": [Function], + "target": Object {}, + "value": "dolor sit", + }, + ], +] +`; + +exports[`TextInput Component should return an emulated onClear event on escape with type search: emulated event, esc, type search 1`] = ` +Array [ + Array [ + Object { + "checked": undefined, + "currentTarget": Object { + "value": "", + }, + "id": undefined, + "keyCode": 27, + "name": undefined, + "persist": [Function], + "target": Object {}, + "value": "", + }, + ], +] +`; + +exports[`TextInput Component should return an emulated onClear event on escape: emulated event, esc 1`] = ` +Array [ + Array [ + Object { + "checked": undefined, + "currentTarget": Object { + "value": "", + }, + "id": undefined, + "keyCode": 27, + "name": undefined, + "persist": [Function], + "target": Object {}, + "value": "", + }, + ], +] +`; diff --git a/src/components/form/__tests__/formHelpers.test.js b/src/components/form/__tests__/formHelpers.test.js new file mode 100644 index 00000000..2a948872 --- /dev/null +++ b/src/components/form/__tests__/formHelpers.test.js @@ -0,0 +1,23 @@ +import { formHelpers } from '../formHelpers'; + +describe('FormHelpers', () => { + it('should have specific helpers', () => { + expect(formHelpers).toMatchSnapshot('helpers'); + }); + + it('should return a mocked event object', () => { + expect(formHelpers.createMockEvent()).toMatchSnapshot('mock event'); + }); + + it('should return a boolean for not having the correct number of characters', () => { + expect(formHelpers.doesNotHaveMinimumCharacters(null)).toMatchSnapshot('null'); + expect(formHelpers.doesNotHaveMinimumCharacters(undefined)).toMatchSnapshot('undefined'); + expect(formHelpers.doesNotHaveMinimumCharacters({})).toMatchSnapshot('plain object'); + expect(formHelpers.doesNotHaveMinimumCharacters([])).toMatchSnapshot('array'); + expect(formHelpers.doesNotHaveMinimumCharacters('l', 1)).toMatchSnapshot('string, 1 chars expect 1'); + expect(formHelpers.doesNotHaveMinimumCharacters('lo', 2)).toMatchSnapshot('string, 2 chars expect 2'); + expect(formHelpers.doesNotHaveMinimumCharacters('lor', 2)).toMatchSnapshot('string, 3 chars expect 2'); + expect(formHelpers.doesNotHaveMinimumCharacters('lore', 5)).toMatchSnapshot('string, 4 chars expect 5'); + expect(formHelpers.doesNotHaveMinimumCharacters('lorem', 5)).toMatchSnapshot('string, 5 chars expect 5'); + }); +}); diff --git a/src/components/form/__tests__/textInput.test.js b/src/components/form/__tests__/textInput.test.js new file mode 100644 index 00000000..43f3bfb8 --- /dev/null +++ b/src/components/form/__tests__/textInput.test.js @@ -0,0 +1,93 @@ +import React from 'react'; +import { TextInput as PfTextInput } from '@patternfly/react-core'; +import { TextInput } from '../textInput'; +import { helpers } from '../../../common'; + +describe('TextInput Component', () => { + it('should render a basic component', async () => { + const props = {}; + + const component = await shallowHookComponent(); + expect(component.render()).toMatchSnapshot('basic component'); + }); + + it('should handle readOnly, disabled', async () => { + const props = { + isReadOnly: true + }; + + const component = await mountHookComponent(); + expect(component.render()).toMatchSnapshot('readOnly'); + + component.setProps({ + isReadOnly: false, + isDisabled: true + }); + + expect(component.render()).toMatchSnapshot('disabled'); + + component.setProps({ + isReadOnly: false, + isDisabled: false + }); + + expect(component.render()).toMatchSnapshot('active'); + }); + + it('should return an emulated onChange event', async () => { + const mockOnChange = jest.fn(); + const props = { + onChange: mockOnChange, + value: 'lorem ipsum' + }; + + const component = await shallowHookComponent(); + const mockEvent = { currentTarget: { value: 'dolor sit' }, persist: helpers.noop }; + component.find(PfTextInput).simulate('change', 'hello world', mockEvent); + + expect(mockOnChange.mock.calls).toMatchSnapshot('emulated event, change'); + }); + + it('should return an emulated onClear event on escape', async () => { + const mockOnClear = jest.fn(); + const props = { + onClear: mockOnClear, + value: 'lorem ipsum' + }; + + const component = await shallowHookComponent(); + const mockEvent = { keyCode: 27, currentTarget: { value: '' }, persist: helpers.noop }; + component.find(PfTextInput).simulate('keyup', mockEvent); + + expect(mockOnClear.mock.calls).toMatchSnapshot('emulated event, esc'); + }); + + it('should return an emulated onClear event on escape with type search', async () => { + const mockOnClear = jest.fn(); + const props = { + onClear: mockOnClear, + value: 'lorem ipsum', + type: 'search' + }; + + const component = await shallowHookComponent(); + const mockEvent = { keyCode: 27, currentTarget: { value: '' }, persist: helpers.noop }; + component.find(PfTextInput).simulate('keyup', mockEvent); + + expect(mockOnClear.mock.calls).toMatchSnapshot('emulated event, esc, type search'); + }); + + it('should return a mouseup event on text clear', async () => { + const mockOnMouseUp = jest.fn(); + const props = { + onMouseUp: mockOnMouseUp, + value: 'lorem ipsum' + }; + + const component = await shallowHookComponent(); + const mockEvent = { currentTarget: { value: '' }, persist: helpers.noop }; + component.find(PfTextInput).simulate('mouseup', mockEvent); + + expect(mockOnMouseUp.mock.calls).toMatchSnapshot('emulated event, mouseup'); + }); +}); diff --git a/src/components/form/formHelpers.js b/src/components/form/formHelpers.js new file mode 100644 index 00000000..0ed98dbe --- /dev/null +++ b/src/components/form/formHelpers.js @@ -0,0 +1,43 @@ +import { helpers } from '../../common'; + +/** + * Create a consistent mock event object. + * + * @param {object} event + * @param {boolean} persistEvent + * @returns {{keyCode, currentTarget, name, id: *, persist: Function, value, target}} + */ +const createMockEvent = (event, persistEvent = false) => { + const { checked, currentTarget = {}, keyCode, persist = helpers.noop, target = {} } = { ...event }; + if (persistEvent) { + persist(); + } + + return { + checked, + currentTarget, + keyCode, + id: currentTarget.id || currentTarget.name, + name: currentTarget.name, + persist, + value: currentTarget.value, + target + }; +}; + +/** + * Confirm a string has minimum length. + * + * @param {string} value + * @param {number} characters + * @returns {boolean} + */ +const doesNotHaveMinimumCharacters = (value, characters = 1) => + (typeof value === 'string' && value.length < characters) || typeof value !== 'string'; + +const formHelpers = { + createMockEvent, + doesNotHaveMinimumCharacters +}; + +export { formHelpers as default, formHelpers, createMockEvent, doesNotHaveMinimumCharacters }; diff --git a/src/components/form/textInput.js b/src/components/form/textInput.js new file mode 100644 index 00000000..7b2c3bad --- /dev/null +++ b/src/components/form/textInput.js @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { TextInput as PfTextInput } from '@patternfly/react-core'; +import { createMockEvent } from './formHelpers'; +import { helpers } from '../../common'; + +/** + * A wrapper for Patternfly TextInput. + * Provides a consistent event structure, and an onClear event for the search type. + * + * @fires onKeyUp + * @fires onMouseUp + * @fires onChange + * @param {object} props + * @param {*|string} props.value + * @param {string} props.className + * @param {string} props.id + * @param {boolean} props.isDisabled + * @param {string} props.name + * @param {Function} props.onChange + * @param {Function} props.onClear + * @param {Function} props.onKeyUp + * @param {Function} props.onMouseUp + * @param {boolean} props.isReadOnly + * @param {string} props.type + * @returns {React.ReactNode}; + */ +const TextInput = ({ + className, + id, + isDisabled, + name, + onChange, + onClear, + onKeyUp, + onMouseUp, + isReadOnly, + type, + value, + ...props +}) => { + const [updatedValue, setUpdatedValue] = useState(value); + + /** + * onKeyUp event, provide additional functionality for onClear event. + * + * @event onKeyUp + * @param {object} event + */ + const onTextInputKeyUp = event => { + const { currentTarget, keyCode } = event; + const clonedEvent = { ...event }; + + onKeyUp(createMockEvent(event, true)); + + if (keyCode === 27) { + if (type === 'search' && currentTarget.value === '') { + onClear(createMockEvent(clonedEvent)); + } else { + setUpdatedValue(''); + onClear(createMockEvent({ ...clonedEvent, ...{ currentTarget: { ...clonedEvent.currentTarget, value: '' } } })); + } + } + }; + + /** + * onMouseUp event, provide additional functionality for onClear event. + * + * @event onMouseUp + * @param {object} event + */ + const onTextInputMouseUp = event => { + const { currentTarget } = event; + const clonedEvent = { ...event }; + + onMouseUp(createMockEvent(event, true)); + + if (type !== 'search' || currentTarget.value === '') { + return; + } + + window.setTimeout(() => { + if (currentTarget.value === '') { + onClear(createMockEvent(clonedEvent)); + } + }); + }; + + /** + * onChange event, provide restructured event. + * + * @event onChange + * @param {string} changedValue + * @param {object} event + */ + const onTextInputChange = (changedValue, event) => { + const clonedEvent = { ...event }; + + setUpdatedValue(changedValue); + onChange(createMockEvent(clonedEvent)); + }; + + const updatedName = name || helpers.generateId(); + const updatedId = id || updatedName; + + return ( + + ); +}; + +/** + * Prop types + * + * @type {{onKeyUp: Function, isReadOnly: boolean, onChange: Function, onClear: Function, name: string, + * className: string, id: string, isDisabled: boolean, onMouseUp: Function, type: string, value: string}} + */ +TextInput.propTypes = { + className: PropTypes.string, + id: PropTypes.string, + isDisabled: PropTypes.bool, + isReadOnly: PropTypes.bool, + name: PropTypes.string, + onChange: PropTypes.func, + onClear: PropTypes.func, + onKeyUp: PropTypes.func, + onMouseUp: PropTypes.func, + type: PropTypes.string, + value: PropTypes.string +}; + +/** + * Default props + * + * @type {{onKeyUp: Function, isReadOnly: boolean, onChange: Function, onClear: Function, name: null, + * className: string, id: null, isDisabled: boolean, onMouseUp: Function, type: string, value: string}} + */ +TextInput.defaultProps = { + className: '', + id: null, + isDisabled: false, + isReadOnly: false, + name: null, + onChange: helpers.noop, + onClear: helpers.noop, + onKeyUp: helpers.noop, + onMouseUp: helpers.noop, + type: 'text', + value: '' +}; + +export { TextInput as default, TextInput };