Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
[MM-28199] Dropdown Input Common Component (#6543)
Browse files Browse the repository at this point in the history
* WIP

* [MM-28199] Dropdown Input Common Component

* Remove commented code

* Inset focus borders

* More CSS fixes

* Refactor to use classnames

* Blank line

* Added cursor:pointer for the input and menu

Co-authored-by: Mattermod <[email protected]>
  • Loading branch information
devinbinnie and mattermod committed Oct 1, 2020
1 parent abe2586 commit f8e7fc9
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 26 deletions.
15 changes: 14 additions & 1 deletion components/admin_console/billing/billing_subscriptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {FormattedMessage} from 'react-intl';
import {Tooltip} from 'react-bootstrap';

import AlertBanner from 'components/alert_banner';
import DropdownInput from 'components/dropdown_input';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import OverlayTrigger from 'components/overlay_trigger';
import FormattedAdminHeader from 'components/widgets/admin_console/formatted_admin_header';
Expand Down Expand Up @@ -106,6 +107,8 @@ const BillingSubscriptions: React.FC<Props> = () => {
const [showWarning, setShowWarning] = useState(true);
const [showInfo, setShowInfo] = useState(true);

const [dropdownValue, setDropdownValue] = useState<{label: string, value: string} | undefined>(undefined);

return (
<div className='wrapper--fixed BillingSubscriptions'>
<FormattedAdminHeader
Expand Down Expand Up @@ -142,8 +145,18 @@ const BillingSubscriptions: React.FC<Props> = () => {
className='BillingSubscriptions__topWrapper'
style={{marginTop: '20px'}}
>
<div style={{border: '1px solid #000', width: '568px'}}>
<div style={{border: '1px solid #000', width: '568px', padding: '8px', backgroundColor: '#fff'}}>
{'Plan Details Card'}
<DropdownInput
onChange={(value) => setDropdownValue(value)}
value={dropdownValue}
options={[{label: 'Option 1', value: 'option-1'}, {label: 'Option 2', value: 'option-2'}, {label: 'Option 3', value: 'option-3'}]}
legend={'Test dropdown'}
placeholder='Select item here'
name='BillingSubscriptions__testDropdown'
error={dropdownValue ? undefined : 'This field is required'}
/>
<br/>
<OverlayTrigger
delayShow={500}
placement='bottom'
Expand Down
50 changes: 50 additions & 0 deletions components/dropdown_input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.DropdownInput {
&.Input_container {
margin-top: 20px;
}

.Input_wrapper {
padding: 0 1px;
}

.Input {
font-size: 14px;
line-height: 20px;

&.Input___focus .a11y--focused {
box-shadow: none;
}
}

.Input_legend {
margin-left: 8px;
}
}

.DropdownInput__indicatorsContainer {
margin-right: 8px;

i {
color: rgba(var(--center-channel-color-rgb), 0.64);
font-weight: normal;

&::before {
margin: 0;
}
}
}

.DropdownInput__option > div {
padding: 10px 24px;
line-height: 16px;
cursor: pointer;
}

.DropdownInput__option.selected > div {
background-color: rgba(var(--button-bg-rgb), 0.08);
color: var(--center-channel-color);
}

.DropdownInput__option.focused > div {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
144 changes: 144 additions & 0 deletions components/dropdown_input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useState, CSSProperties} from 'react';
import ReactSelect, {Props as SelectProps, ActionMeta, components} from 'react-select';
import classNames from 'classnames';

import './dropdown_input.scss';

// TODO: This component needs work, should not be used outside of AddressInfo until this comment is removed.

type ValueType = {
label: string;
value: string;
}

type Props<T> = Omit<SelectProps<T>, 'onChange'> & {
value?: T;
legend?: string;
error?: string;
onChange: (value: T, action: ActionMeta<T>) => void;
};

const baseStyles = {
input: (provided: CSSProperties) => ({
...provided,
color: 'var(--center-channel-color)',
}),
control: (provided: CSSProperties) => ({
...provided,
border: 'none',
boxShadow: 'none',
padding: '0 2px',
cursor: 'pointer',
}),
indicatorSeparator: (provided: CSSProperties) => ({
...provided,
display: 'none',
}),
};

const IndicatorsContainer = (props: any) => {
return (
<div className='DropdownInput__indicatorsContainer'>
<components.IndicatorsContainer {...props}>
<i className='icon icon-chevron-down'/>
</components.IndicatorsContainer>
</div>
);
};

const Option = (props: any) => {
return (
<div
className={classNames('DropdownInput__option', {
selected: props.isSelected,
focused: props.isFocused,
})}
>
<components.Option {...props}/>
</div>
);
};

const renderError = (error?: string) => {
if (!error) {
return null;
}

return (
<div className='Input___error'>
<i className='icon icon-alert-outline'/>
<span>{error}</span>
</div>
);
};

const DropdownInput = <T extends ValueType>(props: Props<T>) => {
const {value, placeholder, className, addon, name, textPrefix, legend, onChange, styles, options, error, ...otherProps} = props;

const [focused, setFocused] = useState(false);

const onInputFocus = (event: React.FocusEvent<HTMLElement>) => {
const {onFocus} = props;

setFocused(true);

if (onFocus) {
onFocus(event);
}
};

const onInputBlur = (event: React.FocusEvent<HTMLElement>) => {
const {onBlur} = props;

setFocused(false);

if (onBlur) {
onBlur(event);
}
};

const showLegend = Boolean(focused || value);

return (
<div className='DropdownInput Input_container'>
<fieldset
className={classNames('Input_fieldset', className, {
Input_fieldset___error: error,
Input_fieldset___legend: showLegend,
})}
>
<legend className={classNames('Input_legend', {Input_legend___focus: showLegend})}>
{showLegend ? (legend || placeholder) : null}
</legend>
<div
className='Input_wrapper'
onFocus={onInputFocus}
onBlur={onInputBlur}
>
{textPrefix && <span>{textPrefix}</span>}
<ReactSelect
id={`DropdownInput_${name}`}
options={options}
placeholder={focused ? '' : placeholder}
components={{
IndicatorsContainer,
Option,
}}
className={classNames('Input', className, {Input__focus: showLegend})}
value={value}
onChange={onChange as any} // types are not working correctly for multiselect
styles={{...baseStyles, ...styles}}
{...otherProps}
/>
</div>
{addon}
</fieldset>
{renderError(error)}
</div>
);
};

export default DropdownInput;
21 changes: 15 additions & 6 deletions components/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
font-size: 12px;
line-height: 30px;
display: flex;
margin: 2px 0;
}

.Input_container {
Expand Down Expand Up @@ -74,9 +75,13 @@
padding: 0 1px;
}

.Input_fieldset:hover {
border-color: rgba(var(--center-channel-color-rgb), 0.48);
}

.Input_fieldset:focus-within {
border-color: transparent;
box-shadow: 0 0 0 2px var(--button-bg);
border-color: var(--button-bg);
box-shadow: inset 0 0 0 1px var(--button-bg);
color: var(--button-bg);
}

Expand All @@ -89,12 +94,16 @@

.Input_fieldset___error {
color: var(--error-text);
border-color: transparent;
box-shadow: 0 0 0 1px var(--error-text);
border-color: var(--error-text);
}
.Input_fieldset___error:focus-within {
color: var(--error-text);
box-shadow: 0 0 0 2px var(--error-text);
border-color: var(--error-text);
box-shadow: inset 0 0 0 1px var(--error-text);
}

.Input_fieldset___error:hover {
border-color: var(--error-text);
}

.Input_legend {
Expand All @@ -109,7 +118,7 @@
font-size: 10px;
line-height: 16px;
z-index: 4;
top: -10px;
top: -8px;
background-color: var(--center-channel-bg);
padding: 0 4px;
white-space: nowrap;
Expand Down
23 changes: 12 additions & 11 deletions components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See LICENSE.txt for license information.

import React from 'react';
import classNames from 'classnames';

import './input.css';

Expand Down Expand Up @@ -98,26 +99,26 @@ export default class Input extends React.PureComponent<Props, State> {
public render() {
const {value, placeholder, className, error: propError, hasError, addon, name, textPrefix, ...otherProps} = this.props;
const {focused, error: stateError} = this.state;
let inputClass = className ? `Input form-control ${className}` : 'Input form-control';
let fieldsetClass = className ? `Input_fieldset ${className}` : 'Input_fieldset';
let fieldsetErrorClass = className ? `Input_fieldset Input_fieldset___error ${className}` : 'Input_fieldset Input_fieldset___error';
const showLegend = Boolean(focused || value);

inputClass = showLegend ? inputClass + ' Input___focus' : inputClass;
fieldsetClass = showLegend ? fieldsetClass + ' Input_fieldset___legend' : fieldsetClass;
fieldsetErrorClass = showLegend ? fieldsetErrorClass + ' Input_fieldset___legend' : fieldsetErrorClass;

const showLegend = Boolean(focused || value);
const error = propError || stateError;

return (
<div className='Input_container'>
<fieldset className={error || hasError ? fieldsetErrorClass : fieldsetClass}>
<legend className={showLegend ? 'Input_legend Input_legend___focus' : 'Input_legend'}>{showLegend ? placeholder : null}</legend>
<fieldset
className={classNames('Input_fieldset', className, {
Input_fieldset___error: error || hasError,
Input_fieldset___legend: showLegend,
})}
>
<legend className={classNames('Input_legend', {Input_legend___focus: showLegend})}>
{showLegend ? placeholder : null}
</legend>
<div className='Input_wrapper'>
{textPrefix && <span>{textPrefix}</span>}
<input
id={`input_${name}`}
className={inputClass}
className={classNames('Input form-control', className, {Input__focus: showLegend})}
value={value}
placeholder={focused ? '' : placeholder}
name={name}
Expand Down
17 changes: 9 additions & 8 deletions components/multi_input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,18 @@ const MultiInput = <T extends ValueType>(props: Props<T>) => {
}
};

let inputClass = className ? `Input ${className}` : 'Input';
let fieldsetClass = className ? `Input_fieldset ${className}` : 'Input_fieldset';
const showLegend = Boolean(focused || value.length);

inputClass = showLegend ? inputClass + ' Input___focus' : inputClass;
fieldsetClass = showLegend ? fieldsetClass + ' Input_fieldset___legend' : fieldsetClass;

return (
<div className='MultiInput Input_container'>
<fieldset className={fieldsetClass}>
<legend className={showLegend ? 'Input_legend Input_legend___focus' : 'Input_legend'}>{showLegend ? (legend || placeholder) : null}</legend>
<fieldset
className={classNames('Input_fieldset', className, {
Input_fieldset___legend: showLegend,
})}
>
<legend className={classNames('Input_legend', {Input_legend___focus: showLegend})}>
{showLegend ? (legend || placeholder) : null}
</legend>
<div
className='Input_wrapper'
onFocus={onInputFocus}
Expand All @@ -109,7 +110,7 @@ const MultiInput = <T extends ValueType>(props: Props<T>) => {
openMenuOnFocus={false}
menuIsOpen={false}
placeholder={focused ? '' : placeholder}
className={inputClass}
className={classNames('Input', className, {Input__focus: showLegend})}
value={value}
onChange={onChange as any} // types are not working correctly for multiselect
styles={{...baseStyles, ...styles}}
Expand Down

0 comments on commit f8e7fc9

Please sign in to comment.