Add modal

This commit is contained in:
Adrian Wozniak 2020-04-01 10:36:05 +02:00
parent 4be598b9ae
commit 5758c79f35
13 changed files with 221 additions and 57 deletions

View File

@ -0,0 +1,80 @@
.modal {
z-index: var(--modal);
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.modal > .clickableOverlay {
min-height: 100%;
background: rgba(9, 30, 66, 0.54);
}
.modal > .clickableOverlay.center {
display: flex;
justify-content: center;
align-items: center;
padding: 50px;
}
.modal > .clickableOverlay > .styledModal {
display: inline-block;
position: relative;
width: 100%;
background: #fff;
}
.modal > .clickableOverlay > .styledModal.center {
/*max-width: ${props => props.width}px;*/
vertical-align: middle;
border-radius: 3px;
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);
}
.modal > .clickableOverlay > .styledModal.aside {
/*max-width: ${props => props.width}px;*/
min-height: 100vh;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
}
.modal > .clickableOverlay > .styledModal.aside > .styledIcon {
position: absolute;
font-size: 25px;
color: var(--textMedium);
transition: all 0.1s;
cursor: pointer;
user-select: none;
}
.modal > .clickableOverlay > .styledModal.aside > .styledIcon.modalVariantCenter {
top: 10px;
right: 12px;
padding: 3px 5px 0 5px;
border-radius: 4px;
}
.modal > .clickableOverlay > .styledModal.aside > .styledIcon.modalVariantCenter:hover {
background: var(--backgroundLight);
}
.modal > .clickableOverlay > .styledModal.aside > .styledIcon.modalVariantAside {
top: 10px;
right: -30px;
width: 50px;
height: 50px;
padding-top: 10px;
border-radius: 3px;
text-align: center;
background: #fff;
border: 1px solid var(--borderLightest);
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);
}
.modal > .clickableOverlay > .styledModal.aside > .styledIcon.modalVariantAside:hover {
color: var(--primary);
}

View File

@ -9,4 +9,5 @@
@import "css/styledTooltip.css"; @import "css/styledTooltip.css";
@import "css/styledAvatar.css"; @import "css/styledAvatar.css";
@import "css/app.css"; @import "css/app.css";
@import "css/modal.css";
@import "css/project.css"; @import "css/project.css";

View File

