mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 10:42:12 +00:00
modals refactored
This commit is contained in:
parent
486db927db
commit
09ecae54e2
21 changed files with 227 additions and 194 deletions
18
src/App/App.js
Normal file
18
src/App/App.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
export default App;
|
||||
35
src/App/ModalsContainerProvider.js
Normal file
35
src/App/ModalsContainerProvider.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ModalsContainerContext } from 'stremio-common';
|
||||
|
||||
class ModalsContainerProvider extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.modalsContainerRef = React.createRef();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.children !== this.props.children ||
|
||||
nextProps.modalsContainerClassName !== this.props.modalsContainerClassName;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalsContainerContext.Provider value={this.modalsContainerRef.current}>
|
||||
{this.props.children}
|
||||
<div ref={this.modalsContainerRef} className={this.props.modalsContainerClassName} />
|
||||
</ModalsContainerContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ModalsContainerProvider.propTypes = {
|
||||
modalsContainerClassName: PropTypes.string
|
||||
};
|
||||
|
||||
export default ModalsContainerProvider;
|
||||
67
src/App/RouterFocusableProvider.js
Normal file
67
src/App/RouterFocusableProvider.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import React, { Component } from 'react';
|
||||
import { FocusableContext, withModalsContainer } from 'stremio-common';
|
||||
|
||||
class RouterFocusableProvider extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.routesContainerRef = React.createRef();
|
||||
this.modalsContainerDomTreeObserver = new MutationObserver(this.onModalsContainerDomTreeChange);
|
||||
this.state = {
|
||||
focusable: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.modalsContainer !== null) {
|
||||
this.onModalsContainerDomTreeChange();
|
||||
this.modalsContainerDomTreeObserver.observe(this.props.modalsContainer, {
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.modalsContainerDomTreeObserver.disconnect();
|
||||
}
|
||||
|
||||
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');
|
||||
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 = () => {
|
||||
this.setState({ focusable: this.props.modalsContainer.childElementCount === 0 });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FocusableContext.Provider value={this.state.focusable}>
|
||||
<div ref={this.routesContainerRef} className={this.props.routesContainerClassName}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</FocusableContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withModalsContainer(RouterFocusableProvider);
|
||||
3
src/App/index.js
Normal file
3
src/App/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import App from './App';
|
||||
|
||||
export default App;
|
||||
|
|
@ -71,6 +71,33 @@ html, body, :global(#app) {
|
|||
font-family: 'Roboto', 'sans-serif';
|
||||
line-height: 1;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--scroll-bar-width);
|
||||
}
|
||||
|
|
@ -82,38 +109,4 @@ html, body, :global(#app) {
|
|||
::-webkit-scrollbar-track {
|
||||
background-color: var(--color-backgroundlight);
|
||||
}
|
||||
}
|
||||
|
||||
: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(#modals) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import React, { PureComponent, StrictMode } from 'react';
|
||||
import { Router, Modal } from 'stremio-common';
|
||||
import routerConfig from './routerConfig';
|
||||
import styles from './styles';
|
||||
|
||||
class App extends PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<StrictMode>
|
||||
<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;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import App from './app';
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Focusable } from 'stremio-common';
|
||||
import { withFocusable } from 'stremio-common';
|
||||
|
||||
class Button extends PureComponent {
|
||||
onClick = (event) => {
|
||||
|
|
@ -20,23 +20,25 @@ class Button extends PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { stopPropagation, forwardedRef, ...props } = this.props;
|
||||
const { forwardedRef, focusable, stopPropagation, ...props } = this.props;
|
||||
return (
|
||||
<Focusable ref={forwardedRef}>
|
||||
<div
|
||||
{...props}
|
||||
onClick={this.onClick}
|
||||
onKeyUp={this.onKeyUp}
|
||||
/>
|
||||
</Focusable>
|
||||
<div
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
tabIndex={focusable ? 0 : -1}
|
||||
onClick={this.onClick}
|
||||
onKeyUp={this.onKeyUp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
focusable: PropTypes.bool.isRequired,
|
||||
stopPropagation: PropTypes.bool.isRequired
|
||||
};
|
||||
Button.defaultProps = {
|
||||
focusable: false,
|
||||
stopPropagation: true
|
||||
};
|
||||
|
||||
|
|
@ -46,4 +48,4 @@ const ButtonWithForwardedRef = React.forwardRef((props, ref) => (
|
|||
|
||||
ButtonWithForwardedRef.displayName = 'ButtonWithForwardedRef';
|
||||
|
||||
export default ButtonWithForwardedRef;
|
||||
export default withFocusable(ButtonWithForwardedRef);
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import FocusableContext from './FocusableContext';
|
||||
|
||||
const Focusable = React.forwardRef(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<FocusableContext.Consumer>
|
||||
{focusable => React.cloneElement(React.Children.only(children), { ...props, ref, tabIndex: focusable ? 0 : -1 })}
|
||||
</FocusableContext.Consumer>
|
||||
);
|
||||
});
|
||||
|
||||
Focusable.displayName = 'Focusable';
|
||||
|
||||
export default Focusable;
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FocusableContext from './FocusableContext';
|
||||
|
||||
class FocusableProvider extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.childElementRef = React.createRef();
|
||||
this.observers = props.elements.map((element) => ({
|
||||
element,
|
||||
observer: new MutationObserver(this.onDomTreeChange)
|
||||
}));
|
||||
this.state = {
|
||||
focusable: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onDomTreeChange();
|
||||
this.observers.forEach(({ element, observer }) => observer.observe(element, {
|
||||
childList: true
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.observers.forEach(({ observer }) => observer.disconnect());
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.children !== this.props.children ||
|
||||
nextState.focusable !== this.state.focusable;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.state.focusable) {
|
||||
const focusedElement = this.childElementRef.current.querySelector(':focus');
|
||||
if (focusedElement) {
|
||||
focusedElement.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDomTreeChange = () => {
|
||||
this.props.onDomTreeChange({
|
||||
child: this.childElementRef.current,
|
||||
onFocusableChange: this.onFocusableChange
|
||||
});
|
||||
}
|
||||
|
||||
onFocusableChange = (focusable) => {
|
||||
this.setState({ focusable });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FocusableContext.Provider value={this.state.focusable}>
|
||||
{React.cloneElement(React.Children.only(this.props.children), { ref: this.childElementRef })}
|
||||
</FocusableContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FocusableProvider.propTypes = {
|
||||
onDomTreeChange: PropTypes.func.isRequired,
|
||||
elements: PropTypes.arrayOf(PropTypes.instanceOf(HTMLElement).isRequired).isRequired
|
||||
};
|
||||
|
||||
export default FocusableProvider;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import Focusable from './Focusable';
|
||||
import FocusableProvider from './FocusableProvider';
|
||||
import FocusableContext from './FocusableContext';
|
||||
import withFocusable from './withFocusable';
|
||||
|
||||
export {
|
||||
Focusable,
|
||||
FocusableProvider
|
||||
FocusableContext,
|
||||
withFocusable
|
||||
};
|
||||
|
|
|
|||
14
src/common/Focusable/withFocusable.js
Normal file
14
src/common/Focusable/withFocusable.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import FocusableContext from './FocusableContext';
|
||||
|
||||
const withFocusable = (Component) => {
|
||||
return function withFocusable(props) {
|
||||
return (
|
||||
<FocusableContext.Consumer>
|
||||
{focusable => <Component {...props} focusable={focusable} />}
|
||||
</FocusableContext.Consumer>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withFocusable;
|
||||
|
|
@ -1,26 +1,21 @@
|
|||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { FocusableProvider } from 'stremio-common';
|
||||
import { FocusableContext } from 'stremio-common';
|
||||
import withModalsContainer from './withModalsContainer';
|
||||
|
||||
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);
|
||||
};
|
||||
});
|
||||
const Modal = ({ modalsContainer, children }) => {
|
||||
if (modalsContainer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<FocusableProvider elements={[Modal.modalsContainer]} onDomTreeChange={onDomTreeChange}>
|
||||
{React.Children.only(children)}
|
||||
</FocusableProvider>,
|
||||
modalContainer
|
||||
<FocusableContext.Provider value={true}>
|
||||
<div className={'modal-container'}>
|
||||
{children}
|
||||
</div>
|
||||
</FocusableContext.Provider>,
|
||||
modalsContainer
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
export default withModalsContainer(Modal);
|
||||
|
|
|
|||
3
src/common/Modal/ModalsContainerContext.js
Normal file
3
src/common/Modal/ModalsContainerContext.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import React from 'react';
|
||||
|
||||
export default React.createContext(null);
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
import ModalsContainerContext from './ModalsContainerContext';
|
||||
import Modal from './Modal';
|
||||
import withModalsContainer from './withModalsContainer';
|
||||
|
||||
export default Modal;
|
||||
export {
|
||||
ModalsContainerContext,
|
||||
Modal,
|
||||
withModalsContainer
|
||||
};
|
||||
|
|
|
|||
14
src/common/Modal/withModalsContainer.js
Normal file
14
src/common/Modal/withModalsContainer.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import ModalsContainerContext from './ModalsContainerContext';
|
||||
|
||||
const withModalsContainer = (Component) => {
|
||||
return function withModalsContainer(props) {
|
||||
return (
|
||||
<ModalsContainerContext.Consumer>
|
||||
{modalsContainer => <Component {...props} modalsContainer={modalsContainer} />}
|
||||
</ModalsContainerContext.Consumer>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withModalsContainer;
|
||||
|
|
@ -2,7 +2,6 @@ import React, { Component, Fragment } from 'react';
|
|||
import pathToRegexp from 'path-to-regexp';
|
||||
import PathUtils from 'path';
|
||||
import UrlUtils from 'url';
|
||||
import { Modal, FocusableProvider } from 'stremio-common';
|
||||
|
||||
class Router extends Component {
|
||||
constructor(props) {
|
||||
|
|
@ -48,10 +47,6 @@ 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);
|
||||
|
|
@ -105,9 +100,7 @@ class Router extends Component {
|
|||
this.state.views
|
||||
.filter(({ element }) => React.isValidElement(element))
|
||||
.map(({ path, element }) => (
|
||||
<FocusableProvider key={path} elements={[Router.routesContainer, Modal.modalsContainer]} onDomTreeChange={this.onDomTreeChange}>
|
||||
<div className={Router.routeClassName}>{element}</div>
|
||||
</FocusableProvider>
|
||||
<div key={path} className={this.props.routeClassName}>{element}</div>
|
||||
))
|
||||
}
|
||||
</Fragment>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Checkbox from './Checkbox';
|
||||
import Popup from './Popup';
|
||||
import NavBar from './NavBar';
|
||||
import Modal from './Modal';
|
||||
import { ModalsContainerContext, Modal, withModalsContainer } from './Modal';
|
||||
import MetadataItem from './MetadataItem';
|
||||
import Router from './Router';
|
||||
import LibraryItemList from './LibraryItemList';
|
||||
|
|
@ -9,14 +9,16 @@ import MetaItem from './MetaItem';
|
|||
import ShareAddon from './ShareAddon';
|
||||
import UserPanel from './UserPanel';
|
||||
import Slider from './Slider';
|
||||
import { Focusable, FocusableProvider } from './Focusable';
|
||||
import { FocusableContext, withFocusable } from './Focusable';
|
||||
import Button from './Button';
|
||||
|
||||
export {
|
||||
Checkbox,
|
||||
Popup,
|
||||
NavBar,
|
||||
ModalsContainerContext,
|
||||
Modal,
|
||||
withModalsContainer,
|
||||
MetadataItem,
|
||||
Router,
|
||||
LibraryItemList,
|
||||
|
|
@ -24,7 +26,7 @@ export {
|
|||
ShareAddon,
|
||||
UserPanel,
|
||||
Slider,
|
||||
Focusable,
|
||||
FocusableProvider,
|
||||
FocusableContext,
|
||||
withFocusable,
|
||||
Button
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="modals"></div>
|
||||
<script src="https://www.youtube.com/iframe_api"></script>
|
||||
</body>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './app';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(<App />, App.containerElement);
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue