Skip to content
This repository has been archived by the owner on Jun 11, 2021. It is now read-only.

Commit

Permalink
feat(react): introduce inputRef for focus management (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Feb 26, 2020
1 parent 553ea68 commit 4d804fe
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 4 deletions.
22 changes: 18 additions & 4 deletions packages/autocomplete-react/src/Autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsx h */

import { h } from 'preact';
import { useRef, useEffect } from 'preact/hooks';
import { useRef, useEffect, Ref } from 'preact/hooks';
import { createPortal } from 'preact/compat';

import {
Expand All @@ -25,10 +25,18 @@ interface PublicRendererProps {
* The dropdown placement related to the container.
*/
dropdownPlacement?: 'start' | 'end';
/**
* The ref to the input element.
*
* Useful for managing focus.
*/
inputRef?: Ref<HTMLInputElement | null>;
}

export interface RendererProps extends Required<PublicRendererProps> {
export interface RendererProps extends PublicRendererProps {
dropdownContainer: HTMLElement;
dropdownPlacement: 'start' | 'end';
inputRef?: Ref<HTMLInputElement | null>;
}

interface PublicProps<TItem>
Expand All @@ -47,6 +55,7 @@ export function getDefaultRendererProps<TItem>(
)
: autocompleteProps.environment.document.body,
dropdownPlacement: rendererProps.dropdownPlacement ?? 'start',
inputRef: rendererProps.inputRef,
};
}

Expand All @@ -56,15 +65,20 @@ export function Autocomplete<TItem extends {}>(
const {
dropdownContainer,
dropdownPlacement,
inputRef: providedInputRef,
...autocompleteProps
} = providedProps;
const props = getDefaultProps(autocompleteProps);
const rendererProps = getDefaultRendererProps(
{ dropdownContainer, dropdownPlacement },
{
dropdownContainer,
dropdownPlacement,
inputRef: providedInputRef,
},
props
);

const inputRef = useRef<HTMLInputElement | null>(null);
const inputRef = providedInputRef ?? useRef<HTMLInputElement | null>(null);
const searchBoxRef = useRef<HTMLFormElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);

Expand Down
189 changes: 189 additions & 0 deletions stories/display.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @jsx h */

import { h, render } from 'preact';
import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
import { createPortal } from 'preact/compat';
import { storiesOf } from '@storybook/html';
import algoliasearch from 'algoliasearch/lite';

Expand Down Expand Up @@ -90,4 +92,191 @@ storiesOf('Display', module)
searchBoxPosition: 'end',
}
)
)
.add(
'Modal',
withPlayground(
({ container, dropdownContainer }) => {
function App() {
const modalRef = useRef(null);
const inputRef = useRef(null);
const [isShowing, setIsShowing] = useState(false);

const toggleModal = useCallback(() => {
if (isShowing) {
setIsShowing(false);
return;
}

setIsShowing(true);
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 0);
}, [isShowing, setIsShowing]);

useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (
(event.key === 'Escape' && isShowing) ||
(event.key === 'k' && (event.metaKey || event.ctrlKey))
) {
event.preventDefault();
toggleModal();
}
}

window.addEventListener('keydown', onKeyDown);

return () => {
window.removeEventListener('keydown', onKeyDown);
};
}, [toggleModal, isShowing]);

return (
<div>
<button
style={{
cursor: 'pointer',
background: '#fff',
border: '1px solid #ddd',
borderRadius: 6,
fontSize: 'inherit',
padding: '6px 12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: 180,
}}
onClick={toggleModal}
>
<div>
<svg
height="16"
viewBox="0 0 16 16"
width="16"
style={{ marginRight: 12, height: 12, width: 12 }}
>
<path
d="M12.6 11.2c.037.028.073.059.107.093l3 3a1 1 0 1 1-1.414 1.414l-3-3a1.009 1.009 0 0 1-.093-.107 7 7 0 1 1 1.4-1.4zM7 12A5 5 0 1 0 7 2a5 5 0 0 0 0 10z"
fillRule="evenodd"
></path>
</svg>
Search
</div>

<kbd
style={{
border: '1px solid #ddd',
padding: '2px 4px',
borderRadius: 3,
background: '#f9f8f8',
}}
>
Cmd+K
</kbd>
</button>

{isShowing &&
createPortal(
<div
ref={modalRef}
onClick={event => {
if (event.target === modalRef.current) {
setIsShowing(false);
}
}}
style={{
display: 'flex',
paddingTop: 120,
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, .24)',
bottom: 0,
left: 0,
overflowY: 'auto',
position: 'fixed',
top: 0,
right: 0,
}}
>
<div
style={{
width: 480,
maxWidth: 'calc(100vw - 32px)',
height: 0,
}}
>
<Autocomplete
openOnFocus={true}
placeholder="Search..."
defaultHighlightedIndex={0}
inputRef={inputRef}
getSources={({ query }) => {
if (!query) {
return [
{
getInputValue({ suggestion }) {
return suggestion.query;
},
getSuggestions() {
return [
{
query: 'GitHub',
_highlightResult: {
query: { value: 'GitHub' },
},
},
{
query: 'Twitter',
_highlightResult: {
query: { value: 'Twitter' },
},
},
];
},
},
];
}

return [
{
getInputValue({ suggestion }) {
return suggestion.query;
},
getSuggestions({ query }) {
return getAlgoliaHits({
searchClient,
queries: [
{
indexName:
'instant_search_demo_query_suggestions',
query,
params: {
hitsPerPage: 4,
},
},
],
});
},
},
];
}}
/>
</div>
</div>,
dropdownContainer
)}
</div>
);
}

render(<App />, container);

return container;
},
{
searchBoxPosition: 'end',
}
)
);

0 comments on commit 4d804fe

Please sign in to comment.