-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(select a11y): add <SingleSelectA11yFieldFF/>
- Loading branch information
Showing
4 changed files
with
340 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
collections/forms/src/SingleSelectA11yFieldFF/SingleSelectA11yFieldFF.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { requiredIf } from '@dhis2/prop-types' | ||
import { SingleSelectA11yField } from '@dhis2-ui/select' | ||
import PropTypes from 'prop-types' | ||
import React from 'react' | ||
import { | ||
createFocusHandler, | ||
createBlurHandler, | ||
hasError, | ||
isLoading, | ||
isValid, | ||
getValidationText, | ||
} from '../shared/helpers.js' | ||
import { inputPropType, metaPropType } from '../shared/propTypes.js' | ||
|
||
export const SingleSelectA11yFieldFF = ({ | ||
error, | ||
input, | ||
loading, | ||
meta, | ||
showLoadingStatus, | ||
showValidStatus, | ||
valid, | ||
validationText, | ||
onBlur, | ||
onFocus, | ||
...rest | ||
}) => { | ||
return ( | ||
<SingleSelectA11yField | ||
{...rest} | ||
error={hasError(meta, error)} | ||
valid={isValid(meta, valid, showValidStatus)} | ||
loading={isLoading(meta, loading, showLoadingStatus)} | ||
validationText={getValidationText(meta, validationText, error)} | ||
onFocus={createFocusHandler(input, onFocus)} | ||
onChange={(value) => input.onChange(value)} | ||
onBlur={createBlurHandler(input, onBlur)} | ||
value={input.value || ''} | ||
/> | ||
) | ||
} | ||
|
||
SingleSelectA11yFieldFF.propTypes = { | ||
/** necessary for IDs that are required for accessibility **/ | ||
idPrefix: PropTypes.string.isRequired, | ||
|
||
/** `input` props received from Final Form `Field` */ | ||
input: inputPropType.isRequired, | ||
|
||
/** Label displayed above the input **/ | ||
label: PropTypes.string.isRequired, | ||
|
||
/** `meta` props received from Final Form `Field` */ | ||
meta: metaPropType.isRequired, | ||
|
||
options: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
label: PropTypes.string.isRequired, | ||
value: PropTypes.string.isRequired, | ||
}) | ||
).isRequired, | ||
|
||
/** Will focus the select initially **/ | ||
autoFocus: PropTypes.bool, | ||
|
||
/** Additional class names that will be applied to the root element **/ | ||
className: PropTypes.string, | ||
|
||
/** This will allow us to put an aria-label on the clear button **/ | ||
clearText: requiredIf((props) => props.clearable, PropTypes.string), | ||
|
||
/** Whether a clear button should be displayed or not **/ | ||
clearable: PropTypes.bool, | ||
|
||
/** Allows to override what's rendered inside the `button[role="option"]`. | ||
* Can be overriden on an individual option basis **/ | ||
customOption: PropTypes.elementType, | ||
|
||
/** A value for a `data-test` attribute on the root element **/ | ||
dataTest: PropTypes.string, | ||
|
||
/** Renders a select with lower height **/ | ||
dense: PropTypes.bool, | ||
|
||
/** Disables all interactions with the select (except focussing) **/ | ||
disabled: PropTypes.bool, | ||
|
||
/** Text or component to display when there are no options **/ | ||
empty: PropTypes.node, | ||
|
||
/** Applies 'error' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props **/ | ||
error: PropTypes.bool, | ||
|
||
/** Help text that will be displayed below the input **/ | ||
filterHelpText: PropTypes.string, | ||
|
||
/** Value will be used as aria-label attribute on the filter input **/ | ||
filterLabel: PropTypes.string, | ||
|
||
/** Placeholder for the filter input **/ | ||
filterPlaceholder: PropTypes.string, | ||
|
||
/** Value of the filter input **/ | ||
filterValue: PropTypes.string, | ||
|
||
/** Whether the select should display a filter input **/ | ||
filterable: PropTypes.bool, | ||
|
||
/** Help text, displayed below the input **/ | ||
helpText: PropTypes.string, | ||
|
||
/** Will show a loading indicator at the end of the options-list **/ | ||
loading: PropTypes.bool, | ||
|
||
/** Text that will be displayed next to the loading indicator **/ | ||
menuLoadingText: PropTypes.string, | ||
|
||
/** Allows to modify the max height of the menu **/ | ||
menuMaxHeight: PropTypes.string, | ||
|
||
/** String that will be displayed when the select is being filtered but the options array is empty **/ | ||
noMatchText: requiredIf((props) => props.filterable, PropTypes.string), | ||
|
||
/** For a11y: How aggressively the user should be updated about changes in options **/ | ||
optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']), | ||
|
||
/** String to show when there's no value and no valueLabel **/ | ||
placeholder: PropTypes.string, | ||
|
||
/** String that will be displayed before the label of the selected option **/ | ||
prefix: PropTypes.string, | ||
|
||
/** Whether a value is required or not **/ | ||
required: PropTypes.bool, | ||
|
||
showLoadingStatus: PropTypes.bool, | ||
|
||
showValidStatus: PropTypes.bool, | ||
|
||
/** Standard HTML tab-index attribute that will be put on the combobox's root element **/ | ||
tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||
|
||
valid: PropTypes.bool, | ||
|
||
validationText: PropTypes.string, | ||
|
||
/** | ||
* When the option is not in the options list (e.g. not loaded or list is | ||
* filtered), but a selected value needs to be displayed, then this prop can | ||
* be used to supply the text to be shown. | ||
**/ | ||
valueLabel: requiredIf((props) => { | ||
if (props.options.find(({ value }) => props.value === value)) { | ||
return false | ||
} | ||
|
||
return props.value | ||
}, PropTypes.string), | ||
|
||
/** Applies 'warning' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props **/ | ||
warning: PropTypes.bool, | ||
|
||
/** Will be called when the combobox is loses focus **/ | ||
onBlur: PropTypes.func, | ||
|
||
/** Will be called when the last option is scrolled into the visible area **/ | ||
onEndReached: PropTypes.func, | ||
|
||
/** Will be called when the filter value changes **/ | ||
onFilterChange: PropTypes.func, | ||
|
||
/** Will be called when the combobox is being focused **/ | ||
onFocus: PropTypes.func, | ||
} |
81 changes: 81 additions & 0 deletions
81
collections/forms/src/SingleSelectA11yFieldFF/SingleSelectA11yFieldFF.prod.stories.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React from 'react' | ||
import { Field } from 'react-final-form' | ||
import { formDecorator } from '../formDecorator.js' | ||
import { inputArgType, metaArgType } from '../shared/propTypes.js' | ||
import { hasValue } from '../validators/index.js' | ||
import { SingleSelectA11yFieldFF } from './SingleSelectA11yFieldFF.js' | ||
|
||
const description = ` | ||
The \`SingleSelectA11yFieldFF\` is a wrapper around a \`SingleSelectA11yField\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. | ||
#### Final Form | ||
See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). | ||
Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={SingleSelectA11yFieldFF} />\`. See the code samples below for examples. | ||
#### Props | ||
The props shown in the table below are generally provided to the \`SingleSelectA11yFieldFF\` wrapper by the Final Form \`Field\`. | ||
Note that any props beyond the API of the \`Field\` component will be spread to the \`SingleSelectA11yFieldFF\`, which passes any extra props to the underlying \`SingleSelectA11yField\` using \`{...rest}\`. | ||
Therefore, to add any props to the \`SingleSelectA11yFieldFF\` or \`SingleSelectA11yField\`, add those props to the parent Final Form \`Field\` component. | ||
Also see \`SingleSelect\` and \`SingleSelectA11yField\` for notes about props and implementation. | ||
\`\`\`js | ||
import { SingleSelectA11yFieldFF } from '@dhis2/ui' | ||
\`\`\` | ||
Press **Submit** to see the form values logged to the console. | ||
_**Note:** Dropdowns may not appear correctly on this page. See the affected demos in the 'Canvas' tab for propper dropdown placement._ | ||
` | ||
|
||
const options = [ | ||
{ value: '1', label: 'one' }, | ||
{ value: '2', label: 'two' }, | ||
{ value: '3', label: 'three' }, | ||
{ value: '4', label: 'four' }, | ||
{ value: '5', label: 'five' }, | ||
{ value: '6', label: 'six' }, | ||
{ value: '7', label: 'seven' }, | ||
{ value: '8', label: 'eight' }, | ||
{ value: '9', label: 'nine' }, | ||
{ value: '10', label: 'ten' }, | ||
] | ||
|
||
export default { | ||
title: 'SingleSelectA11yField (Final Form)', | ||
component: SingleSelectA11yFieldFF, | ||
decorators: [formDecorator], | ||
parameters: { docs: { description: { component: description } } }, | ||
argTypes: { | ||
input: { ...inputArgType }, | ||
meta: { ...metaArgType }, | ||
}, | ||
} | ||
|
||
export const Default = () => ( | ||
<Field | ||
required | ||
component={SingleSelectA11yFieldFF} | ||
idPrefix="story" | ||
name="agree" | ||
label="Do you agree?" | ||
options={options} | ||
validate={hasValue} | ||
/> | ||
) | ||
|
||
export const InitialValue = () => ( | ||
<Field | ||
component={SingleSelectA11yFieldFF} | ||
idPrefix="story" | ||
name="agree" | ||
label="Do you agree?" | ||
options={options} | ||
initialValue="4" | ||
/> | ||
) |
83 changes: 83 additions & 0 deletions
83
collections/forms/src/SingleSelectA11yFieldFF/SingleSelectA11yFieldFF.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import '@testing-library/jest-dom' | ||
import { Button } from '@dhis2-ui/button' | ||
import { render, fireEvent, screen } from '@testing-library/react' | ||
import React from 'react' | ||
import { Field, Form } from 'react-final-form' | ||
import { hasValue } from '../validators/index.js' | ||
import { SingleSelectA11yFieldFF } from './SingleSelectA11yFieldFF.js' | ||
|
||
describe('<SingleSelectA11yFieldFF/>', () => { | ||
it("should use FF's input for value selection", () => { | ||
const onSubmit = jest.fn() | ||
|
||
render( | ||
<Form onSubmit={onSubmit}> | ||
{(formRenderProps) => ( | ||
<form onSubmit={formRenderProps.handleSubmit}> | ||
<Field | ||
component={SingleSelectA11yFieldFF} | ||
idPrefix="story" | ||
name="selectName" | ||
label="Label text" | ||
options={[ | ||
{ value: '', label: 'None' }, | ||
{ value: 'foo', label: 'Foo' }, | ||
{ value: 'bar', label: 'Bar' }, | ||
]} | ||
/> | ||
|
||
<Button primary type="submit"> | ||
Submit | ||
</Button> | ||
</form> | ||
)} | ||
</Form> | ||
) | ||
|
||
fireEvent.click(screen.getByRole('combobox')) | ||
fireEvent.click(screen.getByText('Foo').parentNode) | ||
fireEvent.click(screen.getByRole('button')) | ||
|
||
expect(onSubmit).toHaveBeenCalledTimes(1) | ||
expect(onSubmit).toHaveBeenCalledWith( | ||
{ selectName: 'foo' }, | ||
expect.anything(), | ||
expect.anything() | ||
) | ||
}) | ||
|
||
it('should display the validation error', () => { | ||
const onSubmit = jest.fn() | ||
|
||
render( | ||
<Form onSubmit={onSubmit}> | ||
{(formRenderProps) => ( | ||
<form onSubmit={formRenderProps.handleSubmit}> | ||
<Field | ||
required | ||
component={SingleSelectA11yFieldFF} | ||
idPrefix="story" | ||
name="selectName" | ||
label="Label text" | ||
validate={hasValue} | ||
options={[ | ||
{ value: '', label: 'None' }, | ||
{ value: 'foo', label: 'Foo' }, | ||
{ value: 'bar', label: 'Bar' }, | ||
]} | ||
/> | ||
|
||
<Button primary type="submit"> | ||
Submit | ||
</Button> | ||
</form> | ||
)} | ||
</Form> | ||
) | ||
|
||
fireEvent.click(screen.getByRole('button')) | ||
|
||
const error = screen.getByText('Please provide a value') | ||
expect(error).not.toBeNull() | ||
}) | ||
}) |