focusability integrated with modals and routes

This commit is contained in:
NikolaBorislavovHristov 2019-01-24 17:47:40 +02:00
parent c5af1c5d8c
commit 486db927db
9 changed files with 159 additions and 90 deletions

View file

@ -1,5 +1,5 @@
import React, { PureComponent, StrictMode } from 'react';
import { Router } from 'stremio-common';
import { Router, Modal } from 'stremio-common';
import routerConfig from './routerConfig';
import styles from './styles';
@ -7,13 +7,19 @@ class App extends PureComponent {
render() {
return (
<StrictMode>
<Router
routeContainerClassName={styles['route-container']}
config={routerConfig}
/>
<Router config={routerConfig} />
</StrictMode>
);
}
}
const appContainerElement = document.getElementById('app');
const modalsContainerElement = document.getElementById('modals');
App.containerElement = appContainerElement;
Router.routesContainer = appContainerElement;
Router.routeClassName = styles['route-container'];
Modal.modalsContainer = modalsContainerElement;
Modal.modalClassName = styles['modal-container'];
export default App;

View file

@ -46,6 +46,8 @@
--scroll-bar-width: 8px;
--landscape-shape-ratio: 0.5625;
--poster-shape-ratio: 1.464;
--window-min-width: 1000px;
--window-min-height: 650px;
}
* {
@ -64,8 +66,8 @@ html, body, :global(#app) {
z-index: 0;
width: 100vw;
height: 100vh;
min-width: 1000px;
min-height: 650px;
min-width: var(--window-min-width);
min-height: var(--window-min-height);
font-family: 'Roboto', 'sans-serif';
line-height: 1;
@ -84,18 +86,34 @@ html, body, :global(#app) {
:global(#app) {
overflow: hidden;
.route-container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
overflow: hidden;
background-color: var(--color-background);
}
}
:global(.modal-container), .route-container {
:global(#modals) {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
overflow: hidden;
}
z-index: 1;
.route-container {
background-color: var(--color-background);
.modal-container {
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;
}
}

View file

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import FocusableContext from './FocusableContext';
class FocusableProvider extends Component {
@ -6,21 +7,24 @@ class FocusableProvider extends Component {
super(props);
this.childElementRef = React.createRef();
this.containerClildListObserver = new MutationObserver(this.containerClildListOnChange);
this.observers = props.elements.map((element) => ({
element,
observer: new MutationObserver(this.onDomTreeChange)
}));
this.state = {
focusable: false
};
}
componentDidMount() {
this.containerClildListOnChange();
this.containerClildListObserver.observe(this.childElementRef.current.parentElement, {
this.onDomTreeChange();
this.observers.forEach(({ element, observer }) => observer.observe(element, {
childList: true
});
}));
}
componentWillUnmount() {
this.containerClildListObserver.disconnect();
this.observers.forEach(({ observer }) => observer.disconnect());
}
shouldComponentUpdate(nextProps, nextState) {
@ -28,7 +32,7 @@ class FocusableProvider extends Component {
nextState.focusable !== this.state.focusable;
}
componentDidUpdate(prevProps, prevState) {
componentDidUpdate() {
if (!this.state.focusable) {
const focusedElement = this.childElementRef.current.querySelector(':focus');
if (focusedElement) {
@ -37,8 +41,15 @@ class FocusableProvider extends Component {
}
}
containerClildListOnChange = () => {
this.setState({ focusable: this.childElementRef.current.nextElementSibling === null });
onDomTreeChange = () => {
this.props.onDomTreeChange({
child: this.childElementRef.current,
onFocusableChange: this.onFocusableChange
});
}
onFocusableChange = (focusable) => {
this.setState({ focusable });
}
render() {
@ -50,4 +61,9 @@ class FocusableProvider extends Component {
}
}
FocusableProvider.propTypes = {
onDomTreeChange: PropTypes.func.isRequired,
elements: PropTypes.arrayOf(PropTypes.instanceOf(HTMLElement).isRequired).isRequired
};
export default FocusableProvider;

View file

@ -1,13 +1,26 @@
import React from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
import { FocusableProvider } from 'stremio-common';
const Modal = (props) => ReactDOM.createPortal(
<FocusableProvider>
<div {...props} className={classnames(Modal.className, props.className)} />
</FocusableProvider>,
Modal.container
);
const Modal = ({ children }) => {
const [modalContainer] = useState(document.createElement('div'));
const onDomTreeChange = useCallback(({ onFocusableChange }) => {
onFocusableChange(modalContainer.nextElementSibling === null);
});
useEffect(() => {
modalContainer.className = Modal.modalClassName;
Modal.modalsContainer.appendChild(modalContainer);
return () => {
Modal.modalsContainer.removeChild(modalContainer);
};
});
return ReactDOM.createPortal(
<FocusableProvider elements={[Modal.modalsContainer]} onDomTreeChange={onDomTreeChange}>
{React.Children.only(children)}
</FocusableProvider>,
modalContainer
);
};
export default Modal;

View file

@ -212,12 +212,22 @@ class Popup extends Component {
this.setState({ open: false });
}
labelOnClick = (event) => {
event.stopPropagation();
this.open();
}
menuContainerOnClick = (event) => {
event.stopPropagation();
}
modalBackgroundOnClick = (event) => {
event.stopPropagation();
this.close();
}
renderLabel(children) {
return React.cloneElement(children, { ref: this.labelRef, onClick: this.open });
return React.cloneElement(children, { ref: this.labelRef, onClick: this.labelOnClick });
}
renderMenu(children) {
@ -226,21 +236,23 @@ class Popup extends Component {
}
return (
<Modal className={this.props.className} onClick={this.close}>
<div ref={this.menuContainerRef} className={styles['menu-container']} onClick={this.menuContainerOnClick}>
<div ref={this.menuScrollRef} className={styles['menu-scroll-container']}>
{React.cloneElement(children, { ref: this.menuChildrenRef })}
<Modal>
<div className={classnames(styles['modal-container'], this.props.className)} onClick={this.modalBackgroundOnClick}>
<div ref={this.menuContainerRef} className={styles['menu-container']} onClick={this.menuContainerOnClick}>
<div ref={this.menuScrollRef} className={styles['menu-scroll-container']}>
{React.cloneElement(children, { ref: this.menuChildrenRef })}
</div>
<div ref={this.menuBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
<div ref={this.menuBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
<div ref={this.menuBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
<div ref={this.menuBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
</div>
<div ref={this.menuBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
<div ref={this.menuBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
<div ref={this.menuBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
<div ref={this.menuBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
<div ref={this.labelBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
<div ref={this.labelBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
<div ref={this.labelBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
<div ref={this.labelBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
<div ref={this.hiddenBorderRef} className={classnames(styles['border'], styles['border-hidden'])} />
</div>
<div ref={this.labelBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
<div ref={this.labelBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
<div ref={this.labelBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
<div ref={this.labelBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
<div ref={this.hiddenBorderRef} className={classnames(styles['border'], styles['border-hidden'])} />
</Modal>
);
}

View file

@ -1,44 +1,49 @@
.menu-container {
position: absolute;
visibility: hidden;
.modal-container {
width: 100%;
height: 100%;
.menu-scroll-container {
box-shadow: var(--box-shadow);
overflow: auto;
}
}
.menu-container {
position: absolute;
visibility: hidden;
.border {
position: absolute;
pointer-events: none;
background-color: var(--border-color);
&-top {
top: 0;
right: 0;
left: 0;
.menu-scroll-container {
box-shadow: var(--box-shadow);
overflow: auto;
}
}
&-bottom {
right: 0;
bottom: 0;
left: 0;
}
.border {
position: absolute;
pointer-events: none;
background-color: var(--border-color);
&-left {
top: 0;
bottom: 0;
left: 0;
}
&-top {
top: 0;
right: 0;
left: 0;
}
&-right {
top: 0;
right: 0;
bottom: 0;
}
&-bottom {
right: 0;
bottom: 0;
left: 0;
}
&-hidden {
display: none;
border: 1px solid;
&-left {
top: 0;
bottom: 0;
left: 0;
}
&-right {
top: 0;
right: 0;
bottom: 0;
}
&-hidden {
display: none;
border: 1px solid;
}
}
}

View file

@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react';
import pathToRegexp from 'path-to-regexp';
import PathUtils from 'path';
import UrlUtils from 'url';
import { FocusableProvider } from 'stremio-common';
import { Modal, FocusableProvider } from 'stremio-common';
class Router extends Component {
constructor(props) {
@ -48,6 +48,10 @@ class Router extends Component {
return nextState.views !== this.state.views;
}
onDomTreeChange = ({ child, onFocusableChange }) => {
onFocusableChange(Modal.modalsContainer.childElementCount === 0 && child.nextElementSibling === null);
}
onLocationChanged = () => {
const hashIndex = window.location.href.indexOf('#');
const hashPath = hashIndex === -1 ? '' : window.location.href.substring(hashIndex + 1);
@ -101,8 +105,8 @@ class Router extends Component {
this.state.views
.filter(({ element }) => React.isValidElement(element))
.map(({ path, element }) => (
<FocusableProvider key={path}>
<div className={this.props.routeContainerClassName}>{element}</div>
<FocusableProvider key={path} elements={[Router.routesContainer, Modal.modalsContainer]} onDomTreeChange={this.onDomTreeChange}>
<div className={Router.routeClassName}>{element}</div>
</FocusableProvider>
))
}

View file

@ -9,6 +9,7 @@
<body>
<div id="app"></div>
<div id="modals"></div>
<script src="https://www.youtube.com/iframe_api"></script>
</body>

View file

@ -1,11 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Modal } from 'stremio-common';
import App from './app';
const container = document.getElementById('app');
Modal.container = container;
Modal.className = 'modal-container';
ReactDOM.render(<App />, container);
ReactDOM.render(<App />, App.containerElement);