Skip to content

Commit

Permalink
feat(select a11y): add <SingleSelectA11yFieldFF/>
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohammer5 committed Nov 27, 2024
1 parent 4debf99 commit 8e1152b
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 2 deletions.
4 changes: 2 additions & 2 deletions collections/forms/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2024-11-07T00:44:30.575Z\n"
"PO-Revision-Date: 2024-11-07T00:44:30.576Z\n"
"POT-Creation-Date: 2024-11-19T08:12:09.695Z\n"
"PO-Revision-Date: 2024-11-19T08:12:09.696Z\n"

msgid "Upload file"
msgstr "Upload file"
Expand Down
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,
}
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"
/>
)
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()
})
})

0 comments on commit 8e1152b

Please sign in to comment.