mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +00:00
router focus implemented based on focus lock
This commit is contained in:
parent
671873aeb3
commit
082470ed16
17 changed files with 68 additions and 161 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
const React = require('react');
|
||||
|
||||
const FocusableContext = React.createContext(false);
|
||||
|
||||
FocusableContext.displayName = 'FocusableContext';
|
||||
|
||||
module.exports = FocusableContext;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const FocusableProvider = require('./FocusableProvider');
|
||||
const useFocusable = require('./useFocusable');
|
||||
|
||||
module.exports = {
|
||||
FocusableProvider,
|
||||
useFocusable
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
const React = require('react');
|
||||
const FocusableContext = require('./FocusableContext');
|
||||
|
||||
const useFocusable = () => {
|
||||
return React.useContext(FocusableContext);
|
||||
};
|
||||
|
||||
module.exports = useFocusable;
|
||||
7
src/router/FocusedRouteContext/FocusedRouteContext.js
Normal file
7
src/router/FocusedRouteContext/FocusedRouteContext.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const React = require('react');
|
||||
|
||||
const FocusedRouteContext = React.createContext(false);
|
||||
|
||||
FocusedRouteContext.displayName = 'FocusedRouteContext';
|
||||
|
||||
module.exports = FocusedRouteContext;
|
||||
7
src/router/FocusedRouteContext/index.js
Normal file
7
src/router/FocusedRouteContext/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const FocusedRouteContext = require('./FocusedRouteContext');
|
||||
const useFocusedRoute = require('./useFocusedRoute');
|
||||
|
||||
module.exports = {
|
||||
FocusedRouteProvider: FocusedRouteContext.Provider,
|
||||
useFocusedRoute
|
||||
};
|
||||
8
src/router/FocusedRouteContext/useFocusedRoute.js
Normal file
8
src/router/FocusedRouteContext/useFocusedRoute.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const React = require('react');
|
||||
const FocusedRouteContext = require('./FocusedRouteContext');
|
||||
|
||||
const useFocusedRoute = () => {
|
||||
return React.useContext(FocusedRouteContext);
|
||||
};
|
||||
|
||||
module.exports = useFocusedRoute;
|
||||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
const React = require('react');
|
||||
|
||||
const RoutesContainerContext = React.createContext(null);
|
||||
|
||||
RoutesContainerContext.displayName = 'RoutesContainerContext';
|
||||
|
||||
module.exports = RoutesContainerContext;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const RoutesContainerProvider = require('./RoutesContainerProvider');
|
||||
const useRoutesContainer = require('./useRoutesContainer');
|
||||
|
||||
module.exports = {
|
||||
RoutesContainerProvider,
|
||||
useRoutesContainer
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
const React = require('react');
|
||||
const RoutesContainerContext = require('./RoutesContainerContext');
|
||||
|
||||
const useRoutesContainer = () => {
|
||||
return React.useContext(RoutesContainerContext);
|
||||
};
|
||||
|
||||
module.exports = useRoutesContainer;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
25
yarn.lock
25
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue