Add react-modal, replace confirm usage with it

This commit is contained in:
Ken-Håvard Lieng 2019-01-05 07:08:34 +01:00
parent 63cf65100d
commit 0085cea5a1
17 changed files with 443 additions and 152 deletions

File diff suppressed because one or more lines are too long

View File

@ -57,6 +57,18 @@ button:active {
background: #6bb758; background: #6bb758;
} }
.button-normal {
background: #222;
}
.button-normal:hover {
background: #111;
}
.button-normal:active {
background: #222;
}
label { label {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
@ -855,6 +867,62 @@ input.message-input-nick.invalid {
color: #ddd; color: #ddd;
} }
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.33);
opacity: 0;
transition: opacity 0.2s;
}
.modal-overlay-opening {
opacity: 1;
}
.modal-overlay-closing {
opacity: 0;
}
.modal {
max-width: 600px;
padding: 15px;
background: #f0f0f0;
border: 1px solid #ddd;
outline: none;
margin: 15px;
text-align: center;
font-family: 'Montserrat', sans-serif;
transform: translateY(-20px);
transition: transform 0.2s;
}
.modal-opening {
transform: translateY(0);
}
.modal-closing {
transform: translateY(-20px);
}
.modal p {
margin-bottom: 5px;
}
.modal button {
width: 120px;
}
.modal button {
margin: 0 5px;
margin-top: 10px;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.tablist { .tablist {
width: 200px; width: 200px;

View File

@ -1,9 +1,10 @@
import React, { Suspense, lazy } from 'react'; import React, { Suspense, lazy, useState } from 'react';
import Route from 'containers/Route'; import Route from 'containers/Route';
import AppInfo from 'components/AppInfo'; import AppInfo from 'components/AppInfo';
import TabList from 'components/TabList'; import TabList from 'components/TabList';
import cn from 'classnames'; import cn from 'classnames';
const Modals = lazy(() => import('components/modals'));
const Chat = lazy(() => import('containers/Chat')); const Chat = lazy(() => import('containers/Chat'));
const Connect = lazy(() => import('containers/Connect')); const Connect = lazy(() => import('containers/Connect'));
const Settings = lazy(() => import('containers/Settings')); const Settings = lazy(() => import('containers/Settings'));
@ -18,8 +19,14 @@ const App = ({
select, select,
push, push,
hideMenu, hideMenu,
newVersionAvailable newVersionAvailable,
hasOpenModals
}) => { }) => {
const [renderModals, setRenderModals] = useState(false);
if (!renderModals && hasOpenModals) {
setRenderModals(true);
}
const mainClass = cn('main-container', { const mainClass = cn('main-container', {
'off-canvas': showTabList 'off-canvas': showTabList
}); });
@ -54,10 +61,8 @@ const App = ({
push={push} push={push}
/> />
<div className={mainClass}> <div className={mainClass}>
<Suspense <Suspense fallback={<div className="suspense-fallback">...</div>}>
maxDuration={1000} {renderModals && <Modals />}
fallback={<div className="suspense-fallback">...</div>}
>
<Route name="chat"> <Route name="chat">
<Chat /> <Chat />
</Route> </Route>

View File

@ -0,0 +1,27 @@
import React, { useCallback } from 'react';
import withModal from 'components/modals/withModal';
import Button from 'components/ui/Button';
const Confirm = ({
payload: { question, confirmation, onConfirm },
onClose
}) => {
const handleConfirm = useCallback(() => {
onClose(false);
onConfirm();
}, []);
return (
<>
<p>{question}</p>
<Button onClick={handleConfirm}>{confirmation || 'OK'}</Button>
<Button category="normal" onClick={onClose}>
Cancel
</Button>
</>
);
};
export default withModal({
name: 'confirm'
})(Confirm);

View File

@ -0,0 +1,10 @@
import React, { memo } from 'react';
import Confirm from 'components/modals/Confirm';
const Modals = () => (
<>
<Confirm />
</>
);
export default memo(Modals, () => true);

View File

@ -0,0 +1,64 @@
import React, { useCallback } from 'react';
import Modal from 'react-modal';
import { createSelector } from 'reselect';
import { getModals, closeModal } from 'state/modals';
import connect from 'utils/connect';
Modal.setAppElement('#root');
export default function withModal({ name, ...modalProps }) {
modalProps = {
className: {
base: `modal modal-${name}`,
afterOpen: 'modal-opening',
beforeClose: 'modal-closing'
},
overlayClassName: {
base: 'modal-overlay',
afterOpen: 'modal-overlay-opening',
beforeClose: 'modal-overlay-closing'
},
closeTimeoutMS: 200,
...modalProps
};
return WrappedComponent => {
const ReduxModal = ({ onRequestClose, ...props }) => {
const handleRequestClose = useCallback(
(dismissed = true) => {
onRequestClose();
if (dismissed && props.payload.onDismiss) {
props.payload.onDismiss();
}
},
[props.payload.onDismiss]
);
return (
<Modal
contentLabel={name}
onRequestClose={handleRequestClose}
{...modalProps}
{...props}
>
<WrappedComponent onClose={handleRequestClose} {...props} />
</Modal>
);
};
const mapState = createSelector(
getModals,
modals => modals[name] || { payload: {} }
);
const mapDispatch = dispatch => ({
onRequestClose: () => dispatch(closeModal(name))
});
return connect(
mapState,
mapDispatch
)(ReduxModal);
};
}

View File

@ -26,10 +26,7 @@ const ChatTitle = ({
let serverError = null; let serverError = null;
if (!tab.name && status.error) { if (!tab.name && status.error) {
serverError = ( serverError = (
<span className="chat-topic error"> <span className="chat-topic error">Error: {status.error}</span>
Error:
{status.error}
</span>
); );
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
const Button = ({ children, ...props }) => ( const Button = ({ children, category, ...props }) => (
<button type="button" {...props}> <button className={`button-${category}`} type="button" {...props}>
{children} {children}
</button> </button>
); );

View File

@ -2,6 +2,7 @@ import { createStructuredSelector } from 'reselect';
import App from 'components/App'; import App from 'components/App';
import { getConnected } from 'state/app'; import { getConnected } from 'state/app';
import { getSortedChannels } from 'state/channels'; import { getSortedChannels } from 'state/channels';
import { getHasOpenModals } from 'state/modals';
import { getPrivateChats } from 'state/privateChats'; import { getPrivateChats } from 'state/privateChats';
import { getServers } from 'state/servers'; import { getServers } from 'state/servers';
import { getSelectedTab, select } from 'state/tab'; import { getSelectedTab, select } from 'state/tab';
@ -16,7 +17,8 @@ const mapState = createStructuredSelector({
servers: getServers, servers: getServers,
showTabList: getShowTabList, showTabList: getShowTabList,
tab: getSelectedTab, tab: getSelectedTab,
newVersionAvailable: state => state.app.newVersionAvailable newVersionAvailable: state => state.app.newVersionAvailable,
hasOpenModals: getHasOpenModals
}); });
const mapDispatch = { push, select, hideMenu }; const mapDispatch = { push, select, hideMenu };

View File

@ -7,6 +7,7 @@ import {
addMessage, addMessage,
addMessages addMessages
} from 'state/messages'; } from 'state/messages';
import { openModal } from 'state/modals';
import { reconnect } from 'state/servers'; import { reconnect } from 'state/servers';
import { select } from 'state/tab'; import { select } from 'state/tab';
import { find, normalizeChannel } from 'utils'; import { find, normalizeChannel } from 'utils';
@ -123,16 +124,18 @@ export default function handleSocket({
}, },
connection_update({ server, errorType }) { connection_update({ server, errorType }) {
if ( if (errorType === 'verify') {
errorType === 'verify' && dispatch(
window.confirm( openModal('confirm', {
'The server is using a self-signed certificate, continue anyway?' question:
) 'The server is using a self-signed certificate, continue anyway?',
) { onConfirm: () =>
dispatch( dispatch(
reconnect(server, { reconnect(server, {
skipVerify: true skipVerify: true
}) })
)
})
); );
} }
}, },

View File

@ -19,6 +19,9 @@ export const FETCH_MESSAGES = 'FETCH_MESSAGES';
export const RAW = 'RAW'; export const RAW = 'RAW';
export const UPDATE_MESSAGE_HEIGHT = 'UPDATE_MESSAGE_HEIGHT'; export const UPDATE_MESSAGE_HEIGHT = 'UPDATE_MESSAGE_HEIGHT';
export const OPEN_MODAL = 'OPEN_MODAL';
export const CLOSE_MODAL = 'CLOSE_MODAL';
export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT'; export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT';
export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT'; export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT';

View File

@ -1,4 +1,3 @@
import assign from 'lodash/assign';
import createReducer from 'utils/createReducer'; import createReducer from 'utils/createReducer';
import * as actions from './actions'; import * as actions from './actions';
@ -31,7 +30,7 @@ const initialState = {
export default createReducer(initialState, { export default createReducer(initialState, {
[actions.APP_SET](state, { key, value }) { [actions.APP_SET](state, { key, value }) {
if (typeof key === 'object') { if (typeof key === 'object') {
assign(state, key); Object.assign(state, key);
} else { } else {
state[key] = value; state[key] = value;
} }

View File

@ -3,6 +3,7 @@ import app from './app';
import channels from './channels'; import channels from './channels';
import input from './input'; import input from './input';
import messages from './messages'; import messages from './messages';
import modals from './modals';
import privateChats from './privateChats'; import privateChats from './privateChats';
import search from './search'; import search from './search';
import servers from './servers'; import servers from './servers';
@ -20,6 +21,7 @@ export default function createReducer(router) {
channels, channels,
input, input,
messages, messages,
modals,
privateChats, privateChats,
search, search,
servers, servers,

50
client/js/state/modals.js Normal file
View File

@ -0,0 +1,50 @@
import { createSelector } from 'reselect';
import createReducer from 'utils/createReducer';
import * as actions from './actions';
export const getModals = state => state.modals;
export const getHasOpenModals = createSelector(
getModals,
modals => {
const keys = Object.keys(modals);
for (let i = 0; i < keys.length; i++) {
if (modals[keys[i]].isOpen) {
return true;
}
}
return false;
}
);
export default createReducer(
{},
{
[actions.OPEN_MODAL](state, { name, payload = {} }) {
state[name] = {
isOpen: true,
payload
};
},
[actions.CLOSE_MODAL](state, { name }) {
state[name].isOpen = false;
}
}
);
export function openModal(name, payload) {
return {
type: actions.OPEN_MODAL,
name,
payload
};
}
export function closeModal(name) {
return {
type: actions.CLOSE_MODAL,
name
};
}

View File

@ -1,4 +1,3 @@
import assign from 'lodash/assign';
import createReducer from 'utils/createReducer'; import createReducer from 'utils/createReducer';
import * as actions from './actions'; import * as actions from './actions';
@ -41,7 +40,7 @@ export default createReducer(
[actions.SETTINGS_SET](state, { key, value, settings }) { [actions.SETTINGS_SET](state, { key, value, settings }) {
if (settings) { if (settings) {
assign(state, settings); Object.assign(state, settings);
} else { } else {
state[key] = value; state[key] = value;
} }

View File

@ -71,6 +71,7 @@
"react": "^16.7.0-alpha.0", "react": "^16.7.0-alpha.0",
"react-dom": "^16.7.0-alpha.0", "react-dom": "^16.7.0-alpha.0",
"react-hot-loader": "^4.6.3", "react-hot-loader": "^4.6.3",
"react-modal": "^3.8.1",
"react-redux": "^6.0.0-beta.2", "react-redux": "^6.0.0-beta.2",
"react-virtualized-auto-sizer": "^1.0.2", "react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.5.0", "react-window": "^1.5.0",

View File

@ -3664,6 +3664,11 @@ execa@^0.7.0:
signal-exit "^3.0.0" signal-exit "^3.0.0"
strip-eof "^1.0.0" strip-eof "^1.0.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exit@^0.1.2: exit@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -8053,7 +8058,7 @@ prompts@^0.1.9:
kleur "^2.0.1" kleur "^2.0.1"
sisteransi "^0.1.1" sisteransi "^0.1.1"
prop-types@^15.6.1, prop-types@^15.6.2: prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
@ -8268,11 +8273,21 @@ react-is@^16.7.0-alpha.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0-alpha.2.tgz#0dd7f95d45ad5318b7f7bcb99dcb84da9385cb57" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0-alpha.2.tgz#0dd7f95d45ad5318b7f7bcb99dcb84da9385cb57"
integrity sha512-1Q3qN8nMWUfFcRz/bBC1f9zSL3il9OcSxMd9CNnpJbeFf4VCX0qYxL3TuwT4f+tFk1TkidwIL11yYgk4HjldYg== integrity sha512-1Q3qN8nMWUfFcRz/bBC1f9zSL3il9OcSxMd9CNnpJbeFf4VCX0qYxL3TuwT4f+tFk1TkidwIL11yYgk4HjldYg==
react-lifecycles-compat@^3.0.4: react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-modal@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.8.1.tgz#7300f94a6f92a2e17994de0be6ccb61734464c9e"
integrity sha512-aLKeZM9pgXpIKVwopRHMuvqKWiBajkqisDA8UzocdCF6S4fyKVfLWmZR5G1Q0ODBxxxxf2XIwiCP8G/11GJAuw==
dependencies:
exenv "^1.2.0"
prop-types "^15.5.10"
react-lifecycles-compat "^3.0.0"
warning "^3.0.0"
react-redux@^6.0.0-beta.2: react-redux@^6.0.0-beta.2:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.0.tgz#09e86eeed5febb98e9442458ad2970c8f1a173ef" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.0.tgz#09e86eeed5febb98e9442458ad2970c8f1a173ef"