Skip to content
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
1 change: 0 additions & 1 deletion src/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export { getGlobalFlag } from './global-flags';
export {
SingleTabStopNavigationAPI,
SingleTabStopNavigationProvider,
SingleTabStopNavigationReset,
useSingleTabStopNavigation,
} from './single-tab-stop';
export { isFocusable, getAllFocusables, getFirstFocusable, getLastFocusable } from './focus-lock-utils/utils';
Expand Down
235 changes: 22 additions & 213 deletions src/internal/single-tab-stop/__tests__/context.test.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,22 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useContext, useEffect, useRef } from 'react';
import { render } from '@testing-library/react';
import React, { useRef } from 'react';
import { render, act } from '@testing-library/react';

import {
SingleTabStopNavigationAPI,
SingleTabStopNavigationContext,
SingleTabStopNavigationProvider,
SingleTabStopNavigationReset,
useSingleTabStopNavigation,
} from '../';
import { SingleTabStopNavigationContext, useSingleTabStopNavigation } from '../';
import { renderWithSingleTabStopNavigation } from './utils';

// Simple STSN subscriber component
function Button(props: React.HTMLAttributes<HTMLButtonElement>) {
const buttonRef = useRef<HTMLButtonElement>(null);
const { tabIndex } = useSingleTabStopNavigation(buttonRef, { tabIndex: props.tabIndex });
return <button {...props} ref={buttonRef} tabIndex={tabIndex} />;
}

// Simple STSN provider component
function Group({
id,
navigationActive,
children,
}: {
id: string;
navigationActive: boolean;
children: React.ReactNode;
}) {
const navigationAPI = useRef<SingleTabStopNavigationAPI>(null);

useEffect(() => {
navigationAPI.current?.updateFocusTarget();
});

return (
<SingleTabStopNavigationProvider
ref={navigationAPI}
navigationActive={navigationActive}
getNextFocusTarget={() => document.querySelector(`#${id}`)!.querySelectorAll('button')[0] as HTMLElement}
>
<div id={id}>
<Button>First</Button>
<Button>Second</Button>
{children}
</div>
</SingleTabStopNavigationProvider>
);
}

function findGroupButton(groupId: string, buttonIndex: number) {
return document.querySelector(`#${groupId}`)!.querySelectorAll('button')[buttonIndex] as HTMLElement;
}
test('subscribed components can be rendered outside single tab stop navigation context', () => {
render(<Button />);
expect(document.querySelector('button')).not.toHaveAttribute('tabIndex');
});

