Modal: implementation
necolas opened this issue ยท 17 comments
Accessible modal implementation (in progress):
- Hides app content from screen-readers when active.
- Traps focus.
- Built-in
Escape
-to-close mechanism.
https://github.com/rayandrews/react-native-web-modal
I'm creating this modal, inspired by kiurchv and react native's modal and based on react native web's components (not css).
Actually the real repo is:
https://github.com/rayandrews/react-native-web-modal/tree/master/packages/modal-react-native-web
escape key etc not implemented.
what do you think @necolas?
All, I do cross-platform development in react-native and providing accessible web pages is required for my work. I have spent the last year doing a few different implementations of Modal that meet both for functionality and accessibility. Thanks to @RayAndrews for sharing the modal-react-native-web and linking to the enhanced modal implementation: https://github.com/davidtheclark/focus-trap-react I now have a solution that is fully working cross platform including web. However, I did have to add a small react package: https://github.com/davidtheclark/focus-trap-react in order to fully trap focus.
For reference, my implementation looks like this:
render(): Element<*> {
const modalContent = Platform.OS === 'web' ? (
<FocusTrap>
<ScopeSelect hideModal={this._toggleModal} headingLevel={1} />
</FocusTrap>
) : (
<ScopeSelect hideModal={this._toggleModal} headingLevel={1} />
);
return (
<View style={Styles.container}>
<Modal
isVisible={this.state.isModalVisible}
onBackdropPress={this._toggleModal}
>
<View style={styles.modalWrapper}>
{this.state.isModalVisible && modalContent}
</View>
</Modal>
<TabsScreen
navigation={this.props.navigation}
screenProps={this.props.screenProps}
onTabChange={this._onTabsScreenTabChange}
showActivity={this.props.showActivity}
hideActivity={this.props.hideActivity}
/>
</View>
);
}
I built a library to manage layers https://github.com/giuseppeg/react-layers-manager let me know if you want to adopt it for this and if you think that the API needs improvements.
If somebody wants to use a custom implementation like modal-enhanced-react-native-web
or any other file, just set up a webpack alias:
resolve: {
alias: {
'react-native$': 'react-native-web',
'react-native-web/dist/exports/Modal': 'modal-enhanced-react-native-web',
...
}
}
This requires that you use the babel-plugin-react-native-web
in your .babelrc
.
@RayAndrews 's implementation looks good.
I made a temporary implementation that has:
- Hides app content from screen-readers when active.
- Traps focus.
- Built-in Escape-to-close mechanism.
But lacks:
- Animations
- Properly working back button handling using
useHistory
https://wetrustplatform.github.io/paramount/#/src-components-modal-modal
We had improved our solution from before, just reposting it here in case anyone wants to use it:
// @flow
/**
* EnhancedModal renders a modal for Web or Native with animations and trapped focus (on web)
* Implementation Note: all children components will receive the props `modalHeight` and
* `modalWidth` which represents the visible area of the modal after layout.
*
* For full list of available props see:
* https://github.com/Dekoruma/react-native-web-modal/blob/master/packages/modal-enhanced-react-native-web/src/index.js
*/
import React from 'react';
import { Platform } from 'react-native';
import Modal from 'react-native-modal';
import type { Element } from 'react';
import type { LayoutEvent } from 'react-native/Libraries/Types/CoreEventTypes';
type PropsType = {
/* react provides all children to this component */
children: Element<*> | Array<Element<*>>,
/* enable FocusTrap on web (default: true) */
focusTrap: boolean,
/* visibility state of modal (required) */
isVisible: boolean
};
type StateType = {
modalHeight: number,
modalWidth: number
};
class EnhancedModal extends React.PureComponent<PropsType, StateType> {
static defaultProps = {
focusTrap: true
};
state = {
modalHeight: 0,
modalWidth: 0
};
_modalLayout = (e: LayoutEvent) => {
const { height, width } = e.nativeEvent.layout;
if (height || width) {
this.setState({
modalHeight: height,
modalWidth: width
});
}
};
_decorateChildren = (children: Element<*> | Array<*>): Array<*> => (
React.Children.map(children, (child: Element<*>): Element<*> =>
React.cloneElement(child, { ...this.state }))
);
_wrap = (kids: Array<*>): Element<*> => {
const FocusTrap = require('focus-trap-react');
return <FocusTrap>{kids}</FocusTrap>;
};
render(): Element<*> {
const {
children,
focusTrap,
isVisible,
...modalProps
} = this.props;
return (
<Modal
{...modalProps}
isVisible={isVisible}
onLayout={this._modalLayout}
>
{isVisible
&& focusTrap
&& Platform.OS === 'web'
&& this._wrap(this._decorateChildren(children)) }
{isVisible
&& !(focusTrap
&& Platform.OS === 'web')
&& this._decorateChildren(children) }
</Modal>
);
}
}
export default EnhancedModal;
And remember in your webpack config to:
resolve: {
alias: {
"react-native": "react-native-web",
"react-native-modal": "modal-enhanced-react-native-web"
}
},
There's another implementation here:
https://github.com/Dekoruma/react-native-web-modal
That one also mentioned yet another one here:
Perhaps one of those could be brought into this libe to simplify deps?
It would be nice if the Modal API moved out of React Native and web support fell under the React Navigation umbrella.
so what'd you suggest for a universal Modal @EvanBacon ? Next React Navigation for Web would support this? I guess showing a modal is not the same as navigating to a page (handling back/forward buttons, url paths, etc.). What about using portals and/or context? react-native-modal/react-native-modal#261
I am just reposting my feature request #1629 bellow
Is your feature request related to a problem? Please describe.
support import { Modal } from 'react-native;
in react-native-web
Describe a solution you'd like
I want to be able to use the official modal in react native web
Describe alternatives you've considered
Using react-native-paper modal, but it does not animate at all like in the official modal demo
Additional context
This is a core component of react-native and unfortunately, it is not compatible with react-native-web so we have to look for alternatives (do you have some)
This is react-native official <Modal />
documentation:
https://reactnative.dev/docs/modal.html
They have a live demo here
The demo is for iOS and Android, I want to use react-native-web
to target the web devices.
This is the same live demo used on the web: https://snack.expo.io/@kopax/excited-bagel
As you can see, the modal is not hidden, and animation does not trigger when clicking on hide/show
By the way, the modal does not include a clickable backdrop overlay. The react-native-paper version of the modal include one but then we can't animate the modal. I haven't found any solution to this yet that can target iOS/Android and the web, if anyone know the alternatives, it would be nice for others like me to list them here.
Add modal-enhanced-react-native-web
into web/aliases/react-native/index.js
used by react-app-rewired
can work well too.
npm install --save-dev react-app-rewired modal-enhanced-react-native-web
Create a config-overrides.js
in your project root
// used by react-app-rewired
const webpack = require('webpack');
const path = require('path');
module.exports = {
webpack: function (config, env) {
config.module.rules[1].use[0].options.baseConfig.extends = [
path.resolve('.eslintrc.js'),
];
// To let alias like 'react-native/Libraries/Components/StaticRenderer'
// take effect, must set it before alias 'react-native'
delete config.resolve.alias['react-native'];
config.resolve.alias['react-native/Libraries/Components/StaticRenderer'] =
'react-native-web/dist/vendor/react-native/StaticRenderer';
config.resolve.alias['react-native'] = path.resolve(
'web/aliases/react-native',
);
// Let's force our code to bundle using the same bundler react native does.
config.plugins.push(
new webpack.DefinePlugin({
__DEV__: env === 'development',
}),
);
// Need this rule to prevent `Attempted import error: 'SOME' is not exported from` when `react-app-rewired build`
// Need this rule to prevent `TypeError: Cannot assign to read only property 'exports' of object` when `react-app-rewired start`
config.module.rules.push({
test: /\.(js|tsx?)$/,
// You can exclude the exclude property if you don't want to keep adding individual node_modules
// just keep an eye on how it effects your build times, for this example it's negligible
// exclude: /node_modules[/\\](?!@react-navigation|react-native-gesture-handler|react-native-screens)/,
use: {
loader: 'babel-loader',
},
});
return config;
},
paths: function (paths, env) {
paths.appIndexJs = path.resolve('index.web.js');
paths.appSrc = path.resolve('.');
paths.moduleFileExtensions.push('ios.js');
return paths;
},
};
Also create a web/aliases/react-native/index.js
// ref to https://levelup.gitconnected.com/react-native-typescript-and-react-native-web-an-arduous-but-rewarding-journey-8f46090ca56b
import {Text as RNText, Image as RNImage} from 'react-native-web';
import RNModal from 'modal-enhanced-react-native-web';
// Let's export everything from react-native-web
export * from 'react-native-web';
// And let's stub out everything that's missing!
export const ViewPropTypes = {
style: () => {},
};
RNText.propTypes = {
style: () => {},
};
RNImage.propTypes = {
style: () => {},
source: () => {},
};
export const Text = RNText;
export const Image = RNImage;
export const Modal = RNModal;
// export const ToolbarAndroid = {};
export const requireNativeComponent = () => {};
Now you can just run react-app-rewired start
instead of react-scripts start
Could you publish Modal component