mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 19:02:15 +00:00
Modals/Router framework reimplemented because of design issues with focusability
This commit is contained in:
parent
ed6b8bb302
commit
ddd95be447
9 changed files with 101 additions and 72 deletions
|
|
@ -1,17 +1,11 @@
|
|||
import React, { StrictMode } from 'react';
|
||||
import { Router } from 'stremio-common';
|
||||
import ModalsContainerProvider from './ModalsContainerProvider';
|
||||
import RouterFocusableProvider from './RouterFocusableProvider';
|
||||
import routerConfig from './routerConfig';
|
||||
import styles from './styles';
|
||||
|
||||
const App = () => (
|
||||
<StrictMode>
|
||||
<ModalsContainerProvider modalsContainerClassName={styles['application-layer']}>
|
||||
<RouterFocusableProvider routesContainerClassName={styles['application-layer']}>
|
||||
<Router routeClassName={styles['route']} config={routerConfig} />
|
||||
</RouterFocusableProvider>
|
||||
</ModalsContainerProvider>
|
||||
<Router className={styles['router']} config={routerConfig} />
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -70,32 +70,11 @@ html, body, :global(#app) {
|
|||
min-height: var(--window-min-height);
|
||||
font-family: 'Roboto', 'sans-serif';
|
||||
line-height: 1;
|
||||
background-color: var(--color-background);
|
||||
|
||||
.application-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
|
||||
:global(.modal-container), .route {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-width: var(--window-min-width);
|
||||
min-height: var(--window-min-height);
|
||||
overflow: hidden;
|
||||
|
||||
&.route {
|
||||
background-color: var(--color-background);
|
||||
|
||||
&:not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.router {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
import React from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { FocusableContext } from 'stremio-common';
|
||||
import withModalsContainer from './withModalsContainer';
|
||||
|
||||
const Modal = ({ modalsContainer, children }) => {
|
||||
if (modalsContainer === null) {
|
||||
return null;
|
||||
}
|
||||
const modalContainerRef = useRef(null);
|
||||
const [focusable, setFocusable] = useState(false);
|
||||
useEffect(() => {
|
||||
const nextFocusable = modalsContainer.lastElementChild === modalContainerRef.current;
|
||||
if (nextFocusable !== focusable) {
|
||||
setFocusable(nextFocusable);
|
||||
}
|
||||
});
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<FocusableContext.Provider value={true}>
|
||||
<div className={'modal-container'}>
|
||||
<FocusableContext.Provider value={focusable}>
|
||||
<div ref={modalContainerRef} className={'modal-container'}>
|
||||
{children}
|
||||
</div>
|
||||
</FocusableContext.Provider>,
|
||||
|
|
|
|||
|
|
@ -6,22 +6,25 @@ class ModalsContainerProvider extends Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.modalsContainerRef = React.createRef();
|
||||
this.state = {
|
||||
modalsContainer: null
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.children !== this.props.children ||
|
||||
nextProps.modalsContainerClassName !== this.props.modalsContainerClassName;
|
||||
return nextState.modalsContainer !== this.state.modalsContainer ||
|
||||
nextProps.modalsContainerClassName !== this.props.modalsContainerClassName ||
|
||||
nextProps.children !== this.props.children;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.forceUpdate();
|
||||
modalsContainerRef = (modalsContainer) => {
|
||||
this.setState({ modalsContainer });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalsContainerContext.Provider value={this.modalsContainerRef.current}>
|
||||
{this.props.children}
|
||||
<ModalsContainerContext.Provider value={this.state.modalsContainer}>
|
||||
{this.state.modalsContainer ? this.props.children : null}
|
||||
<div ref={this.modalsContainerRef} className={this.props.modalsContainerClassName} />
|
||||
</ModalsContainerContext.Provider>
|
||||
);
|
||||
26
src/common/Router/Route/Route.js
Normal file
26
src/common/Router/Route/Route.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React, { Component } from 'react';
|
||||
import ModalsContainerProvider from './ModalsContainerProvider';
|
||||
import RouteFocusableProvider from './RouteFocusableProvider';
|
||||
import styles from './styles';
|
||||
|
||||
class Route extends Component {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.children !== this.props.children;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles['route']}>
|
||||
<ModalsContainerProvider modalsContainerClassName={styles['modals-container']}>
|
||||
<RouteFocusableProvider>
|
||||
<div className={styles['route-content']}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</RouteFocusableProvider>
|
||||
</ModalsContainerProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Route;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { Component } from 'react';
|
||||
import { FocusableContext, withModalsContainer } from 'stremio-common';
|
||||
|
||||
class RouterFocusableProvider extends Component {
|
||||
class RouteFocusableProvider extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.routesContainerRef = React.createRef();
|
||||
this.routeContentRef = React.createRef();
|
||||
this.modalsContainerDomTreeObserver = new MutationObserver(this.onModalsContainerDomTreeChange);
|
||||
this.state = {
|
||||
focusable: false
|
||||
|
|
@ -13,12 +13,10 @@ class RouterFocusableProvider extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.modalsContainer !== null) {
|
||||
this.onModalsContainerDomTreeChange();
|
||||
this.modalsContainerDomTreeObserver.observe(this.props.modalsContainer, {
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
this.onModalsContainerDomTreeChange();
|
||||
this.modalsContainerDomTreeObserver.observe(this.props.modalsContainer, {
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
@ -27,26 +25,17 @@ class RouterFocusableProvider extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.focusable !== this.state.focusable ||
|
||||
nextProps.routesContainerClassName !== this.props.routesContainerClassName ||
|
||||
nextProps.modalsContainer !== this.props.modalsContainer ||
|
||||
nextProps.children !== this.props.children;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.focusable && !this.state.focusable) {
|
||||
const focusedElement = this.routesContainerRef.current.querySelector(':focus');
|
||||
const focusedElement = this.routeContentRef.current.querySelector(':focus');
|
||||
if (focusedElement !== null) {
|
||||
focusedElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
if (prevProps.modalsContainer !== this.props.modalsContainer) {
|
||||
this.onModalsContainerDomTreeChange();
|
||||
this.modalsContainerDomTreeObserver.disconnect();
|
||||
this.modalsContainerDomTreeObserver.observe(this.props.modalsContainer, {
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onModalsContainerDomTreeChange = () => {
|
||||
|
|
@ -56,12 +45,10 @@ class RouterFocusableProvider extends Component {
|
|||
render() {
|
||||
return (
|
||||
<FocusableContext.Provider value={this.state.focusable}>
|
||||
<div ref={this.routesContainerRef} className={this.props.routesContainerClassName}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
{React.cloneElement(React.Children.only(this.props.children), { ref: this.routeContentRef })}
|
||||
</FocusableContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withModalsContainer(RouterFocusableProvider);
|
||||
export default withModalsContainer(RouteFocusableProvider);
|
||||
3
src/common/Router/Route/index.js
Normal file
3
src/common/Router/Route/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Route from './Route';
|
||||
|
||||
export default Route;
|
||||
30
src/common/Router/Route/styles.less
Normal file
30
src/common/Router/Route/styles.less
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
.route {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.route-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modals-container {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
>:global(.modal-container) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import PathUtils from 'path';
|
||||
import UrlUtils from 'url';
|
||||
import Route from './Route';
|
||||
|
||||
class Router extends Component {
|
||||
constructor(props) {
|
||||
|
|
@ -44,7 +45,8 @@ class Router extends Component {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.views !== this.state.views;
|
||||
return nextState.views !== this.state.views ||
|
||||
nextProps.className !== this.props.className;
|
||||
}
|
||||
|
||||
onLocationChanged = () => {
|
||||
|
|
@ -95,15 +97,15 @@ class Router extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={this.props.className}>
|
||||
{
|
||||
this.state.views
|
||||
.filter(({ element }) => React.isValidElement(element))
|
||||
.map(({ path, element }) => (
|
||||
<div key={path} className={this.props.routeClassName}>{element}</div>
|
||||
<Route key={path}>{element}</Route>
|
||||
))
|
||||
}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue