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

feat: restore focus after closing modal #1735

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
15 changes: 15 additions & 0 deletions cypress/integration/search/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
describe('Start', () => {
beforeEach(() => {
cy.visit(Cypress.config().baseUrl!);
// seems we could run into race condition
// if we don't wait for .DocSearch-Button-Key to render
cy.get('.DocSearch-Button-Key');
shortcuts marked this conversation as resolved.
Show resolved Hide resolved
});

it('Open modal on search button click', () => {
Expand Down Expand Up @@ -147,4 +150,16 @@ describe('Recent and Favorites', () => {
.trigger('click');
cy.contains('No recent searches').should('be.visible');
});

describe('A11y', () => {
beforeEach(() => {
cy.visit(Cypress.config().baseUrl!);
});
it('Restore focus to stored document.activeElement before modal is open', () => {
cy.get('[data-testid="btn"]').focus();
cy.get('body').type('{ctrl}k');
cy.closeModal();
cy.focused().should('have.data', 'testid', 'btn');
});
});
});
Binary file added examples/cypress/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions examples/cypress/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<link rel="shortcut icon" href="favicon.png" type="image/x-icon" />

<title>DocSearch v3 - React - Cypress</title>
</head>

<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>

<script type="module" src="src/index.js"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions examples/cypress/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@docsearch/cypress",
"description": "DocSearch v3 Cypress testing",
"version": "3.3.1",
"private": true,
"license": "MIT",
"scripts": {
"build": "parcel build index.html",
"start": "parcel index.html --port 3000 --no-cache"
},
"dependencies": {
"@docsearch/css": "3.3.1",
"@docsearch/react": "3.3.1",
"react": "^18.1.0",
"react-dom": "^18.1.0"
},
"devDependencies": {
"parcel": "2.7.0"
}
}
32 changes: 32 additions & 0 deletions examples/cypress/src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { DocSearch } from '@docsearch/react';

import './app.css';
import '@docsearch/css';

function App() {
return (
<div>
<h1>DocSearch v3 - React</h1>
<button data-testid="btn">A button</button>
<DocSearch
indexName="docsearch"
appId="R2IYF7ETH7"
apiKey="599cec31baffa4868cae4e79f180729b"
transformItems={
(items) => { // transform absolute url into relative ones to solve CORS problem in cypress
return items.map((item) => {
const { origin } = new URL(item.url);
return {
...item,
url: item.url.replace(new RegExp(`^${origin}`), ''),
};
})
}
}
/>
</div>
);
}

export default App;
4 changes: 4 additions & 0 deletions examples/cypress/src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
7 changes: 7 additions & 0 deletions examples/cypress/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import { createRoot } from 'react-dom/client';

import App from './App';

const root = createRoot(document.getElementById('root'));
root.render(<App />);
2 changes: 1 addition & 1 deletion examples/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "MIT",
"scripts": {
"build": "parcel build index.html",
"start": "parcel index.html"
"start": "parcel index.html --port 3000 --no-cache"
shortcuts marked this conversation as resolved.
Show resolved Hide resolved
},
"dependencies": {
"@docsearch/css": "3.3.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/js-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "MIT",
"scripts": {
"build": "parcel build index.html",
"start": "parcel index.html"
"start": "parcel index.html --port 3000 --no-cache"
},
"dependencies": {
"@docsearch/css": "3.3.1",
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
"build:esm": "lerna run build:esm --scope @docsearch/*",
"build:types": "lerna run build:types --scope @docsearch/*",
"build:umd": "lerna run build:umd --scope @docsearch/*",
"build": "lerna run build --scope @docsearch/*",
"build": "lerna run build --scope @docsearch/* --ignore '@docsearch/@(cypress|react-example|js-example)'",
"cy:clean": "rm -rf cypress/screenshots",
"cy:info": "cypress info",
"cy:run:chrome": "yarn run cy:run --browser chrome",
"cy:run:edge": "yarn run cy:run --browser edge",
"cy:run:firefox": "yarn run cy:run --browser firefox",
"cy:run": "start-server-and-test 'yarn website:test' http://localhost:3000 'cypress run --spec 'cypress/integration/**/*' --headless'",
"cy:run": "start-server-and-test 'yarn playground:cypress' http://localhost:3000 'cypress run --spec 'cypress/integration/**/*' --headless'",
"cy:verify": "cypress verify",
"lint:css": "stylelint **/src/**/*.css",
"lint": "eslint --ext .js,.ts,.tsx .",
"playground:cypress": "yarn workspace @docsearch/cypress start",
"playground:start": "yarn workspace @docsearch/react-example start",
"playground-js:start": "yarn workspace @docsearch/js-example start",
"release": "shipjs prepare",
Expand Down
15 changes: 14 additions & 1 deletion packages/docsearch-react/src/DocSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,37 @@ export interface DocSearchProps {
initialQuery?: string;
navigator?: AutocompleteOptions<InternalDocSearchHit>['navigator'];
translations?: DocSearchTranslations;
getMissingResultsUrl?: ({ query: string }) => string;
getMissingResultsUrl?: ({ query }: { query: string }) => string;
shortcuts marked this conversation as resolved.
Show resolved Hide resolved
}

export function DocSearch(props: DocSearchProps) {
const searchButtonRef = React.useRef<HTMLButtonElement>(null);
const activeElementRef = React.useRef<Element | null>(null);
const [isOpen, setIsOpen] = React.useState(false);
const [initialQuery, setInitialQuery] = React.useState<string | undefined>(
props?.initialQuery || undefined
);

const onOpen = React.useCallback(() => {
setIsOpen(true);
activeElementRef.current = document.activeElement || document.body;
}, [setIsOpen]);

const onClose = React.useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);

React.useEffect(() => {
if (
!isOpen &&
activeElementRef.current &&
activeElementRef.current instanceof HTMLElement
) {
activeElementRef.current.focus();
activeElementRef.current = null;
}
}, [isOpen]);

const onInput = React.useCallback(
(event: KeyboardEvent) => {
setIsOpen(true);
Expand Down