Skip to content

Commit 5268a79

Browse files
committed
fix(many): fix form label not read by NVDA in hover mode and other layout issues
This commit fixes the following: - Form labels not read in NVDA hover - FormFieldMessage was right aligned in inline layouts - When layout=inline and inline=true the messages are now not spanning the whole bottom area - When using the new error type and its a group the errors will be above the controls as per design requirements Fixes INSTUI-4444
1 parent e057ba4 commit 5268a79

File tree

34 files changed

+442
-590
lines changed

34 files changed

+442
-590
lines changed

cypress/component/FormField.cy.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* SOFTWARE.
2323
*/
2424
import React from 'react'
25+
// eslint-disable-next-line @instructure/no-relative-imports
2526
import { FormFieldLayout } from '../../packages/ui'
2627

2728
import '../support/component'
@@ -36,7 +37,7 @@ describe('<FormField/>', () => {
3637
</FormFieldLayout>
3738
)
3839

39-
cy.get('span[class$="-formFieldLabel"]')
40+
cy.get('span[class$="-formFieldLayout__label"]')
4041
.contains('Username')
4142
.should('have.css', 'text-align', 'end')
4243
})
@@ -50,7 +51,7 @@ describe('<FormField/>', () => {
5051
</FormFieldLayout>
5152
)
5253

53-
cy.get('span[class$="-formFieldLabel"]')
54+
cy.get('span[class$="-formFieldLayout__label"]')
5455
.contains('Username')
5556
.should('have.css', 'text-align', 'start')
5657
})

packages/shared-types/src/ComponentThemeVariables.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,12 +595,15 @@ export type FormFieldGroupTheme = {
595595
errorFieldsPadding: Spacing['xSmall']
596596
}
597597

