diff --git a/e2e/components/ProgressIndicator/ProgressIndicator-test.avt.e2e.js b/e2e/components/ProgressIndicator/ProgressIndicator-test.avt.e2e.js
new file mode 100644
index 000000000000..f81463351fbb
--- /dev/null
+++ b/e2e/components/ProgressIndicator/ProgressIndicator-test.avt.e2e.js
@@ -0,0 +1,141 @@
+/**
+ * Copyright IBM Corp. 2016, 2023
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+const { expect, test } = require('@playwright/test');
+const { visitStory } = require('../../test-utils/storybook');
+
+test.describe('ProgressIndicator', () => {
+ test('accessibility-checker @avt', async ({ page }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--default',
+ globals: {
+ theme: 'white',
+ },
+ });
+ await expect(page).toHaveNoACViolations('ProgressIndicator');
+ });
+
+ test('accessibility-checker interactive progressindicator @avt', async ({
+ page,
+ }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--interactive',
+ globals: {
+ theme: 'white',
+ },
+ });
+ await expect(page).toHaveNoACViolations('ProgressIndicator-interactive');
+ });
+
+ test('accessibility-checker skeleton progressindicator @avt', async ({
+ page,
+ }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--skeleton',
+ globals: {
+ theme: 'white',
+ },
+ });
+ await expect(page).toHaveNoACViolations('ProgressIndicator-skeleton');
+ });
+
+ test('accessibility-checker - onHover @avt', async ({ page }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--default',
+ globals: {
+ theme: 'white',
+ },
+ });
+
+ await expect(page.getByText('First step')).toBeVisible();
+
+ page.getByText('First step').hover();
+
+ await expect(page).toHaveNoACViolations('ProgressIndicator-onhover');
+ });
+
+ test('accessibility-checker - complete @avt', async ({ page }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--default',
+ globals: {
+ theme: 'white',
+ },
+ });
+
+ // Checking if the 'complete' prop is adding the correct class
+ expect(page.locator('.cds--progress-step--complete')).toBeTruthy();
+ });
+
+ test('accessibility-checker - current @avt', async ({ page }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--default',
+ globals: {
+ theme: 'white',
+ },
+ });
+
+ // Checking if the 'current' prop is adding the correct class
+ expect(page.locator('.cds--progress-step--current')).toBeTruthy();
+ });
+
+ test('accessibility-checker - interactive onHover @avt', async ({ page }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--interactive',
+ globals: {
+ theme: 'white',
+ },
+ });
+
+ await expect(page.getByText('Click me')).toBeVisible();
+
+ page.getByText('Click me').hover();
+
+ await expect(page).toHaveNoACViolations(
+ 'ProgressIndicator-interactive-onhover'
+ );
+ });
+
+ test('progress indicator - keyboard nav', async ({ page }) => {
+ await visitStory(page, {
+ component: 'ProgressIndicator',
+ id: 'components-progressindicator--interactive',
+ globals: {
+ theme: 'white',
+ },
+ });
+ // Testing the first element interaction
+ await page.keyboard.press('Tab');
+ await expect(page.getByRole('button', { name: 'Click me' })).toBeVisible();
+ await page.keyboard.press('Tab');
+ await expect(page.getByRole('button', { name: 'Click me' })).toBeFocused();
+
+ await page.keyboard.press('Enter');
+ await page.keyboard.press('Escape');
+
+ // Testing the third element interaction
+ await page.keyboard.press('Tab');
+ await expect(
+ page.getByRole('button', { name: 'Third step' })
+ ).toBeFocused();
+
+ await page.keyboard.press('Enter');
+ await page.keyboard.press('Escape');
+
+ await expect(
+ page.getByRole('button', { name: 'Third step' })
+ ).toBeFocused();
+ });
+});
diff --git a/e2e/components/ProgressIndicator/ProgressIndicator-test.e2e.js b/e2e/components/ProgressIndicator/ProgressIndicator-test.e2e.js
index 6a9ba21bf9b8..17a6ab6c400e 100644
--- a/e2e/components/ProgressIndicator/ProgressIndicator-test.e2e.js
+++ b/e2e/components/ProgressIndicator/ProgressIndicator-test.e2e.js
@@ -7,9 +7,9 @@
'use strict';
-const { expect, test } = require('@playwright/test');
+const { test } = require('@playwright/test');
const { themes } = require('../../test-utils/env');
-const { snapshotStory, visitStory } = require('../../test-utils/storybook');
+const { snapshotStory } = require('../../test-utils/storybook');
test.describe('ProgressIndicator', () => {
themes.forEach((theme) => {
@@ -31,15 +31,4 @@ test.describe('ProgressIndicator', () => {
});
});
});
-
- test('accessibility-checker @avt', async ({ page }) => {
- await visitStory(page, {
- component: 'ProgressIndicator',
- id: 'components-progressindicator--default',
- globals: {
- theme: 'white',
- },
- });
- await expect(page).toHaveNoACViolations('ProgressIndicator');
- });
});
diff --git a/packages/react/examples/custom-css-prefix/yarn.lock b/packages/react/examples/custom-css-prefix/yarn.lock
index 9a6c87881de1..1a96340d26c7 100644
--- a/packages/react/examples/custom-css-prefix/yarn.lock
+++ b/packages/react/examples/custom-css-prefix/yarn.lock
@@ -4175,4 +4175,4 @@ yargs@^12.0.2:
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1 || ^4.0.0"
- yargs-parser "^11.1.1"
+ yargs-parser "^11.1.1"
\ No newline at end of file
diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap
index 29ef8b3b353c..6c2b45d94c35 100644
--- a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap
+++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap
@@ -484,6 +484,7 @@ exports[`DataTable behaves as expected selection should render and match snapsho
>
@@ -890,6 +890,7 @@ exports[`DataTable renders as expected - Component API should render and match s
>
diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap
index 53d9d2aaace3..6eb7f7836dd1 100644
--- a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap
+++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarSearch-test.js.snap
@@ -9,6 +9,7 @@ exports[`TableToolbarSearch renders as expected - Component API should render 1`
>
diff --git a/packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js b/packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js
index 2128a0e60019..628f82bfa7e8 100644
--- a/packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js
+++ b/packages/react/src/components/ExpandableSearch/ExpandableSearch-test.js
@@ -58,6 +58,30 @@ describe('ExpandableSearch', () => {
expect(container.firstChild).toHaveClass(`${prefix}--search--expanded`);
});
+ it('expands on enter', async () => {
+ const { container } = render(
+
+ );
+
+ await screen.getAllByRole('button')[0].focus();
+
+ await userEvent.keyboard('[Enter]');
+
+ expect(container.firstChild).toHaveClass(`${prefix}--search--expanded`);
+ });
+
+ it('expands on space', async () => {
+ const { container } = render(
+
+ );
+
+ await screen.getAllByRole('button')[0].focus();
+
+ await userEvent.keyboard('[Space]');
+
+ expect(container.firstChild).toHaveClass(`${prefix}--search--expanded`);
+ });
+
it('places focus on the input after expansion', async () => {
render();
@@ -107,5 +131,31 @@ describe('ExpandableSearch', () => {
expect(container.firstChild).toHaveClass(`${prefix}--search--expanded`);
});
+
+ it('closes and clears value on escape', async () => {
+ const { container } = render(
+
+ );
+
+ await userEvent.click(screen.getAllByRole('button')[0]);
+
+ expect(container.firstChild).toHaveClass(`${prefix}--search--expanded`);
+
+ await userEvent.type(screen.getByRole('searchbox'), 'test-value');
+
+ expect(screen.getByRole('searchbox')).toHaveValue('test-value');
+
+ await userEvent.keyboard('[Escape]');
+
+ expect(screen.getByRole('searchbox')).not.toHaveValue('test-value');
+
+ expect(container.firstChild).toHaveClass(`${prefix}--search--expanded`);
+
+ await userEvent.keyboard('[Escape]');
+
+ expect(container.firstChild).not.toHaveClass(
+ `${prefix}--search--expanded`
+ );
+ });
});
});
diff --git a/packages/react/src/components/ExpandableSearch/ExpandableSearch.tsx b/packages/react/src/components/ExpandableSearch/ExpandableSearch.tsx
index d97f7d46b177..412451f747b6 100644
--- a/packages/react/src/components/ExpandableSearch/ExpandableSearch.tsx
+++ b/packages/react/src/components/ExpandableSearch/ExpandableSearch.tsx
@@ -10,12 +10,13 @@ import classnames from 'classnames';
import Search, { type SearchProps } from '../Search';
import { usePrefix } from '../../internal/usePrefix';
import { composeEventHandlers } from '../../tools/events';
+import { match, keys } from '../../internal/keyboard';
function ExpandableSearch({
onBlur,
onChange,
onExpand,
- onFocus,
+ onKeyDown,
defaultValue,
isExpanded,
...props
@@ -25,12 +26,6 @@ function ExpandableSearch({
const searchRef = useRef(null);
const prefix = usePrefix();
- function handleFocus() {
- if (!expanded) {
- setExpanded(true);
- }
- }
-
function handleBlur(evt) {
const relatedTargetIsAllowed =
evt.relatedTarget &&
@@ -46,9 +41,21 @@ function ExpandableSearch({
}
function handleExpand() {
+ setExpanded(true);
searchRef.current?.focus?.();
}
+ function handleKeyDown(evt) {
+ if (expanded && match(evt, keys.Escape)) {
+ evt.stopPropagation();
+
+ // escape key only clears if the input is empty, otherwise it clears the input
+ if (!evt.target?.value) {
+ setExpanded(false);
+ }
+ }
+ }
+
const classes = classnames(
`${prefix}--search--expandable`,
{
@@ -64,10 +71,10 @@ function ExpandableSearch({
isExpanded={expanded}
ref={searchRef}
className={classes}
- onFocus={composeEventHandlers([onFocus, handleFocus])}
onBlur={composeEventHandlers([onBlur, handleBlur])}
onChange={composeEventHandlers([onChange, handleChange])}
onExpand={composeEventHandlers([onExpand, handleExpand])}
+ onKeyDown={composeEventHandlers([onKeyDown, handleKeyDown])}
/>
);
}
diff --git a/packages/react/src/components/Search/Search-test.js b/packages/react/src/components/Search/Search-test.js
index 3dd7fb323b17..0a4b16ccf745 100644
--- a/packages/react/src/components/Search/Search-test.js
+++ b/packages/react/src/components/Search/Search-test.js
@@ -103,6 +103,18 @@ describe('Search', () => {
await userEvent.click(screen.getAllByRole('button')[0]);
expect(onExpand).toHaveBeenCalled();
+
+ await screen.getAllByRole('button')[0].focus();
+
+ await userEvent.keyboard('[Space]');
+
+ expect(onExpand).toHaveBeenCalledTimes(2);
+
+ await screen.getAllByRole('button')[0].focus();
+
+ await userEvent.keyboard('[Enter]');
+
+ expect(onExpand).toHaveBeenCalledTimes(3);
});
it('should call onKeyDown when expected', async () => {
@@ -114,6 +126,42 @@ describe('Search', () => {
expect(onKeyDown).toHaveBeenCalled();
});
+ it('should call focus expand button on Escape when expanded', async () => {
+ render(
+ {}} isExpanded={true} />
+ );
+
+ await screen.getByRole('searchbox').focus();
+
+ await userEvent.keyboard('[Escape]');
+
+ expect(screen.getAllByRole('button')[0]).toHaveFocus();
+ });
+
+ it('should have tabbable button and untabbable input if expandable and not expanded', async () => {
+ render(
+ {}}
+ isExpanded={false}
+ />
+ );
+
+ expect(screen.getAllByRole('button')[0]).toHaveAttribute('tabIndex', '1');
+ expect(screen.getByRole('searchbox')).toHaveAttribute('tabIndex', '-1');
+ });
+
+ it('should have tabbable input and untabbable button if not expandable', async () => {
+ render();
+
+ // will only have 1 button which is the close button
+ expect(screen.getAllByRole('button').length).toBe(1);
+ expect(screen.getByRole('searchbox')).not.toHaveAttribute(
+ 'tabIndex',
+ '-1'
+ );
+ });
+
it('should respect placeholder prop', () => {
render();
diff --git a/packages/react/src/components/Search/Search.tsx b/packages/react/src/components/Search/Search.tsx
index ad0be45a7d38..ad9e10bbaa14 100644
--- a/packages/react/src/components/Search/Search.tsx
+++ b/packages/react/src/components/Search/Search.tsx
@@ -17,6 +17,7 @@ import React, {
type KeyboardEvent,
type ComponentType,
type FunctionComponent,
+ type MouseEvent,
} from 'react';
import { focus } from '../../internal/focus';
import { keys, match } from '../../internal/keyboard';
@@ -83,7 +84,9 @@ export interface SearchProps extends InputPropsBase {
/**
* Optional callback called when the magnifier icon is clicked in ExpandableSearch.
*/
- onExpand?(): void;
+ onExpand?(
+ e: MouseEvent | KeyboardEvent
+ ): void;
/**
* Provide an optional placeholder text for the Search.
@@ -150,6 +153,7 @@ const Search = React.forwardRef(function Search(
const { isFluid } = useContext(FormContext);
const inputRef = useRef(null);
const ref = useMergedRefs([forwardRef, inputRef]);
+ const expandButtonRef = useRef(null);
const inputId = useId('search-input');
const uniqueId = id || inputId;
const searchId = `${uniqueId}-search`;
@@ -199,7 +203,22 @@ const Search = React.forwardRef(function Search(
function handleKeyDown(event: KeyboardEvent) {
if (match(event, keys.Escape)) {
event.stopPropagation();
- clearInput();
+ if (inputRef.current?.value) {
+ clearInput();
+ }
+ // ExpandableSearch closes on escape when isExpanded, focus search activation button
+ else if (onExpand && isExpanded) {
+ expandButtonRef.current?.focus();
+ }
+ }
+ }
+
+ function handleExpandButtonKeyDown(event: KeyboardEvent) {
+ if (match(event, keys.Enter) || match(event, keys.Space)) {
+ event.stopPropagation();
+ if (onExpand) {
+ onExpand(event);
+ }
}
}
@@ -212,7 +231,12 @@ const Search = React.forwardRef(function Search(
aria-labelledby={onExpand ? uniqueId : undefined}
role={onExpand ? 'button' : undefined}
className={`${prefix}--search-magnifier`}
- onClick={onExpand}>
+ onClick={onExpand}
+ onKeyDown={handleExpandButtonKeyDown}
+ tabIndex={onExpand && !isExpanded ? 1 : -1}
+ ref={expandButtonRef}
+ aria-expanded={onExpand && isExpanded ? true : undefined}
+ aria-controls={onExpand ? uniqueId : undefined}>