Skip to content

Commit c184bc3

Browse files
committed
fix(ui-dialog,ui-date-input,ui-calendar): add aria-live to calendar header, descriptive nav button labels with target month, aria-modal on focused dialogs
1 parent 5c1a12f commit c184bc3

11 files changed

Lines changed: 175 additions & 76 deletions

File tree

packages/__docs__/src/Nav/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,8 @@ class Nav extends Component<NavProps, NavState> {
451451
themeSelected = themeSelected || isSelected
452452

453453
const renderThemeName = (rawName: string) => {
454-
if (rawName !== 'canvas' && rawName !== 'canvas-high-contrast') {
455-
return `${rawName} (beta)`
456-
}
454+
if (rawName === 'canvas') return 'legacy-canvas'
455+
if (rawName === 'canvas-high-contrast') return 'legacy-canvas-high-contrast'
457456
return rawName
458457
}
459458

packages/ui-calendar/src/Calendar/__tests__/Calendar.test.tsx

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,12 @@ describe('<Calendar />', () => {
294294
{generateDays()}
295295
</Calendar>
296296
)
297-
const defaultPrevButton = screen.getByText('Previous month')
298-
const defaultNextButton = screen.getByText('Next month')
297+
const defaultPrevButton = screen.getByRole('button', {
298+
name: /^Previous month/
299+
})
300+
const defaultNextButton = screen.getByRole('button', {
301+
name: /^Next month/
302+
})
299303

300304
expect(defaultPrevButton).toBeInTheDocument()
301305
expect(defaultNextButton).toBeInTheDocument()
@@ -536,4 +540,52 @@ describe('<Calendar />', () => {
536540
expect(calendar.tagName).toBe('UL')
537541
expect(calendar).toHaveTextContent(weekdayLabels.join(''))
538542
})
543+
544+
describe('navigation button targetMonthSrLabel', () => {
545+
it('should include the target month in the default button screen reader labels', () => {
546+
render(
547+
<Calendar
548+
renderWeekdayLabels={weekdayLabels}
549+
visibleMonth="2023-12-01"
550+
locale="en"
551+
selectedLabel="Selected"
552+
>
553+
{generateDays()}
554+
</Calendar>
555+
)
556+
557+
expect(
558+
screen.getByRole('button', { name: 'Previous month, November 2023' })
559+
).toBeInTheDocument()
560+
expect(
561+
screen.getByRole('button', { name: 'Next month, January 2024' })
562+
).toBeInTheDocument()
563+
})
564+
565+
it('should pass targetMonthSrLabel to function render props', () => {
566+
render(
567+
<Calendar
568+
renderWeekdayLabels={weekdayLabels}
569+
visibleMonth="2023-12-01"
570+
locale="en"
571+
selectedLabel="Selected"
572+
renderPrevMonthButton={({ targetMonthSrLabel }) => (
573+
<button aria-label={`Go to ${targetMonthSrLabel}`}>prev</button>
574+
)}
575+
renderNextMonthButton={({ targetMonthSrLabel }) => (
576+
<button aria-label={`Go to ${targetMonthSrLabel}`}>next</button>
577+
)}
578+
>
579+
{generateDays()}
580+
</Calendar>
581+
)
582+
583+
expect(
584+
screen.getByRole('button', { name: 'Go to November 2023' })
585+
).toBeInTheDocument()
586+
expect(
587+
screen.getByRole('button', { name: 'Go to January 2024' })
588+
).toBeInTheDocument()
589+
})
590+
})
539591
})

