Adds styling, and dynamic rendering of extra fields

This commit is contained in:
Alex Corey 2021-04-15 15:49:45 -04:00
parent 264b13f33c
commit 98375a0328
4 changed files with 373 additions and 154 deletions

View File

@ -1,16 +1,19 @@
import React, { useState } from 'react';
import { useField, useFormikContext } from 'formik';
import React from 'react';
import { useField } from 'formik';
import { t } from '@lingui/macro';
import { FormGroup, TextInput, Button } from '@patternfly/react-core';
import {
FormGroup,
TextInput,
Button,
InputGroup as PFInputGroup,
Tooltip,
} from '@patternfly/react-core';
import PFCheckIcon from '@patternfly/react-icons/dist/js/icons/check-icon';
import styled from 'styled-components';
import Popover from '../Popover';
const InputWrapper = styled.span`
&& {
display: flex;
padding-bottom: 5px;
}
const InputGroup = styled(PFInputGroup)`
padding-bottom: 5px;
`;
const CheckIcon = styled(PFCheckIcon)`
color: var(--pf-c-button--m-plain--disabled--Color);
@ -18,34 +21,17 @@ const CheckIcon = styled(PFCheckIcon)`
props.isSelected &&
`color: var(--pf-c-button--m-secondary--active--Color)`};
`;
function TextAndCheckboxField({
id,
label,
helperText,
isRequired,
isValid,
tooltip,
name,
rows,
...rest
}) {
const { values: formikValues } = useFormikContext();
function TextAndCheckboxField({ label, helperText, tooltip }) {
const [choicesField, choicesMeta, choicesHelpers] = useField('choices');
// const [fields, setFields] = useState(choicesField.value.split('\n'));
// const [defaultValue, setDefaultValue] = useState(
// formikValues.default.split('\n')
// );
const [, , defaultHelpers] = useField('default');
const [isNewValueChecked, setIsNewValueChecked] = useState(false);
console.log('set');
const [typeField] = useField('type');
const [defaultField, , defaultHelpers] = useField('default');
const handleCheckboxChange = v =>
defaultSplit.includes(v)
? defaultHelpers.setValue(defaultSplit.filter(d => d !== v).join('\n'))
: defaultHelpers.setValue(formikValues.default.concat(`\n${v}`));
: defaultHelpers.setValue(defaultField.value.concat(`\n${v}`));
const choicesSplit = choicesField.value.split('\n');
const defaultSplit = formikValues.default.split('\n');
const defaultSplit = defaultField.value?.split('\n');
return (
<FormGroup
helperText={helperText}
@ -53,64 +39,58 @@ function TextAndCheckboxField({
label={label}
labelIcon={<Popover content={tooltip} />}
>
{choicesSplit
.map((v, i) => (
<InputWrapper>
<TextInput
value={v}
onChange={value => {
defaultHelpers.setValue(
defaultSplit.filter(d => d !== v).join('\n')
);
{choicesSplit.map((v, i) => (
<InputGroup>
<TextInput
onKeyDown={e => {
if (e.key === 'Enter' && i === choicesSplit.length - 1) {
choicesHelpers.setValue(choicesField.value.concat('\n'));
}
const newFields = choicesSplit
.map((choice, index) => (i === index ? value : choice))
if (e.key === 'Backspace' && v.length <= 1) {
const removeEmptyField = choicesSplit
.filter((choice, index) => index !== i)
.join('\n');
choicesHelpers.setValue(removeEmptyField);
}
}}
value={v}
onChange={value => {
defaultHelpers.setValue(
defaultSplit.filter(d => d !== v).join('\n')
);
return value === ''
? choicesHelpers.setValue(
choicesSplit.filter(d => d !== v).join('\n')
)
: choicesHelpers.setValue(newFields);
}}
/>
const newFields = choicesSplit
.map((choice, index) => (i === index ? value : choice))
.join('\n');
return value === ''
? choicesHelpers.setValue(
choicesSplit.filter(d => d !== v).join('\n')
)
: choicesHelpers.setValue(newFields);
}}
/>
<Tooltip
content={t`Click to select this answer as a default answer.`}
position="right"
trigger="mouseenter"
>
<Button
variant="control"
aria-label={t`Click to toggle default value`}
ouiaId={v}
onClick={() =>
formikValues.type === 'multiselect'
typeField.value === 'multiselect'
? handleCheckboxChange(v)
: defaultHelpers.setValue(`${v}`)
}
>
<CheckIcon isSelected={defaultSplit.includes(v)} />
<CheckIcon isSelected={defaultSplit?.includes(v) || false} />
</Button>
</InputWrapper>
))
.concat(
<InputWrapper>
<TextInput
value=""
onChange={(value, event) => {
choicesHelpers.setValue([...choicesSplit, value].join('\n'));
}}
/>
<Button
variant="control"
aria-label={t`Click to toggle default value`}
ouiaId="new input"
onClick={
() => {}
// formikValues.type === 'multiselect'
// ? handleCheckboxChange(v)
// : defaultHelpers.setValue(`${v}`)
}
>
<CheckIcon isSelected={false} />
</Button>
</InputWrapper>
)}
</Tooltip>
</InputGroup>
))}
</FormGroup>
);
}

View File

@ -0,0 +1,168 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { Formik } from 'formik';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import TextAndCheckboxField from './TextAndCheckboxField';
describe('<TextAndCheckboxField/>', () => {
test('should activate default values, multiselect', async () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<Formik
initialValues={{
choices: 'alex\napollo\nathena',
default: 'alex\napollo',
type: 'multiselect',
}}
>
<TextAndCheckboxField id="question-options" name="choices" />
</Formik>
);
});
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onChange')('alex')
);
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
await act(() => wrapper.find('Button[ouiaId="alex"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onKeyDown')({ key: 'Enter' })
);
wrapper.update();
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
3
);
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(1)
.prop('onChange')('spencer')
);
wrapper.update();
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
3
);
await act(() => wrapper.find('Button[ouiaId="spencer"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="spencer"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
await act(() => wrapper.find('Button[ouiaId="alex"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
});
test('should select default, multiplechoice', async () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<Formik
initialValues={{
choices: 'alex\napollo\nathena',
default: 'alex\napollo',
type: 'multiplechoice',
}}
>
<TextAndCheckboxField id="question-options" name="choices" />
</Formik>
);
});
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onChange')('alex')
);
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
await act(() => wrapper.find('Button[ouiaId="alex"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
3
);
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onKeyDown')({ key: 'Enter' })
);
wrapper.update();
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(1)
.prop('onChange')('spencer')
);
wrapper.update();
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
3
);
await act(() => wrapper.find('Button[ouiaId="spencer"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="spencer"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
});
});