test('does not override tab index when keyboard navigation is not active', () => {
renderWithSingleTabStopNavigation(<Button id="button" />, { navigationActive: false });
Expand All @@ -71,11 +34,14 @@ test('does not override tab index for suppressed elements', () => {
</div>,
{ navigationActive: true }
);
setCurrentTarget(document.querySelector('#button1'), [
document.querySelector('#button1'),
document.querySelector('#button2'),
document.querySelector('#button3'),
]);
act(() => {
setCurrentTarget(document.querySelector('#button1'), [
document.querySelector('#button1'),
document.querySelector('#button2'),
document.querySelector('#button3'),
]);
});

expect(document.querySelector('#button1')).toHaveAttribute('tabIndex', '0');
expect(document.querySelector('#button2')).toHaveAttribute('tabIndex', '0');
expect(document.querySelector('#button3')).toHaveAttribute('tabIndex', '-1');
Expand All @@ -90,7 +56,9 @@ test('overrides tab index when keyboard navigation is active', () => {
<Button id="button2" />
</div>
);
setCurrentTarget(document.querySelector('#button1'));
act(() => {
setCurrentTarget(document.querySelector('#button1'));
});
expect(document.querySelector('#button1')).toHaveAttribute('tabIndex', '0');
expect(document.querySelector('#button2')).toHaveAttribute('tabIndex', '-1');
});
Expand All @@ -102,7 +70,9 @@ test('does not override explicit tab index with 0', () => {
<Button id="button2" tabIndex={-2} />
</div>
);
setCurrentTarget(document.querySelector('#button1'));
act(() => {
setCurrentTarget(document.querySelector('#button1'));
});
expect(document.querySelector('#button1')).toHaveAttribute('tabIndex', '-2');
expect(document.querySelector('#button2')).toHaveAttribute('tabIndex', '-2');
});
Expand All @@ -114,9 +84,7 @@ test('propagates and suppresses navigation active state', () => {
}
function Test({ navigationActive }: { navigationActive: boolean }) {
return (
<SingleTabStopNavigationContext.Provider
value={{ navigationActive, registerFocusable: () => () => {}, resetFocusTarget: () => {} }}
>
<SingleTabStopNavigationContext.Provider value={{ navigationActive, registerFocusable: () => () => {} }}>
<Component />
</SingleTabStopNavigationContext.Provider>
);
Expand All @@ -128,162 +96,3 @@ test('propagates and suppresses navigation active state', () => {
rerender(<Test navigationActive={false} />);
expect(document.querySelector('div')).toHaveTextContent('false');
});

test('subscriber components can be used without provider', () => {
function TestComponent(props: React.HTMLAttributes<HTMLButtonElement>) {
const ref = useRef(null);
const contextResult = useContext(SingleTabStopNavigationContext);
const hookResult = useSingleTabStopNavigation(ref, { tabIndex: props.tabIndex });
useEffect(() => {
contextResult.registerFocusable(ref.current!, () => {});
contextResult.resetFocusTarget();
});
return (
<div ref={ref}>
Context: {`${contextResult.navigationActive}`}, Hook: {`${hookResult.navigationActive}:${hookResult.tabIndex}`}
</div>
);
}
const { container } = render(<TestComponent />);
expect(container.textContent).toBe('Context: false, Hook: false:undefined');
});

describe('nested contexts', () => {
test('tab indices are distributed correctly when switching contexts from inner to outer', () => {
const { rerender } = render(
<Group id="outer-most" navigationActive={false}>
<Group id="outer" navigationActive={false}>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</Group>
</Group>
);
expect(findGroupButton('outer-most', 0)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer-most', 1)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer', 0)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer', 1)).not.toHaveAttribute('tabindex');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');

rerender(
<Group id="outer-most" navigationActive={false}>
<Group id="outer" navigationActive={true}>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</Group>
</Group>
);
expect(findGroupButton('outer-most', 0)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer-most', 1)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('outer', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');

rerender(
<Group id="outer-most" navigationActive={true}>
<Group id="outer" navigationActive={true}>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</Group>
</Group>
);
expect(findGroupButton('outer-most', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('outer-most', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('outer', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('outer', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');
});

test('tab indices are distributed correctly when switching contexts from outer to inner', () => {
const { rerender } = render(
<Group id="outer-most" navigationActive={true}>
<Group id="outer" navigationActive={true}>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</Group>
</Group>
);
expect(findGroupButton('outer-most', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('outer-most', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('outer', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('outer', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');

rerender(
<Group id="outer-most" navigationActive={false}>
<Group id="outer" navigationActive={true}>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</Group>
</Group>
);
expect(findGroupButton('outer-most', 0)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer-most', 1)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('outer', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');

rerender(
<Group id="outer-most" navigationActive={false}>
<Group id="outer" navigationActive={false}>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</Group>
</Group>
);
expect(findGroupButton('outer-most', 0)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer-most', 1)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer', 0)).not.toHaveAttribute('tabindex');
expect(findGroupButton('outer', 1)).not.toHaveAttribute('tabindex');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');
});

test('ignores parent context when reset is used', () => {
const { rerender } = render(
<Group id="outer-most" navigationActive={true}>
<SingleTabStopNavigationReset>
<Group id="outer" navigationActive={true}>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</Group>
</SingleTabStopNavigationReset>
</Group>
);
expect(findGroupButton('outer-most', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('outer-most', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('outer', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('outer', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');

rerender(
<Group id="outer-most" navigationActive={true}>
<Group id="outer" navigationActive={true}>
<SingleTabStopNavigationReset>
<Group id="inner" navigationActive={true}>
{null}
</Group>
</SingleTabStopNavigationReset>
</Group>
</Group>
);
expect(findGroupButton('outer-most', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('outer-most', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('outer', 0)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('outer', 1)).toHaveAttribute('tabindex', '-1');
expect(findGroupButton('inner', 0)).toHaveAttribute('tabindex', '0');
expect(findGroupButton('inner', 1)).toHaveAttribute('tabindex', '-1');
});
});
4 changes: 1 addition & 3 deletions src/internal/single-tab-stop/__tests__/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ const FakeSingleTabStopNavigationProvider = forwardRef(
}));

return (
<SingleTabStopNavigationContext.Provider
value={{ navigationActive, registerFocusable, resetFocusTarget: () => {} }}
>
<SingleTabStopNavigationContext.Provider value={{ registerFocusable, navigationActive }}>
{children}
</SingleTabStopNavigationContext.Provider>
);
Expand Down
Loading
Loading