packages/ui-calendar/src/Calendar/v1/index.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,28 +175,41 @@ class Calendar extends Component<CalendarProps, CalendarState> {
175175

176176
renderMonthNavigationButtons = () => {
177177
const { renderNextMonthButton, renderPrevMonthButton } = this.props
178+
const { visibleMonth } = this.state
179+
const prevMonthName = visibleMonth
180+
.clone()
181+
.subtract({ months: 1 })
182+
.format('MMMM YYYY')
183+
const nextMonthName = visibleMonth
184+
.clone()
185+
.add({ months: 1 })
186+
.format('MMMM YYYY')
178187

179188
return {
180189
prevButton: renderPrevMonthButton ? (
181-
callRenderProp(renderPrevMonthButton)
190+
callRenderProp(renderPrevMonthButton, {
191+
targetMonthSrLabel: prevMonthName
192+
})
182193
) : (
183194
<IconButton
184195
size="small"
185196
withBackground={false}
186197
withBorder={false}
187198
renderIcon={<IconArrowOpenStartSolid color="primary" />}
188-
screenReaderLabel="Previous month"
199+
screenReaderLabel={`Previous month, ${prevMonthName}`}
189200
/>
190201
),
191202
nextButton: renderNextMonthButton ? (
192-
callRenderProp(renderNextMonthButton)
203+
callRenderProp(renderNextMonthButton, {
204+
targetMonthSrLabel: nextMonthName
205+
})
193206
) : (
194207
<IconButton
195208
size="small"
196209
withBackground={false}
197210
withBorder={false}
198211
renderIcon={<IconArrowOpenEndSolid color="primary" />}
199-
screenReaderLabel="Next month"
212+
screenReaderLabel={`Next month, ${nextMonthName}`}
200213
/>
201214
)
202215
}
@@ -299,12 +312,12 @@ class Calendar extends Component<CalendarProps, CalendarState> {
299312
{renderNavigationLabel ? (
300313
callRenderProp(renderNavigationLabel)
301314
) : (
302-
<span>
315+
<div aria-live="polite" aria-atomic="true">
303316
<div>{visibleMonth.format('MMMM')}</div>
304317
{!withYearPicker ? (
305318
<div>{visibleMonth.format('YYYY')}</div>
306319
) : null}
307-
</span>
320+
</div>
308321
)}
309322
{nextButton &&
310323
cloneButton(nextButton, this.handleMonthChange('next'))}

packages/ui-calendar/src/Calendar/v1/props.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,23 @@ type CalendarOwnProps = {
9898
*/
9999
renderNavigationLabel?: Renderable
100100
/**
101-
* A button to render in the navigation header. The recommendation is to
102-
* compose it with the [IconButton](IconButton) component by setting the `size`
103-
* prop to `small`, `withBorder` and `withBackground` to `false`, and setting
104-
* `renderIcon` to [IconArrowOpenEnd](icons).
105-
*/
106-
renderNextMonthButton?: Renderable
107-
/**
108-
* A button to render in the navigation header. The recommendation is to
109-
* compose it with the [IconButton](Button) component by setting the `size`
110-
* prop to `small`, `withBorder` and `withBackground` to `false`, and setting
111-
* `renderIcon` to [IconArrowOpenStart](icons).
112-
*/
113-
renderPrevMonthButton?: Renderable
101+
* A button to render in the navigation header.
102+
* When passed as a function, receives `{ targetMonthSrLabel }` —
103+
* a pre-formatted screen reader label for the target month (e.g. "November 2023").
104+
* The recommendation is to compose it with the [IconButton](Button) component
105+
* by setting the `size` prop to `small`, `withBorder` and `withBackground` to `false`,
106+
* and setting `renderIcon` to [IconArrowOpenStart](icons).
107+
*/
108+
renderNextMonthButton?: Renderable<{ targetMonthSrLabel: string }>
109+
/**
110+
* A button to render in the navigation header.
111+
* When passed as a function, receives `{ targetMonthSrLabel }` —
112+
* a pre-formatted screen reader label for the target month (e.g. "November 2023").
113+
* The recommendation is to compose it with the [IconButton](Button) component
114+
* by setting the `size` prop to `small`, `withBorder` and `withBackground` to `false`,
115+
* and setting `renderIcon` to [IconArrowOpenStart](icons).
116+
*/
117+
renderPrevMonthButton?: Renderable<{ targetMonthSrLabel: string }>
114118
/**
115119
* An array of labels containing the name of each day of the week. The visible
116120
* portion of the label should be abbreviated (no longer than three characters).

packages/ui-calendar/src/Calendar/v2/README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -174,23 +174,26 @@ type: example
174174

175175
const date = parseDate(renderedDate)
176176

177-
const buttonProps = (type = 'prev') => ({
178-
size: 'small',
179-
withBackground: false,
180-
withBorder: false,
181-
renderIcon:
182-
type === 'prev' ? (
183-
<ChevronLeftInstUIIcon color="baseColor" />
184-
) : (
185-
<ChevronRightInstUIIcon color="baseColor" />
186-
),
187-
screenReaderLabel: type === 'prev' ? 'Previous month' : 'Next month'
188-
})
177+
const renderMonthButton = (type = 'prev') => ({ targetMonthSrLabel }) => (
178+
<IconButton
179+
size="small"
180+
withBackground={false}
181+
withBorder={false}
182+
renderIcon={
183+
type === 'prev' ? (
184+
<ChevronLeftInstUIIcon color="baseColor" />
185+
) : (
186+
<ChevronRightInstUIIcon color="baseColor" />
187+
)
188+
}
189+
screenReaderLabel={`${type === 'prev' ? 'Previous month' : 'Next month'}, ${targetMonthSrLabel}`}
190+
/>
191+
)
189192

190193
return (
191194
<Calendar
192-
renderPrevMonthButton={<IconButton {...buttonProps('prev')} />}
193-
renderNextMonthButton={<IconButton {...buttonProps('next')} />}
195+
renderPrevMonthButton={renderMonthButton('prev')}
196+
renderNextMonthButton={renderMonthButton('next')}
194197
renderNavigationLabel={
195198
<span>
196199
<div>{date.format('MMMM')}</div>
@@ -251,7 +254,4 @@ the abbreviation. ex. `[<AccessibleContent alt="Sunday">Sun</AccessibleContent>,
251254

252255
#### Rendering next and previous month buttons
253256

254-
The `renderNextMonthButton` and `renderPrevMonthButton` can be supplied using the
255-
[IconButton](IconButton) component with the `size` prop set to
256-
`small`, the `withBackground` and `withBorder` props both set to `false`, and the `renderIcon` prop set to `ChevronLeftInstUIIcon` or
257-
`ChevronRightInstUIIcon`.
257+
The `renderNextMonthButton` and `renderPrevMonthButton` can be supplied as a function that receives `{ targetMonthSrLabel }` — a pre-formatted screen reader label for the target month (e.g. "November 2023"). Use this to compose an [IconButton](IconButton) component with the `size` prop set to `small`, the `withBackground` and `withBorder` props both set to `false`, and the `renderIcon` prop set to `ChevronLeftInstUIIcon` or `ChevronRightInstUIIcon`. The `screenReaderLabel` should include the target month for better accessibility: e.g. "Previous month, November 2023".

packages/ui-calendar/src/Calendar/v2/index.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,28 +174,41 @@ class Calendar extends Component<CalendarProps, CalendarState> {
174174

175175
renderMonthNavigationButtons = () => {
176176
const { renderNextMonthButton, renderPrevMonthButton } = this.props
177+
const { visibleMonth } = this.state
178+
const prevMonthName = visibleMonth
179+
.clone()
180+
.subtract({ months: 1 })
181+
.format('MMMM YYYY')
182+
const nextMonthName = visibleMonth
183+
.clone()
184+
.add({ months: 1 })
185+
.format('MMMM YYYY')
177186

178187
return {
179188
prevButton: renderPrevMonthButton ? (
180-
callRenderProp(renderPrevMonthButton)
189+
callRenderProp(renderPrevMonthButton, {
190+
targetMonthSrLabel: prevMonthName
191+
})
181192
) : (
182193
<IconButton
183194
size="small"
184195
withBackground={false}
185196
withBorder={false}
186197
renderIcon={<ChevronLeftInstUIIcon color="baseColor" />}
187-
screenReaderLabel="Previous month"
198+
screenReaderLabel={`Previous month, ${prevMonthName}`}
188199
/>
189200
),
190201
nextButton: renderNextMonthButton ? (
191-
callRenderProp(renderNextMonthButton)
202+
callRenderProp(renderNextMonthButton, {
203+
targetMonthSrLabel: nextMonthName
204+
})
192205
) : (
193206
<IconButton
194207
size="small"
195208
withBackground={false}
196209
withBorder={false}
197210
renderIcon={<ChevronRightInstUIIcon color="baseColor" />}
198-
screenReaderLabel="Next month"
211+
screenReaderLabel={`Next month, ${nextMonthName}`}
199212
/>
200213
)
201214
}
@@ -298,12 +311,16 @@ class Calendar extends Component<CalendarProps, CalendarState> {
298311
{renderNavigationLabel ? (
299312
callRenderProp(renderNavigationLabel)
300313
) : (
301-
<span>
302-
<div>{visibleMonth.format('MMMM')}</div>
314+
<h2
315+
aria-live="polite"
316+
aria-atomic="true"
317+
css={styles?.navigationLabel}
318+
>
319+
<span>{visibleMonth.format('MMMM')}</span>
303320
{!withYearPicker ? (
304-
<div>{visibleMonth.format('YYYY')}</div>
321+
<span>{visibleMonth.format('YYYY')}</span>
305322
) : null}
306-
</span>
323+
</h2>
307324
)}
308325
{nextButton &&
309326
cloneButton(nextButton, this.handleMonthChange('next'))}
@@ -312,7 +329,7 @@ class Calendar extends Component<CalendarProps, CalendarState> {
312329
{withYearPicker ? (
313330
<div css={styles?.yearPicker}>
314331
<SimpleSelect
315-
width="90px"
332+
width="95px"
316333
renderLabel=""
317334
placeholder="--"
318335
assistiveText={withYearPicker.screenReaderLabel}

packages/ui-calendar/src/Calendar/v2/props.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,23 @@ type CalendarOwnProps = {
9898
*/
9999
renderNavigationLabel?: Renderable
100100
/**
101-
* A button to render in the navigation header. The recommendation is to
102-
* compose it with the [IconButton](IconButton) component by setting the `size`
103-
* prop to `small`, `withBorder` and `withBackground` to `false`, and setting
104-
* `renderIcon` to [IconArrowOpenEnd](icons).
101+
* A button to render in the navigation header.
102+
* When passed as a function, receives `{ targetMonthSrLabel }` —
103+
* a pre-formatted screen reader label for the target month (e.g. "January 2024").
104+
* The recommendation is to compose it with the [IconButton](Button) component
105+
* by setting the `size` prop to `small`, `withBorder` and `withBackground` to `false`,
106+
* and setting `renderIcon` to [IconArrowOpenEnd](icons).
105107
*/
106-
renderNextMonthButton?: Renderable
108+
renderNextMonthButton?: Renderable<{ targetMonthSrLabel: string }>
107109
/**
108-
* A button to render in the navigation header. The recommendation is to
109-
* compose it with the [IconButton](Button) component by setting the `size`
110-
* prop to `small`, `withBorder` and `withBackground` to `false`, and setting
111-
* `renderIcon` to [IconArrowOpenStart](icons).
110+
* A button to render in the navigation header.
111+
* When passed as a function, receives `{ targetMonthSrLabel }` —
112+
* a pre-formatted screen reader label for the target month (e.g. "November 2023").
113+
* The recommendation is to compose it with the [IconButton](Button) component
114+
* by setting the `size` prop to `small`, `withBorder` and `withBackground` to `false`,
115+
* and setting `renderIcon` to [IconArrowOpenStart](icons).
112116
*/
113-
renderPrevMonthButton?: Renderable
117+
renderPrevMonthButton?: Renderable<{ targetMonthSrLabel: string }>
114118
/**
115119
* An array of labels containing the name of each day of the week. The visible
116120
* portion of the label should be abbreviated (no longer than three characters).
@@ -182,7 +186,11 @@ type CalendarProps = CalendarOwnProps &
182186
WithDeterministicIdProps
183187

184188
type CalendarStyle = ComponentStyle<
185-
'navigation' | 'navigationWithButtons' | 'weekdayHeader' | 'yearPicker'
189+
| 'navigation'
190+
| 'navigationWithButtons'
191+
| 'navigationLabel'
192+
| 'weekdayHeader'
193+
| 'yearPicker'
186194
>
187195
const allowedProps: AllowedPropKeys = [
188196
'as',

packages/ui-calendar/src/Calendar/v2/styles.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ const generateStyle = (
6363
maxWidth: componentTheme.maxHeaderWidth,
6464
lineHeight: componentTheme.lineHeight
6565
},
66+
navigationLabel: {
67+
label: 'calendar__navigation-label',
68+
margin: 0,
69+
fontSize: 'inherit',
70+
fontWeight: 'inherit',
71+
'& span': {
72+
display: 'block'
73+
}
74+
},
6675
yearPicker: {
6776
display: 'flex',
6877
justifyContent: 'center',

packages/ui-date-input/src/DateInput/__tests__/DateInput.test.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,21 +220,15 @@ describe('<DateInput />', () => {
220220

221221
await waitFor(() => {
222222
const prevMonthButton = screen.getByRole('button', {
223-
name: prevMonthLabel
223+
name: new RegExp(`^${prevMonthLabel}`)
224224
})
225225
const nextMonthButton = screen.getByRole('button', {
226-
name: nextMonthLabel
226+
name: new RegExp(`^${nextMonthLabel}`)
227227
})
228228

229229
expect(prevMonthButton).toBeInTheDocument()
230230
expect(nextMonthButton).toBeInTheDocument()
231231

232-
const prevButtonLabel = screen.getByText(prevMonthLabel)
233-
const nextButtonLabel = screen.getByText(nextMonthLabel)
234-
235-
expect(prevButtonLabel).toBeInTheDocument()
236-
expect(nextButtonLabel).toBeInTheDocument()
237-
238232
const prevMonthIcon = prevMonthButton.querySelector(
239233
'svg[name="ChevronLeft"]'
240234
)

0 commit comments

Comments
 (0)