Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(cauldron): Release 6.14.0 #1778

Merged
merged 7 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | |
Loading