router focus implemented based on focus lock

This commit is contained in:
NikolaBorislavovHristov 2019-10-05 17:39:22 +03:00
parent 671873aeb3
commit 082470ed16
17 changed files with 68 additions and 161 deletions

View file

@ -20,10 +20,11 @@
"prop-types": "15.7.2",
"react": "16.9.0",
"react-dom": "16.9.0",
"react-focus-lock": "2.1.1",
"spatial-navigation-polyfill": "git+ssh://git@github.com/NikolaBorislavovHristov/spatial-navigation.git#964d09bf2b0853e27af6c25924b595d6621a019d",
"stremio-colors": "git+ssh://git@github.com/Stremio/stremio-colors.git#v2.0.4",
"stremio-icons": "git+ssh://git@github.com/Stremio/stremio-icons.git#v1.0.11",
"stremio-core-web": "git+ssh://git@github.com/stremio/stremio-core-web.git#v0.6.0",
"stremio-icons": "git+ssh://git@github.com/Stremio/stremio-icons.git#v1.0.11",
"vtt.js": "0.13.0"
},
"devDependencies": {
@ -54,4 +55,4 @@
"webpack-cli": "3.3.9",
"webpack-dev-server": "3.8.1"
}
}
}

View file

@ -1,7 +0,0 @@
const React = require('react');
const FocusableContext = React.createContext(false);
FocusableContext.displayName = 'FocusableContext';
module.exports = FocusableContext;

View file

@ -1,57 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const { useModalsContainer } = require('../ModalsContainerContext');
const { useRoutesContainer } = require('../RoutesContainerContext');
const FocusableContext = require('./FocusableContext');
const FocusableProvider = ({ children, onRoutesContainerChildrenChange, onModalsContainerChildrenChange }) => {
const routesContainer = useRoutesContainer();
const modalsContainer = useModalsContainer();
const contentContainerRef = React.useRef(null);
const [focusable, setFocusable] = React.useState(false);
React.useEffect(() => {
const onContainerChildrenChange = () => {
setFocusable(
onRoutesContainerChildrenChange({
routesContainer: routesContainer,
contentContainer: contentContainerRef.current
})
&&
onModalsContainerChildrenChange({
modalsContainer: modalsContainer,
contentContainer: contentContainerRef.current
})
);
};
const routesContainerChildrenObserver = new MutationObserver(onContainerChildrenChange);
const modalsContainerChildrenObserver = new MutationObserver(onContainerChildrenChange);
routesContainerChildrenObserver.observe(routesContainer, { childList: true });
modalsContainerChildrenObserver.observe(modalsContainer, { childList: true });
onContainerChildrenChange();
return () => {
routesContainerChildrenObserver.disconnect();
modalsContainerChildrenObserver.disconnect();
};
}, [routesContainer, modalsContainer, onRoutesContainerChildrenChange, onModalsContainerChildrenChange]);
React.useEffect(() => {
if (focusable && !contentContainerRef.current.contains(document.activeElement)) {
contentContainerRef.current.focus();
}
}, [focusable]);
return (
<FocusableContext.Provider value={focusable}>
{React.cloneElement(React.Children.only(children), {
ref: contentContainerRef,
tabIndex: -1
})}
</FocusableContext.Provider>
);
};
FocusableProvider.propTypes = {
children: PropTypes.node.isRequired,
onRoutesContainerChildrenChange: PropTypes.func.isRequired,
onModalsContainerChildrenChange: PropTypes.func.isRequired
};
module.exports = FocusableProvider;

View file

@ -1,7 +0,0 @@
const FocusableProvider = require('./FocusableProvider');
const useFocusable = require('./useFocusable');
module.exports = {
FocusableProvider,
useFocusable
};

View file

@ -1,8 +0,0 @@
const React = require('react');
const FocusableContext = require('./FocusableContext');
const useFocusable = () => {
return React.useContext(FocusableContext);
};
module.exports = useFocusable;

View file

@ -0,0 +1,7 @@
const React = require('react');
const FocusedRouteContext = React.createContext(false);
FocusedRouteContext.displayName = 'FocusedRouteContext';
module.exports = FocusedRouteContext;

