Skip to content

feat: restore focus after closing modal - #1735

Open
chenxsan wants to merge 18 commits into
algolia:mainfrom
chenxsan:feature/restore-focus
Open

feat: restore focus after closing modal#1735
chenxsan wants to merge 18 commits into
algolia:mainfrom
chenxsan:feature/restore-focus

Conversation

@chenxsan

@chenxsan chenxsan commented Dec 28, 2022

Copy link
Copy Markdown
Contributor

Related issue #1370

cc @patrickhlauke if you don't mind please help review this one.

Basically the code would store the document.activeElement when modal is opened, and call focus on that stored document.activeElement when modal is closed.

I've added a button to the examples so we can test if the focus can be retained.

@netlify

netlify Bot commented Dec 28, 2022

Copy link
Copy Markdown

Deploy Preview for docsearch canceled.

Name Link
🔨 Latest commit 715e120
🔍 Latest deploy log https://app.netlify.com/sites/docsearch/deploys/63aefd2b2e5af50008198490

@patrickhlauke

patrickhlauke commented Dec 28, 2022

Copy link
Copy Markdown

is there an easy way for me to check this live without having to work out how to run it locally?

from looking over the code itself, this looks correct though.

@shortcuts

Copy link
Copy Markdown
Member

Thanks a lot for the PR!

is there an easy way for me to check this live without having to work out how to run it locally?

For a concrete test you'd have to run de playground locally, it should only require installing deps and run yarn playground:start

@shortcuts shortcuts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this, would you mind adding a cypress test for this case?

Comment thread packages/docsearch-react/src/DocSearch.tsx
@chenxsan

Copy link
Copy Markdown
Contributor Author

Thanks a lot for the PR!

is there an easy way for me to check this live without having to work out how to run it locally?

For a concrete test you'd have to run de playground locally, it should only require installing deps and run yarn playground:start

I can see there's a netlify deploy preview, but it's canceled, any chance to make it work so anyone can test it online instead of running it locally?

@chenxsan

Copy link
Copy Markdown
Contributor Author

Thanks for tackling this, would you mind adding a cypress test for this case?

Sure, I'll add one.

@shortcuts

Copy link
Copy Markdown
Member

Thanks a lot for the PR!

is there an easy way for me to check this live without having to work out how to run it locally?

For a concrete test you'd have to run de playground locally, it should only require installing deps and run yarn playground:start

I can see there's a netlify deploy preview, but it's canceled, any chance to make it work so anyone can test it online instead of running it locally?

You can remove the ignore build command of the netlify.toml file, but I don't know if we have a button to re-focus on the website.

@chenxsan
chenxsan marked this pull request as draft December 29, 2022 11:36
@chenxsan

Copy link
Copy Markdown
Contributor Author

Converted into draft until I figure out the cypress test here.

@chenxsan

chenxsan commented Dec 29, 2022

Copy link
Copy Markdown
Contributor Author

@shortcuts Seems we can't test this feature against the website as docusaurus customizes its search function based on DocSearch per https://github.com/facebook/docusaurus/blob/0985fa0af338068b4906f409a3f874a319f5359c/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx#L148.

Hence I would suggest running cypress against the example site, either react-example or js-example should be ok. Testing against a customized search function based on DocSearch where we have no control does't make sense to me.

@chenxsan

Copy link
Copy Markdown
Contributor Author

is there an easy way for me to check this live without having to work out how to run it locally?

from looking over the code itself, this looks correct though.

FYI, if you're going to run locally, you would need Node 16. 18 won't work as far as I see. Here's how you would run the project locally:

  1. clone
  2. run yarn
  3. run yarn build
  4. run yarn playground:start

This reverts commit 730f271.
@shortcuts

Copy link
Copy Markdown
Member

Hence I would suggest running cypress against the example site, either react-example or js-example should be ok. Testing against a customized search function based on DocSearch where we have no control does't make sense to me.

Makes a lot of sense indeed! Changing the website:test here https://github.com/algolia/docsearch/blob/main/package.json#L24 with playground:start should solve this, no?

@shortcuts

Copy link
Copy Markdown
Member

(again thanks a lot for the time spent here!)

Comment thread examples/demo/package.json
Comment thread examples/demo/src/App.js Outdated
Comment thread package.json Outdated
Comment thread cypress/integration/search/actions.spec.ts
@chenxsan
chenxsan marked this pull request as ready for review December 30, 2022 13:08
@chenxsan

Copy link
Copy Markdown
Contributor Author

(again thanks a lot for the time spent here!)

It's my pleasure to improve a project I use every day :)

I think it's ready for review now, I've added some comments to explain why I made such change.

@shortcuts shortcuts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks a lot

Comment thread cypress/integration/search/actions.spec.ts
Comment thread examples/demo/src/App.js Outdated
Comment thread examples/js-demo/app.js Outdated
Comment thread package.json Outdated
Comment thread packages/docsearch-react/src/DocSearch.tsx Outdated

@shortcuts shortcuts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last comment!

Comment thread packages/docsearch-react/src/DocSearch.tsx Outdated
@chenxsan

Copy link
Copy Markdown
Contributor Author

@shortcuts

shortcuts commented Dec 30, 2022

Copy link
Copy Markdown
Member

Ooops, CI failed due to https://support.circleci.com/hc/en-us/articles/115014359648-Exit-Code-137-Out-of-Memory

oops D: you can update https://github.com/algolia/docsearch/blob/main/package.json#L18 to prevent building the playground, we don't need it on the CI anyway

I think adding --ignore @docsearch/(cypress|react-example|js-example) should do it

@francoischalifour francoischalifour left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @chenxsan!


export function DocSearch(props: DocSearchProps) {
const searchButtonRef = React.useRef<HTMLButtonElement>(null);
const activeElementRef = React.useRef<Element>(document.body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this working in server environments where document isn't defined?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I've never thought of rendering DocSearch in server side.

I guess it won't work.

Any suggestion?

@chenxsan chenxsan Dec 30, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this:

I've reran the tests locally, seems fine, though we need to apply type assertion for the initial object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the effect covered by the onOpen() callback?

I think we better have activeElementRef accept null as initial value, and update its value in the onOpen() and onClose() callbacks. This way, the effect can also be removed, and there's no document in the initial render path.

 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);
+
+    if (activeElementRef.current) {
+      activeElementRef.current.focus();
+      activeElementRef.current = null;
+    }
   }, [setIsOpen]);

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

   // ...
 }

@chenxsan chenxsan Dec 30, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this won't work,

   const onClose = React.useCallback(() => {
     setIsOpen(false);
+
+    if (activeElementRef.current) {
+      activeElementRef.current.focus();
+      activeElementRef.current = null;
+    }
   }, [setIsOpen]);

I believe I tried this in the beginning, and it would cause two tests to fail:

I.e., clicking the button to open dialog, then invoking shortcut to close the dialog would fail. I haven't yet figured out what could be wrong. Guess operating DOM right after calling setIsOpen(false) doesn't fit well with React. Introducing another useEffect would let React schedule the jobs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants