From 19bd74f2926aae4b6a032b3f336373a93e4cc038 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 16 Mar 2022 14:48:10 +0100 Subject: [PATCH 1/4] GenericInput: Allow number inputs to be cleared --- .../helper-plugin/lib/src/components/GenericInput/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 95813c0cfb..2860da3e25 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -216,12 +216,12 @@ const GenericInput = ({ hint={hint} name={name} onValueChange={value => { - onChange({ target: { name, value, type } }); + onChange({ target: { name, value: value ?? null, type } }); }} placeholder={formattedPlaceholder} required={required} step={step} - value={value ?? undefined} + value={value ?? null} /> ); } From 36d1215be9b4e184c94b3134a6a20167c04e8690 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Thu, 17 Mar 2022 09:34:13 +0100 Subject: [PATCH 2/4] GenericInput: Add test for type=number --- .../tests/__snapshots__/index.test.js.snap | 311 ++++++++++++++++++ .../GenericInput/tests/index.test.js | 112 +++++++ 2 files changed, 423 insertions(+) create mode 100644 packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap create mode 100644 packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..f2624e2d5e --- /dev/null +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenericInput number renders and matches the snapshot 1`] = ` +.c13 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.c8 { + padding-right: 12px; + padding-left: 8px; +} + +.c10 { + color: #8e8ea9; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c1 { + font-weight: 600; + color: #32324d; + font-size: 0.75rem; + line-height: 1.33; +} + +.c3 { + color: #d02b20; + font-size: 0.875rem; + line-height: 1.43; +} + +.c4 { + line-height: 0; +} + +.c7 { + border: none; + border-radius: 4px; + padding-left: 16px; + padding-right: 0; + color: #32324d; + font-weight: 400; + font-size: 0.875rem; + display: block; + width: 100%; + background: inherit; +} + +.c7::-webkit-input-placeholder { + color: #8e8ea9; + opacity: 1; +} + +.c7::-moz-placeholder { + color: #8e8ea9; + opacity: 1; +} + +.c7:-ms-input-placeholder { + color: #8e8ea9; + opacity: 1; +} + +.c7::placeholder { + color: #8e8ea9; + opacity: 1; +} + +.c7[aria-disabled='true'] { + color: inherit; +} + +.c7:focus { + outline: none; + box-shadow: none; +} + +.c6 { + border: 1px solid #dcdce4; + border-radius: 4px; + background: #ffffff; + height: 2.5rem; + outline: none; + box-shadow: 0; + -webkit-transition-property: border-color,box-shadow,fill; + transition-property: border-color,box-shadow,fill; + -webkit-transition-duration: 0.2s; + transition-duration: 0.2s; +} + +.c6:focus-within { + border: 1px solid #4945ff; + box-shadow: #4945ff 0px 0px 0px 2px; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; +} + +.c0 > * { + margin-top: 0; + margin-bottom: 0; +} + +.c0 > * + * { + margin-top: 4px; +} + +.c11 path { + fill: #8e8ea9; +} + +.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + height: 1rem; + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; + -webkit-transform: translateY(-2px); + -ms-transform: translateY(-2px); + transform: translateY(-2px); +} + +.c9 svg { + display: block; + height: 0.25rem; + -webkit-transform: rotateX(180deg); + -ms-transform: rotateX(180deg); + transform: rotateX(180deg); +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + height: 1rem; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-transform: translateY(2px); + -ms-transform: translateY(2px); + transform: translateY(2px); +} + +.c12 svg { + display: block; + height: 0.25rem; +} + +
+
+
+ +
+ +
+ + +
+
+
+
+
+

+

+

+
+`; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js new file mode 100644 index 0000000000..44616aac28 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js @@ -0,0 +1,112 @@ +import React from 'react'; +import { ThemeProvider, lightTheme } from '@strapi/design-system'; +import { IntlProvider } from 'react-intl'; +import { render, fireEvent } from '@testing-library/react'; + +import GenericInput from '../index'; + +function ComponentFixture(props) { + return ( + + + + + + ); +} + +function setupNumber(props) { + const NUMBER_FIXTURE_PROPS = { + type: 'number', + name: 'number', + intlLabel: { + id: 'label.test', + defaultMessage: 'Default label', + }, + placeholder: { + id: 'placeholder.test', + defaultMessage: 'Default placeholder', + }, + hint: 'Hint message', + value: null, + required: true, + onChange: jest.fn, + ...props, + }; + + const rendered = render(); + const input = rendered.container.querySelector('input'); + + return { + ...rendered, + input, + }; +} + +describe('GenericInput', () => { + describe('number', () => { + test('renders and matches the snapshot', () => { + const { container } = setupNumber(); + expect(container).toMatchSnapshot(); + }); + + test('renders an error message', () => { + const { getByText } = setupNumber({ error: 'Error message' }); + expect(getByText('Error message')).toBeInTheDocument(); + }); + + test('renders a number (int) value', () => { + const { container } = setupNumber({ value: 1 }); + expect(container.querySelector('[type="text"]').value).toBe('1'); + }); + + test('renders a number (float) value', () => { + const { container } = setupNumber({ value: 1.3333 }); + expect(container.querySelector('input').value).toBe('1.3333'); + }); + + test('does not call onChange callback on first render', () => { + const spy = jest.fn(); + setupNumber({ value: null, onChange: spy }); + expect(spy).not.toHaveBeenCalled(); + }); + + test('does not call onChange callback if the value does not change', () => { + const spy = jest.fn(); + const { input } = setupNumber({ value: 23, onChange: spy }); + + fireEvent.change(input, { target: { value: 23 } }); + + expect(spy).not.toHaveBeenCalledWith(); + }); + + test('does call onChange callback with number (int) value', () => { + const spy = jest.fn(); + const { input } = setupNumber({ value: null, onChange: spy }); + + fireEvent.change(input, { target: { value: '23' } }); + + expect(spy).toHaveBeenCalledWith({ target: { name: 'number', type: 'number', value: 23 } }); + }); + + test('does call onChange callback with number (float) value', () => { + const spy = jest.fn(); + const { input } = setupNumber({ value: null, onChange: spy }); + + fireEvent.change(input, { target: { value: '1.3333' } }); + + expect(spy).toHaveBeenCalledWith({ + target: { name: 'number', type: 'number', value: 1.3333 }, + }); + }); + + test('does call onChange callback with null on empty value', () => { + const spy = jest.fn(); + const { input } = setupNumber({ value: 1, onChange: spy }); + + fireEvent.change(input, { target: { value: '' } }); + + expect(spy).toHaveBeenCalledWith({ target: { name: 'number', type: 'number', value: null } }); + }); + }); +}); From 4da9e8a8dca6471f6038893653e2a56ecbc108b0 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Thu, 17 Mar 2022 09:42:35 +0100 Subject: [PATCH 3/4] GenericInput: Add a special 0 !== null test, just to be safe --- .../src/components/GenericInput/tests/index.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js index 44616aac28..e90125c2ea 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/index.test.js @@ -100,6 +100,17 @@ describe('GenericInput', () => { }); }); + test('does call onChange callback with number (0) value', () => { + const spy = jest.fn(); + const { input } = setupNumber({ value: null, onChange: spy }); + + fireEvent.change(input, { target: { value: '0' } }); + + expect(spy).toHaveBeenCalledWith({ + target: { name: 'number', type: 'number', value: 0 }, + }); + }); + test('does call onChange callback with null on empty value', () => { const spy = jest.fn(); const { input } = setupNumber({ value: 1, onChange: spy }); From 43b2108643ed2efcbe156a3c50f88a2303fe8c85 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Thu, 17 Mar 2022 15:16:31 +0100 Subject: [PATCH 4/4] GenericInput: cast nullish values for input types to undefined --- .../lib/src/components/GenericInput/index.js | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 2860da3e25..8ad1668ab2 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -38,7 +38,7 @@ const GenericInput = ({ required, step, type, - value, + value: defaultValue, isNullable, ...rest }) => { @@ -47,6 +47,10 @@ const GenericInput = ({ const CustomInput = customInputs ? customInputs[type] : null; + // the API always returns null, which throws an error in React, + // therefore we cast this case to undefined + const value = defaultValue ?? undefined; + if (CustomInput) { return ( onChange({ target: { name, value: null, type } })} placeholder={formattedPlaceholder} required={required} - value={value ? new Date(value) : null} + value={value && new Date(value)} selectedDateLabel={formattedDate => `Date picker, current is ${formattedDate}`} /> ); @@ -221,7 +225,7 @@ const GenericInput = ({ placeholder={formattedPlaceholder} required={required} step={step} - value={value ?? null} + value={value} /> ); } @@ -240,7 +244,7 @@ const GenericInput = ({ placeholder={formattedPlaceholder} required={required} type="email" - value={value || ''} + value={value} /> ); } @@ -261,7 +265,7 @@ const GenericInput = ({ placeholder={formattedPlaceholder} required={required} type="text" - value={value || ''} + value={value} /> ); } @@ -303,7 +307,7 @@ const GenericInput = ({ placeholder={formattedPlaceholder} required={required} type={showPassword ? 'text' : 'password'} - value={value || ''} + value={value} /> ); } @@ -322,7 +326,7 @@ const GenericInput = ({ }} placeholder={formattedPlaceholder} required={required} - value={value || ''} + value={value} > {options.map(({ metadatas: { intlLabel, disabled, hidden }, key, value }) => { return ( @@ -348,7 +352,7 @@ const GenericInput = ({ required={required} placeholder={formattedPlaceholder} type={type} - value={value || ''} + value={value} > {value} @@ -415,7 +419,7 @@ GenericInput.defaultProps = { required: false, options: [], step: 1, - value: '', + value: undefined, }; GenericInput.propTypes = {