View file

@ -0,0 +1,7 @@
const FocusedRouteContext = require('./FocusedRouteContext');
const useFocusedRoute = require('./useFocusedRoute');
module.exports = {
FocusedRouteProvider: FocusedRouteContext.Provider,
useFocusedRoute
};

View file

@ -0,0 +1,8 @@
const React = require('react');
const FocusedRouteContext = require('./FocusedRouteContext');
const useFocusedRoute = () => {
return React.useContext(FocusedRouteContext);
};
module.exports = useFocusedRoute;

View file

@ -1,21 +1,15 @@
const React = require('react');
const ReactDOM = require('react-dom');
const classnames = require('classnames');
const { FocusableProvider } = require('../FocusableContext');
const FocusLock = require('react-focus-lock').default;
const { useModalsContainer } = require('../ModalsContainerContext');
const Modal = (props) => {
const Modal = ({ className, children, ...props }) => {
const modalsContainer = useModalsContainer();
const onRoutesContainerChildrenChange = React.useCallback(({ routesContainer, contentContainer }) => {
return routesContainer.lastElementChild.contains(contentContainer);
}, []);
const onModalsContainerChildrenChange = React.useCallback(({ modalsContainer, contentContainer }) => {
return modalsContainer.lastElementChild === contentContainer;
}, []);
return ReactDOM.createPortal(
<FocusableProvider onRoutesContainerChildrenChange={onRoutesContainerChildrenChange} onModalsContainerChildrenChange={onModalsContainerChildrenChange}>
<div {...props} className={classnames(props.className, 'modal-container')} />
</FocusableProvider>,
<FocusLock className={classnames(className, 'modal-container')} autoFocus={false} lockProps={props}>
{children}
</FocusLock>,
modalsContainer
);
};

View file

@ -1,21 +1,15 @@
const React = require('react');
const PropTypes = require('prop-types');
const { FocusableProvider } = require('../FocusableContext');
const FocusLock = require('react-focus-lock').default;
const { ModalsContainerProvider } = require('../ModalsContainerContext');
const Route = ({ children }) => {
const onRoutesContainerChildrenChange = React.useCallback(({ routesContainer, contentContainer }) => {
return routesContainer.lastElementChild.contains(contentContainer);
}, []);
const onModalsContainerChildrenChange = React.useCallback(({ modalsContainer }) => {
return modalsContainer.childElementCount === 0;
}, []);
return (
<div className={'route-container'}>
<ModalsContainerProvider>
<FocusableProvider onRoutesContainerChildrenChange={onRoutesContainerChildrenChange} onModalsContainerChildrenChange={onModalsContainerChildrenChange}>
<div className={'route-content'}>{children}</div>
</FocusableProvider>
<FocusLock className={'route-content'} autoFocus={false}>
{children}
</FocusLock>
</ModalsContainerProvider>
</div>
);

View file

@ -1,9 +1,10 @@
const React = require('react');
const ReactIs = require('react-is');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const UrlUtils = require('url');
const { FocusedRouteProvider } = require('../FocusedRouteContext');
const Route = require('../Route');
const { RoutesContainerProvider } = require('../RoutesContainerContext');
const Router = ({ className, onPathNotMatch, ...props }) => {
const [{ homePath, viewsConfig }] = React.useState(() => ({
@ -96,17 +97,19 @@ const Router = ({ className, onPathNotMatch, ...props }) => {
};
}, [onPathNotMatch]);
return (
<RoutesContainerProvider className={className}>
<div className={classnames(className, 'routes-container')}>
{
views
.filter(view => view !== null)
.map(({ key, component, urlParams, queryParams }) => (
<Route key={key}>
{React.createElement(component, { urlParams, queryParams })}
</Route>
.map(({ key, component, urlParams, queryParams }, index, views) => (
<FocusedRouteProvider key={key} value={index === views.length - 1}>
<Route>
{React.createElement(component, { urlParams, queryParams })}
</Route>
</FocusedRouteProvider>
))
}
</RoutesContainerProvider>
</div>
);
};

View file

@ -1,7 +0,0 @@
const React = require('react');
const RoutesContainerContext = React.createContext(null);
RoutesContainerContext.displayName = 'RoutesContainerContext';
module.exports = RoutesContainerContext;

View file

@ -1,25 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const RoutesContainerContext = require('./RoutesContainerContext');
const RoutesContainerProvider = ({ className, children }) => {
const [container, setContainer] = React.useState(null);
return (
<RoutesContainerContext.Provider value={container}>
<div ref={setContainer} className={classnames(className, 'routes-container')}>
{container instanceof HTMLElement ? children : null}
</div>
</RoutesContainerContext.Provider>
);
};
RoutesContainerProvider.propTypes = {
className: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
};
module.exports = RoutesContainerProvider;

View file

@ -1,7 +0,0 @@
const RoutesContainerProvider = require('./RoutesContainerProvider');
const useRoutesContainer = require('./useRoutesContainer');
module.exports = {
RoutesContainerProvider,
useRoutesContainer
};

View file

@ -1,8 +0,0 @@
const React = require('react');
const RoutesContainerContext = require('./RoutesContainerContext');
const useRoutesContainer = () => {
return React.useContext(RoutesContainerContext);
};
module.exports = useRoutesContainer;

View file

@ -1,9 +1,9 @@
const { useFocusable } = require('./FocusableContext');
const { useFocusedRoute } = require('./FocusedRouteContext');
const Modal = require('./Modal');
const Router = require('./Router');
module.exports = {
useFocusable,
useFocusedRoute,
Modal,
Router
};

View file

@ -4404,7 +4404,7 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
focus-lock@^0.6.3:
focus-lock@^0.6.3, focus-lock@^0.6.5:
version "0.6.5"
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.5.tgz#f6eb37832a9b1b205406175f5277396a28c0fce1"
integrity sha512-i/mVBOoa9o+tl+u9owOJUF8k8L85odZNIsctB+JAK2HFT8jckiBwmk+3uydlm6FN8czgnkIwQtBv6yyAbrzXjw==
@ -7662,7 +7662,7 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-clientside-effect@^1.2.0:
react-clientside-effect@^1.2.0, react-clientside-effect@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
integrity sha512-nRmoyxeok5PBO6ytPvSjKp9xwXg9xagoTK1mMjwnQxqM9Hd7MNPl+LS1bOSOe+CV2+4fnEquc7H/S8QD3q697A==
@ -7761,6 +7761,17 @@ react-fast-compare@2.0.4:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-focus-lock@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.1.1.tgz#49762377119ecd52eb56519ddd10a87c0c1ddd98"
integrity sha512-IKfloS8Ifx5v+Gwm64hoTqT0NmzXksTKYROOAq+HlBIxUqUS2yA5NNzQJtuOsx3nyPs7wrgycbVsffRfcA5OTw==
dependencies:
"@babel/runtime" "^7.0.0"
focus-lock "^0.6.5"
prop-types "^15.6.2"
react-clientside-effect "^1.2.2"
use-sidecar "^1.0.1"
react-focus-lock@^1.18.3:
version "1.19.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-1.19.1.tgz#2f3429793edaefe2d077121f973ce5a3c7a0651a"
@ -9132,7 +9143,7 @@ ts-pnp@^1.1.2:
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.4.tgz#ae27126960ebaefb874c6d7fa4729729ab200d90"
integrity sha512-1J/vefLC+BWSo+qe8OnJQfWTYRS6ingxjwqmHMqaMxXMj7kFtKLgAaYW3JeX3mktjgUL+etlU8/B4VUAUI9QGw==
tslib@^1.9.0:
tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
@ -9330,6 +9341,14 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
use-sidecar@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.1.tgz#75c7a5fdacc14bd3ab64992c638e45a396ad2fad"
integrity sha512-CLTDS2AZmUcXXFnxP/h/OadtvBOoHHnLYMMpKGntb5vKOQT94icrXMXX0mEdGiMhQU8vxHlndB72sRwRBHXTzw==
dependencies:
detect-node "^2.0.4"
tslib "^1.9.3"
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"