focusability in modals implemented with max code reuse

This commit is contained in:
NikolaBorislavovHristov 2019-01-28 16:17:49 +02:00
parent 49457afa52
commit 8e07f82eca
5 changed files with 50 additions and 32 deletions

View file

@ -1,12 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FocusableContext, withModalsContainer } from 'stremio-common';
import withModalsContainer from '../Modal/withModalsContainer';
import FocusableContext from './FocusableContext';
class RouteFocusableProvider extends Component {
class FocusableProvider extends Component {
constructor(props) {
super(props);
this.routeContentRef = React.createRef();
this.contentRef = React.createRef();
this.modalsContainerDomTreeObserver = new MutationObserver(this.onModalsContainerDomTreeChange);
this.state = {
focusable: false
@ -32,32 +33,45 @@ class RouteFocusableProvider extends Component {
componentDidUpdate(prevProps, prevState) {
if (prevState.focusable && !this.state.focusable) {
const focusedElement = this.routeContentRef.current.querySelector(':focus');
const focusedElement = this.contentRef.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 });
const focusable = this.props.onModalsContainerDomTreeChange({
modalsContainerElement: this.props.modalsContainer,
contentElement: this.contentRef.current
});
this.setState({ focusable });
}
render() {
return (
<FocusableContext.Provider value={this.state.focusable}>
{React.cloneElement(React.Children.only(this.props.children), { ref: this.routeContentRef })}
{React.cloneElement(React.Children.only(this.props.children), { ref: this.contentRef })}
</FocusableContext.Provider>
);
}
}
RouteFocusableProvider.propTypes = {
modalsContainer: PropTypes.instanceOf(HTMLElement).isRequired
FocusableProvider.propTypes = {
modalsContainer: PropTypes.instanceOf(HTMLElement).isRequired,
onModalsContainerDomTreeChange: PropTypes.func.isRequired
};
const RouteFocusableProviderWithModalsContainer = withModalsContainer(RouteFocusableProvider);
const FocusableProviderWithModalsContainer = withModalsContainer(FocusableProvider);
RouteFocusableProviderWithModalsContainer.displayName = 'RouteFocusableProviderWithModalsContainer';
FocusableProviderWithModalsContainer.displayName = 'FocusableProviderWithModalsContainer';
export default RouteFocusableProviderWithModalsContainer;
export default FocusableProviderWithModalsContainer;

View file

@ -1,7 +1,7 @@
import FocusableContext from './FocusableContext';
import FocusableProvider from './FocusableProvider';
import withFocusable from './withFocusable';
export {
FocusableContext,
FocusableProvider,
withFocusable
};

View file

@ -1,28 +1,28 @@
import React, { useRef, useState, useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { FocusableContext } from 'stremio-common';
import { FocusableProvider } from 'stremio-common';
import withModalsContainer from './withModalsContainer';
const Modal = ({ modalsContainer, children }) => {
const modalContainerRef = useRef(null);
const [focusable, setFocusable] = useState(false);
useEffect(() => {
const nextFocusable = modalsContainer.lastElementChild === modalContainerRef.current;
if (nextFocusable !== focusable) {
setFocusable(nextFocusable);
}
});
const onModalsContainerDomTreeChange = ({ modalsContainerElement, contentElement }) => {
return modalsContainerElement.lastElementChild === contentElement;
};
const Modal = ({ modalsContainer, children }) => {
return ReactDOM.createPortal(
<FocusableContext.Provider value={focusable}>
<div ref={modalContainerRef} className={'modal-container'}>
<FocusableProvider onModalsContainerDomTreeChange={onModalsContainerDomTreeChange}>
<div className={'modal-container'}>
{children}
</div>
</FocusableContext.Provider>,
</FocusableProvider>,
modalsContainer
);
};
Modal.propTypes = {
modalsContainer: PropTypes.instanceOf(HTMLElement).isRequired
};
const ModalWithModalsContainer = withModalsContainer(Modal);
ModalWithModalsContainer.displayName = 'ModalWithModalsContainer';

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { FocusableProvider } from 'stremio-common';
import ModalsContainerProvider from './ModalsContainerProvider';
import RouteFocusableProvider from './RouteFocusableProvider';
import styles from './styles';
class Route extends Component {
@ -8,15 +8,19 @@ class Route extends Component {
return nextProps.children !== this.props.children;
}
onModalsContainerDomTreeChange = ({ modalsContainerElement }) => {
return modalsContainerElement.childElementCount === 0;
};
render() {
return (
<div className={styles['route']}>
<ModalsContainerProvider modalsContainerClassName={styles['modals-container']}>
<RouteFocusableProvider>
<FocusableProvider onModalsContainerDomTreeChange={this.onModalsContainerDomTreeChange}>
<div className={styles['route-content']}>
{this.props.children}
</div>
</RouteFocusableProvider>
</FocusableProvider>
</ModalsContainerProvider>
</div>
);

View file

@ -1,5 +1,5 @@
import Slider from './Slider';
import { FocusableContext, withFocusable } from './Focusable';
import { FocusableProvider, withFocusable } from './Focusable';
import Button from './Button';
import Checkbox from './Checkbox';
import TextInput from './TextInput';
@ -28,7 +28,7 @@ export {
ShareAddon,
UserPanel,
Slider,
FocusableContext,
FocusableProvider,
withFocusable,
Button
};