598-
export type FormFieldLabelTheme = {
598+
export type FormFieldLayoutTheme = {
599599
color: Colors['contrasts']['grey125125']
600600
fontFamily: Typography['fontFamily']
601601
fontWeight: Typography['fontWeightBold']
602602
fontSize: Typography['fontSizeMedium']
603603
lineHeight: Typography['lineHeightFit']
604+
inlinePadding: Spacing['xxSmall']
605+
stackedOrInlineBreakpoint: Breakpoints['medium']
606+
spacing: any // TODO remove any
604607
}
605608

606609
export type FormFieldMessageTheme = {
@@ -1748,7 +1751,6 @@ export interface ThemeVariables {
17481751
FileDrop: FileDropTheme
17491752
Flex: FlexTheme
17501753
FormFieldGroup: FormFieldGroupTheme
1751-
FormFieldLabel: FormFieldLabelTheme
17521754
FormFieldMessage: FormFieldMessageTheme
17531755
FormFieldMessages: FormFieldMessagesTheme
17541756
Grid: GridTheme

packages/ui-checkbox/src/CheckboxGroup/__new-tests__/CheckboxGroup.test.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ describe('<CheckboxGroup />', () => {
8888
const { container } = renderCheckboxGroup({
8989
messages: [{ text: TEST_ERROR_MESSAGE, type: 'error' }]
9090
})
91-
const fieldset = screen.getByRole('group')
92-
const ariaDesc = fieldset.getAttribute('aria-describedby')
91+
const group = screen.getByRole('group')
92+
const ariaDesc = group.getAttribute('aria-describedby')
9393
const messageById = container.querySelector(`[id="${ariaDesc}"]`)
9494

9595
expect(messageById).toBeInTheDocument()
@@ -98,7 +98,9 @@ describe('<CheckboxGroup />', () => {
9898

9999
it('displays description message inside the legend', () => {
100100
const { container } = renderCheckboxGroup({ description: TEST_DESCRIPTION })
101-
const legend = container.querySelector('legend')
101+
const legend = container.querySelector(
102+
'span[class$="-formFieldLayout__label"]'
103+
)
102104

103105
expect(legend).toBeInTheDocument()
104106
expect(legend).toHaveTextContent(TEST_DESCRIPTION)
@@ -217,5 +219,19 @@ describe('<CheckboxGroup />', () => {
217219

218220
expect(axeCheck).toBe(true)
219221
})
222+
223+
it('adds the correct ARIA attributes', () => {
224+
const { container } = renderCheckboxGroup({
225+
disabled: true,
226+
messages: [{ type: 'newError', text: 'abc' }],
227+
// @ts-ignore This is a valid attribute
228+
'data-id': 'group'
229+
})
230+
const group = container.querySelector(`[data-id="group"]`)
231+
expect(group).toBeInTheDocument()
232+
expect(group).toHaveAttribute('aria-disabled', 'true')
233+
// ARIA role 'group' cannot have the 'aria-invalid' attribute
234+
expect(group).not.toHaveAttribute('aria-invalid')
235+
})
220236
})
221237
})

packages/ui-checkbox/src/CheckboxGroup/props.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* SOFTWARE.
2323
*/
2424

25-
import React from 'react'
25+
import React, { type InputHTMLAttributes } from 'react'
2626
import PropTypes from 'prop-types'
2727

2828
import { FormPropTypes } from '@instructure/ui-form-field'
@@ -32,7 +32,10 @@ import {
3232
} from '@instructure/ui-prop-types'
3333

3434
import type { FormMessage } from '@instructure/ui-form-field'
35-
import type { PropValidators } from '@instructure/shared-types'
35+
import type {
36+
OtherHTMLAttributes,
37+
PropValidators
38+
} from '@instructure/shared-types'
3639
import type { WithDeterministicIdProps } from '@instructure/ui-react-utils'
3740

3841
import { Checkbox } from '../Checkbox'
@@ -58,7 +61,12 @@ type PropKeys = keyof CheckboxGroupOwnProps
5861

5962
type AllowedPropKeys = Readonly<Array<PropKeys>>
6063

61-
type CheckboxGroupProps = CheckboxGroupOwnProps & WithDeterministicIdProps
64+
type CheckboxGroupProps = CheckboxGroupOwnProps &
65+
OtherHTMLAttributes<
66+
CheckboxGroupOwnProps,
67+
InputHTMLAttributes<CheckboxGroupOwnProps & Element>
68+
> &
69+
WithDeterministicIdProps
6270

6371
const propTypes: PropValidators<PropKeys> = {
6472
name: PropTypes.string.isRequired,

packages/ui-date-input/src/DateInput/__new-tests__/DateInput.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,9 @@ describe('<DateInput />', () => {
381381
{generateDays()}
382382
</DateInput>
383383
)
384-
const dateInput = container.querySelector("[class$='-formFieldLabel']")
384+
const dateInput = container.querySelector(
385+
'span[class$="-formFieldLayout__label"]'
386+
)
385387

386388
expect(dateInput).toHaveTextContent('Choose date')
387389

packages/ui-date-input/src/DateInput2/__new-tests__/DateInput2.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,10 @@ describe('<DateInput2 />', () => {
7777
it('should render an input label', async () => {
7878
const { container } = render(<DateInputExample />)
7979

80-
const dateInput = container.querySelector('input')
8180
const label = container.querySelector('label')
8281

8382
expect(label).toBeInTheDocument()
8483
expect(label).toHaveTextContent(LABEL_TEXT)
85-
expect(dateInput?.id).toBe(label?.htmlFor)
8684
})
8785

8886
it('should render an input placeholder', async () => {

packages/ui-form-field/src/FormField/README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,35 @@ components. In most cases it shouldn't be used directly.
99
---
1010
type: example
1111
---
12-
<FormField id="_foo123" label="Opacity" width="200px">
13-
<input style={{display: 'block', width: '100%'}} id="_foo123"/>
14-
</FormField>
12+
<div>
13+
<FormField id="_foo121" label="Stacked layout" width="400px" layout="stacked"
14+
messages={[{type:'success', text: 'This is a success message'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
15+
<TextInput id="_foo121"/>
16+
</FormField>
17+
test
18+
<hr/>
19+
<FormField id="_foo122" label="Stacked layout (inline=true)" width="400px" layout="stacked" inline
20+
messages={[{type:'success', text: 'This is a success message'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
21+
<TextInput id="_foo122"/>
22+
</FormField>
23+
test
24+
<hr/>
25+
<FormField id="_foo123" label="Inline layout" width="400px" layout="inline"
26+
messages={[{type:'success', text: 'success!'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
27+
<TextInput id="_foo123"/>
28+
</FormField>
29+
test
30+
<hr/>
31+
<FormField id="_foo124" label="Inline layout (inline=true)" width="400px" layout="inline" inline
32+
messages={[{type:'success', text: 'success!'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
33+
<TextInput id="_foo124"/>
34+
</FormField>
35+
test
36+
<hr/>
37+
<FormField id="_foo121" label={<ScreenReaderContent>hidden text</ScreenReaderContent>} width="400px" layout="stacked">
38+
<TextInput id="_foo121" />
39+
</FormField>
40+
test
41+
<hr/>
42+
</div>
1543
```

packages/ui-form-field/src/FormField/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ class FormField extends Component<FormFieldProps> {
6868
label={this.props.label}
6969
vAlign={this.props.vAlign}
7070
as="label"
71-
htmlFor={this.props.id}
7271
elementRef={this.handleRef}
7372
margin={this.props.margin}
7473
/>

packages/ui-form-field/src/FormFieldGroup/__new-tests__/FormFieldGroup.test.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('<FormFieldGroup />', () => {
6666
)
6767

6868
const formFieldGroup = container.querySelector(
69-
"fieldset[class$='-formFieldLayout']"
69+
"span[class$='-formFieldLayout__label']"
7070
)
7171
const firstNameInput = screen.getByLabelText('First:')
7272
const middleNameInput = screen.getByLabelText('Middle:')
@@ -94,9 +94,7 @@ describe('<FormFieldGroup />', () => {
9494
</FormFieldGroup>
9595
)
9696

97-
const formFieldGroup = container.querySelector(
98-
"fieldset[class$='-formFieldLayout']"
99-
)
97+
const formFieldGroup = container.querySelector('label')
10098

10199
expect(formFieldGroup).toBeInTheDocument()
102100
})
@@ -136,7 +134,7 @@ describe('<FormFieldGroup />', () => {
136134
expect(message).toHaveAttribute('id', messagesId)
137135
})
138136

139-
it('displays description message inside the legend', () => {
137+
it('displays description message inside the label', () => {
140138
const description = 'Please enter your full name'
141139

142140
const { container } = render(
@@ -154,7 +152,7 @@ describe('<FormFieldGroup />', () => {
154152
)
155153

156154
const legend = container.querySelector(
157-
"legend[class$='-screenReaderContent']"
155+
"span[class$='-formFieldLayout__label']"
158156
)
159157

160158
expect(legend).toBeInTheDocument()

packages/ui-form-field/src/FormFieldGroup/index.tsx

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*/
2424

2525
/** @jsx jsx */
26-
import { Component, Children, ReactElement } from 'react'
26+
import { Component, Children, ReactElement, AriaAttributes } from 'react'
2727

2828
import { Grid } from '@instructure/ui-grid'
2929
import { pickProps, omitProps } from '@instructure/ui-react-utils'
@@ -53,7 +53,8 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
5353
disabled: false,
5454
rowSpacing: 'medium',
5555
colSpacing: 'small',
56-
vAlign: 'middle'
56+
vAlign: 'middle',
57+
isGroup: true
5758
}
5859

5960
ref: Element | null = null
@@ -77,14 +78,20 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
7778
}
7879

7980
get makeStylesVariables(): FormFieldGroupStyleProps {
80-
return { invalid: this.invalid }
81+
// new form errors dont need borders
82+
const oldInvalid =
83+
!!this.props.messages &&
84+
this.props.messages.findIndex((message) => {
85+
return message.type === 'error'
86+
}) >= 0
87+
return { invalid: oldInvalid }
8188
}
8289

8390
get invalid() {
8491
return (
8592
!!this.props.messages &&
8693
this.props.messages.findIndex((message) => {
87-
return message.type === 'error'
94+
return message.type === 'error' || message.type === 'newError'
8895
}) >= 0
8996
)
9097
}
@@ -134,7 +141,35 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
134141

135142
render() {
136143
const { styles, makeStyles, isGroup, ...props } = this.props
137-
144+
// This is quite ugly, but according to ARIA spec the `aria-invalid` prop
145+
// can only be used with certain roles see
146+
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid#associated_roles
147+
// `aria-invalid` is put on in FormFieldLayout because the error message
148+
// DOM part gets there its ID.
149+
let ariaInvalid: AriaAttributes['aria-invalid'] = undefined
150+
if (
151+
this.props.role &&
152+
this.invalid &&
153+
[
154+
'application',
155+
'checkbox',
156+
'combobox',
157+
'gridcell',
158+
'listbox',
159+
'radiogroup',
160+
'slider',
161+
'spinbutton',
162+
'textbox',
163+
'tree',
164+
'columnheader',
165+
'rowheader',
166+
'searchbox',
167+
'switch',
168+
'treegrid'
169+
].includes(this.props.role)
170+
) {
171+
ariaInvalid = 'true'
172+
}
138173
return (
139174
<FormFieldLayout
140175
{...omitProps(props, FormFieldGroup.allowedProps)}
@@ -143,7 +178,7 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
143178
layout={props.layout === 'inline' ? 'inline' : 'stacked'}
144179
label={props.description}
145180
aria-disabled={props.disabled ? 'true' : undefined}
146-
aria-invalid={this.invalid ? 'true' : undefined}
181+
aria-invalid={ariaInvalid}
147182
elementRef={this.handleRef}
148183
isGroup={isGroup}
149184
>

0 commit comments

Comments
 (0)