Modals/Router framework reimplemented because of design issues with focusability

This commit is contained in:
NikolaBorislavovHristov 2019-01-28 12:21:09 +02:00
parent ed6b8bb302
commit ddd95be447
9 changed files with 101 additions and 72 deletions

View file

@ -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>
);

View file

@ -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 {

View file

@ -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>,

View file

@ -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>
);

View 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;

View file

@ -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);

View file

@ -0,0 +1,3 @@
import Route from './Route';
export default Route;

View 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;
}
}

View file

@ -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>
);
}
}