@ -57,6 +57,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
crate::shared::update(&msg, model, orders); crate::shared::update(&msg, model, orders);
match model.page { match model.page {
Page::Project => project::update(msg, model, orders), Page::Project => project::update(msg, model, orders),
Page::EditIssue(_id) => project::update(msg, model, orders),
Page::ProjectSettings => project_settings::update(msg, model, orders), Page::ProjectSettings => project_settings::update(msg, model, orders),
Page::Login => login::update(msg, model, orders), Page::Login => login::update(msg, model, orders),
Page::Register => register::update(msg, model, orders), Page::Register => register::update(msg, model, orders),
@ -69,6 +70,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
fn view(model: &model::Model) -> Node<Msg> { fn view(model: &model::Model) -> Node<Msg> {
match model.page { match model.page {
Page::Project => project::view(model), Page::Project => project::view(model),
Page::EditIssue(_id) => project::view(model),
Page::ProjectSettings => project_settings::view(model), Page::ProjectSettings => project_settings::view(model),
Page::Login => login::view(model), Page::Login => login::view(model),
Page::Register => register::view(model), Page::Register => register::view(model),
@ -82,6 +84,10 @@ fn routes(url: Url) -> Option<Msg> {
match url.path[0].as_ref() { match url.path[0].as_ref() {
"board" => Some(Msg::ChangePage(model::Page::Project)), "board" => Some(Msg::ChangePage(model::Page::Project)),
"issues" => match url.path.get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Some(Msg::ChangePage(model::Page::EditIssue(id))),
_ => None,
},
"project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)), "project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)),
"login" => Some(Msg::ChangePage(model::Page::Login)), "login" => Some(Msg::ChangePage(model::Page::Login)),
"register" => Some(Msg::ChangePage(model::Page::Register)), "register" => Some(Msg::ChangePage(model::Page::Register)),

View File

@ -13,6 +13,7 @@ pub type ProjectId = i32;
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum Page { pub enum Page {
Project, Project,
EditIssue(IssueId),
ProjectSettings, ProjectSettings,
Login, Login,
Register, Register,
@ -21,12 +22,12 @@ pub enum Page {
impl Page { impl Page {
pub fn to_path(&self) -> String { pub fn to_path(&self) -> String {
match self { match self {
Page::Project => "/board", Page::Project => "/board".to_string(),
Page::ProjectSettings => "/project-settings", Page::EditIssue(id) => format!("/issues/{id}", id = id),
Page::Login => "/login", Page::ProjectSettings => "/project-settings".to_string(),
Page::Register => "/register", Page::Login => "/login".to_string(),
Page::Register => "/register".to_string(),
} }
.to_string()
} }
} }

View File

@ -200,7 +200,7 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
|| project_page.recently_updated_filter || project_page.recently_updated_filter
|| !project_page.active_avatar_filters.is_empty() || !project_page.active_avatar_filters.is_empty()
{ {
true => button![ true => seed::button![
id!["clearAllFilters"], id!["clearAllFilters"],
"Clear all", "Clear all",
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters), mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
@ -360,9 +360,7 @@ fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node<Ms
div![ div![
attrs![At::Class => "bottom"], attrs![At::Class => "bottom"],
div![ div![
// <IssueTypeIcon type={issue.type} />
div![attrs![At::Class => "issueTypeIcon"], issue_type_icon], div![attrs![At::Class => "issueTypeIcon"], issue_type_icon],
// <IssuePriorityIcon priority={issue.priority} top={-1} left={4} />
div![attrs![At::Class => "issuePriorityIcon"], priority_icon] div![attrs![At::Class => "issuePriorityIcon"], priority_icon]
], ],
div![attrs![At::Class => "assignees"], avatars,], div![attrs![At::Class => "assignees"], avatars,],

View File

@ -5,6 +5,7 @@ use crate::model::{Icon, Model};
use crate::Msg; use crate::Msg;
pub mod aside; pub mod aside;
pub mod modal;
pub mod navbar_left; pub mod navbar_left;
pub mod styled_avatar; pub mod styled_avatar;
pub mod styled_button; pub mod styled_button;

View File

@ -0,0 +1,68 @@
use seed::{prelude::*, *};
use crate::model::Icon;
use crate::shared::{styled_icon, ToNode};
use crate::Msg;
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
pub enum Variant {
Center,
Aside,
}
impl Variant {
pub fn to_class_name(&self) -> &str {
match self {
Variant::Center => "center",
Variant::Aside => "aside",
}
}
pub fn to_icon_class_name(&self) -> &str {
match self {
Variant::Center => "modalVariantCenter",
Variant::Aside => "modalVariantAside",
}
}
}
#[derive(Debug)]
pub struct Modal {
pub variant: Variant,
pub width: usize,
pub with_icon: bool,
pub children: Vec<Node<Msg>>,
}
impl ToNode for Modal {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
pub fn render(values: Modal) -> Node<Msg> {
let Modal {
variant,
width,
with_icon,
children,
} = values;
let icon = if with_icon {
let mut styled_icon = styled_icon(Icon::Close);
styled_icon.add_class(variant.to_icon_class_name().to_string());
styled_icon
} else {
empty![]
};
div![
attrs![At::Class => "modal"],
div![
attrs![At::Class => format!("clickableOverlay {}", variant.to_class_name())],
div![
attrs![At::Class => format!("styledModal {}", variant.to_class_name())],
icon,
children
]
]
]
}

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color } from 'shared/utils/styles';
export const List = styled.div` export const List = styled.div`
display: flex; display: flex;
@ -17,7 +17,9 @@ export const Title = styled.div`
text-transform: uppercase; text-transform: uppercase;
color: ${color.textMedium}; color: ${color.textMedium};
font-size: 12.5px; font-size: 12.5px;
${mixin.truncateText} overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`; `;
export const IssuesCount = styled.span` export const IssuesCount = styled.span`

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles'; import { color, font, mixin, sizes, zIndexValues } from 'shared/utils/styles';
export const Sidebar = styled.div` export const Sidebar = styled.div`
position: fixed; position: fixed;
@ -12,7 +12,9 @@ export const Sidebar = styled.div`
padding: 0 16px 24px; padding: 0 16px 24px;
background: ${color.backgroundLightest}; background: ${color.backgroundLightest};
border-right: 1px solid ${color.borderLightest}; border-right: 1px solid ${color.borderLightest};
${mixin.scrollableY} overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
${mixin.customScrollbar()} ${mixin.customScrollbar()}
@media (max-width: 1100px) { @media (max-width: 1100px) {
width: ${sizes.secondarySideBarWidth - 10}px; width: ${sizes.secondarySideBarWidth - 10}px;

View File

@ -92,7 +92,9 @@ export const TimeSection = styled.div`
width: 90px; width: 90px;
padding: 5px 0; padding: 5px 0;
border-left: 1px solid ${color.borderLight}; border-left: 1px solid ${color.borderLight};
${mixin.scrollableY} overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
`; `;
export const Time = styled.div` export const Time = styled.div`

View File

@ -1,6 +1,6 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { color, mixin, zIndexValues } from 'shared/utils/styles'; import { color, zIndexValues } from 'shared/utils/styles';
import Icon from 'shared/components/Icon'; import Icon from 'shared/components/Icon';
export const ScrollOverlay = styled.div` export const ScrollOverlay = styled.div`
@ -10,7 +10,9 @@ export const ScrollOverlay = styled.div`
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
${mixin.scrollableY} overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
`; `;
export const ClickableOverlay = styled.div` export const ClickableOverlay = styled.div`
@ -42,11 +44,11 @@ const modalStyles = {
max-width: ${props => props.width}px; max-width: ${props => props.width}px;
vertical-align: middle; vertical-align: middle;
border-radius: 3px; border-radius: 3px;
${mixin.boxShadowMedium} box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);
`, `,
aside: css` aside: css`
min-height: 100vh;
max-width: ${props => props.width}px; max-width: ${props => props.width}px;
min-height: 100vh;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
`, `,
}; };
@ -57,7 +59,7 @@ export const CloseIcon = styled(Icon)`
color: ${color.textMedium}; color: ${color.textMedium};
transition: all 0.1s; transition: all 0.1s;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
${props => closeIconStyles[props.variant]} ${props => closeIconStyles[props.variant]}
`; `;
@ -81,7 +83,7 @@ const closeIconStyles = {
text-align: center; text-align: center;
background: #fff; background: #fff;
border: 1px solid ${color.borderLightest}; border: 1px solid ${color.borderLightest};
${mixin.boxShadowMedium}; box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);;
&:hover { &:hover {
color: ${color.primary}; color: ${color.primary};
} }

View File

@ -1,34 +1,11 @@
import React, { Fragment, useState, useRef, useEffect, useCallback } from 'react'; import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown'; import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Styles'; import { ClickableOverlay, CloseIcon, ScrollOverlay, StyledModal } from './Styles';
const propTypes = {
className: PropTypes.string,
testid: PropTypes.string,
variant: PropTypes.oneOf(['center', 'aside']),
width: PropTypes.number,
withCloseIcon: PropTypes.bool,
isOpen: PropTypes.bool,
onClose: PropTypes.func,
renderLink: PropTypes.func,
renderContent: PropTypes.func.isRequired,
};
const defaultProps = {
className: undefined,
testid: 'modal',
variant: 'center',
width: 600,
withCloseIcon: true,
isOpen: undefined,
onClose: () => {},
renderLink: () => {},
};
const Modal = ({ const Modal = ({
className, className,
@ -76,26 +53,48 @@ const Modal = ({
<ScrollOverlay> <ScrollOverlay>
<ClickableOverlay variant={variant} ref={$clickableOverlayRef}> <ClickableOverlay variant={variant} ref={$clickableOverlayRef}>
<StyledModal <StyledModal
className={className} className={className}
variant={variant} variant={ variant }
width={width} width={ width }
data-testid={testid} data-testid={ testid }
ref={$modalRef} ref={ $modalRef }
> >
{withCloseIcon && <CloseIcon type="close" variant={variant} onClick={closeModal} />} { withCloseIcon && <CloseIcon type="close" variant={ variant } onClick={ closeModal }/> }
{renderContent({ close: closeModal })} { renderContent({ close: closeModal }) }
</StyledModal> </StyledModal>
</ClickableOverlay> </ClickableOverlay>
</ScrollOverlay>, </ScrollOverlay>,
$root, $root,
)} ) }
</Fragment> </Fragment>
); );
}; };
const $root = document.getElementById('root'); const $root = document.getElementById('root');
Modal.propTypes = propTypes; Modal.propTypes = {
Modal.defaultProps = defaultProps; className: PropTypes.string,
testid: PropTypes.string,
variant: PropTypes.oneOf([ 'center', 'aside' ]),
width: PropTypes.number,
withCloseIcon: PropTypes.bool,
isOpen: PropTypes.bool,
onClose: PropTypes.func,
renderLink: PropTypes.func,
renderContent: PropTypes.func.isRequired,
};
Modal.defaultProps = {
className: undefined,
testid: 'modal',
variant: 'center',
width: 600,
withCloseIcon: true,
isOpen: undefined,
onClose: () => {
},
renderLink: () => {
},
};
export default Modal; export default Modal;

View File

@ -1,6 +1,6 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { color, font, mixin, zIndexValues } from 'shared/utils/styles'; import { color, mixin, zIndexValues } from 'shared/utils/styles';
import Icon from 'shared/components/Icon'; import Icon from 'shared/components/Icon';
export const StyledSelect = styled.div` export const StyledSelect = styled.div`
@ -124,7 +124,9 @@ export const ClearIcon = styled(Icon)`
export const Options = styled.div` export const Options = styled.div`
max-height: 200px; max-height: 200px;
${mixin.scrollableY}; overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;;
${mixin.customScrollbar()}; ${mixin.customScrollbar()};
`; `;