Skip to content

Commit

Permalink
Merge pull request #1778 from dequelabs/release-v6.14.0
Browse files Browse the repository at this point in the history
chore(cauldron): Release 6.14.0
  • Loading branch information
scurker authored Dec 20, 2024
2 parents 4fd9069 + 79950c8 commit 13f0eea
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 11 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [6.14.0](https://github.com/dequelabs/cauldron/compare/v6.13.0...v6.14.0) (2024-12-20)


### Features

* **react:** add focusTrap options to AnchoredOverlay ([#1775](https://github.com/dequelabs/cauldron/issues/1775)) ([0a00d24](https://github.com/dequelabs/cauldron/commit/0a00d24f75b0183ee90e30f3e7362eeebc1c6847))


### Bug Fixes

* **react:** fix a11y active-descendant issue with controlled Listboxes ([#1776](https://github.com/dequelabs/cauldron/issues/1776)) ([897963b](https://github.com/dequelabs/cauldron/commit/897963ba1310fad155020bdb43738730d82f1283))
* **react:** fix listbox focus issue when listbox options have changed ([#1777](https://github.com/dequelabs/cauldron/issues/1777)) ([55d370c](https://github.com/dequelabs/cauldron/commit/55d370c097b8ff0b9dab012d364253fc44ab8a17))

## [6.13.0](https://github.com/dequelabs/cauldron/compare/v6.12.0...v6.13.0) (2024-12-18)


Expand Down
10 changes: 10 additions & 0 deletions docs/pages/components/AnchoredOverlay.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ function AnchoredOverlayWithOffsetExample() {
type: 'number',
description: 'An optional offset number to position the anchor element from its anchored target.'
},
{
name: 'focusTrap',
type: 'boolean',
description: 'When set, traps focus within the AnchoredOverlay.'
},
{
name: 'focusTrapOptions',
type: 'object',
description: 'When `focusTrap` is true, optional arguments to configure the focus trap.'
},
{
name: 'as',
type: 'React.ElementType',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cauldron",
"private": true,
"version": "6.13.0",
"version": "6.14.0",
"license": "MPL-2.0",
"scripts": {
"clean": "rimraf dist docs/dist",
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@deque/cauldron-react",
"version": "6.13.0",
"version": "6.14.0",
"license": "MPL-2.0",
"description": "Fully accessible react components library for Deque Cauldron",
"homepage": "https://cauldron.dequelabs.com/",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AnchoredOverlay from './';
import axe from '../../axe';
Expand Down Expand Up @@ -123,6 +123,76 @@ test('should call onPlacementChange with initial placement', () => {
expect(onPlacementChange).toHaveBeenCalledWith('top');
});

test('should not trap focus when focusTrap is false', async () => {
const targetRef = { current: document.createElement('button') };
const user = userEvent.setup();

render(
<>
<button>Outside Before</button>
<AnchoredOverlay target={targetRef} open focusTrap={false}>
<button>Inside</button>
</AnchoredOverlay>
<button>Outside After</button>
</>
);

const buttons = screen.getAllByRole('button');

buttons[0].focus();
expect(buttons[0]).toHaveFocus();
await user.tab();
expect(buttons[1]).toHaveFocus();
await user.tab();
expect(buttons[2]).toHaveFocus();
});

test('should trap focus when focusTrap is true', async () => {
const targetRef = { current: document.createElement('button') };
const user = userEvent.setup();

render(
<>
<button>Outside Before</button>
<AnchoredOverlay target={targetRef} open focusTrap data-testid="overlay">
<button>First</button>
<button>Second</button>
<button>Third</button>
</AnchoredOverlay>
<button>Outside After</button>
</>
);

const buttons = within(screen.getByTestId('overlay')).getAllByRole('button');

expect(buttons[0]).toHaveFocus();
await user.tab();
expect(buttons[1]).toHaveFocus();
await user.tab();
expect(buttons[2]).toHaveFocus();
await user.tab();
expect(buttons[0]).toHaveFocus();
});

test('should restore focus when focusTrap is unmounted', async () => {
const targetRef = { current: document.createElement('button') };
const outsideButton = document.createElement('button');
document.body.appendChild(outsideButton);
outsideButton.focus();

const { unmount } = render(
<AnchoredOverlay target={targetRef} open focusTrap data-testid="overlay">
<button>Inside Button</button>
</AnchoredOverlay>
);

expect(screen.getByText('Inside Button')).toHaveFocus();
unmount();
expect(outsideButton).toHaveFocus();

document.body.removeChild(outsideButton);
});

test('should support ref prop', () => {
const targetRef = { current: document.createElement('button') };
const ref = React.createRef<HTMLDivElement>();
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/components/AnchoredOverlay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type PolymorphicProps } from '../../utils/polymorphicComponent';
import resolveElement from '../../utils/resolveElement';
import useSharedRef from '../../utils/useSharedRef';
import useEscapeKey from '../../utils/useEscapeKey';
import useFocusTrap from '../../utils/useFocusTrap';

type AnchoredOverlayProps<
Overlay extends HTMLElement,
Expand All @@ -27,6 +28,10 @@ type AnchoredOverlayProps<
onPlacementChange?: (placement: Placement) => void;
/** An optional offset number to position the anchor element from its anchored target. */
offset?: number;
/** When set, traps focus within the AnchoredOverlay. */
focusTrap?: boolean;
/** When `focusTrap` is true, optional arguments to configure the focus trap. */
focusTrapOptions?: Parameters<typeof useFocusTrap>[1];
children?: React.ReactNode;
} & PolymorphicProps<React.HTMLAttributes<Overlay>>;

Expand Down Expand Up @@ -56,6 +61,8 @@ const AnchoredOverlay = forwardRef(
style,
open = false,
offset,
focusTrap,
focusTrapOptions,
onOpenChange,
onPlacementChange,
...props
Expand Down Expand Up @@ -99,6 +106,8 @@ const AnchoredOverlay = forwardRef(
}
});

useFocusTrap(ref, !focusTrap ? { disabled: true } : focusTrapOptions);

useEffect(() => {
if (typeof onPlacementChange === 'function') {
onPlacementChange(placement);
Expand Down
15 changes: 11 additions & 4 deletions packages/react/src/components/Listbox/Listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,19 @@ const Listbox = forwardRef<
)
);
setSelectedOptions(matchingOptions);
setActiveOption(matchingOptions[0] || null);
if (!activeOption) {
setActiveOption(matchingOptions[0] || null);
}
} else {
const matchingOption = options.find((option) =>
optionMatchesValue(option, listboxValue)
);
setSelectedOptions(matchingOption ? [matchingOption] : []);
setActiveOption(matchingOption || null);
if (!activeOption) {
setActiveOption(matchingOption || null);
}
}
}, [isControlled, options, value, defaultValue]);
}, [isControlled, options, value, defaultValue, activeOption]);

useEffect(() => {
if (activeOption) {
Expand Down Expand Up @@ -243,7 +247,10 @@ const Listbox = forwardRef<

const handleFocus = useCallback(
(event: React.FocusEvent<HTMLElement>) => {
if (!activeOption) {
if (
!activeOption ||
!options.some((option) => option.element === activeOption.element)
) {
const firstOption = options.find(
(option) => !isDisabledOption(option)
);
Expand Down
40 changes: 38 additions & 2 deletions packages/react/src/components/Listbox/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Listbox from './';
import { ListboxGroup, ListboxOption } from './';
import axe from '../../axe';
Expand Down Expand Up @@ -202,7 +202,7 @@ test('should set the first non-disabled option as active on focus', () => {
</Listbox>
);

fireEvent.focus(screen.getByRole('option', { name: 'Banana' }));
fireEvent.focus(screen.getByRole('listbox'));
expect(screen.getByRole('option', { name: 'Banana' })).toHaveClass(
'ListboxOption--active'
);
Expand All @@ -212,6 +212,42 @@ test('should set the first non-disabled option as active on focus', () => {
);
});

test('should set the first non-disabled option as active on focus when the options have changed', () => {
const { rerender } = render(
<Listbox>
<ListboxOption disabled>Apple</ListboxOption>
<ListboxOption>Banana</ListboxOption>
<ListboxOption>Cantaloupe</ListboxOption>
</Listbox>
);

waitFor(() => {
fireEvent.focus(screen.getByRole('listbox'));
expect(screen.getByRole('listbox')).toHaveFocus();
});

rerender(
<Listbox>
<ListboxOption disabled>Dragon Fruit</ListboxOption>
<ListboxOption>Elderberry</ListboxOption>
<ListboxOption>Fig</ListboxOption>
</Listbox>
);

waitFor(() => {
fireEvent.focus(screen.getByRole('listbox'));
expect(screen.getByRole('listbox')).toHaveFocus();
});

expect(screen.getByRole('option', { name: 'Elderberry' })).toHaveClass(
'ListboxOption--active'
);
expect(screen.getByRole('listbox')).toHaveAttribute(
'aria-activedescendant',
screen.getByRole('option', { name: 'Elderberry' }).getAttribute('id')
);
});

test('should set selected value with "value" prop when listbox option only has text label', () => {
render(
<Listbox value="Banana">
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/components/MenuItem/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ const user = userEvent.setup();

test('clicks first direct child link given a click', async () => {
const onClick = sinon.spy();
// Note: Using a hash link instead of a url link because jsdom doesn't correctly
// support navigation and throws a noisy console error we don't care about
render(
<MenuItem>
<a href="/foo" onClick={onClick}>
<a href="#foo" onClick={onClick}>
Foo
</a>
</MenuItem>
Expand Down
2 changes: 1 addition & 1 deletion packages/styles/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@deque/cauldron-styles",
"version": "6.13.0",
"version": "6.14.0",
"license": "MPL-2.0",
"description": "deque cauldron pattern library styles",
"repository": "https://github.com/dequelabs/cauldron",
Expand Down
65 changes: 65 additions & 0 deletions vpats/2024-12-19-cauldron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Cauldron Accessibility Conformance Report WCAG Edition

**Name of Product**: Cauldron

**Report Date**: 2024-12-19

## Table 1: Success Criteria, Level A

| Criteria | Conformance Level | Remarks and Explanations |
| --- | --- | --- |
| [1.1.1 Non-text Content](http://www.w3.org/TR/WCAG20/#text-equiv-all) (Level A) | Supports | |
| [1.2.1 Audio-only and Video-only (Prerecorded)](http://www.w3.org/TR/WCAG20/#media-equiv-av-only-alt) (Level A) | Supports | |
| [1.2.2 Captions (Prerecorded)](http://www.w3.org/TR/WCAG20/#media-equiv-captions) (Level A) | Supports | |
| [1.2.3 Audio Description or Media Alternative (Prerecorded)](http://www.w3.org/TR/WCAG20/#media-equiv-audio-desc) (Level A) | Supports | |
| [1.3.1 Info and Relationships](http://www.w3.org/TR/WCAG20/#content-structure-separation-programmatic) (Level A) | Supports | |
| [1.3.2 Meaningful Sequence](http://www.w3.org/TR/WCAG20/#content-structure-separation-sequence) (Level A) | Supports | |
| [1.3.3 Sensory Characteristics](http://www.w3.org/TR/WCAG20/#content-structure-separation-understanding) (Level A) | Supports | |
| [1.4.1 Use of Color](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-without-color) (Level A) | Supports | |
| [1.4.2 Audio Control](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-dis-audio) (Level A) | Supports | |
| [2.1.1 Keyboard](http://www.w3.org/TR/WCAG20/#keyboard-operation-keyboard-operable) (Level A) | Supports | |
| [2.1.2 No Keyboard Trap](http://www.w3.org/TR/WCAG20/#keyboard-operation-trapping) (Level A) | Supports | |
| [2.1.4 Character Key Shortcuts](http://www.w3.org/TR/WCAG20/#keyboard-operation-keyboard-operable) (Level A) | Supports | |
| [2.2.1 Timing Adjustable](http://www.w3.org/TR/WCAG20/#time-limits-required-behaviors) (Level A) | Supports | |
| [2.2.2 Pause, Stop, Hide](http://www.w3.org/TR/WCAG20/#time-limits-pause) (Level A) | Supports | |
| [2.3.1 Three Flashes or Below Threshold](http://www.w3.org/TR/WCAG20/#seizure-does-not-violate) (Level A) | Supports | |
| [2.4.1 Bypass Blocks](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-skip) (Level A) | Supports | |
| [2.4.2 Page Titled](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-title) (Level A) | Supports | |
| [2.4.3 Focus Order](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-focus-order) (Level A) | Supports | |
| [2.4.4 Link Purpose (In Context)](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-refs) (Level A) | Supports | |
| [2.5.1 Pointer Gestures](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-mult-loc) (Level A) | Supports | |
| [2.5.2 Pointer Cancellation](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-mult-loc) (Level A) | Supports | |
| [2.5.3 Label in Name](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-descriptive) (Level A) | Supports | |
| [2.5.4 Motion Actuation](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-motion-actuation) (Level A) | Supports | |
| [3.1.1 Language of Page](http://www.w3.org/TR/WCAG20/#meaning-doc-lang-id) (Level A) | Supports | |
| [3.2.1 On Focus](http://www.w3.org/TR/WCAG20/#consistent-behavior-receive-focus) (Level A) | Supports | |
| [3.2.2 On Input](http://www.w3.org/TR/WCAG20/#consistent-behavior-unpredictable-change) (Level A) | Supports | |
| [3.3.1 Error Identification](http://www.w3.org/TR/WCAG20/#minimize-error-identified) (Level A) | Supports | |
| [3.3.2 Labels or Instructions](http://www.w3.org/TR/WCAG20/#minimize-error-cues) (Level A) | Supports | |
| [4.1.1 Parsing](http://www.w3.org/TR/WCAG20/#ensure-compat-parses) (Level A) | Supports | |
| [4.1.2 Name, Role, Value](http://www.w3.org/TR/WCAG20/#ensure-compat-rsv) (Level A) | Supports | |

## Table 2: Success Criteria, Level AA

| Criteria | Conformance Level | Remarks and Explanations |
| --- | --- | --- |
| [1.2.4 Captions (Prerecorded)](http://www.w3.org/TR/WCAG20/#media-equiv-captions) (Level AA) | Supports | |
| [1.2.5 Audio Description or Media Alternative (Prerecorded)](http://www.w3.org/TR/WCAG20/#media-equiv-audio-desc) (Level AA) | Supports | |
| [1.3.4 Orientation](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-orientation) (Level AA) | Supports | |
| [1.3.5 Identify Input Purpose](http://www.w3.org/TR/WCAG20/#input-purposes) (Level AA) | Supports | |
| [1.4.3 Contrast (Minimum)](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast) (Level AA) | Supports | |
| [1.4.4 Resize text](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-scale) (Level AA) | Supports | |
| [1.4.5 Images of Text](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-text-presentation) (Level AA) | Supports | |
| [1.4.10 Reflow](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-scale) (Level AA) | Supports | |
| [1.4.11 Non-text Contrast](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast) (Level AA) | Supports | |
| [1.4.12 Text Spacing](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-spacing) (Level AA) | Supports | |
| [1.4.13 Content on Hover or Focus](http://www.w3.org/TR/WCAG20/#visual-audio-contrast-dis-audio) (Level AA) | Supports | |
| [2.4.5 Multiple Ways](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-mult-loc) (Level AA) | Supports | |
| [2.4.6 Headings and Labels](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-descriptive) (Level AA) | Partially Supports | <ul> <li>[[#1393] [A11y] - Programmatic label does not convey purpose of control](https://github.com/dequelabs/cauldron/issues/1393) (2024-03-08)</li> </ul> |
| [2.4.7 Focus Visible](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-focus-visible) (Level AA) | Supports | |
| [3.1.2 Language of Parts](http://www.w3.org/TR/WCAG20/#meaning-doc-lang-id) (Level AA) | Supports | |
| [3.2.3 Consistent Navigation](http://www.w3.org/TR/WCAG20/#consistent-behavior-consistent-locations) (Level AA) | Supports | |
| [3.2.4 Consistent Identification](http://www.w3.org/TR/WCAG20/#consistent-behavior-consistent-functionality) (Level AA) | Supports | |
| [3.3.3 Error Suggestion](http://www.w3.org/TR/WCAG20/#minimize-error-suggestions) (Level AA) | Supports | |
| [3.3.4 Error Prevention (Legal, Financial, Data)](http://www.w3.org/TR/WCAG20/#minimize-error-reversible) (Level AA) | Supports | |
| [4.1.3 Status Messages](http://www.w3.org/TR/WCAG20/#ensure-compat-rsv) (Level AA) | Supports | |

0 comments on commit 13f0eea

Please sign in to comment.