View File

@ -104,20 +104,6 @@ function SurveyQuestionForm({
handleCancel,
submitError,
}) {
const defaultIsNotAvailable = choices => {
return defaultValue => {
let errorMessage;
const found = [...defaultValue].every(dA => {
return choices.indexOf(dA) > -1;
});
if (!found) {
errorMessage = t`Default choice must be answered from the choices listed.`;
}
return errorMessage;
};
};
return (
<Formik
enableReinitialize
@ -236,29 +222,14 @@ function SurveyQuestionForm({
/>
)}
{['multiplechoice', 'multiselect'].includes(formik.values.type) && (
<>
<TextAndCheckboxField
id="question-options"
name="choices"
type={formik.values.type}
label={t`Multiple Choice Options`}
validate={required()}
tooltip={t`Each answer choice must be on a separate line.`}
isRequired
rows="10"
/>
<FormField
id="question-default"
name="default"
validate={defaultIsNotAvailable(formik.values.choices)}
type={
formik.values.type === 'multiplechoice'
? 'text'
: 'textarea'
}
label={t`Default answer`}
/>
</>
<TextAndCheckboxField
id="question-options"
name="choices"
label={t`Multiple Choice Options`}
validate={required()}
tooltip={t`Type answer choices and click the check next the default choice(s). Multiple Choice (multi select) can have more than 1 default answer. Multiple Choice (single select) can only have 1 default answer. Press enter to get additional inputs`}
isRequired
/>
)}
</FormColumnLayout>
<FormSubmitError error={submitError} />

View File

@ -17,12 +17,15 @@ const noop = () => {};
async function selectType(wrapper, type) {
await act(async () => {
wrapper.find('AnsibleSelect#question-type').invoke('onChange')({
target: {
name: 'type',
value: type,
wrapper.find('AnsibleSelect#question-type').invoke('onChange')(
{
target: {
name: 'type',
value: type,
},
},
});
type
);
});
wrapper.update();
}
@ -146,12 +149,15 @@ describe('<SurveyQuestionForm />', () => {
});
await selectType(wrapper, 'multiplechoice');
expect(wrapper.find('FormField#question-options').prop('type')).toEqual(
'textarea'
);
expect(wrapper.find('FormField#question-default').prop('type')).toEqual(
'text'
expect(wrapper.find('TextAndCheckboxField').length).toBe(1);
expect(wrapper.find('TextAndCheckboxField').find('TextInput').length).toBe(
1
);
expect(
wrapper
.find('TextAndCheckboxField')
.find('Button[aria-label="Click to toggle default value"]').length
).toBe(1);
});
test('should provide fields for multi-select question', async () => {
@ -168,12 +174,15 @@ describe('<SurveyQuestionForm />', () => {
});
await selectType(wrapper, 'multiselect');
expect(wrapper.find('FormField#question-options').prop('type')).toEqual(
'textarea'
);
expect(wrapper.find('FormField#question-default').prop('type')).toEqual(
'textarea'
expect(wrapper.find('TextAndCheckboxField').length).toBe(1);
expect(wrapper.find('TextAndCheckboxField').find('TextInput').length).toBe(
1
);
expect(
wrapper
.find('TextAndCheckboxField')
.find('Button[aria-label="Click to toggle default value"]').length
).toBe(1);
});
test('should provide fields for integer question', async () => {
@ -225,7 +234,7 @@ describe('<SurveyQuestionForm />', () => {
wrapper.find('FormField#question-default input').prop('type')
).toEqual('number');
});
test('should not throw validation error', async () => {
test('should activate default values, multiselect', async () => {
let wrapper;
act(() => {
@ -239,25 +248,71 @@ describe('<SurveyQuestionForm />', () => {
});
await selectType(wrapper, 'multiselect');
await act(async () =>
wrapper.find('textarea#question-options').simulate('change', {
target: { value: 'a \n b', name: 'choices' },
})
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onChange')('alex')
);
await act(async () =>
wrapper.find('textarea#question-options').simulate('change', {
target: { value: 'b \n a', name: 'default' },
})
);
wrapper.find('FormField#question-default').prop('validate')('b \n a', {});
wrapper.update();
expect(
wrapper
.find('FormGroup[fieldId="question-default"]')
.prop('helperTextInvalid')
).toBe(undefined);
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
await act(() => wrapper.find('Button[ouiaId="alex"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onKeyDown')({ key: 'Enter' })
);
wrapper.update();
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
2
);
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(1)
.prop('onChange')('spencer')
);
wrapper.update();
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
2
);
await act(() => wrapper.find('Button[ouiaId="spencer"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="spencer"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
await act(() => wrapper.find('Button[ouiaId="alex"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
});
test('should throw validation error', async () => {
test('should select default, multiplechoice', async () => {
let wrapper;
act(() => {
@ -269,23 +324,68 @@ describe('<SurveyQuestionForm />', () => {
/>
);
});
await selectType(wrapper, 'multiselect');
await selectType(wrapper, 'multiplechoice');
await act(async () =>
wrapper.find('textarea#question-options').simulate('change', {
target: { value: 'a \n b', name: 'choices' },
})
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onChange')('alex')
);
await act(async () =>
wrapper.find('textarea#question-default').simulate('change', {
target: { value: 'c', name: 'default' },
})
);
wrapper.find('FormField#question-default').prop('validate')('c', {});
wrapper.update();
expect(
wrapper
.find('FormGroup[fieldId="question-default"]')
.prop('helperTextInvalid')
).toBe('Default choice must be answered from the choices listed.');
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
await act(() => wrapper.find('Button[ouiaId="alex"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
1
);
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(0)
.prop('onKeyDown')({ key: 'Enter' })
);
wrapper.update();
await act(async () =>
wrapper
.find('TextAndCheckboxField')
.find('TextAndCheckboxField')
.find('TextInput')
.at(1)
.prop('onChange')('spencer')
);
wrapper.update();
expect(wrapper.find('TextAndCheckboxField').find('InputGroup').length).toBe(
2
);
await act(() => wrapper.find('Button[ouiaId="spencer"]').prop('onClick')());
wrapper.update();
expect(
wrapper
.find('Button[ouiaId="spencer"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(true);
expect(
wrapper
.find('Button[ouiaId="alex"]')
.find('CheckIcon')
.prop('isSelected')
).toBe(false);
});
});