Better select

This commit is contained in:
Adrian Wozniak 2020-04-01 22:41:26 +02:00
parent 41fe733be5
commit 801f07648d
11 changed files with 238 additions and 92 deletions

View File

@ -28,6 +28,27 @@
margin-left: 4px;
}
.issueDetails > .topActions .styledSelect > .dropDown > .options > .option > .type {
display: flex;
align-items: center;
}
.issueDetails > .topActions .styledSelect > .dropDown > .options > .option > .type > .styledIcon {
font-size: 18px;
}
.issueDetails > .topActions .styledSelect > .dropDown > .options > .option > .type > .typeLabel {
padding: 0 5px 0 7px;
font-size: 15px
}
.issueDetails > .topActions .styledSelect > .valueContainer > .value {
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--textMedium);
font-size: 13px
}
.issueDetails > .sectionTitle {
margin: 24px 0 5px;
text-transform: uppercase;

View File

@ -35,7 +35,29 @@
box-shadow: none;
}
.styledSelect > .dropDownInput {
.styledSelect > .valueContainer {
display: flex;
align-items: center;
width: 100%;
}
.styledSelect > .valueContainer.normal {
min-height: 32px;
padding: 5px 5px 5px 10px;
}
.styledSelect > .dropDown {
z-index: var(--dropdown);
position: absolute;
top: 100%;
left: 0;
border-radius: 0 0 4px 4px;
background: #fff;
box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px;
width: 100%;
}
.styledSelect > .dropDown > .dropDownInput {
padding: 10px 14px 8px;
width: 100%;
border: none;
@ -43,7 +65,7 @@
background: none;
}
.styledSelect > .dropDownInput:focus {
.styledSelect > .dropDown > .dropDownInput:focus {
outline: none;
}
@ -54,34 +76,38 @@
-webkit-overflow-scrolling: touch;
}
.styledSelect > .options::-webkit-scrollbar {
.styledSelect > .dropDown > .options::-webkit-scrollbar {
width: 8px;
}
.styledSelect > .options::-webkit-scrollbar-track {
.styledSelect > .dropDown > .options::-webkit-scrollbar-track {
background: none;
}
.styledSelect > .options::-webkit-scrollbar-thumb {
.styledSelect > .dropDown > .options::-webkit-scrollbar-thumb {
border-radius: 99px;
background: var(--backgroundMedium);
}
.styledSelect > .options > .option {
.styledSelect > .dropDown > .options > .option {
padding: 8px 14px;
word-break: break-word;
cursor: pointer;
}
.styledSelect > .options > .option:last-of-type {
.styledSelect > .dropDown > .options > .option:last-of-type {
margin-bottom: 8px;
}
.styledSelect > .options > .option.jira-select-option-is-active {
.styledSelect > .dropDown > .options > .option.jira-select-option-is-active {
background: var(--backgroundLightPrimary);
}
.styledSelect > .noOptions {
.styledSelect > .dropDown > .options > .option:hover {
background: var(--backgroundLightPrimary);
}
.styledSelect > .dropDown > .noOptions {
padding: 5px 15px 15px;
color: var(--textLight);
}

View File

@ -142,6 +142,15 @@ pub enum Icon {
ArrowRight,
}
impl Icon {
pub fn to_color(self) -> Option<String> {
match self {
Icon::Bug | Icon::Task | Icon::Story => Some(format!("var(--{})", self)),
_ => None,
}
}
}
impl std::fmt::Display for Icon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let code = match self {

View File

@ -9,7 +9,7 @@ use crate::shared::styled_button::{StyledButton, Variant as ButtonVariant};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::StyledSelect;
use crate::shared::{drag_ev, find_issue, inner_layout, ToNode};
use crate::Msg;
use crate::{IssueId, Msg};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
match msg {
@ -177,6 +177,7 @@ fn header() -> Node<Msg> {
text: Some("Github Repo".to_string()),
icon: Some(Icon::Github),
on_click: None,
children: vec![],
}
.into_node();
div![
@ -208,6 +209,7 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
text: Some("Only My Issues".to_string()),
icon: None,
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)),
children: vec![],
}
.into_node();
@ -219,6 +221,7 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
text: Some("Recently Updated".to_string()),
icon: None,
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)),
children: vec![],
}
.into_node();
@ -392,23 +395,58 @@ fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node<Ms
]
}
impl ToNode for IssueType {
fn into_node(self) -> Node<Msg> {
div![self.to_string()]
#[derive(PartialOrd, PartialEq, Debug)]
struct IssueTypeOption(IssueId, IssueType);
impl crate::shared::styled_select::SelectOption for IssueTypeOption {
fn into_option(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
let mut icon = crate::shared::styled_icon(self.1.into());
icon.add_class("issueTypeIcon");
div![
attrs![At::Class => "type"],
icon,
div![attrs![At::Class => "typeLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let issue_id = self.0;
let name = self.1.to_label().to_owned();
StyledButton {
variant: ButtonVariant::Empty,
icon_only: true,
disabled: false,
active: false,
text: None,
icon: Some(self.1.into()),
on_click: None,
children: vec![span![format!("{}-{}", name, issue_id)]],
}
.into_node()
}
}
fn issue_details(_model: &Model, _issue: &Issue) -> Node<Msg> {
fn issue_details(_model: &Model, issue: &Issue) -> Node<Msg> {
let issue_id = issue.id;
let issue_type_select = StyledSelect {
on_change: mouse_ev(Ev::Click, |_| Msg::NoOp),
variant: crate::shared::styled_select::Variant::Empty,
width: 150,
name: None,
dropdown_width: Some(150),
name: Some("type".to_string()),
placeholder: None,
valid: false,
valid: true,
is_multi: false,
allow_clear: true,
options: vec![IssueType::Story, IssueType::Task, IssueType::Bug],
allow_clear: false,
options: vec![
IssueTypeOption(issue_id, IssueType::Story),
IssueTypeOption(issue_id, IssueType::Task),
IssueTypeOption(issue_id, IssueType::Bug),
],
selected: Some(IssueTypeOption(issue_id, IssueType::Story)),
}
.into_node();

View File

@ -27,7 +27,14 @@ pub trait ToNode {
}
pub fn styled_icon(icon: Icon) -> Node<Msg> {
i![attrs![At::Class => format!("styledIcon {}", icon)], ""]
let style = icon
.to_color()
.map(|s| format!("color: {}", s))
.unwrap_or_default();
i![
attrs![At::Class => format!("styledIcon {}", icon), At::Style => style],
""
]
}
pub fn divider() -> Node<Msg> {

View File

@ -51,6 +51,7 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
icon_only: false,
icon: None,
on_click: None,
children: vec![],
}
.into_node();
let github_repo = StyledButton {
@ -61,6 +62,7 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
icon_only: false,
icon: Some(Icon::Github),
on_click: None,
children: vec![],
}
.into_node();

View File

@ -34,6 +34,7 @@ pub struct StyledButton {
pub text: Option<String>,
pub icon: Option<Icon>,
pub on_click: Option<EventHandler<Msg>>,
pub children: Vec<Node<Msg>>,
}
impl ToNode for StyledButton {
@ -51,6 +52,7 @@ pub fn render(values: StyledButton) -> Node<Msg> {
active,
icon,
on_click,
children,
} = values;
let mut class_list = vec!["styledButton".to_string(), variant.to_string()];
if icon_only {
@ -72,7 +74,7 @@ pub fn render(values: StyledButton) -> Node<Msg> {
Some(i) => styled_icon(i),
};
button![
seed::button![
attrs![
At::Class => class_list.join(" "),
],
@ -82,6 +84,10 @@ pub fn render(values: StyledButton) -> Node<Msg> {
false => vec![],
},
icon_node,
span![attrs![At::Class => "text"], text.unwrap_or_default()],
span![
attrs![At::Class => "text"],
text.unwrap_or_default(),
children
],
]
}

View File

@ -9,24 +9,40 @@ pub enum Variant {
Normal,
}
impl std::fmt::Display for Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Variant::Empty => f.write_str("empty"),
Variant::Normal => f.write_str("normal"),
}
}
}
pub trait SelectOption {
fn into_option(self) -> Node<Msg>;
fn into_value(self) -> Node<Msg>;
}
pub struct StyledSelect<Child>
where
Child: ToNode,
Child: SelectOption + PartialEq,
{
pub on_change: EventHandler<Msg>,
pub variant: Variant,
pub width: usize,
pub dropdown_width: Option<usize>,
pub name: Option<String>,
pub placeholder: Option<String>,
pub valid: bool,
pub is_multi: bool,
pub allow_clear: bool,
pub options: Vec<Child>,
pub selected: Option<Child>,
}
impl<Child> ToNode for StyledSelect<Child>
where
Child: ToNode,
Child: SelectOption + PartialEq,
{
fn into_node(self) -> Node<Msg> {
render(self)
@ -35,59 +51,85 @@ where
pub fn render<Child>(values: StyledSelect<Child>) -> Node<Msg>
where
Child: ToNode,
Child: SelectOption + PartialEq,
{
let StyledSelect {
on_change,
variant,
width,
dropdown_width,
name,
placeholder,
placeholder: _,
valid,
is_multi,
is_multi: _,
allow_clear,
options,
selected,
} = values;
let select_style = format!("width: {width}px", width = width);
let mut select_class = vec!["styledSelect"];
let dropdown_style = dropdown_width
.map(|n| format!("width: {}px", n))
.unwrap_or_default();
let mut select_class = vec!["styledSelect".to_string(), format!("{}", variant)];
if !valid {
select_class.push("invalid");
select_class.push("invalid".to_string());
}
let children: Vec<Node<Msg>> = options
.into_iter()
.map(|child| render_option(child.into_node()))
.filter(|o| Some(o) != selected.as_ref())
.map(|child| render_option(child.into_option()))
.collect();
let value = selected
.map(|m| render_value(m.into_value()))
.unwrap_or_else(|| empty![]);
let clear_icon = match allow_clear {
true => crate::shared::styled_icon(crate::model::Icon::Close),
false => empty![],
};
seed::div![
on_change.clone(),
attrs![At::Class => "styledSelect", At::Style => select_style],
seed::input![
attrs![
At::Class => "dropDownInput",
At::Type => "text"
At::Placeholder => "Search"
At::AutoFocus => true,
],
on_change,
],
clear_icon,
let option_list = if children.is_empty() {
seed::div![attrs![At::Class => "noOptions"], "No results"]
} else {
seed::div![
attrs![
At::Class => "options",
],
children
]
};
seed::div![
on_change.clone(),
attrs![At::Class => select_class.join(" ")],
div![
attrs![At::Class => format!("valueContainer {}", variant)],
value
],
seed::div![attrs![At::Class => "noOptions"], "No results"]
div![
attrs![At::Class => "dropDown", At::Style => dropdown_style],
seed::input![
attrs![
At::Name => name.unwrap_or_default(),
At::Class => "dropDownInput",
At::Type => "text"
At::Placeholder => "Search"
At::AutoFocus => true,
],
on_change,
],
clear_icon,
option_list
]
]
}
pub fn render_option(content: Node<Msg>) -> Node<Msg> {
seed::div![attrs![At::Class => "option"], content,]
fn render_option(content: Node<Msg>) -> Node<Msg> {
div![attrs![At::Class => "option"], content]
}
fn render_value(mut content: Node<Msg>) -> Node<Msg> {
content.add_class("value");
content
}

View File

@ -28,15 +28,12 @@ pub enum IssueType {
Story,
}
impl FromStr for IssueType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"task" => Ok(IssueType::Task),
"bug" => Ok(IssueType::Bug),
"story" => Ok(IssueType::Story),
_ => Err(format!("Unknown type {:?}", s)),
impl IssueType {
pub fn to_label(&self) -> &str {
match self {
IssueType::Task => "Task",
IssueType::Bug => "Bug",
IssueType::Story => "Story",
}
}
}

View File

@ -1,42 +1,40 @@
import React from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { IssueType, IssueTypeCopy } from '../../../../shared/constants/issues';
import { IssueTypeIcon, Select } from '../../../../shared/components';
import { IssueTypeIcon, Select } from '../../../../shared/components';
import { Type, TypeButton, TypeLabel } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
<Select
variant="empty"
dropdownWidth={ 150 }
withClearValue={ false }
name="type"
value={ issue.type }
options={ Object.values(IssueType).map(type => ({
value: type,
label: IssueTypeCopy[type],
})) }
onChange={ type => updateIssue({ type }) }
renderValue={ ({ value: type }) => (
<TypeButton variant="empty" icon={ <IssueTypeIcon type={ type }/> }>
{ `${ IssueTypeCopy[type] }-${ issue.id }` }
</TypeButton>
) }
renderOption={ ({ value: type }) => (
<Type key={ type } onClick={ () => updateIssue({ type }) }>
<IssueTypeIcon type={ type } top={ 1 }/>
<TypeLabel>{ IssueTypeCopy[type] }</TypeLabel>
</Type>
) }
/>
);
ProjectBoardIssueDetailsType.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
<Select
variant="empty"
dropdownWidth={150}
withClearValue={false}
name="type"
value={issue.type}
options={Object.values(IssueType).map(type => ({
value: type,
label: IssueTypeCopy[type],
}))}
onChange={type => updateIssue({ type })}
renderValue={({ value: type }) => (
<TypeButton variant="empty" icon={<IssueTypeIcon type={type} />}>
{`${IssueTypeCopy[type]}-${issue.id}`}
</TypeButton>
)}
renderOption={({ value: type }) => (
<Type key={type} onClick={() => updateIssue({ type })}>
<IssueTypeIcon type={type} top={1} />
<TypeLabel>{IssueTypeCopy[type]}</TypeLabel>
</Type>
)}
/>
);
ProjectBoardIssueDetailsType.propTypes = propTypes;
export default ProjectBoardIssueDetailsType;

View File

@ -1,7 +1,7 @@
import styled, { css } from 'styled-components';
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`
position: relative;
@ -96,7 +96,7 @@ export const Dropdown = styled.div`
left: 0;
border-radius: 0 0 4px 4px;
background: #fff;
${mixin.boxShadowDropdown}
box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px;
${props => (props.width ? `width: ${props.width}px;` : 'width: 100%;')}
`;