Merge pull request #12861 from strapi/fix/number-clear

GenericInput: Allow number inputs to be cleared
This commit is contained in:
Gustav Hansen 2022-03-22 08:41:54 +01:00 committed by GitHub
commit 7cbf20c762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 449 additions and 11 deletions

View File

@ -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 (
<CustomInput
@ -109,7 +113,7 @@ const GenericInput = ({
return (
<ToggleInput
checked={value === null ? null : value || false}
checked={defaultValue === null ? null : defaultValue || false}
disabled={disabled}
hint={hint}
label={label}
@ -170,7 +174,7 @@ const GenericInput = ({
onClear={() => 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}`}
/>
);
@ -216,12 +220,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}
/>
);
}
@ -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}
</Textarea>
@ -415,7 +419,7 @@ GenericInput.defaultProps = {
required: false,
options: [],
step: 1,
value: '',
value: undefined,
};
GenericInput.propTypes = {

View File

@ -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;
}
<div>
<div>
<div
class="c0"
spacing="1"
>
<label
class="c1"
for="number"
required=""
>
<div
class="c2"
>
Default label
<span
class="c3 c4"
>
*
</span>
</div>
</label>
<div
class="c5 c6"
>
<input
aria-disabled="false"
aria-invalid="false"
class="c7"
id="number"
name="number"
placeholder="Default placeholder"
type="text"
value=""
/>
<div
class="c8"
>
<button
aria-hidden="true"
class="c9"
data-testid="ArrowUp"
tabindex="-1"
type="button"
>
<svg
class="c10 c11"
fill="none"
height="1em"
viewBox="0 0 14 8"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
fill="#32324D"
fill-rule="evenodd"
/>
</svg>
</button>
<button
aria-hidden="true"
class="c12"
data-testid="ArrowDown"
tabindex="-1"
type="button"
>
<svg
class="c10 c11"
fill="none"
height="1em"
viewBox="0 0 14 8"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
fill="#32324D"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
<div
class="c13"
>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-log"
role="log"
/>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-status"
role="status"
/>
<p
aria-live="assertive"
aria-relevant="all"
id="live-region-alert"
role="alert"
/>
</div>
</div>
`;

View File

@ -0,0 +1,123 @@
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 (
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<GenericInput {...props} />
</ThemeProvider>
</IntlProvider>
);
}
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(<ComponentFixture {...NUMBER_FIXTURE_PROPS} />);
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 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 });
fireEvent.change(input, { target: { value: '' } });
expect(spy).toHaveBeenCalledWith({ target: { name: 'number', type: 'number', value: null } });
});
});
});