From 9a3a700c79fb2fe44a829092abef65662f4c87e3 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Tue, 20 Jun 2017 12:13:26 -0700 Subject: [PATCH 01/16] add smart list and text-dropdown --- .../bulk-actions/actions-dropdown.css | 12 +- .../bulk-actions/actions-dropdown.jsx | 17 +- ashes/src/components/core/dropdown/index.js | 2 + .../core/dropdown/smart-list/smart-list.css | 26 +++ .../core/dropdown/smart-list/smart-list.jsx | 193 ++++++++++++++++++ .../core/dropdown/smart-list/smart-list.md | 12 ++ .../dropdown/text-dropdown/text-dropdown.css | 56 +++++ .../dropdown/text-dropdown/text-dropdown.jsx | 182 +++++++++++++++++ .../dropdown/text-dropdown/text-dropdown.md | 27 +++ .../src/components/docs/colors/bg-colors.jsx | 2 +- .../components/docs/colors/text-colors.jsx | 2 +- .../src/components/dropdown/.dropdown.jsx.swo | Bin 16384 -> 0 bytes .../components/dropdown/generic-dropdown.jsx | 60 +++--- ashes/src/less/modules/_tables.less | 1 + ashes/styleguide/config.styleguide.js | 9 + 15 files changed, 558 insertions(+), 43 deletions(-) create mode 100644 ashes/src/components/core/dropdown/index.js create mode 100644 ashes/src/components/core/dropdown/smart-list/smart-list.css create mode 100644 ashes/src/components/core/dropdown/smart-list/smart-list.jsx create mode 100644 ashes/src/components/core/dropdown/smart-list/smart-list.md create mode 100644 ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css create mode 100644 ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx create mode 100644 ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md delete mode 100644 ashes/src/components/dropdown/.dropdown.jsx.swo diff --git a/ashes/src/components/bulk-actions/actions-dropdown.css b/ashes/src/components/bulk-actions/actions-dropdown.css index 9736f3024e..d002db630b 100644 --- a/ashes/src/components/bulk-actions/actions-dropdown.css +++ b/ashes/src/components/bulk-actions/actions-dropdown.css @@ -1,5 +1,9 @@ -/* @todo refactor dropdown */ -.button { - padding: 10px !important; - width: auto !important; +.actions { + display: flex; + flex: 1 1 auto; + align-items: baseline; +} + +.dropdown { + margin-right: 10px; } diff --git a/ashes/src/components/bulk-actions/actions-dropdown.jsx b/ashes/src/components/bulk-actions/actions-dropdown.jsx index 39f64414a6..89b7fc6875 100644 --- a/ashes/src/components/bulk-actions/actions-dropdown.jsx +++ b/ashes/src/components/bulk-actions/actions-dropdown.jsx @@ -5,12 +5,13 @@ import PropTypes from 'prop-types'; // components import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; // styles import s from './actions-dropdown.css'; function getActionsHandler(actions, allChecked, toggledIds) { - return (value) => { + return ({ value }) => { const handler = _.find(actions, ([label, handler]) => label === value)[1]; handler(allChecked, toggledIds); }; @@ -18,20 +19,20 @@ function getActionsHandler(actions, allChecked, toggledIds) { const ActionsDropdown = ({actions, disabled, allChecked, toggledIds, total}) => { const totalSelected = allChecked ? total - toggledIds.length : toggledIds.length; + const items = actions.map(([title]) => ({ value: title })); return ( -
- + [title, title])} - buttonClassName={s.button} + items={items} + stateless /> { totalSelected > 0 ? ( - + {totalSelected} Selected ) : null} diff --git a/ashes/src/components/core/dropdown/index.js b/ashes/src/components/core/dropdown/index.js new file mode 100644 index 0000000000..eb97807de3 --- /dev/null +++ b/ashes/src/components/core/dropdown/index.js @@ -0,0 +1,2 @@ +export SmartList from './smart-list/smart-list'; +export TextDropdown from './text-dropdown/text-dropdown'; diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.css b/ashes/src/components/core/dropdown/smart-list/smart-list.css new file mode 100644 index 0000000000..e070be0bb1 --- /dev/null +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.css @@ -0,0 +1,26 @@ +@import 'variables.css'; + +.block { + position: absolute; + max-height: 90vh; + min-width: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + + @media(max-height: 300px) { + max-height: 60vh; + } +} + +.list { + overflow-y: auto; + flex: 1 1 auto; +} + +.item { + &:hover, + &.active { + background-color: var(--bg-grey-headers); + } +} diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx new file mode 100644 index 0000000000..f67d424ca1 --- /dev/null +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -0,0 +1,193 @@ +/* @flow */ + +// libs +import _ from 'lodash'; +import React, { Element, Component } from 'react'; +import { autobind } from 'core-decorators'; +import classNames from 'classnames'; + +import Icon from 'components/core/icon'; +import BodyPortal from 'components/body-portal/body-portal'; + +// styles +import s from './smart-list.css'; + +type Props = { + /** Array of elements inside the list */ + children: Array; + /** Element which prepends the list */ + before?: Element | string; + /** Element which goes after the list */ + after?: Element | string; + align?: 'left' | 'right'; // @todo + /** Base element: list will try to stick around it. E.g. dropdown current value box. + * Default value: previous sibling or parent. */ + pivot?: HTMLElement; + className?: string; +}; + +type State = { + pointedValueIndex: number, // current hovered item, used for keyboard navigation +}; + +// looper for awwor keys navigation +function getNewItemIndex(itemsCount, currentIndex, increment = 1) { + const startIndex = increment > 0 ? -1 : 0; + const index = Math.max(currentIndex, startIndex); + + return (itemsCount + index + increment) % itemsCount; +} + +/** + * Smart List from dropdowns. + * This component knows how to position itself around pivot element and fit to the screen. + * Also component knows how to handle scroll and keyboard events. + */ +export default class SmartList extends Component { + props: Props; + + static defaultProps = { + align: 'right', + }; + + state: State = { + pointedValueIndex: -1, + }; + + componentDidMount() { + window.addEventListener('keydown', this.handleKeyPress, true); + this.setPosition(); + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.handleKeyPress, true); + } + + componentDidUpdate(prevProps: Props, prevState: State) { + this.setPosition(); + } + + get pivot() { + return this.props.pivot || this._block.previousElementSibling || this._block.parentElement; + } + + setDetachedCoords() { + const { detached } = this.props; + + if (!detached) { + return; + } + // `detached=true` – rare case, but we need to handle it + + const pivotDim = this.pivot.getBoundingClientRect(); + + this._block.style.minWidth = `${this.pivot.offsetWidth}px`; + this._block.style.top = `${pivotDim.top + pivotDim.height + window.scrollY}px`; + this._block.style.left = `${pivotDim.left}px`; + } + + setPosition() { + this.setDetachedCoords(); + + const viewportHeight = window.innerHeight; + + const pivotDim = this.pivot.getBoundingClientRect(); + const spaceAtTop = pivotDim.top; + const spaceAtBottom = viewportHeight - pivotDim.bottom; + const listRect = this._block.getBoundingClientRect(); + + if (spaceAtBottom < listRect.height && spaceAtBottom < spaceAtTop) { + this._block.style.transform = `translateY(calc(-100% - ${pivotDim.height}px))`; + } else { + this._block.style.transform = ''; + } + } + + scrollViewport(movingUp: boolean = false) { + const newIndex = this.state.pointedValueIndex; + const item = this._items.children[newIndex]; + + const containerTop = this._items.scrollTop; + const containerVisibleHeight = this._items.clientHeight; + const itemTop = item.offsetTop; + const itemHeight = item.offsetHeight; + + // shift height when compare to viewport top position - item height if moving up, zero otherwise + const heightShift = movingUp ? itemHeight : 0; + + const elementBelowViewport = containerTop + containerVisibleHeight <= itemTop + itemHeight; + const elementAboveViewport = containerTop > itemTop + heightShift; + + if (elementBelowViewport) { + this._items.scrollTop = itemTop + itemHeight - containerVisibleHeight; + } + if (elementAboveViewport) { + this._items.scrollTop = itemTop; + } + } + + @autobind + handleKeyPress(e: KeyboardEvent) { + const { pointedValueIndex } = this.state; + const itemsCount = React.Children.count(this.props.children); + + switch (e.keyCode) { + // enter + case 13: + e.stopPropagation(); + e.preventDefault(); + + if (pointedValueIndex > -1) { + this._items.children[pointedValueIndex].click(); + } + + break; + // up + case 38: + e.preventDefault(); + + this.setState({ + pointedValueIndex: getNewItemIndex(itemsCount, pointedValueIndex, -1), + }, () => this.scrollViewport(true)); + + break; + // down + case 40: + e.preventDefault(); + + this.setState({ + pointedValueIndex: getNewItemIndex(itemsCount, pointedValueIndex), + }, () => this.scrollViewport(false)); + + break; + } + } + + renderItems() { + const { children, emptyMessage } = this.props; + + // @todo only one child + return React.Children.map(children, (item, index) => { + const props: any = { + className: classNames(item.props.className, s.item, { [s.active]: index === this.state.pointedValueIndex }) + }; + + return React.cloneElement(item, props); + }); + } + + render() { + const { before, after, className } = this.props; + const cls = classNames(s.block, className); + + return ( +
this._block = m}> + {before} +
this._items = i}> + {this.renderItems()} +
+ {after} +
+ ); + } +} diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.md b/ashes/src/components/core/dropdown/smart-list/smart-list.md new file mode 100644 index 0000000000..14a58d2386 --- /dev/null +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.md @@ -0,0 +1,12 @@ +``` +
+
+ +
One
+
Two
+
Three
+
Four
+
Five
+
+
+``` diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css new file mode 100644 index 0000000000..a3fa3a2d11 --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css @@ -0,0 +1,56 @@ +@import 'variables.css'; + +.block { + position: relative; + font: var(--font-labels); + + &.open { + z-index: 1; + } +} + +.pivot { + display: flex; + padding: 10px; + background-color: var(--bg-grey-buttons); + cursor: pointer; + + &:hover { + background-color: var(--bg-grey-buttons-hover); + } + + .block.disabled & { + background-color: var(--bg-grey-buttons-hover); + color: var(--color-additional-text); + cursor: default; + } +} + +.displayText { + flex: 1 1 auto; + margin-right: 10px; + + .placeholder & { + color: var(--color-additional-text); + } +} + +.menu { + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5); +} + +.item { + padding: 12px 20px; + background-color: white; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 70vw; + font: var(--font-nav); +} + +.toggleBtn { + width: 40px; + height: 40px; +} diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx new file mode 100644 index 0000000000..4e6e5bedaa --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -0,0 +1,182 @@ +/* @flow */ + +// libs +import _ from 'lodash'; +import React, { Element, Component } from 'react'; +import createFragment from 'react-addons-create-fragment'; +import { autobind } from 'core-decorators'; +import classNames from 'classnames'; + +import Icon from 'components/core/icon'; +import { SmartList } from 'components/core/dropdown'; +import { Button } from 'components/core/button'; +import BodyPortal from 'components/body-portal/body-portal'; + +// styles +import s from './text-dropdown.css'; + +type Item = { + value: string; + displayText?: string; +}; + +type Props = { + /** Input name which will be used by form */ + name?: string, // input name + /** Input value */ + value?: string, // input value + /** Text which is visible when no value */ + placeholder?: string, + /** Additional root className */ + className?: string, + /** An array of all possible values which will be in a list */ + items?: Array, + /** If true, you cant open dropdown or change its value from UI */ + disabled?: bool, + /** Callback which fires when the value has been changes */ + onChange?: Function, + /** Goes to `bodyPortal`, e.g. case with overflow `/customers/10/storecredit` */ + detached?: boolean, + /** If true, the component can change its value only via props */ + stateless?: boolean, +}; + +type State = { + open: bool, // show or hide the menu + selectedValue: string, // current selected value of menu +}; + +/** + * Simple Dropdown component + * + * WARNING: It's important to implement shouldComponentUpdate hook in host components + */ +export default class TextDropdown extends Component { + props: Props; + + static defaultProps = { + name: '', + value: '', + placeholder: 'Select value', + disabled: false, + detached: false, + onChange: () => {}, + stateless: false, + }; + + state: State = { + open: false, + selectedValue: this.props.value, + }; + + _block: HTMLElement; // root element, used for clickOutside and others + + componentDidMount() { + window.addEventListener('click', this.handleClickOutside, true); + } + + componentWillUnmount() { + window.removeEventListener('click', this.handleClickOutside, true); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.value !== this.props.value) { + this.setState({ selectedValue: nextProps.value }); + } + } + + @autobind + handleClickOutside({ target }: { target: HTMLElement }) { + if (this._block && !this._block.contains(target) && this.state.open) { + this.closeMenu(); + } + } + + @autobind + handleToggleClick(event: any) { + event.preventDefault(); + + if (this.props.disabled) { + return; + } + + this.toggleMenu(); + } + + handleItemClick(item) { + const { stateless } = this.props; + let nextState = { open: false }; + + if (item.value !== this.state.selectedValue) { + if (!stateless) { + nextState.selectedValue = item.value; + } + + this.props.onChange(item); + } + + this.setState(nextState); + } + + toggleMenu() { + this.setState({ open: !this.state.open }); + } + + closeMenu() { + this.setState({ open: false }); + } + + get displayText() { + const { items, placeholder } = this.props; + const item = _.find(items, item => item.value === this.state.selectedValue); + + return (item && item.displayText) || this.state.selectedValue || placeholder; + } + + renderItems() { + const { items } = this.props; + + return ( + + {items.map(item => ( +
this.handleItemClick(item)}> + {item.displayText || item.value} +
+ ))} +
+ ); + } + + get menu(): ?Element { + if (!this.state.open) { + return; + } + + return ( + + {this.renderItems()} + + ); + } + + render() { + const { disabled, name, placeholder, className } = this.props; + const { selectedValue, open } = this.state; + const cls = classNames(s.block, className, { + [s.disabled]: disabled, + [s.open]: open, + }); + const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; + + return ( +
this._block = c} tabIndex="0"> +
+
{this.displayText}
+ + +
+ {this.menu} +
+ ); + } +} diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md new file mode 100644 index 0000000000..3c5d1cb821 --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md @@ -0,0 +1,27 @@ +#### Basic usage + +```javascript +import { TextDropdown } from 'components/core/dropdown'; + +const items = [ + { value: 'One' }, + { value: 'Two' }, + { value: 'Three', displayText: 'Three!' } +]; + + +``` + +``` +const items = [{ value: 'One' }, { value: 'Two' }, { value: 'Three', displayText: 'Three!' }]; + +
+
+ console.log(e)} value="One" /> +
+
+ +
+ +
+``` diff --git a/ashes/src/components/docs/colors/bg-colors.jsx b/ashes/src/components/docs/colors/bg-colors.jsx index 3441c7d852..1c765f4149 100644 --- a/ashes/src/components/docs/colors/bg-colors.jsx +++ b/ashes/src/components/docs/colors/bg-colors.jsx @@ -27,7 +27,7 @@ const BgColor = props => { export const BgColors = () => { return (
- {colors.map(cl => )} + {colors.map(cl => )}
); }; diff --git a/ashes/src/components/docs/colors/text-colors.jsx b/ashes/src/components/docs/colors/text-colors.jsx index 82ca0390a4..1487009935 100644 --- a/ashes/src/components/docs/colors/text-colors.jsx +++ b/ashes/src/components/docs/colors/text-colors.jsx @@ -27,7 +27,7 @@ const TextColor = props => { export const TextColors = () => { return (
- {colors.map(cl => )} + {colors.map(cl => )}
); }; diff --git a/ashes/src/components/dropdown/.dropdown.jsx.swo b/ashes/src/components/dropdown/.dropdown.jsx.swo deleted file mode 100644 index 930f77b3e2aa15680915a48bc0610d2527f6bfa0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI2TZklA8OM*&Y;@OMqwD2C#BauAyE^t%C*FuX)8on*b-=xtY(mVq%+yq!?yhWg zRc)Q>otgA@c0qkq@KGO(f#e||sGtc1pA_975(E*%3lY%-ToFYdGz#ng+^VbR5`FMN z2Yk-KPAtTyg3wR0Fh~wCV~@@>*zRT9?LpihBtaVSq8*3<7j035?JyhWS<2H= zw4)-+qiid!{_!6c+XuDp%AEpEfddp6mA!?#kFxi^=PtF>-!(r+$KQ1S0miP_Dc}@v z3OEIv0!{&^fK$LJ;1u|ORG`dnVplM*o6O)oWB$Hs*WXv0Ya9`%&u%+Y?EW|foB~b( zr+`zyDc}@v3OEIv0!{&^fK$LJ@IRsVf_x5}JU*EvkU%++n6Yvz6fXg5S zU2p{a`WRy`fS-bIfUkp3g0tZL;C9J9ry-D>w|EdL3-RXTif@4fx<4;Kpki`#Ja;xC$NuC%`S>Sbckv7xpkjk^w#}s7?+-EOBu9C z7Mzu~%@vQ<@7zuT-g9+!N$r|~R_R%Q7EK{Dc^1?RSE*Qj6fZuTCAHwXF z93fh2f5g0CBxPJS6GSvd42E0?S?ZN>nee97Z>e5`F3FJUj@6$DUEn+2@y2cA6mv0I zxXl!AOehu~kE4jE-Ld^|qB}FOuubyfxKgi&WpOs{Z*Vb(hJjYhUve zQt~J+<^0PiQmfHVc-k)q^dVBOTH$r8@=O?xih`#h_moMKI+(T??xDAx`f9Qcy>i3a z$}AE!+wfcN5wc-*U2jtr{0Eb+=0a~cW8EB0P;I)~)Ega>l)N<^eq{=Iav@DIV7z4} zCscS%84c5L&@IVmh~i7rAXtjid{nBIyD}hh;j4+Iawq5A`GK0z`CTDU@E|&qCOg!g zHZ)eVSOpL|p*XTl+Be*keyh=h!XyylG=|fi?}ZEY_WHVth zYx`9b&8CB1QcYI{OL7ihd7|v}n-%V)n1$>jft@?qF;!bS)<(4k_ZjV3g<_k8$fgTU)Cg zrEZS8YLB;e^}#MOzMW@9X&cmYMRw~Zq$g|#f~t%nyPD{Z)Ks10YPNJ)u!@dXcxaG?C4Wm>oknPExfNRXo3 zNx0s3bd&Q{K0`g3&f>|&WO*zvmfEt)GWK^|S^wntq(c}~$BajN0~_lLQd!4DtGp*= zQVGE-?5(Zi`3!-fGu?eEz&%;e#M9*`vMpY$1cG}_A7RS3&-8Q(o4wam1FLka=O^i( z>aar}E{b4hCYztbzP@a7mvvZssVAU%6#VzCG;RB8TUQOs%&o^NbG^y%dYEe6!(Vn@ zhDO8gZ4G&$^VZpd=ZILfR!q@pY!T;Vtknx`gsW{r-{8Z%+&P7ZnV@e>l^3|FM^!y% zV^nKq6_~|_iNpTVVlv^?7n{01D>EYMe2k~Oh(mRWA;frh8w*dFjP-LF1!BgkMU^V8 zYE7;pGfSP2<$Faoq=w(_>z1n=h+WV&uYy%nmNrZJp%AdbW3e`<>?>W+%br@YYt%BH zoAKBhmGwT}m>NoKV^@23y_Xe3(JryqEW~ox){adLD(A*3<;sn>8hn@)d?DiagIDhh zIVRKBy*voZ7L99LmlX_;9--{SSgsq7k}Sfj^}6#U-V}_r@1T>tB-^4p+Dv}`Pw<`m zIUv9P+u!%E;k*6!z<0s7!4>c*xCkBsE8s2QDEK4l%J2WrgC7Ff)-~WhP64NYQ@|P%Q&*`q*H6Gtlh{2JN_2RQwOL-IV( h{+gkG$gl%9t*X-<`}-<>Kvd_VwQF^@W3Ct3zW{jYUe^Es diff --git a/ashes/src/components/dropdown/generic-dropdown.jsx b/ashes/src/components/dropdown/generic-dropdown.jsx index 30a8b89054..033992a56f 100644 --- a/ashes/src/components/dropdown/generic-dropdown.jsx +++ b/ashes/src/components/dropdown/generic-dropdown.jsx @@ -28,41 +28,42 @@ export type RenderDropdownFunction = ( ) => Element<*>; export type Props = { - id?: string, - dropdownValueId?: string, - name: string, - value: ValueType, - className?: string, - listClassName?: string, + id?: string, // id for root — why? + dropdownValueId?: string, // not used + name: string, // input name + value: ValueType, // input value? What is NullTitle? + className?: string, // additional root className + listClassName?: string, // aditional className for `ul` placeholder?: string | Element<*>, - emptyMessage?: string | Element<*>, - open?: boolean, - children?: Element<*>, + emptyMessage?: string | Element<*>, // shows when open and no children + open?: bool, // open/closed dropdown menu + children?: Element<*>, // what inside menu items?: Array, - primary?: boolean, - editable?: boolean, - changeable?: boolean, - disabled?: boolean, - inputFirst?: boolean, + primary?: bool, // primary styling, looks like it is not used + editable?: bool, + changeable?: bool, + disabled?: bool, + inputFirst?: bool, renderDropdownInput?: RenderDropdownFunction, - renderNullTitle?: Function, - renderPrepend?: Function, - renderAppend?: Function, + renderNullTitle?: Function, // fallback when no title found in this.findTitleByValue + renderPrepend?: Function, // before + renderAppend?: Function, // and after the `ul` onChange?: Function, - dropdownProps?: Object, - detached?: boolean, - noControls?: boolean, - toggleColumnsBtn?: boolean, - buttonClassName?: string, + dropdownProps?: Object, // props for arrow button (e.g. icon) + detached?: boolean, // goes to `bodyPortal`, e.g. case with overflow `/customers/10/storecredit` + noControls?: boolean, // no arrow button (e.g. taxons) + toggleColumnsBtn?: boolean, // changes button size? + buttonClassName?: string, // another mod for button }; type State = { - open: boolean, - dropup: boolean, - selectedValue: ValueType, - pointedValueIndex: number, + open: bool, // show or hide the menu + dropup: bool, // drop down or up (depends on screen position) + selectedValue: ValueType, // current selected value of menu + pointedValueIndex: number, // current hovered item, used for keyboard navigation }; +// looper for awwor keys navigation function getNewItemIndex(itemsCount, currentIndex, increment = 1) { const startIndex = increment > 0 ? -1 : 0; const index = Math.max(currentIndex, startIndex); @@ -99,9 +100,9 @@ export default class GenericDropdown extends Component { pointedValueIndex: -1, }; - _menu: HTMLElement; - _items: HTMLElement; - _block: HTMLElement; + _menu: HTMLElement; // ul parent (includes before and after, so consider it as a whole wrapper) + _items: HTMLElement; // ul + _block: HTMLElement; // root element, used for clickOutside and others componentDidMount() { window.addEventListener('keydown', this.handleKeyPress, true); @@ -186,6 +187,7 @@ export default class GenericDropdown extends Component { } } + // root className get dropdownClassName(): string { const { primary, editable, disabled, className } = this.props; diff --git a/ashes/src/less/modules/_tables.less b/ashes/src/less/modules/_tables.less index c95b63c3c5..141aa5c465 100644 --- a/ashes/src/less/modules/_tables.less +++ b/ashes/src/less/modules/_tables.less @@ -11,6 +11,7 @@ .fc-table-wrap { position: relative; border: 1px solid @color-border; + z-index: 0; &::before, &::after { content: " "; diff --git a/ashes/styleguide/config.styleguide.js b/ashes/styleguide/config.styleguide.js index 1e9b35694d..49242531a0 100644 --- a/ashes/styleguide/config.styleguide.js +++ b/ashes/styleguide/config.styleguide.js @@ -25,9 +25,11 @@ module.exports = { name: 'Colors and Typo', sections: [ { + name: 'Conventions', content: '../docs/colors-and-typos.md', }, { + name: 'Variables', components: () => [ path.resolve(__dirname, '../src/components/docs/colors/text-colors.jsx'), path.resolve(__dirname, '../src/components/docs/colors/bg-colors.jsx'), @@ -80,6 +82,13 @@ module.exports = { path.resolve(__dirname, '../src/components/core/save-cancel/save-cancel.jsx'), ], }, + { + name: 'Dropdowns', + components: () => [ + path.resolve(__dirname, '../src/components/core/dropdown/smart-list/smart-list.jsx'), + path.resolve(__dirname, '../src/components/core/dropdown/text-dropdown/text-dropdown.jsx'), + ], + }, { name: 'Navigation', components: () => [path.resolve(__dirname, '../src/components/core/page-nav/page-nav.jsx')], From bc249a1609d5b4b8de03628a405f303e7722c307 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Tue, 20 Jun 2017 17:30:25 -0700 Subject: [PATCH 02/16] replace Dropdown in bulk and pages --- .../components/body-portal/body-portal.jsx | 30 ++++++++++++++--- .../core/dropdown/smart-list/smart-list.jsx | 24 ++++++++++++-- .../dropdown/text-dropdown/text-dropdown.css | 2 +- .../dropdown/text-dropdown/text-dropdown.jsx | 32 +++---------------- ashes/src/components/table/pagesize.css | 3 ++ ashes/src/components/table/pagesize.jsx | 15 ++++++--- 6 files changed, 65 insertions(+), 41 deletions(-) create mode 100644 ashes/src/components/table/pagesize.css diff --git a/ashes/src/components/body-portal/body-portal.jsx b/ashes/src/components/body-portal/body-portal.jsx index e2a622cfc3..6cfa04c96a 100644 --- a/ashes/src/components/body-portal/body-portal.jsx +++ b/ashes/src/components/body-portal/body-portal.jsx @@ -1,12 +1,14 @@ // libs -import { Component, Children, Element } from 'react'; +import React, { Component, Children, Element } from 'react'; import ReactDOM from 'react-dom'; +import classNames from 'classnames'; type Props = { active?: boolean, left: ?number, top: ?number, className: ?string, + getRef?: Function, }; export default class BodyPortal extends Component { @@ -17,6 +19,7 @@ export default class BodyPortal extends Component { left: 0, top: 0, className: '', + getRef: () => {}, }; _target: HTMLElement; // HTMLElement, a div that is appended to the body @@ -60,6 +63,8 @@ export default class BodyPortal extends Component { const { className } = this.props; const container = document.createElement('div'); + + // @todo looks like this not working at all, because it is just a wrapper container.className = className; this._target = document.body.appendChild(container); @@ -69,15 +74,32 @@ export default class BodyPortal extends Component { renderContent() { if (!this.props.children) { - return; + return null; } this.updateStyle(); - ReactDOM.unstable_renderSubtreeIntoContainer(this, Children.only(this.props.children), this._target); + const componentNode = ReactDOM.unstable_renderSubtreeIntoContainer( + this, + Children.only(this.props.children), + this._target + ); + const domNode = ReactDOM.findDOMNode(componentNode); + + this.props.getRef(domNode); } render() { - return this.props.active ? null : this.props.children; + const { active, className, children, getRef } = this.props; + + if (this.props.active) { + return null; // see renderContent() + } + + return ( +
+ {children} +
+ ); } } diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx index f67d424ca1..b97a4d0b41 100644 --- a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -48,6 +48,7 @@ export default class SmartList extends Component { static defaultProps = { align: 'right', + onEsc: () => {}, }; state: State = { @@ -56,17 +57,29 @@ export default class SmartList extends Component { componentDidMount() { window.addEventListener('keydown', this.handleKeyPress, true); + window.addEventListener('click', this.handleClickOutside, true); this.setPosition(); } componentWillUnmount() { window.removeEventListener('keydown', this.handleKeyPress, true); + window.removeEventListener('click', this.handleClickOutside, true); } componentDidUpdate(prevProps: Props, prevState: State) { this.setPosition(); } + @autobind + handleClickOutside({ target }: { target: HTMLElement }) { + const { onEsc } = this.props; + const pivot = this.pivot; + + if (this._block && !this._block.contains(target) && pivot && !pivot.contains(target)) { + onEsc(); + } + } + get pivot() { return this.props.pivot || this._block.previousElementSibling || this._block.parentElement; } @@ -141,6 +154,11 @@ export default class SmartList extends Component { this._items.children[pointedValueIndex].click(); } + break; + // esc + case 27: + this.props.onEsc(); + break; // up case 38: @@ -177,17 +195,17 @@ export default class SmartList extends Component { } render() { - const { before, after, className } = this.props; + const { before, after, className, detached } = this.props; const cls = classNames(s.block, className); return ( -
this._block = m}> + this._block = m}> {before}
this._items = i}> {this.renderItems()}
{after} -
+ ); } } diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css index a3fa3a2d11..c15da6af51 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css @@ -36,7 +36,7 @@ } .menu { - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5); + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1); } .item { diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index 4e6e5bedaa..41686b4df6 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -3,14 +3,11 @@ // libs import _ from 'lodash'; import React, { Element, Component } from 'react'; -import createFragment from 'react-addons-create-fragment'; import { autobind } from 'core-decorators'; import classNames from 'classnames'; import Icon from 'components/core/icon'; import { SmartList } from 'components/core/dropdown'; -import { Button } from 'components/core/button'; -import BodyPortal from 'components/body-portal/body-portal'; // styles import s from './text-dropdown.css'; @@ -69,29 +66,12 @@ export default class TextDropdown extends Component { selectedValue: this.props.value, }; - _block: HTMLElement; // root element, used for clickOutside and others - - componentDidMount() { - window.addEventListener('click', this.handleClickOutside, true); - } - - componentWillUnmount() { - window.removeEventListener('click', this.handleClickOutside, true); - } - componentWillReceiveProps(nextProps: Props) { if (nextProps.value !== this.props.value) { this.setState({ selectedValue: nextProps.value }); } } - @autobind - handleClickOutside({ target }: { target: HTMLElement }) { - if (this._block && !this._block.contains(target) && this.state.open) { - this.closeMenu(); - } - } - @autobind handleToggleClick(event: any) { event.preventDefault(); @@ -134,10 +114,10 @@ export default class TextDropdown extends Component { } renderItems() { - const { items } = this.props; + const { items, detached } = this.props; return ( - + this.closeMenu()} detached={detached}> {items.map(item => (
this.handleItemClick(item)}> {item.displayText || item.value} @@ -152,11 +132,7 @@ export default class TextDropdown extends Component { return; } - return ( - - {this.renderItems()} - - ); + return this.renderItems(); } render() { @@ -169,7 +145,7 @@ export default class TextDropdown extends Component { const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; return ( -
this._block = c} tabIndex="0"> +
{this.displayText}
diff --git a/ashes/src/components/table/pagesize.css b/ashes/src/components/table/pagesize.css new file mode 100644 index 0000000000..7c7ed2b564 --- /dev/null +++ b/ashes/src/components/table/pagesize.css @@ -0,0 +1,3 @@ +.dropdown { + min-width: 80px; +} diff --git a/ashes/src/components/table/pagesize.jsx b/ashes/src/components/table/pagesize.jsx index 65bd4552a0..72cc551814 100644 --- a/ashes/src/components/table/pagesize.jsx +++ b/ashes/src/components/table/pagesize.jsx @@ -1,9 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { autobind } from 'core-decorators'; -import Dropdown from '../dropdown/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES } from '../../modules/pagination/base'; +// styles +import s from './pagesize.css'; + class TablePaginator extends React.Component { static propTypes = { value: PropTypes.number, @@ -15,18 +18,20 @@ class TablePaginator extends React.Component { }; @autobind - onPageSizeChange(value) { + onPageSizeChange({ value }) { this.props.setState({ size: +value }); } render() { + const items = DEFAULT_PAGE_SIZES.map(([ value, title ]) => ({ value, title })); + return ( - ); From b4b2ad8be01ef839a8e37e8261b20bd6b1105418 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Wed, 21 Jun 2017 12:19:38 -0700 Subject: [PATCH 03/16] replace dropdown in customer groups --- .../addresses/address-form/address-form.css | 4 +- .../addresses/address-form/address-form.jsx | 16 +++-- .../bulk-actions/actions-dropdown.jsx | 2 +- .../core/dropdown/smart-list/smart-list.css | 4 +- .../core/dropdown/smart-list/smart-list.jsx | 32 ++++++++-- .../dropdown/text-dropdown/text-dropdown.css | 5 +- .../dropdown/text-dropdown/text-dropdown.jsx | 59 ++++++++++++------- .../dropdown/text-dropdown/text-dropdown.md | 2 +- .../components/core/text-mask/text-mask.jsx | 4 +- .../credit-cards/card-expiration-block.jsx | 28 +++++---- .../customers-groups/editor/group-editor.css | 3 + .../customers-groups/editor/group-editor.jsx | 19 ++++-- .../editor/inputs/dropdown.jsx | 14 +++-- .../customers-groups/stats/stats.css | 2 +- .../components/new-payment/new-payment.jsx | 5 +- ashes/src/components/table/pagesize.jsx | 6 +- ashes/styleguide/config.styleguide.js | 2 +- 17 files changed, 129 insertions(+), 78 deletions(-) create mode 100644 ashes/src/components/customers-groups/editor/group-editor.css diff --git a/ashes/src/components/addresses/address-form/address-form.css b/ashes/src/components/addresses/address-form/address-form.css index fd029a31da..56af5147e4 100644 --- a/ashes/src/components/addresses/address-form/address-form.css +++ b/ashes/src/components/addresses/address-form/address-form.css @@ -1,5 +1,3 @@ .countryList { - & :global .fc-dropdown__items { - width: 100%; - } + width: 100%; } diff --git a/ashes/src/components/addresses/address-form/address-form.jsx b/ashes/src/components/addresses/address-form/address-form.jsx index 32a5db301a..3bec248923 100644 --- a/ashes/src/components/addresses/address-form/address-form.jsx +++ b/ashes/src/components/addresses/address-form/address-form.jsx @@ -13,7 +13,7 @@ import FormField from '../../forms/formfield'; import FoxyForm from '../../forms/foxy-form'; import { ApiErrors } from 'components/utils/errors'; import SaveCancel from 'components/core/save-cancel'; -import { Dropdown } from '../../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import AutoScroll from 'components/utils/auto-scroll'; import TextInput from 'components/core/text-input'; @@ -135,13 +135,13 @@ export default class AddressForm extends React.Component { get regionItems() { const regions = _.get(this.country, 'regions', []); - return _.map(regions, region => [region.id, region.name]); + return _.map(regions, region => ({ value: region.id, displayText: region.name })); } get countryItems() { const countries = _.get(this.props, 'countries', []); - return _.map(countries, country => [country.id, country.name]); + return _.map(countries, country => ({ value: country.id, displayText: country.name })); } get errorMessages() { @@ -248,12 +248,11 @@ export default class AddressForm extends React.Component {
  • - this.handleCountryChange(Number(value))} + onChange={(value) => this.handleCountryChange(Number(value))} items={this.countryItems} /> @@ -275,11 +274,10 @@ export default class AddressForm extends React.Component {
  • - this.handleStateChange(Number(value))} + onChange={(value) => this.handleStateChange(Number(value))} items={this.regionItems} /> diff --git a/ashes/src/components/bulk-actions/actions-dropdown.jsx b/ashes/src/components/bulk-actions/actions-dropdown.jsx index 89b7fc6875..f85499c5e2 100644 --- a/ashes/src/components/bulk-actions/actions-dropdown.jsx +++ b/ashes/src/components/bulk-actions/actions-dropdown.jsx @@ -11,7 +11,7 @@ import { TextDropdown } from 'components/core/dropdown'; import s from './actions-dropdown.css'; function getActionsHandler(actions, allChecked, toggledIds) { - return ({ value }) => { + return (value) => { const handler = _.find(actions, ([label, handler]) => label === value)[1]; handler(allChecked, toggledIds); }; diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.css b/ashes/src/components/core/dropdown/smart-list/smart-list.css index e070be0bb1..a69fc351e4 100644 --- a/ashes/src/components/core/dropdown/smart-list/smart-list.css +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.css @@ -2,7 +2,6 @@ .block { position: absolute; - max-height: 90vh; min-width: 100%; overflow: hidden; display: flex; @@ -16,6 +15,9 @@ .list { overflow-y: auto; flex: 1 1 auto; + left: 0; + + /* define `top` in the hoist component */ } .item { diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx index b97a4d0b41..97e94ed8cf 100644 --- a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -19,11 +19,18 @@ type Props = { before?: Element | string; /** Element which goes after the list */ after?: Element | string; + /** Element which is rendered instead of the list, when no items */ + emptyMessage?: Element; align?: 'left' | 'right'; // @todo /** Base element: list will try to stick around it. E.g. dropdown current value box. * Default value: previous sibling or parent. */ pivot?: HTMLElement; + /** If true, BodyPortal is used */ + detached?: boolean; + /** Additional className for block */ className?: string; + /** Callback on user actions which intends to close menu */ + onEsc: Function; }; type State = { @@ -55,6 +62,9 @@ export default class SmartList extends Component { pointedValueIndex: -1, }; + _items: HTMLElement; + _block: HTMLElement; + componentDidMount() { window.addEventListener('keydown', this.handleKeyPress, true); window.addEventListener('click', this.handleClickOutside, true); @@ -80,31 +90,43 @@ export default class SmartList extends Component { } } - get pivot() { - return this.props.pivot || this._block.previousElementSibling || this._block.parentElement; + get pivot(): any { + return this.props.pivot || + (this._block && (this._block.previousElementSibling || this._block.parentElement)); } setDetachedCoords() { const { detached } = this.props; + const pivot = this.pivot; if (!detached) { return; } // `detached=true` – rare case, but we need to handle it - const pivotDim = this.pivot.getBoundingClientRect(); + if (!pivot || !this._block) { + return; + } + + const pivotDim = pivot.getBoundingClientRect(); - this._block.style.minWidth = `${this.pivot.offsetWidth}px`; + this._block.style.minWidth = `${pivot.offsetWidth}px`; this._block.style.top = `${pivotDim.top + pivotDim.height + window.scrollY}px`; this._block.style.left = `${pivotDim.left}px`; } setPosition() { + const pivot = this.pivot; + this.setDetachedCoords(); + if (!pivot || !this._block) { + return; + } + const viewportHeight = window.innerHeight; - const pivotDim = this.pivot.getBoundingClientRect(); + const pivotDim = pivot.getBoundingClientRect(); const spaceAtTop = pivotDim.top; const spaceAtBottom = viewportHeight - pivotDim.bottom; const listRect = this._block.getBoundingClientRect(); diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css index c15da6af51..f2d6474b3f 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css @@ -36,7 +36,9 @@ } .menu { - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1); + top: 40px; + max-height: 264px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, .2); } .item { @@ -46,7 +48,6 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 70vw; font: var(--font-nav); } diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index 41686b4df6..8b5ba05faf 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -12,30 +12,33 @@ import { SmartList } from 'components/core/dropdown'; // styles import s from './text-dropdown.css'; -type Item = { +type InternalItem = { value: string; displayText?: string; }; +type Item = [string, string]; + type Props = { + /** An array of all possible values which will be in a list */ + // $FlowFixMe + items: Array, /** Input name which will be used by form */ - name?: string, // input name + name: string, // input name /** Input value */ - value?: string, // input value + value: string, // input value /** Text which is visible when no value */ - placeholder?: string, + placeholder: string, /** Additional root className */ className?: string, - /** An array of all possible values which will be in a list */ - items?: Array, /** If true, you cant open dropdown or change its value from UI */ - disabled?: bool, - /** Callback which fires when the value has been changes */ - onChange?: Function, + disabled: bool, /** Goes to `bodyPortal`, e.g. case with overflow `/customers/10/storecredit` */ - detached?: boolean, + detached: boolean, /** If true, the component can change its value only via props */ - stateless?: boolean, + stateless: boolean, + /** Callback which fires when the value has been changes */ + onChange: Function, }; type State = { @@ -44,7 +47,8 @@ type State = { }; /** - * Simple Dropdown component + * Text Dropdown component. + * It knows how to render a list through SmartList and how to store and change (or not change) the `value`. * * WARNING: It's important to implement shouldComponentUpdate hook in host components */ @@ -54,11 +58,12 @@ export default class TextDropdown extends Component { static defaultProps = { name: '', value: '', - placeholder: 'Select value', + placeholder: '- Select -', disabled: false, detached: false, onChange: () => {}, stateless: false, + items: [], }; state: State = { @@ -83,16 +88,16 @@ export default class TextDropdown extends Component { this.toggleMenu(); } - handleItemClick(item) { + handleItemClick(item: InternalItem) { const { stateless } = this.props; - let nextState = { open: false }; + let nextState = { open: false, selectedValue: this.state.selectedValue }; if (item.value !== this.state.selectedValue) { if (!stateless) { nextState.selectedValue = item.value; } - this.props.onChange(item); + this.props.onChange(item.value); } this.setState(nextState); @@ -106,19 +111,31 @@ export default class TextDropdown extends Component { this.setState({ open: false }); } - get displayText() { - const { items, placeholder } = this.props; - const item = _.find(items, item => item.value === this.state.selectedValue); + get displayText(): string { + const { placeholder } = this.props; + const item = _.find(this.items, item => item.value == this.state.selectedValue); // could be number == string return (item && item.displayText) || this.state.selectedValue || placeholder; } + get items(): Array { + const { items } = this.props; + + if (Array.isArray(items[0])) { + return items.map(([value, displayText]) => ({ value, displayText })); + } else if (typeof items[0] === 'string') { + return items.map((value: string) => ({ value, displayText: value })); + } + + return items; + } + renderItems() { - const { items, detached } = this.props; + const { detached } = this.props; return ( this.closeMenu()} detached={detached}> - {items.map(item => ( + {this.items.map(item => (
    this.handleItemClick(item)}> {item.displayText || item.value}
    diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md index 3c5d1cb821..9174791203 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md @@ -22,6 +22,6 @@ const items = [{ value: 'One' }, { value: 'Two' }, { value: 'Three', displayText
    - +
  • ``` diff --git a/ashes/src/components/core/text-mask/text-mask.jsx b/ashes/src/components/core/text-mask/text-mask.jsx index f215ebb56d..db8c910ba6 100644 --- a/ashes/src/components/core/text-mask/text-mask.jsx +++ b/ashes/src/components/core/text-mask/text-mask.jsx @@ -15,6 +15,8 @@ type Props = { keepCharPositions?: boolean; /** [react-text-mask#showmask](https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#showmask) */ showMask?: boolean; + /** Injected prop from FormField */ + error?: boolean; }; // Create masks related to react-text-mask component @@ -34,7 +36,7 @@ export function strMaskToRegExp(stringPattern: string) { } export const TextMask = (props: Props) => { - const innerProps = {...props}; + const { error, ...innerProps } = props; // omit error from FormField if (isEmpty(props.mask)) { innerProps.mask = false; diff --git a/ashes/src/components/credit-cards/card-expiration-block.jsx b/ashes/src/components/credit-cards/card-expiration-block.jsx index 2c5ccc71a4..2312543f79 100644 --- a/ashes/src/components/credit-cards/card-expiration-block.jsx +++ b/ashes/src/components/credit-cards/card-expiration-block.jsx @@ -4,7 +4,7 @@ import React, { Component, Element } from 'react'; // components -import Dropdown from '../dropdown/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import FormField from '../forms/formfield'; // utils @@ -33,23 +33,25 @@ export default class ExpirationBlock extends Component {
    month} validationLabel="Month" required> - +
    year} validationLabel="Year" required> - +
    diff --git a/ashes/src/components/customers-groups/editor/group-editor.css b/ashes/src/components/customers-groups/editor/group-editor.css new file mode 100644 index 0000000000..55aaf6473d --- /dev/null +++ b/ashes/src/components/customers-groups/editor/group-editor.css @@ -0,0 +1,3 @@ +.dropdown { + display: inline-flex; +} diff --git a/ashes/src/components/customers-groups/editor/group-editor.jsx b/ashes/src/components/customers-groups/editor/group-editor.jsx index 6779696ad9..8853ffa791 100644 --- a/ashes/src/components/customers-groups/editor/group-editor.jsx +++ b/ashes/src/components/customers-groups/editor/group-editor.jsx @@ -1,11 +1,11 @@ -//libs +// libs import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -//data +// data import operators from 'paragons/customer-groups/operators'; import { setType, @@ -15,16 +15,22 @@ import { GROUP_TYPE_MANUAL, } from 'modules/customer-groups/details/group'; -//helpers +// helpers import { prefix } from 'lib/text-utils'; //components import FormField from 'components/forms/formfield'; -import { Dropdown } from 'components/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import QueryBuilder from './query-builder'; import TextInput from 'components/core/text-input'; -const SELECT_CRITERIA = [[operators.and, 'all'], [operators.or, 'any']]; +// styles +import s from './group-editor.css'; + +const SELECT_CRITERIA = [ + [operators.and, 'all'], + [operators.or, 'any'] +]; const prefixed = prefix('fc-customer-group-edit'); @@ -94,7 +100,8 @@ class GroupEditor extends React.Component {
    Customers match - setMainCondition(value)} diff --git a/ashes/src/components/customers-groups/editor/inputs/dropdown.jsx b/ashes/src/components/customers-groups/editor/inputs/dropdown.jsx index 019829c98c..0cb316eeb0 100644 --- a/ashes/src/components/customers-groups/editor/inputs/dropdown.jsx +++ b/ashes/src/components/customers-groups/editor/inputs/dropdown.jsx @@ -1,16 +1,18 @@ -//libs +// libs import _ from 'lodash'; import React from 'react'; -//components -import Dropdown from 'components/dropdown/dropdown'; +// components +import { TextDropdown } from 'components/core/dropdown'; import propTypes from '../widgets/propTypes'; export const Input = ({criterion, value, changeValue}) => { return ( - + ); }; Input.propTypes = propTypes; diff --git a/ashes/src/components/customers-groups/stats/stats.css b/ashes/src/components/customers-groups/stats/stats.css index 71a4f6eda7..422089261a 100644 --- a/ashes/src/components/customers-groups/stats/stats.css +++ b/ashes/src/components/customers-groups/stats/stats.css @@ -51,7 +51,7 @@ &:checked { & + label { background: var(--bg-green-buttons); - color: var(--color-light-text); + color: var(--color-nav); } } } diff --git a/ashes/src/components/new-payment/new-payment.jsx b/ashes/src/components/new-payment/new-payment.jsx index 6ae9732172..bb3c6df0de 100644 --- a/ashes/src/components/new-payment/new-payment.jsx +++ b/ashes/src/components/new-payment/new-payment.jsx @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import _ from 'lodash'; import AutoScroll from 'components/utils/auto-scroll'; -import { Dropdown } from 'components/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import { ApiErrors } from 'components/utils/errors'; import { Form, FormField } from 'components/forms'; import NewGiftCard from './new-gift-card'; @@ -87,8 +87,7 @@ class NewPayment extends Component { labelClassName="fc-new-order-payment__payment-type-label" label="Payment Type" > - ({ value, title })); - return ( ); diff --git a/ashes/styleguide/config.styleguide.js b/ashes/styleguide/config.styleguide.js index 49242531a0..83c43ba9f2 100644 --- a/ashes/styleguide/config.styleguide.js +++ b/ashes/styleguide/config.styleguide.js @@ -85,7 +85,7 @@ module.exports = { { name: 'Dropdowns', components: () => [ - path.resolve(__dirname, '../src/components/core/dropdown/smart-list/smart-list.jsx'), + // path.resolve(__dirname, '../src/components/core/dropdown/smart-list/smart-list.jsx'), path.resolve(__dirname, '../src/components/core/dropdown/text-dropdown/text-dropdown.jsx'), ], }, From 9b73c5bfda38a11c919ac0249a0605eb3de4a381 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Wed, 21 Jun 2017 17:01:28 -0700 Subject: [PATCH 04/16] replace all simple dropdowns with TextDropdown --- ashes/src/components/analytics/analytics.css | 1 + ashes/src/components/analytics/analytics.jsx | 14 ++----- .../analytics/static-column-selector.jsx | 4 +- .../components/body-portal/body-portal.jsx | 7 ++-- .../bulk-actions/actions-dropdown.jsx | 1 - ashes/src/components/catalog/details.jsx | 20 ++++++--- .../core/dropdown/smart-list/smart-list.css | 3 +- .../core/dropdown/smart-list/smart-list.jsx | 4 +- .../dropdown/text-dropdown/text-dropdown.css | 11 ++++- .../dropdown/text-dropdown/text-dropdown.jsx | 42 ++++++++++++++----- .../dropdown/text-dropdown/text-dropdown.md | 5 ++- .../text-dropdown/text-dropdown.spec.jsx | 34 +++++++++++++++ ashes/src/components/core/icon/icon.jsx | 4 -- .../core/radio-button/radio-button.css | 1 + .../editor/criterion-edit.jsx | 6 +-- .../store-credits/new-store-credit.jsx | 30 ++++++------- .../customers/store-credits/store-credits.jsx | 7 ++-- .../store-credits/storecredit-row.jsx | 8 ++-- ashes/src/components/fields/cancel-reason.jsx | 14 +++---- ashes/src/components/gift-cards/gift-card.jsx | 5 +-- .../components/gift-cards/gift-cards-new.jsx | 10 ++--- .../merchant-applications/details.jsx | 6 +-- .../object-form/object-form-inner.jsx | 9 +++- .../object-scheduler/object-scheduler.jsx | 7 +--- ashes/src/components/orders/order.jsx | 18 +++----- .../components/products/custom-property.jsx | 5 +-- ashes/src/components/products/page.jsx | 4 +- .../components/promotions/discount-attrs.jsx | 5 +-- .../components/promotions/discounts/index.jsx | 13 ++++-- .../promotions/widgets/select-products.jsx | 15 +++---- .../rich-text-editor/rich-text-editor.jsx | 2 +- .../select-verical/select-vertical.jsx | 4 +- .../taxonomies/taxonomy-dropdown.jsx | 4 +- ashes/src/components/users/account-state.jsx | 13 +++--- 34 files changed, 198 insertions(+), 138 deletions(-) create mode 100644 ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx diff --git a/ashes/src/components/analytics/analytics.css b/ashes/src/components/analytics/analytics.css index cb3537ec2a..44c56a7153 100644 --- a/ashes/src/components/analytics/analytics.css +++ b/ashes/src/components/analytics/analytics.css @@ -19,4 +19,5 @@ margin-top: 40px; width: 220px; max-width: 220px; + display: inline-block; } diff --git a/ashes/src/components/analytics/analytics.jsx b/ashes/src/components/analytics/analytics.jsx index b8f177aba8..ff6eda1d5e 100644 --- a/ashes/src/components/analytics/analytics.jsx +++ b/ashes/src/components/analytics/analytics.jsx @@ -15,7 +15,7 @@ import type { Props as QuestionBoxType } from './question-box'; import Currency from 'components/utils/currency'; import TrendButton, { TrendType } from './trend-button'; import StaticColumnSelector from './static-column-selector'; -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import ProductConversionChart from './charts/product-conversion-chart'; import TotalRevenueChart, { ChartSegmentType } from './charts/total-revenue-chart'; import SegmentControlList from './segment-control-list'; @@ -593,17 +593,13 @@ export class Analytics extends React.Component { return (
    - [id, displayText])} placeholder="Comparison Period" - changeable={true} onChange={this.onComparisonPeriodChange} value={comparisonPeriod.dateDisplay} - renderNullTitle={(value, placeholder) => { - return _.isNil(value) ? placeholder : value; - }} />
    - [id, displayText])} placeholder={`${moment().format(datePickerFormat)}`} - changeable={true} onChange={this.onDatePickerChange} value={dateDisplay} - renderNullTitle={(value, placeholder) => { - return _.isNull(value) ? placeholder : value; - }} /> , -} +}; export default class StaticColumnSelector extends React.Component { diff --git a/ashes/src/components/body-portal/body-portal.jsx b/ashes/src/components/body-portal/body-portal.jsx index 6cfa04c96a..d623142a32 100644 --- a/ashes/src/components/body-portal/body-portal.jsx +++ b/ashes/src/components/body-portal/body-portal.jsx @@ -79,14 +79,13 @@ export default class BodyPortal extends Component { this.updateStyle(); - const componentNode = ReactDOM.unstable_renderSubtreeIntoContainer( + ReactDOM.unstable_renderSubtreeIntoContainer( this, - Children.only(this.props.children), +
    {this.props.children}
    , this._target ); - const domNode = ReactDOM.findDOMNode(componentNode); - this.props.getRef(domNode); + this.props.getRef(this._target); } render() { diff --git a/ashes/src/components/bulk-actions/actions-dropdown.jsx b/ashes/src/components/bulk-actions/actions-dropdown.jsx index f85499c5e2..da0bd65f54 100644 --- a/ashes/src/components/bulk-actions/actions-dropdown.jsx +++ b/ashes/src/components/bulk-actions/actions-dropdown.jsx @@ -4,7 +4,6 @@ import React from 'react'; import PropTypes from 'prop-types'; // components -import { Dropdown } from '../dropdown'; import { TextDropdown } from 'components/core/dropdown'; // styles diff --git a/ashes/src/components/catalog/details.jsx b/ashes/src/components/catalog/details.jsx index cc934255ce..4f268b87a4 100644 --- a/ashes/src/components/catalog/details.jsx +++ b/ashes/src/components/catalog/details.jsx @@ -5,7 +5,7 @@ import React from 'react'; import Content from 'components/core/content/content'; import ContentBox from 'components/content-box/content-box'; -import { Dropdown } from 'components/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import Alert from 'components/core/alert'; import Form from 'components/forms/form'; import TextInput from 'components/core/text-input'; @@ -34,7 +34,7 @@ const CatalogDetails = (props: Props) => { const country = _.find(countries, { id: countryId }); let languages = _.get(country, 'languages', []); - if (_.indexOf('en') == -1) { + if (languages.indexOf('en') == -1) { languages = ['en', ...languages]; } @@ -53,16 +53,24 @@ const CatalogDetails = (props: Props) => { onChange('site', v)} value={site} /> - - + onChange('countryId', c)} /> - - + | string; /** Element which goes after the list */ after?: Element | string; - /** Element which is rendered instead of the list, when no items */ - emptyMessage?: Element; align?: 'left' | 'right'; // @todo /** Base element: list will try to stick around it. E.g. dropdown current value box. * Default value: previous sibling or parent. */ @@ -204,7 +202,7 @@ export default class SmartList extends Component { } renderItems() { - const { children, emptyMessage } = this.props; + const { children } = this.props; // @todo only one child return React.Children.map(children, (item, index) => { diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css index f2d6474b3f..cc65a4ff8a 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css @@ -3,6 +3,7 @@ .block { position: relative; font: var(--font-labels); + text-align: left; /* in case if dropdown in centered stuff, e.g. Order State */ &.open { z-index: 1; @@ -29,6 +30,9 @@ .displayText { flex: 1 1 auto; margin-right: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; .placeholder & { color: var(--color-additional-text); @@ -44,11 +48,16 @@ .item { padding: 12px 20px; background-color: white; - cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font: var(--font-nav); + cursor: default; + + .block:not(.empty) &:hover { + background-color: var(--bg-grey-headers); + cursor: pointer; + } } .toggleBtn { diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index 8b5ba05faf..33e3b39856 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -23,10 +23,12 @@ type Props = { /** An array of all possible values which will be in a list */ // $FlowFixMe items: Array, + /** Message to be shown when no items */ + emptyMessage: string, /** Input name which will be used by form */ name: string, // input name /** Input value */ - value: string, // input value + value: string | number | null, // input value /** Text which is visible when no value */ placeholder: string, /** Additional root className */ @@ -59,6 +61,7 @@ export default class TextDropdown extends Component { name: '', value: '', placeholder: '- Select -', + emptyMessage: '- Empty -', disabled: false, detached: false, onChange: () => {}, @@ -68,15 +71,21 @@ export default class TextDropdown extends Component { state: State = { open: false, - selectedValue: this.props.value, + selectedValue: this.getValue(this.props.value), }; + _pivot: HTMLElement; + componentWillReceiveProps(nextProps: Props) { if (nextProps.value !== this.props.value) { - this.setState({ selectedValue: nextProps.value }); + this.setState({ selectedValue: this.getValue(nextProps.value) }); } } + getValue(value: any) { + return value ? String(value) : ''; + } + @autobind handleToggleClick(event: any) { event.preventDefault(); @@ -131,15 +140,25 @@ export default class TextDropdown extends Component { } renderItems() { - const { detached } = this.props; + const { detached, emptyMessage } = this.props; + let list = this.items.map(item => ( +
    this.handleItemClick(item)}> + {item.displayText || item.value} +
    + )); + + if (!this.items.length) { + list =
    {emptyMessage}
    ; + } return ( - this.closeMenu()} detached={detached}> - {this.items.map(item => ( -
    this.handleItemClick(item)}> - {item.displayText || item.value} -
    - ))} + this.closeMenu()} + detached={detached} + pivot={this._pivot} + > + {list} ); } @@ -158,12 +177,13 @@ export default class TextDropdown extends Component { const cls = classNames(s.block, className, { [s.disabled]: disabled, [s.open]: open, + [s.empty]: !this.items.length }); const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; return (
    -
    +
    this._pivot = p} onClick={this.handleToggleClick}>
    {this.displayText}
    diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md index 9174791203..2bfb7e0c1b 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md @@ -22,6 +22,9 @@ const items = [{ value: 'One' }, { value: 'Two' }, { value: 'Three', displayText
    - +
    + +
    +
    ``` diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx new file mode 100644 index 0000000000..11bb93ddcb --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; + +import TextDropdown from './text-dropdown'; + +describe.only('TextDropdown', function () { + + it('should set phaceholder if no value', function () { + const phaceholderText = 'ahuilerhg'; + const textDropdown = mount( + + ); + + expect(textDropdown.find('.displayText')).text().to.equal(phaceholderText); + }); + + it('should convert null to empty string value', function () { + const textDropdown = mount( + + ); + + expect(textDropdown.state().selectedValue).to.equal(''); + }); + + it('should convert number to string value', function () { + const textDropdown = mount( + + ); + + expect(textDropdown.state().selectedValue).to.equal('4'); + }); + +}); diff --git a/ashes/src/components/core/icon/icon.jsx b/ashes/src/components/core/icon/icon.jsx index 7d27838a32..1fece8f70f 100644 --- a/ashes/src/components/core/icon/icon.jsx +++ b/ashes/src/components/core/icon/icon.jsx @@ -6,11 +6,7 @@ type Props = { /** icon type */ name: string, /** additional className */ -<<<<<<< HEAD - className?: string -======= className?: string, ->>>>>>> origin/master }; /** diff --git a/ashes/src/components/core/radio-button/radio-button.css b/ashes/src/components/core/radio-button/radio-button.css index dae7c2e347..aee999200c 100644 --- a/ashes/src/components/core/radio-button/radio-button.css +++ b/ashes/src/components/core/radio-button/radio-button.css @@ -35,6 +35,7 @@ .label { position: relative; + z-index: 0; display: flex; align-items: center; font: var(--font-nav); diff --git a/ashes/src/components/customers-groups/editor/criterion-edit.jsx b/ashes/src/components/customers-groups/editor/criterion-edit.jsx index 827ee4529d..08d2d5e33f 100644 --- a/ashes/src/components/customers-groups/editor/criterion-edit.jsx +++ b/ashes/src/components/customers-groups/editor/criterion-edit.jsx @@ -11,7 +11,7 @@ import criterions, { getCriterion, getOperators, getWidget } from 'paragons/cust import { prefix } from 'lib/text-utils'; //components -import { Dropdown } from 'components/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import Icon from 'components/core/icon'; const prefixed = prefix('fc-customer-group-builder'); @@ -30,7 +30,7 @@ class Criterion extends Component { return (
    - { const operators = _.map(getOperators(criterion), (label, operator) => [operator, label]); return ( -
    - +
    {this.storeCreditTypeError} @@ -200,7 +200,7 @@ export default class NewStoreCredit extends React.Component { const { form, changeScFormData } = this.props; return ( -
    @@ -246,12 +246,12 @@ export default class NewStoreCredit extends React.Component {
    - changeScFormData('subTypeId', value)} /> + changeScFormData('subTypeId', value)} + />
    @@ -336,6 +336,8 @@ export default class NewStoreCredit extends React.Component { ? this.giftCardConvertForm : this.storeCreditForm; + console.log('this.props.form.type', this.props.form.type); + return (
    diff --git a/ashes/src/components/customers/store-credits/store-credits.jsx b/ashes/src/components/customers/store-credits/store-credits.jsx index 9c4caa8b35..97429beb96 100644 --- a/ashes/src/components/customers/store-credits/store-credits.jsx +++ b/ashes/src/components/customers/store-credits/store-credits.jsx @@ -24,7 +24,7 @@ import Summary from './summary'; import BulkActions from 'components/bulk-actions/bulk-actions'; import BulkMessages from 'components/bulk-actions/bulk-messages'; import { ChangeStateModal, CancelModal } from 'components/bulk-actions/modal'; -import Dropdown from 'components/dropdown/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import ConfirmationModal from 'components/core/confirmation-modal'; import SelectableSearchList from 'components/list-page/selectable-search-list'; import StoreCreditRow from './storecredit-row'; @@ -211,12 +211,11 @@ class StoreCredits extends Component {
    - this.props.stateActions.reasonChange(this.customerId, value)} + onChange={(value) => this.props.stateActions.reasonChange(this.customerId, value)} />
    diff --git a/ashes/src/components/customers/store-credits/storecredit-row.jsx b/ashes/src/components/customers/store-credits/storecredit-row.jsx index ff0635e368..4f996cd7d1 100644 --- a/ashes/src/components/customers/store-credits/storecredit-row.jsx +++ b/ashes/src/components/customers/store-credits/storecredit-row.jsx @@ -10,7 +10,7 @@ import MultiSelectRow from 'components/table/multi-select-row'; import Initials from 'components/user-initials/initials'; import OriginType from 'components/common/origin-type'; import State from 'components/common/state'; -import Dropdown from 'components/dropdown/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; const activeStateTransitions = [ ['onHold', 'On Hold'], @@ -44,7 +44,7 @@ const StoreCreditRow = (props: Props) => { switch(rowState) { case 'active': return ( - { value={ rowState } detached={true} onChange={(value) => changeState(rowId, value)} + stateless /> ); case 'onHold': return ( - { value={ rowState } detached={true} onChange={(value) => changeState(rowId, value)} + stateless /> ); default: diff --git a/ashes/src/components/fields/cancel-reason.jsx b/ashes/src/components/fields/cancel-reason.jsx index a9f6fc9b4c..8813debfca 100644 --- a/ashes/src/components/fields/cancel-reason.jsx +++ b/ashes/src/components/fields/cancel-reason.jsx @@ -12,7 +12,7 @@ import { ReasonType } from '../../lib/reason-utils'; import { fetchReasons } from '../../modules/reasons'; // components -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; const mapStateToProps = ({reasons}, {reasonType}) => { @@ -57,12 +57,12 @@ export default class CancelReason extends React.Component { *
    - [id, body])} + [id, body])} />
    ); diff --git a/ashes/src/components/gift-cards/gift-card.jsx b/ashes/src/components/gift-cards/gift-card.jsx index 9cdb122845..089974d885 100644 --- a/ashes/src/components/gift-cards/gift-card.jsx +++ b/ashes/src/components/gift-cards/gift-card.jsx @@ -16,7 +16,7 @@ import Spinner from 'components/core/spinner'; import { PageTitle } from '../section-title'; import Panel from '../panel/panel'; import { PanelList, PanelListItem } from '../panel/panel-list'; -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import PageNav from 'components/core/page-nav'; import ConfirmationModal from 'components/core/confirmation-modal'; import State, { formattedStatus } from '../common/state'; @@ -204,9 +204,8 @@ export default class GiftCard extends React.Component {
    - this.props.changeCancellationReason(this.props.params.giftCard, reasonId)} diff --git a/ashes/src/components/gift-cards/gift-cards-new.jsx b/ashes/src/components/gift-cards/gift-cards-new.jsx index ed95a049d9..1bf2971f86 100644 --- a/ashes/src/components/gift-cards/gift-cards-new.jsx +++ b/ashes/src/components/gift-cards/gift-cards-new.jsx @@ -12,7 +12,7 @@ import { transitionTo, transitionToLazy } from 'browserHistory'; // components import Counter from 'components/core/counter'; -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import { Form } from '../forms'; import SaveCancel from 'components/core/save-cancel'; import CurrencyInput from '../forms/currency-input'; @@ -137,11 +137,10 @@ export default class NewGiftCard extends React.Component { return (
    - props.changeFormData('subTypeId', Number(value))} - items={props.subTypes.map(subType => [subType.id, subType.title])} + items={props.subTypes.map(subType =>[subType.id, subType.title])} />
    ); @@ -171,8 +170,7 @@ export default class NewGiftCard extends React.Component {
    - changeFormData('originType', value)} items={types.map((entry, idx) => [entry.originType, typeTitles[entry.originType]])} diff --git a/ashes/src/components/merchant-applications/details.jsx b/ashes/src/components/merchant-applications/details.jsx index 00f8dd3864..7c6904635c 100644 --- a/ashes/src/components/merchant-applications/details.jsx +++ b/ashes/src/components/merchant-applications/details.jsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import _ from 'lodash'; // components -import { Dropdown } from 'components/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import { PageTitle } from 'components/section-title'; import { PrimaryButton } from 'components/core/button'; import ContentBox from 'components/content-box/content-box'; @@ -115,12 +115,12 @@ class MerchantApplicationDetails extends Component { return (
    -
    diff --git a/ashes/src/components/object-form/object-form-inner.jsx b/ashes/src/components/object-form/object-form-inner.jsx index fa7283fdca..4e94cddd2b 100644 --- a/ashes/src/components/object-form/object-form-inner.jsx +++ b/ashes/src/components/object-form/object-form-inner.jsx @@ -17,7 +17,7 @@ import CurrencyInput from '../forms/currency-input'; import CustomProperty from '../products/custom-property'; import DatePicker from '../datepicker/datepicker'; import RichTextEditor from '../rich-text-editor/rich-text-editor'; -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import SwatchInput from 'components/core/swatch-input'; import TextInput from 'components/core/text-input'; import Icon from 'components/core/icon'; @@ -234,6 +234,7 @@ export default class ObjectFormInner extends Component { return renderFormField(name, stringInput, options); } + // @todo never used? renderOptions(name: string, value: any, options: AttrOptions) { const fieldOptions = this.props.fieldsOptions && this.props.fieldsOptions[name]; if (!fieldOptions) throw new Error('You must define fieldOptions for options fields'); @@ -244,7 +245,11 @@ export default class ObjectFormInner extends Component { return (
    {options.label}
    - + {error && }
    ); diff --git a/ashes/src/components/object-scheduler/object-scheduler.jsx b/ashes/src/components/object-scheduler/object-scheduler.jsx index 8cd2e5fc4d..7e2f143628 100644 --- a/ashes/src/components/object-scheduler/object-scheduler.jsx +++ b/ashes/src/components/object-scheduler/object-scheduler.jsx @@ -11,11 +11,10 @@ import { isActive } from 'paragons/common'; import { trackEvent } from 'lib/analytics'; // components -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import DateTimePicker from '../date-time-picker/date-time-picker'; import Icon from 'components/core/icon'; - type Props = { attributes: Attributes, onChange: (attributes: Attributes) => void, @@ -207,9 +206,7 @@ export default class ObjectScheduler extends Component { const isDisabled = !this.isActive && this.state.showActiveFromPicker; return ( - { + get orderStateDropdown(): Element { const order = this.order; const claims = getClaims(); @@ -182,21 +182,13 @@ export default class Order extends React.Component { }); return ( - [state, states.order[state]])} - placeholder={'Order state'} + placeholder="Order state" value={order.orderState} onChange={this.onStateChange} - changeable={false} - renderNullTitle={(value, placeholder) => { - if (value in states.order) { - return states.order[value]; - } - return placeholder; - }} + stateless /> ); } diff --git a/ashes/src/components/products/custom-property.jsx b/ashes/src/components/products/custom-property.jsx index 75fd492c7d..d753944713 100644 --- a/ashes/src/components/products/custom-property.jsx +++ b/ashes/src/components/products/custom-property.jsx @@ -9,7 +9,7 @@ import _ from 'lodash'; // components import Modal from 'components/core/modal'; -import { Dropdown } from 'components/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import { FormField } from 'components/forms'; import SaveCancel from 'components/core/save-cancel'; import TextInput from 'components/core/text-input'; @@ -112,8 +112,7 @@ export default class CustomProperty extends Component { label="Field Type" labelClassName="fc-product-details__field-label" > - [entry.type, entry.title]); return ( - i.scope == discountType).list; + return ( - - {this.props.label} - {this.productReferences}
    diff --git a/ashes/src/components/rich-text-editor/rich-text-editor.jsx b/ashes/src/components/rich-text-editor/rich-text-editor.jsx index 801a4ac3c1..c3754e12eb 100644 --- a/ashes/src/components/rich-text-editor/rich-text-editor.jsx +++ b/ashes/src/components/rich-text-editor/rich-text-editor.jsx @@ -13,7 +13,7 @@ import { stateToMarkdown } from 'draft-js-export-markdown'; // components import { ContentBlock, ContentState, Editor, EditorState, RichUtils } from 'draft-js'; -import { Dropdown } from '../dropdown'; +import { Dropdown } from '../dropdown'; // @todo replace with personal dropdown import ToggleButton from './toggle-button'; import s from './rich-text-editor.css'; import Icon from 'components/core/icon'; diff --git a/ashes/src/components/select-verical/select-vertical.jsx b/ashes/src/components/select-verical/select-vertical.jsx index e1772b432c..66cd846730 100644 --- a/ashes/src/components/select-verical/select-vertical.jsx +++ b/ashes/src/components/select-verical/select-vertical.jsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import { assoc, dissoc } from 'sprout-data'; // components -import Dropdown from '../dropdown/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import { Button } from 'components/core/button'; import Icon from 'components/core/icon'; @@ -120,7 +120,7 @@ export default class SelectVertical extends Component { return (
    - { +const TaxonomyDropdown = (props: Props) => { const { taxonomy, linkedTaxonomy = {}, onTaxonClick, onNewValueClick } = props; const items = buildTaxonsDropDownItems(taxonomy.taxons, ''); @@ -79,3 +79,5 @@ export default (props: Props) => { /> ); }; + +export default TaxonomyDropdown; diff --git a/ashes/src/components/users/account-state.jsx b/ashes/src/components/users/account-state.jsx index 932c7f2142..9c35d03457 100644 --- a/ashes/src/components/users/account-state.jsx +++ b/ashes/src/components/users/account-state.jsx @@ -6,7 +6,7 @@ import { autobind } from 'core-decorators'; import { connect } from 'react-redux'; // components -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import ContentBox from '../content-box/content-box'; import ConfirmationModal from 'components/core/confirmation-modal'; @@ -76,11 +76,12 @@ class AccountState extends Component { return (
    - this.handleDropdownChange(value)} - disabled={this.props.disabled} - items={SELECT_STATE} - changeable={false} + this.handleDropdownChange(value)} + disabled={this.props.disabled} + items={SELECT_STATE} + stateless /> Date: Thu, 22 Jun 2017 11:10:42 -0700 Subject: [PATCH 05/16] tune TextDropdown --- .../core/dropdown/smart-list/smart-list.jsx | 43 +++++++++++-------- .../dropdown/text-dropdown/text-dropdown.jsx | 41 ++++++++---------- .../text-dropdown/text-dropdown.spec.jsx | 22 +++------- 3 files changed, 48 insertions(+), 58 deletions(-) diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx index c97e70ad0b..aa1791461c 100644 --- a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -14,21 +14,21 @@ import s from './smart-list.css'; type Props = { /** Array of elements inside the list */ - children: Array; + children: Array, /** Element which prepends the list */ - before?: Element | string; + before?: Element | string, /** Element which goes after the list */ - after?: Element | string; - align?: 'left' | 'right'; // @todo + after?: Element | string, + align?: 'left' | 'right', // @todo /** Base element: list will try to stick around it. E.g. dropdown current value box. * Default value: previous sibling or parent. */ - pivot?: HTMLElement; + pivot?: HTMLElement, /** If true, BodyPortal is used */ - detached?: boolean; + detached?: boolean, /** Additional className for block */ - className?: string; + className?: string, /** Callback on user actions which intends to close menu */ - onEsc: Function; + onEsc: Function, }; type State = { @@ -89,8 +89,7 @@ export default class SmartList extends Component { } get pivot(): any { - return this.props.pivot || - (this._block && (this._block.previousElementSibling || this._block.parentElement)); + return this.props.pivot || (this._block && (this._block.previousElementSibling || this._block.parentElement)); } setDetachedCoords() { @@ -184,18 +183,24 @@ export default class SmartList extends Component { case 38: e.preventDefault(); - this.setState({ - pointedValueIndex: getNewItemIndex(itemsCount, pointedValueIndex, -1), - }, () => this.scrollViewport(true)); + this.setState( + { + pointedValueIndex: getNewItemIndex(itemsCount, pointedValueIndex, -1), + }, + () => this.scrollViewport(true) + ); break; // down case 40: e.preventDefault(); - this.setState({ - pointedValueIndex: getNewItemIndex(itemsCount, pointedValueIndex), - }, () => this.scrollViewport(false)); + this.setState( + { + pointedValueIndex: getNewItemIndex(itemsCount, pointedValueIndex), + }, + () => this.scrollViewport(false) + ); break; } @@ -207,7 +212,7 @@ export default class SmartList extends Component { // @todo only one child return React.Children.map(children, (item, index) => { const props: any = { - className: classNames(item.props.className, s.item, { [s.active]: index === this.state.pointedValueIndex }) + className: classNames(item.props.className, s.item, { [s.active]: index === this.state.pointedValueIndex }), }; return React.cloneElement(item, props); @@ -219,9 +224,9 @@ export default class SmartList extends Component { const cls = classNames(s.block, className); return ( - this._block = m}> + (this._block = m)}> {before} -
    this._items = i}> +
    (this._items = i)}> {this.renderItems()}
    {after} diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index 33e3b39856..53f34ee948 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -13,8 +13,8 @@ import { SmartList } from 'components/core/dropdown'; import s from './text-dropdown.css'; type InternalItem = { - value: string; - displayText?: string; + value: string, + displayText?: string, }; type Item = [string, string]; @@ -23,18 +23,18 @@ type Props = { /** An array of all possible values which will be in a list */ // $FlowFixMe items: Array, - /** Message to be shown when no items */ - emptyMessage: string, /** Input name which will be used by form */ name: string, // input name /** Input value */ value: string | number | null, // input value /** Text which is visible when no value */ placeholder: string, + /** Message to be shown when no items */ + emptyMessage: string, /** Additional root className */ className?: string, /** If true, you cant open dropdown or change its value from UI */ - disabled: bool, + disabled: boolean, /** Goes to `bodyPortal`, e.g. case with overflow `/customers/10/storecredit` */ detached: boolean, /** If true, the component can change its value only via props */ @@ -44,15 +44,15 @@ type Props = { }; type State = { - open: bool, // show or hide the menu + open: boolean, // show or hide the menu selectedValue: string, // current selected value of menu }; /** * Text Dropdown component. - * It knows how to render a list through SmartList and how to store and change (or not change) the `value`. - * - * WARNING: It's important to implement shouldComponentUpdate hook in host components + * This component is about to render simple text list and current value. + * This component is not responsible for different skins, TextInputs, infinite lists, or any other complex stuff. + * If you need any functionality which is not exists here, try different Dropdown or build new one. */ export default class TextDropdown extends Component { props: Props; @@ -112,12 +112,10 @@ export default class TextDropdown extends Component { this.setState(nextState); } - toggleMenu() { - this.setState({ open: !this.state.open }); - } + toggleMenu(nextOpen: ?boolean) { + const open = nextOpen != null ? nextOpen : !this.state.open; - closeMenu() { - this.setState({ open: false }); + this.setState({ open }); } get displayText(): string { @@ -141,23 +139,18 @@ export default class TextDropdown extends Component { renderItems() { const { detached, emptyMessage } = this.props; - let list = this.items.map(item => ( + let list = this.items.map(item =>
    this.handleItemClick(item)}> {item.displayText || item.value}
    - )); + ); if (!this.items.length) { list =
    {emptyMessage}
    ; } return ( - this.closeMenu()} - detached={detached} - pivot={this._pivot} - > + this.toggleMenu(false)} detached={detached} pivot={this._pivot}> {list} ); @@ -177,13 +170,13 @@ export default class TextDropdown extends Component { const cls = classNames(s.block, className, { [s.disabled]: disabled, [s.open]: open, - [s.empty]: !this.items.length + [s.empty]: !this.items.length, }); const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; return (
    -
    this._pivot = p} onClick={this.handleToggleClick}> +
    (this._pivot = p)} onClick={this.handleToggleClick}>
    {this.displayText}
    diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx index 11bb93ddcb..f26bdb668f 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx @@ -4,31 +4,23 @@ import { mount } from 'enzyme'; import TextDropdown from './text-dropdown'; -describe.only('TextDropdown', function () { - - it('should set phaceholder if no value', function () { +describe.only('TextDropdown', function() { + it('should set phaceholder if no value', function() { const phaceholderText = 'ahuilerhg'; - const textDropdown = mount( - - ); + const textDropdown = mount(); expect(textDropdown.find('.displayText')).text().to.equal(phaceholderText); }); - it('should convert null to empty string value', function () { - const textDropdown = mount( - - ); + it('should convert null to empty string value', function() { + const textDropdown = mount(); expect(textDropdown.state().selectedValue).to.equal(''); }); - it('should convert number to string value', function () { - const textDropdown = mount( - - ); + it('should convert number to string value', function() { + const textDropdown = mount(); expect(textDropdown.state().selectedValue).to.equal('4'); }); - }); From 5e622f606ff59ad020674eb02075283a4e468a5c Mon Sep 17 00:00:00 2001 From: Diokuz Date: Thu, 22 Jun 2017 11:58:47 -0700 Subject: [PATCH 06/16] Add SearchDropdown --- .../addresses/address-form/address-form.jsx | 4 +- ashes/src/components/core/dropdown/index.js | 1 + .../search-dropdown/search-dropdown.css | 84 +++++++++ .../search-dropdown/search-dropdown.jsx | 177 ++++++++++++++++++ .../search-dropdown/search-dropdown.md | 30 +++ .../search-dropdown/search-dropdown.spec.jsx | 26 +++ ashes/styleguide/config.styleguide.js | 10 +- 7 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css create mode 100644 ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx create mode 100644 ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md create mode 100644 ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx diff --git a/ashes/src/components/addresses/address-form/address-form.jsx b/ashes/src/components/addresses/address-form/address-form.jsx index 3bec248923..6bc3e9c1d9 100644 --- a/ashes/src/components/addresses/address-form/address-form.jsx +++ b/ashes/src/components/addresses/address-form/address-form.jsx @@ -135,13 +135,13 @@ export default class AddressForm extends React.Component { get regionItems() { const regions = _.get(this.country, 'regions', []); - return _.map(regions, region => ({ value: region.id, displayText: region.name })); + return _.map(regions, region => [region.id, region.name]); } get countryItems() { const countries = _.get(this.props, 'countries', []); - return _.map(countries, country => ({ value: country.id, displayText: country.name })); + return _.map(countries, country => [country.id, country.name]); } get errorMessages() { diff --git a/ashes/src/components/core/dropdown/index.js b/ashes/src/components/core/dropdown/index.js index eb97807de3..78f90fe235 100644 --- a/ashes/src/components/core/dropdown/index.js +++ b/ashes/src/components/core/dropdown/index.js @@ -1,2 +1,3 @@ export SmartList from './smart-list/smart-list'; export TextDropdown from './text-dropdown/text-dropdown'; +export SearchDropdown from './text-dropdown/text-dropdown'; diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css new file mode 100644 index 0000000000..af6067bdb1 --- /dev/null +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css @@ -0,0 +1,84 @@ +@import 'variables.css'; + +.block { + position: relative; + font: var(--font-labels); + text-align: left; /* in case if dropdown in centered stuff, e.g. Order State */ + + &.open { + z-index: 1; + } +} + +.pivot { + display: flex; + padding: 10px; + background-color: var(--bg-grey-buttons); + cursor: pointer; + + &:hover { + background-color: var(--bg-grey-buttons-hover); + } + + .block.disabled & { + background-color: var(--bg-grey-buttons-hover); + color: var(--color-additional-text); + cursor: default; + } +} + +.displayText { + flex: 1 1 auto; + margin-right: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .placeholder & { + color: var(--color-additional-text); + } +} + +.menu { + top: 40px; + max-height: 264px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, .2); +} + +.searchBar { + margin: 10px; +} + +.loupeIcon { + position: relative; + left: 15px; + top: -50px; + font-size: 20px; + width: 20px; + color: var(--color-additional-text); + line-height: 35px; +} + +.searchBarInput { + padding-left: 30px; +} + +.item { + padding: 12px 20px; + background-color: white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font: var(--font-nav); + cursor: default; + + .block:not(.empty) &:hover { + background-color: var(--bg-grey-headers); + cursor: pointer; + } +} + +.toggleBtn { + width: 40px; + height: 40px; +} diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx new file mode 100644 index 0000000000..160bfa5616 --- /dev/null +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx @@ -0,0 +1,177 @@ +/* @flow */ + +// libs +import _ from 'lodash'; +import React, { Element, Component } from 'react'; +import { autobind } from 'core-decorators'; +import classNames from 'classnames'; + +import Icon from 'components/core/icon'; +import { SmartList } from 'components/core/dropdown'; + +// styles +import s from './search-dropdown.css'; + +type InternalItem = { + value: string, + displayText?: string, +}; + +type Item = [string, string]; + +type Props = { + /** An array of all possible values which will be in a list */ + // $FlowFixMe + items: Array, + /** Input name which will be used by form */ + name: string, // input name + /** Input value */ + value: string | number | null, // input value + /** Text which is visible when no value */ + placeholder: string, + /** Additional root className */ + className?: string, + /** If true, you cant open dropdown or change its value from UI */ + disabled: boolean, + /** If true, the component can change its value only via props */ + stateless: boolean, + /** Callback which fires when the value has been changes */ + onChange: Function, +}; + +type State = { + open: boolean, // show or hide the menu + selectedValue: string, // current selected value of menu +}; + +/** + * Text Dropdown component. + * This component is about to render simple text list and current value. + * This component is not responsible for different skins, TextInputs, infinite lists, or any other complex stuff. + * If you need any functionality which is not exists here, try different Dropdown or build new one. + */ +export default class SearchDropdown extends Component { + props: Props; + + static defaultProps = { + name: '', + value: '', + placeholder: '- Select -', + disabled: false, + onChange: () => {}, + stateless: false, + items: [], + }; + + state: State = { + open: false, + selectedValue: this.getValue(this.props.value), + }; + + _pivot: HTMLElement; + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.value !== this.props.value) { + this.setState({ selectedValue: this.getValue(nextProps.value) }); + } + } + + getValue(value: any) { + return value ? String(value) : ''; + } + + @autobind + handleToggleClick(event: any) { + event.preventDefault(); + + if (this.props.disabled) { + return; + } + + this.toggleMenu(); + } + + handleItemClick(item: InternalItem) { + const { stateless } = this.props; + let nextState = { open: false, selectedValue: this.state.selectedValue }; + + if (item.value !== this.state.selectedValue) { + if (!stateless) { + nextState.selectedValue = item.value; + } + + this.props.onChange(item.value); + } + + this.setState(nextState); + } + + toggleMenu(nextOpen: ?boolean) { + const open = nextOpen != null ? nextOpen : !this.state.open; + + this.setState({ open }); + } + + get displayText(): string { + const { placeholder } = this.props; + const item = _.find(this.items, item => item.value == this.state.selectedValue); // could be number == string + + return (item && item.displayText) || this.state.selectedValue || placeholder; + } + + get items(): Array { + const { items } = this.props; + + if (Array.isArray(items[0])) { + return items.map(([value, displayText]) => ({ value, displayText })); + } else if (typeof items[0] === 'string') { + return items.map((value: string) => ({ value, displayText: value })); + } + + return items; + } + + renderItems() { + let list = this.items.map(item => +
    this.handleItemClick(item)}> + {item.displayText || item.value} +
    + ); + + return ( + this.toggleMenu(false)} pivot={this._pivot}> + {list} + + ); + } + + get menu(): ?Element { + if (!this.state.open) { + return; + } + + return this.renderItems(); + } + + render() { + const { disabled, name, placeholder, className } = this.props; + const { selectedValue, open } = this.state; + const cls = classNames(s.block, className, { + [s.disabled]: disabled, + [s.open]: open, + [s.empty]: !this.items.length, + }); + const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; + + return ( +
    +
    (this._pivot = p)} onClick={this.handleToggleClick}> +
    {this.displayText}
    + + +
    + {this.menu} +
    + ); + } +} diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md new file mode 100644 index 0000000000..64286dc673 --- /dev/null +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md @@ -0,0 +1,30 @@ +#### Basic usage + +```javascript +import { SearchDropdown } from 'components/core/dropdown'; + +const items = [ + { value: 'One' }, + { value: 'Two' }, + { value: 'Three', displayText: 'Three!' } +]; + + +``` + +``` +const items = [{ value: 'One' }, { value: 'Two' }, { value: 'Three', displayText: 'Three!' }]; + +
    +
    + console.log(e)} value="One" /> +
    +
    + +
    +
    + +
    + +
    +``` diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx new file mode 100644 index 0000000000..ce48184caf --- /dev/null +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; + +import SearchDropdown from './search-dropdown'; + +describe.only('SearchDropdown', function() { + it('should set phaceholder if no value', function() { + const phaceholderText = 'ahuilerhg'; + const textDropdown = mount(); + + expect(textDropdown.find('.displayText')).text().to.equal(phaceholderText); + }); + + it('should convert null to empty string value', function() { + const textDropdown = mount(); + + expect(textDropdown.state().selectedValue).to.equal(''); + }); + + it('should convert number to string value', function() { + const textDropdown = mount(); + + expect(textDropdown.state().selectedValue).to.equal('4'); + }); +}); diff --git a/ashes/styleguide/config.styleguide.js b/ashes/styleguide/config.styleguide.js index 83c43ba9f2..fd284a07ed 100644 --- a/ashes/styleguide/config.styleguide.js +++ b/ashes/styleguide/config.styleguide.js @@ -87,6 +87,7 @@ module.exports = { components: () => [ // path.resolve(__dirname, '../src/components/core/dropdown/smart-list/smart-list.jsx'), path.resolve(__dirname, '../src/components/core/dropdown/text-dropdown/text-dropdown.jsx'), + path.resolve(__dirname, '../src/components/core/dropdown/search-dropdown/search-dropdown.jsx'), ], }, { @@ -144,18 +145,15 @@ module.exports = { }, { name: 'Other', - components: () => ([ + components: () => [ path.resolve(__dirname, '../src/components/utils/change/change.jsx'), path.resolve(__dirname, '../src/components/utils/currency/currency.jsx'), - ]), + ], }, ], }, ], }, ], - require: [ - path.join(__dirname, '../src/images/favicons/favicon.ico'), - path.join(__dirname, 'styleguide.css'), - ] + require: [path.join(__dirname, '../src/images/favicons/favicon.ico'), path.join(__dirname, 'styleguide.css')], }; From 6171a153adcd18972ab0fca9bbc670d7510a2fa8 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Thu, 22 Jun 2017 16:36:25 -0700 Subject: [PATCH 07/16] Add tests --- ashes/package.json | 100 +++++++++--------- .../search-dropdown/search-dropdown.css | 13 ++- .../search-dropdown/search-dropdown.jsx | 98 ++++++++++++++--- .../search-dropdown/search-dropdown.md | 15 ++- .../search-dropdown/search-dropdown.spec.jsx | 96 +++++++++++++++-- .../core/dropdown/smart-list/smart-list.jsx | 3 +- .../dropdown/text-dropdown/text-dropdown.css | 5 - .../text-dropdown/text-dropdown.spec.jsx | 16 ++- 8 files changed, 248 insertions(+), 98 deletions(-) diff --git a/ashes/package.json b/ashes/package.json index e172182be2..42b27e3eca 100644 --- a/ashes/package.json +++ b/ashes/package.json @@ -27,7 +27,7 @@ }, "contributors": "https://github.com/FoxComm/highlander/graphs/contributors", "homepage": "https://github.com/FoxComm/highlander#readme", - "dependencies": { + "dependencies": { "cli-color": "^1.2.0", "co": "^4.6.0", "dotenv": "^4.0.0", @@ -42,54 +42,6 @@ }, "devDependencies": { "@foxcomm/wings": "^1.9.12", - "chance": "^0.7.6", - "classnames": "^2.2.5", - "co-body": "^4.0.0", - "core-decorators": "^0.8.0", - "crypto-js": "^3.1.9-1", - "currency-symbol-map": "^2.1.0", - "d3": "^3.5.0", - "draft-js": "^0.10.1", - "draft-js-export-html": "^0.2.2", - "draft-js-export-markdown": "^0.2.0", - "draft-js-import-html": "^0.1.10", - "draft-js-import-markdown": "^0.1.6", - "entity-wharf": "^0.1.1", - "event-source-polyfill": "0.0.6", - "fleck": "^0.5.1", - "history": "3.3.0", - "htmlescape": "^1.1.0", - "invariant": "^2.2.1", - "jsen": "^0.6.1", - "jwt-decode": "^2.1.0", - "moment": "^2.17.1", - "prop-types": "^15.5.8", - "react": "^15.5.4", - "react-addons-create-fragment": "^15.4.0", - "react-cookie": "^0.4.8", - "react-dnd": "^2.1.4", - "react-dnd-html5-backend": "^2.1.2", - "react-dom": "^15.5.4", - "react-motion": "^0.5.0", - "react-redux": "^5.0.5", - "react-router": "3.0.5", - "react-router-redux": "^4.0.8", - "react-text-mask": "^5.0.1", - "react-transition-group": "^1.1.2", - "reduce-reducers": "^0.1.1", - "redux": "^3.6.0", - "redux-act": "^1.0.0", - "redux-logger": "^2.0.3", - "redux-thunk": "^1.0.0", - "request": "^2.58.0", - "reselect": "^2.0.0", - "sprout-data": "^0.2.3", - "superagent": "^3.5.0", - "thunkify-wrap": "^1.0.4", - "use-named-routes": "^0.3.2", - "victory": "^0.15.0", - "zipcodes-regex": "^1.0.0", - "zxcvbn": "^4.4.2", "babel-cli": "^6.18.0", "babel-core": "^6.24.1", "babel-eslint": "^7.2.1", @@ -110,28 +62,49 @@ "babel-runtime": "^6.5.0", "chai": "^3.0.0", "chai-enzyme": "^0.7.1", + "chance": "^0.7.6", + "classnames": "^2.2.5", + "co-body": "^4.0.0", "co-mocha": "^1.1.2", + "core-decorators": "^0.8.0", + "crypto-js": "^3.1.9-1", "css-loader": "^0.28.4", "css-modules-require-hook": "^4.0.6", "cssnano": "^3.7.3", + "currency-symbol-map": "^2.1.0", + "d3": "^3.5.0", + "draft-js": "^0.10.1", + "draft-js-export-html": "^0.2.2", + "draft-js-export-markdown": "^0.2.0", + "draft-js-import-html": "^0.1.10", + "draft-js-import-markdown": "^0.1.6", + "entity-wharf": "^0.1.1", "enzyme": "^2.8.2", "eslint": "^3.19.0", "eslint-plugin-lodash-fp": "^2.1.3", "eslint-plugin-react": "^7.0.1", + "event-source-polyfill": "0.0.6", "extract-text-webpack-plugin": "^2.1.0", "file-loader": "^0.11.1", + "fleck": "^0.5.1", "flow-bin": "^0.46.0", "gulp": "^3.9.0", "gulp-load-plugins": "^1.0.0-rc.1", "gulp-mocha": "^2.1.2", "gulp-spawn-mocha": "^2.2.2", + "history": "3.3.0", + "htmlescape": "^1.1.0", "ignore-styles": "^1.2.0", + "invariant": "^2.2.1", "jsdom": "^9.4.1", + "jsen": "^0.6.1", + "jwt-decode": "^2.1.0", "koa-logger": "^2.0.1", "less": "^2.7.2", "less-loader": "^4.0.3", "localStorage": "^1.0.3", "mocha": "^2.3.0", + "moment": "^2.17.1", "nock": "^9.0.10", "node-notifier": "^4.2.3", "nodemon": "^1.11.0", @@ -147,22 +120,47 @@ "postcss-nested": "^1.0.1", "postcss-url": "^7.0.0", "prettier": "^1.4.4", + "prop-types": "^15.5.8", "query-string": "^4.3.4", + "react": "^15.5.4", + "react-addons-create-fragment": "^15.4.0", + "react-cookie": "^0.4.8", + "react-dnd": "^2.1.4", + "react-dnd-html5-backend": "^2.1.2", + "react-dom": "^15.5.4", + "react-motion": "^0.5.0", + "react-redux": "^5.0.5", + "react-router": "3.0.5", + "react-router-redux": "^4.0.8", "react-shallow-testutils": "^2.0.0", "react-styleguidist": "^5.3.2", "react-test-renderer": "^15.5.4", + "react-text-mask": "^5.0.1", + "react-transition-group": "^1.1.2", + "reduce-reducers": "^0.1.1", + "redux": "^3.6.0", + "redux-act": "^1.0.0", + "redux-logger": "^2.0.3", + "redux-thunk": "^1.0.0", + "request": "^2.58.0", + "reselect": "^2.0.0", "rewire": "^2.3.4", "run-sequence": "^1.2.2", "simulant": "^0.2.2", "sinon": "^1.16.1", + "sprout-data": "^0.2.3", "strip-ansi": "^3.0.0", "style-loader": "^0.16.1", "stylelint": "^7.11.1", "stylelint-config-css-modules": "^1.0.0", "stylelint-config-standard": "^16.0.0", + "superagent": "^3.5.0", + "thunkify-wrap": "^1.0.4", "uglify-js": "^2.7.0", "unexpected": "^10.0.2", "unexpected-react-shallow": "^0.7.0", + "use-named-routes": "^0.3.2", + "victory": "^0.15.0", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^2.3.3", @@ -171,7 +169,9 @@ "webpack-dev-server": "^2.4.5", "webpack-manifest-plugin": "^1.1.0", "webpack-merge": "^4.1.0", - "webpack-svgstore-plugin": "^4.0.0" + "webpack-svgstore-plugin": "^4.0.0", + "zipcodes-regex": "^1.0.0", + "zxcvbn": "^4.4.2" }, "directories": { "test": "test" diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css index af6067bdb1..bd7513f1d0 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css @@ -43,6 +43,7 @@ top: 40px; max-height: 264px; box-shadow: 0 2px 6px 0 rgba(0, 0, 0, .2); + background-color: var(--bg-white); } .searchBar { @@ -50,16 +51,15 @@ } .loupeIcon { - position: relative; + position: absolute; left: 15px; - top: -50px; + top: 20px; font-size: 20px; - width: 20px; color: var(--color-additional-text); - line-height: 35px; } .searchBarInput { + width: 100%; padding-left: 30px; } @@ -78,7 +78,6 @@ } } -.toggleBtn { - width: 40px; - height: 40px; +.spinner { + margin: 0 auto 14px; } diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx index 160bfa5616..cbcaa84ad5 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx @@ -3,11 +3,14 @@ // libs import _ from 'lodash'; import React, { Element, Component } from 'react'; -import { autobind } from 'core-decorators'; +import ReactDOM from 'react-dom'; +import { autobind, debounce } from 'core-decorators'; import classNames from 'classnames'; import Icon from 'components/core/icon'; import { SmartList } from 'components/core/dropdown'; +import TextInput from 'components/core/text-input'; +import Spinner from 'components/core/spinner'; // styles import s from './search-dropdown.css'; @@ -20,7 +23,7 @@ type InternalItem = { type Item = [string, string]; type Props = { - /** An array of all possible values which will be in a list */ + /** An array of initial values which will be in a list */ // $FlowFixMe items: Array, /** Input name which will be used by form */ @@ -29,6 +32,8 @@ type Props = { value: string | number | null, // input value /** Text which is visible when no value */ placeholder: string, + /** Placeholder for search input */ + searchbarPlaceholder: string, /** Additional root className */ className?: string, /** If true, you cant open dropdown or change its value from UI */ @@ -37,11 +42,15 @@ type Props = { stateless: boolean, /** Callback which fires when the value has been changes */ onChange: Function, + /** Callback for token change (fetching new results for the list) */ + fetch: (token: string) => Promise, }; type State = { open: boolean, // show or hide the menu selectedValue: string, // current selected value of menu + items: Array, + isLoading: boolean, }; /** @@ -57,6 +66,7 @@ export default class SearchDropdown extends Component { name: '', value: '', placeholder: '- Select -', + searchbarPlaceholder: 'Start to type...', disabled: false, onChange: () => {}, stateless: false, @@ -66,9 +76,12 @@ export default class SearchDropdown extends Component { state: State = { open: false, selectedValue: this.getValue(this.props.value), + items: this.unifyItems(this.props.items), + isLoading: false, }; _pivot: HTMLElement; + _input: Element; componentWillReceiveProps(nextProps: Props) { if (nextProps.value !== this.props.value) { @@ -76,6 +89,14 @@ export default class SearchDropdown extends Component { } } + componentDidUpdate(prevProps, prevState) { + const input = ReactDOM.findDOMNode(this._input); + + if (this.state.open && !prevState.open && input) { + input.focus(); + } + } + getValue(value: any) { return value ? String(value) : ''; } @@ -114,33 +135,80 @@ export default class SearchDropdown extends Component { get displayText(): string { const { placeholder } = this.props; - const item = _.find(this.items, item => item.value == this.state.selectedValue); // could be number == string + const item = _.find(this.state.items, item => item.value == this.state.selectedValue); // could be number == string return (item && item.displayText) || this.state.selectedValue || placeholder; } - get items(): Array { - const { items } = this.props; + unifyItems(dirtyItems): Array { + if (Array.isArray(dirtyItems[0])) { + return dirtyItems.map(([value, displayText]) => ({ value, displayText })); + } else if (typeof dirtyItems[0] === 'string') { + return dirtyItems.map((value: string) => ({ value, displayText: value })); + } else if (!dirtyItems) { + return []; + } + + return dirtyItems; + } + + @debounce(400) + fetch(token: string) { + this.props + .fetch(token) + .then(data => { + if (data.token === token) { + this.setState({ items: this.unifyItems(data.items), isLoading: false }); + } + }) + .catch(() => this.setState({ isLoading: false })); + } + + onTokenChange(token: string) { + this.setState({ token }); - if (Array.isArray(items[0])) { - return items.map(([value, displayText]) => ({ value, displayText })); - } else if (typeof items[0] === 'string') { - return items.map((value: string) => ({ value, displayText: value })); + if (!token) { + this.setState({ items: [], isLoading: false }); + } else { + this.setState({ isLoading: true }); + this.fetch(token); } + } - return items; + renderSearchBar() { + return ( +
    + + (this._input = i)} + placeholder={this.props.searchbarPlaceholder} + className={s.searchBarInput} + value={this.state.token} + onChange={value => this.onTokenChange(value)} + /> +
    + ); } renderItems() { - let list = this.items.map(item => + let content = this.state.items.map(item =>
    this.handleItemClick(item)}> {item.displayText || item.value}
    ); + if (!content.length && this.state.isLoading) { + content = ; + } + return ( - this.toggleMenu(false)} pivot={this._pivot}> - {list} + this.toggleMenu(false)} + pivot={this._pivot} + before={this.renderSearchBar()} + > + {content} ); } @@ -155,11 +223,11 @@ export default class SearchDropdown extends Component { render() { const { disabled, name, placeholder, className } = this.props; - const { selectedValue, open } = this.state; + const { items, selectedValue, open } = this.state; const cls = classNames(s.block, className, { [s.disabled]: disabled, [s.open]: open, - [s.empty]: !this.items.length, + [s.empty]: !items.length, }); const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md index 64286dc673..2e54283d23 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md @@ -15,16 +15,15 @@ const items = [ ``` const items = [{ value: 'One' }, { value: 'Two' }, { value: 'Three', displayText: 'Three!' }]; +function fetch(token) { + return new Promise(function(resolve, reject) { + setTimeout(() => resolve({ items, token }), 1000); + }); +}; +
    - console.log(e)} value="One" /> -
    -
    - -
    -
    - +
    -
    ``` diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx index ce48184caf..3961a0cf5e 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx @@ -4,23 +4,97 @@ import { mount } from 'enzyme'; import SearchDropdown from './search-dropdown'; -describe.only('SearchDropdown', function() { - it('should set phaceholder if no value', function() { - const phaceholderText = 'ahuilerhg'; - const textDropdown = mount(); +describe('SearchDropdown', function() { + // @todo sinon and promises + it.skip('should set list only for corresponding token', function() { + const clock = sinon.useFakeTimers(); + const stub = sinon.stub(); + const items = ['one', 'two']; + const resolver = sinon.spy((resolve, value) => resolve(value)); - expect(textDropdown.find('.displayText')).text().to.equal(phaceholderText); + function fetch(token) { + console.log('fetch'); + return {}; + + new Promise(function(resolve, reject) { + setTimeout(() => { + resolve({ items, token }); + console.log('resolve'); + }, 10); + }); + } + + for (let i = 0; i < 25; i++) { + setTimeout(() => console.log(i), i); + } + + const searchDropdown = mount(); + + const spySetState = sinon.spy(searchDropdown, 'setState'); + + searchDropdown.find('.pivot').simulate('click'); + + searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); + clock.tick(5); + searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'bar' } }); + clock.tick(6); // foo response ended + console.log('∑∑∑ foo response ended', searchDropdown.state('items').length); + expect(searchDropdown.state('items').length).to.equal(0); // foo response must be cancelled + clock.tick(15); // bar response ended + console.log('∑∑∑ bar response ended'); + console.log(searchDropdown.state('items')); + expect(searchDropdown.state('items').length).to.equal(2); + + clock.restore(); + }); + + it('should open TextInput', function() { + const searchDropdown = mount(); + + searchDropdown.find('.pivot').simulate('click'); // open dropdown + + expect(searchDropdown.find('.searchBarInput').exists()).to.be.true; + expect(searchDropdown.find('.searchBarInput').type()).to.equal('input'); }); - it('should convert null to empty string value', function() { - const textDropdown = mount(); + it('should not call fetch method very often', function() { + const clock = sinon.useFakeTimers(); + const fetch = sinon.spy(() => Promise.resolve([])); + const searchDropdown = mount(); + + searchDropdown.find('.pivot').simulate('click'); // open dropdown + searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); // type `foo` + clock.tick(399); + searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'baz' } }); // type `baz` + clock.tick(399); + searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); // type `bar` + clock.tick(400); - expect(textDropdown.state().selectedValue).to.equal(''); + expect(fetch.calledOnce).to.be.true; + expect(searchDropdown.state('token')).to.equal('foo'); + + clock.restore(); }); - it('should convert number to string value', function() { - const textDropdown = mount(); + it('should be spinner if loading', function() { + const fetch = sinon.spy(() => Promise.resolve([])); + const searchDropdown = mount(); + + searchDropdown.find('.pivot').simulate('click'); // open dropdown + searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); // type `foo` + + expect(searchDropdown.state('isLoading')).to.be.true; + expect(searchDropdown.find('.spinner').exists()).to.be.true; + }); + + it('should not be spinner if loading and there is non-empty result', function() { + const fetch = sinon.spy(() => Promise.resolve([])); + const searchDropdown = mount(); + + searchDropdown.find('.pivot').simulate('click'); // open dropdown + searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); // type `foo` - expect(textDropdown.state().selectedValue).to.equal('4'); + expect(searchDropdown.state('isLoading')).to.be.true; + expect(searchDropdown.find('.spinner').exists()).to.be.false; }); }); diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx index aa1791461c..f7bde69536 100644 --- a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -24,7 +24,7 @@ type Props = { * Default value: previous sibling or parent. */ pivot?: HTMLElement, /** If true, BodyPortal is used */ - detached?: boolean, + detached: boolean, /** Additional className for block */ className?: string, /** Callback on user actions which intends to close menu */ @@ -54,6 +54,7 @@ export default class SmartList extends Component { static defaultProps = { align: 'right', onEsc: () => {}, + detached: false, }; state: State = { diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css index cc65a4ff8a..ce69e62802 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css @@ -59,8 +59,3 @@ cursor: pointer; } } - -.toggleBtn { - width: 40px; - height: 40px; -} diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx index f26bdb668f..9f9fe0d1db 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx @@ -4,7 +4,7 @@ import { mount } from 'enzyme'; import TextDropdown from './text-dropdown'; -describe.only('TextDropdown', function() { +describe('TextDropdown', function() { it('should set phaceholder if no value', function() { const phaceholderText = 'ahuilerhg'; const textDropdown = mount(); @@ -23,4 +23,18 @@ describe.only('TextDropdown', function() { expect(textDropdown.state().selectedValue).to.equal('4'); }); + + it('should trigger onChange and change displayText by click', function() { + const onChange = sinon.spy(); + const textDropdown = mount(); + + expect(textDropdown.find('.displayText').text()).to.equal('plAceholder'); + + textDropdown.find('.pivot').simulate('click'); + textDropdown.find('.item').at(1).simulate('click'); + + expect(textDropdown.find('.displayText').text()).to.equal('two'); + + expect(onChange.calledOnce).to.be.true; + }); }); From d1b3fe73968c0665fc1fe45aacd8829a58712686 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Thu, 22 Jun 2017 18:29:42 -0700 Subject: [PATCH 08/16] Replave DropdownSearch with SearchDropdown --- ashes/src/components/core/dropdown/index.js | 2 +- .../search-dropdown/search-dropdown.jsx | 63 +++++++++-------- .../core/dropdown/smart-list/smart-list.jsx | 1 + .../dropdown/text-dropdown/text-dropdown.jsx | 2 +- .../src/components/dropdown/dropdownItem.jsx | 3 +- .../promotions/widgets/select-product.css | 6 +- .../promotions/widgets/select-product.jsx | 68 ++++++++++++------- 7 files changed, 83 insertions(+), 62 deletions(-) diff --git a/ashes/src/components/core/dropdown/index.js b/ashes/src/components/core/dropdown/index.js index 78f90fe235..3f8c7dbca1 100644 --- a/ashes/src/components/core/dropdown/index.js +++ b/ashes/src/components/core/dropdown/index.js @@ -1,3 +1,3 @@ export SmartList from './smart-list/smart-list'; export TextDropdown from './text-dropdown/text-dropdown'; -export SearchDropdown from './text-dropdown/text-dropdown'; +export SearchDropdown from './search-dropdown/search-dropdown'; diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx index cbcaa84ad5..33c74ef923 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx @@ -30,6 +30,8 @@ type Props = { name: string, // input name /** Input value */ value: string | number | null, // input value + /** Initial display text to be shown (if no renderItem defined) */ + displayText: string, /** Text which is visible when no value */ placeholder: string, /** Placeholder for search input */ @@ -44,11 +46,14 @@ type Props = { onChange: Function, /** Callback for token change (fetching new results for the list) */ fetch: (token: string) => Promise, + /** For custom layout of each item */ + renderItem?: Function, }; type State = { open: boolean, // show or hide the menu selectedValue: string, // current selected value of menu + displayText: string, items: Array, isLoading: boolean, }; @@ -65,6 +70,7 @@ export default class SearchDropdown extends Component { static defaultProps = { name: '', value: '', + displayText: '', placeholder: '- Select -', searchbarPlaceholder: 'Start to type...', disabled: false, @@ -76,6 +82,7 @@ export default class SearchDropdown extends Component { state: State = { open: false, selectedValue: this.getValue(this.props.value), + displayText: this.props.displayText, items: this.unifyItems(this.props.items), isLoading: false, }; @@ -83,12 +90,6 @@ export default class SearchDropdown extends Component { _pivot: HTMLElement; _input: Element; - componentWillReceiveProps(nextProps: Props) { - if (nextProps.value !== this.props.value) { - this.setState({ selectedValue: this.getValue(nextProps.value) }); - } - } - componentDidUpdate(prevProps, prevState) { const input = ReactDOM.findDOMNode(this._input); @@ -113,18 +114,16 @@ export default class SearchDropdown extends Component { } handleItemClick(item: InternalItem) { - const { stateless } = this.props; - let nextState = { open: false, selectedValue: this.state.selectedValue }; + const nextState = { + open: false, + selectedValue: item.value, + displayText: item.displayText || item.value, + }; if (item.value !== this.state.selectedValue) { - if (!stateless) { - nextState.selectedValue = item.value; - } - this.props.onChange(item.value); + this.setState(nextState); } - - this.setState(nextState); } toggleMenu(nextOpen: ?boolean) { @@ -133,13 +132,6 @@ export default class SearchDropdown extends Component { this.setState({ open }); } - get displayText(): string { - const { placeholder } = this.props; - const item = _.find(this.state.items, item => item.value == this.state.selectedValue); // could be number == string - - return (item && item.displayText) || this.state.selectedValue || placeholder; - } - unifyItems(dirtyItems): Array { if (Array.isArray(dirtyItems[0])) { return dirtyItems.map(([value, displayText]) => ({ value, displayText })); @@ -191,11 +183,19 @@ export default class SearchDropdown extends Component { } renderItems() { - let content = this.state.items.map(item => -
    this.handleItemClick(item)}> - {item.displayText || item.value} -
    - ); + let content = this.state.items.map(item => { + let itemContent = item.displayText || item.value; + + if (this.props.renderItem) { + itemContent = this.props.renderItem(item.value); + } + + return ( +
    this.handleItemClick(item)}> + {itemContent} +
    + ); + }); if (!content.length && this.state.isLoading) { content = ; @@ -222,7 +222,7 @@ export default class SearchDropdown extends Component { } render() { - const { disabled, name, placeholder, className } = this.props; + const { disabled, name, placeholder, className, renderItem } = this.props; const { items, selectedValue, open } = this.state; const cls = classNames(s.block, className, { [s.disabled]: disabled, @@ -230,11 +230,16 @@ export default class SearchDropdown extends Component { [s.empty]: !items.length, }); const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; + let displayText = this.state.displayText; + + if (renderItem && this.state.selectedValue) { + displayText = renderItem(this.state.selectedValue); + } return ( -
    +
    (this._pivot = p)} onClick={this.handleToggleClick}> -
    {this.displayText}
    +
    {displayText || this.state.selectedValue || placeholder}
    diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx index f7bde69536..a8a8bbcb12 100644 --- a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -211,6 +211,7 @@ export default class SmartList extends Component { const { children } = this.props; // @todo only one child + // @todo if some child is null, it fails return React.Children.map(children, (item, index) => { const props: any = { className: classNames(item.props.className, s.item, { [s.active]: index === this.state.pointedValueIndex }), diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index 53f34ee948..01c4485967 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -175,7 +175,7 @@ export default class TextDropdown extends Component { const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; return ( -
    +
    (this._pivot = p)} onClick={this.handleToggleClick}>
    {this.displayText}
    diff --git a/ashes/src/components/dropdown/dropdownItem.jsx b/ashes/src/components/dropdown/dropdownItem.jsx index 2c09134712..8e37af0629 100644 --- a/ashes/src/components/dropdown/dropdownItem.jsx +++ b/ashes/src/components/dropdown/dropdownItem.jsx @@ -10,7 +10,7 @@ import s from './dropdown-item.css'; type ItemProps = { onSelect?: Function, - value: number|string|boolean, + value: number | string | boolean, children?: Element, className?: string, isHidden?: boolean, @@ -18,6 +18,7 @@ type ItemProps = { const DropdownItem = ({ children, className = '', isHidden = false, value, onSelect = noop }: ItemProps) => { const handleClick = event => { + debugger; event.preventDefault(); onSelect(value, children); }; diff --git a/ashes/src/components/promotions/widgets/select-product.css b/ashes/src/components/promotions/widgets/select-product.css index ce75aace33..67128879c6 100644 --- a/ashes/src/components/promotions/widgets/select-product.css +++ b/ashes/src/components/promotions/widgets/select-product.css @@ -1,9 +1,5 @@ @import 'variables.css'; -.product-description { +.description { color: var(--color-additional-text); } - -.full-width { - max-width: 100%; -} diff --git a/ashes/src/components/promotions/widgets/select-product.jsx b/ashes/src/components/promotions/widgets/select-product.jsx index 38ccb75078..2f75b56222 100644 --- a/ashes/src/components/promotions/widgets/select-product.jsx +++ b/ashes/src/components/promotions/widgets/select-product.jsx @@ -1,4 +1,3 @@ - /* @flow */ import _ from 'lodash'; @@ -6,26 +5,33 @@ import React, { Component } from 'react'; import { autobind } from 'core-decorators'; import { searchProducts } from '../../../elastic/products'; -import DropdownSearch from '../../dropdown/dropdown-search'; -import DropdownItem from '../../dropdown/dropdownItem'; +import { SearchDropdown } from 'components/core/dropdown'; -import styles from './select-product.css'; +// styles +import s from './select-product.css'; import type { Context } from '../types'; -type RefId = string|number; +type RefId = string | number; type ProductSearch = { - productSearchId: RefId; + productSearchId: RefId, }; type Props = { - context: Context; - name: string; + context: Context, + name: string, +}; + +type State = { + products: Array, }; export default class SelectProduct extends Component { props: Props; + state: State = { + products: [], + }; get search(): Array { return _.get(this.props.context.params, this.props.name, []); @@ -41,45 +47,57 @@ export default class SelectProduct extends Component { return _.get(this.search, '0.productSearchId'); } + @autobind handleProductSearch(token: string): Promise<*> { return searchProducts(token, { omitArchived: true, - omitInactive: true - }).then((result) => { - return result.result; + omitInactive: true, + }).then(result => { + const items = result.result.map(({ id, title }) => [id, title]); + + this.setState({ products: result.result }); + + return { items, token }; }); } @autobind - renderProductOption(product: Object) { + renderProductOption(value: string) { + const product = this.state.products.find(item => item.id == value); + + if (!product) { + return null; + } + return ( - - { product.title } - • ID: { product.id } - +
    + {product.title} + • ID: {product.id} +
    ); } @autobind handleSelectProduct(value: string) { - this.updateSearches([{ - productSearchId: value, - }]); + console.log('value', value); + this.updateSearches([ + { + productSearchId: value, + }, + ]); } render() { + console.log(this.props); return ( - ); } From 4c82a91c0756417cb1b09c17a0581e85df48013a Mon Sep 17 00:00:00 2001 From: Diokuz Date: Fri, 23 Jun 2017 11:15:01 -0700 Subject: [PATCH 09/16] Add more tests for SearchDropdown --- .../search-dropdown/search-dropdown.css | 5 +- .../search-dropdown/search-dropdown.jsx | 28 +++++++---- .../search-dropdown/search-dropdown.md | 2 +- .../search-dropdown/search-dropdown.spec.jsx | 48 ++++++++++++++++++- .../promotions/widgets/select-product.jsx | 4 +- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css index bd7513f1d0..2dea5adb40 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.css @@ -70,11 +70,10 @@ overflow: hidden; text-overflow: ellipsis; font: var(--font-nav); - cursor: default; + cursor: pointer; - .block:not(.empty) &:hover { + &:hover { background-color: var(--bg-grey-headers); - cursor: pointer; } } diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx index 33c74ef923..bc1c7602fd 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx @@ -40,8 +40,6 @@ type Props = { className?: string, /** If true, you cant open dropdown or change its value from UI */ disabled: boolean, - /** If true, the component can change its value only via props */ - stateless: boolean, /** Callback which fires when the value has been changes */ onChange: Function, /** Callback for token change (fetching new results for the list) */ @@ -59,10 +57,9 @@ type State = { }; /** - * Text Dropdown component. - * This component is about to render simple text list and current value. - * This component is not responsible for different skins, TextInputs, infinite lists, or any other complex stuff. - * If you need any functionality which is not exists here, try different Dropdown or build new one. + * Search Dropdown component. + * This component is for fetching and rendering fetched results as a list. + * There are initial value, displayText and items, but after mounting component handle these values internaly. */ export default class SearchDropdown extends Component { props: Props; @@ -75,14 +72,13 @@ export default class SearchDropdown extends Component { searchbarPlaceholder: 'Start to type...', disabled: false, onChange: () => {}, - stateless: false, items: [], }; state: State = { open: false, selectedValue: this.getValue(this.props.value), - displayText: this.props.displayText, + displayText: this.getDisplayText(this.props.items), items: this.unifyItems(this.props.items), isLoading: false, }; @@ -102,6 +98,14 @@ export default class SearchDropdown extends Component { return value ? String(value) : ''; } + getDisplayText(dirtyItems) { + const items = this.unifyItems(dirtyItems); + const value = this.getValue(this.props.value); + const item = items.find(item => item.value === value); + + return item ? item.displayText : this.props.displayText; + } + @autobind handleToggleClick(event: any) { event.preventDefault(); @@ -123,6 +127,8 @@ export default class SearchDropdown extends Component { if (item.value !== this.state.selectedValue) { this.props.onChange(item.value); this.setState(nextState); + } else { + this.setState({ open: false }); } } @@ -183,6 +189,7 @@ export default class SearchDropdown extends Component { } renderItems() { + let after = null; let content = this.state.items.map(item => { let itemContent = item.displayText || item.value; @@ -198,7 +205,8 @@ export default class SearchDropdown extends Component { }); if (!content.length && this.state.isLoading) { - content = ; + content = null; + after = ; } return ( @@ -207,6 +215,7 @@ export default class SearchDropdown extends Component { onEsc={() => this.toggleMenu(false)} pivot={this._pivot} before={this.renderSearchBar()} + after={after} > {content} @@ -227,7 +236,6 @@ export default class SearchDropdown extends Component { const cls = classNames(s.block, className, { [s.disabled]: disabled, [s.open]: open, - [s.empty]: !items.length, }); const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; let displayText = this.state.displayText; diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md index 2e54283d23..3c15e948ac 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md @@ -23,7 +23,7 @@ function fetch(token) {
    - +
    ``` diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx index 3961a0cf5e..1043b4a09e 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx @@ -4,7 +4,7 @@ import { mount } from 'enzyme'; import SearchDropdown from './search-dropdown'; -describe('SearchDropdown', function() { +describe.only('SearchDropdown', function() { // @todo sinon and promises it.skip('should set list only for corresponding token', function() { const clock = sinon.useFakeTimers(); @@ -97,4 +97,50 @@ describe('SearchDropdown', function() { expect(searchDropdown.state('isLoading')).to.be.true; expect(searchDropdown.find('.spinner').exists()).to.be.false; }); + + it('should render value as displayText if no items with that value and no displayText', function() { + const value = 'arfglauefah'; + const searchDropdown = mount(); + + expect(searchDropdown.find('.pivot').text()).to.equal(value); + }); + + it('should find in items and render displayText of initial value', function() { + const value = 'val2'; + const displayText = 'displayText2'; + const items = [['val1', 'displayText1'], ['val2', displayText]]; + const searchDropdown = mount(); + + expect(searchDropdown.find('.pivot').text()).to.equal(displayText); + }); + + it('should use renderItem to display value if passed and value exists', function() { + const displayText = 'bfkyrgfoaui'; + const searchDropdown = mount( displayText} />); + + expect(searchDropdown.find('.pivot').text()).to.equal(displayText); + }); + + it('should not renderItem to display value if passed but no value', function() { + const displayText = 'bfkyrgfoaui'; + const placeholder = 'slurighlirf'; + const searchDropdown = mount( displayText} placeholder={placeholder} />); + + expect(searchDropdown.find('.pivot').text()).to.equal(placeholder); + }); + + it('should close dropdown after clicking the same item second time', function() { + const items = ['val1', 'val2']; + const searchDropdown = mount(); + + searchDropdown.find('.pivot').simulate('click'); // open + expect(searchDropdown.state('open')).to.be.true; + searchDropdown.find('.item').at(1).simulate('click'); // pick + expect(searchDropdown.state('selectedValue')).to.equal('val2'); + expect(searchDropdown.state('open')).to.be.false; // should close + + searchDropdown.find('.pivot').simulate('click'); // open + searchDropdown.find('.item').at(1).simulate('click'); // pick + expect(searchDropdown.state('open')).to.be.false; // should close again + }); }); diff --git a/ashes/src/components/promotions/widgets/select-product.jsx b/ashes/src/components/promotions/widgets/select-product.jsx index 2f75b56222..ef6b6ae17b 100644 --- a/ashes/src/components/promotions/widgets/select-product.jsx +++ b/ashes/src/components/promotions/widgets/select-product.jsx @@ -70,10 +70,10 @@ export default class SelectProduct extends Component { } return ( -
    + {product.title} • ID: {product.id} -
    + ); } From 1040bc62ef85f48f51c1de1d45a1369579964644 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Fri, 23 Jun 2017 11:21:52 -0700 Subject: [PATCH 10/16] remove console.logs --- ashes/src/components/dropdown/dropdownItem.jsx | 1 - ashes/src/components/promotions/widgets/select-product.jsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/ashes/src/components/dropdown/dropdownItem.jsx b/ashes/src/components/dropdown/dropdownItem.jsx index 8e37af0629..1baa0d7396 100644 --- a/ashes/src/components/dropdown/dropdownItem.jsx +++ b/ashes/src/components/dropdown/dropdownItem.jsx @@ -18,7 +18,6 @@ type ItemProps = { const DropdownItem = ({ children, className = '', isHidden = false, value, onSelect = noop }: ItemProps) => { const handleClick = event => { - debugger; event.preventDefault(); onSelect(value, children); }; diff --git a/ashes/src/components/promotions/widgets/select-product.jsx b/ashes/src/components/promotions/widgets/select-product.jsx index ef6b6ae17b..0a9de90fb5 100644 --- a/ashes/src/components/promotions/widgets/select-product.jsx +++ b/ashes/src/components/promotions/widgets/select-product.jsx @@ -79,7 +79,6 @@ export default class SelectProduct extends Component { @autobind handleSelectProduct(value: string) { - console.log('value', value); this.updateSearches([ { productSearchId: value, @@ -88,7 +87,6 @@ export default class SelectProduct extends Component { } render() { - console.log(this.props); return ( Date: Fri, 23 Jun 2017 13:53:49 -0700 Subject: [PATCH 11/16] Fix linters --- .../components/body-portal/body-portal.jsx | 10 +- .../search-dropdown/search-dropdown.jsx | 25 ++-- .../search-dropdown/search-dropdown.spec.jsx | 82 ++++++------- .../core/dropdown/smart-list/smart-list.jsx | 1 - .../dropdown/text-dropdown/text-dropdown.jsx | 8 +- .../store-credits/new-store-credit.jsx | 109 +++++++++--------- .../components/promotions/discounts/index.jsx | 2 - 7 files changed, 114 insertions(+), 123 deletions(-) diff --git a/ashes/src/components/body-portal/body-portal.jsx b/ashes/src/components/body-portal/body-portal.jsx index d623142a32..529b834a2e 100644 --- a/ashes/src/components/body-portal/body-portal.jsx +++ b/ashes/src/components/body-portal/body-portal.jsx @@ -1,5 +1,5 @@ // libs -import React, { Component, Children, Element } from 'react'; +import React, { Component, Element } from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; @@ -79,11 +79,7 @@ export default class BodyPortal extends Component { this.updateStyle(); - ReactDOM.unstable_renderSubtreeIntoContainer( - this, -
    {this.props.children}
    , - this._target - ); + ReactDOM.unstable_renderSubtreeIntoContainer(this,
    {this.props.children}
    , this._target); this.props.getRef(this._target); } @@ -91,7 +87,7 @@ export default class BodyPortal extends Component { render() { const { active, className, children, getRef } = this.props; - if (this.props.active) { + if (active) { return null; // see renderContent() } diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx index bc1c7602fd..013fde0a57 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx @@ -17,14 +17,13 @@ import s from './search-dropdown.css'; type InternalItem = { value: string, - displayText?: string, + displayText: string, }; type Item = [string, string]; type Props = { /** An array of initial values which will be in a list */ - // $FlowFixMe items: Array, /** Input name which will be used by form */ name: string, // input name @@ -52,6 +51,7 @@ type State = { open: boolean, // show or hide the menu selectedValue: string, // current selected value of menu displayText: string, + token: string, items: Array, isLoading: boolean, }; @@ -79,26 +79,27 @@ export default class SearchDropdown extends Component { open: false, selectedValue: this.getValue(this.props.value), displayText: this.getDisplayText(this.props.items), + token: '', items: this.unifyItems(this.props.items), isLoading: false, }; _pivot: HTMLElement; - _input: Element; + _input: React$Component; - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: Props, prevState: State) { const input = ReactDOM.findDOMNode(this._input); - if (this.state.open && !prevState.open && input) { + if (this.state.open && !prevState.open && input instanceof HTMLElement) { input.focus(); } } - getValue(value: any) { + getValue(value: any): string { return value ? String(value) : ''; } - getDisplayText(dirtyItems) { + getDisplayText(dirtyItems: Array): string { const items = this.unifyItems(dirtyItems); const value = this.getValue(this.props.value); const item = items.find(item => item.value === value); @@ -138,7 +139,7 @@ export default class SearchDropdown extends Component { this.setState({ open }); } - unifyItems(dirtyItems): Array { + unifyItems(dirtyItems: Array): Array { if (Array.isArray(dirtyItems[0])) { return dirtyItems.map(([value, displayText]) => ({ value, displayText })); } else if (typeof dirtyItems[0] === 'string') { @@ -232,7 +233,7 @@ export default class SearchDropdown extends Component { render() { const { disabled, name, placeholder, className, renderItem } = this.props; - const { items, selectedValue, open } = this.state; + const { selectedValue, open } = this.state; const cls = classNames(s.block, className, { [s.disabled]: disabled, [s.open]: open, @@ -240,14 +241,14 @@ export default class SearchDropdown extends Component { const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; let displayText = this.state.displayText; - if (renderItem && this.state.selectedValue) { - displayText = renderItem(this.state.selectedValue); + if (renderItem && selectedValue) { + displayText = renderItem(selectedValue); } return (
    (this._pivot = p)} onClick={this.handleToggleClick}> -
    {displayText || this.state.selectedValue || placeholder}
    +
    {displayText || selectedValue || placeholder}
    diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx index 1043b4a09e..3e3b6e5e40 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx @@ -6,47 +6,47 @@ import SearchDropdown from './search-dropdown'; describe.only('SearchDropdown', function() { // @todo sinon and promises - it.skip('should set list only for corresponding token', function() { - const clock = sinon.useFakeTimers(); - const stub = sinon.stub(); - const items = ['one', 'two']; - const resolver = sinon.spy((resolve, value) => resolve(value)); - - function fetch(token) { - console.log('fetch'); - return {}; - - new Promise(function(resolve, reject) { - setTimeout(() => { - resolve({ items, token }); - console.log('resolve'); - }, 10); - }); - } - - for (let i = 0; i < 25; i++) { - setTimeout(() => console.log(i), i); - } - - const searchDropdown = mount(); - - const spySetState = sinon.spy(searchDropdown, 'setState'); - - searchDropdown.find('.pivot').simulate('click'); - - searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); - clock.tick(5); - searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'bar' } }); - clock.tick(6); // foo response ended - console.log('∑∑∑ foo response ended', searchDropdown.state('items').length); - expect(searchDropdown.state('items').length).to.equal(0); // foo response must be cancelled - clock.tick(15); // bar response ended - console.log('∑∑∑ bar response ended'); - console.log(searchDropdown.state('items')); - expect(searchDropdown.state('items').length).to.equal(2); - - clock.restore(); - }); + // it.skip('should set list only for corresponding token', function() { + // const clock = sinon.useFakeTimers(); + // const stub = sinon.stub(); + // const items = ['one', 'two']; + // const resolver = sinon.spy((resolve, value) => resolve(value)); + + // function fetch(token) { + // console.log('fetch'); + // return {}; + + // new Promise(function(resolve, reject) { + // setTimeout(() => { + // resolve({ items, token }); + // console.log('resolve'); + // }, 10); + // }); + // } + + // for (let i = 0; i < 25; i++) { + // setTimeout(() => console.log(i), i); + // } + + // const searchDropdown = mount(); + + // const spySetState = sinon.spy(searchDropdown, 'setState'); + + // searchDropdown.find('.pivot').simulate('click'); + + // searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); + // clock.tick(5); + // searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'bar' } }); + // clock.tick(6); // foo response ended + // console.log('∑∑∑ foo response ended', searchDropdown.state('items').length); + // expect(searchDropdown.state('items').length).to.equal(0); // foo response must be cancelled + // clock.tick(15); // bar response ended + // console.log('∑∑∑ bar response ended'); + // console.log(searchDropdown.state('items')); + // expect(searchDropdown.state('items').length).to.equal(2); + + // clock.restore(); + // }); it('should open TextInput', function() { const searchDropdown = mount(); diff --git a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx index a8a8bbcb12..d1daef7af2 100644 --- a/ashes/src/components/core/dropdown/smart-list/smart-list.jsx +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -6,7 +6,6 @@ import React, { Element, Component } from 'react'; import { autobind } from 'core-decorators'; import classNames from 'classnames'; -import Icon from 'components/core/icon'; import BodyPortal from 'components/body-portal/body-portal'; // styles diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index 01c4485967..fef17fd0be 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -139,11 +139,11 @@ export default class TextDropdown extends Component { renderItems() { const { detached, emptyMessage } = this.props; - let list = this.items.map(item => + let list = this.items.map(item => (
    this.handleItemClick(item)}> {item.displayText || item.value}
    - ); + )); if (!this.items.length) { list =
    {emptyMessage}
    ; @@ -165,7 +165,7 @@ export default class TextDropdown extends Component { } render() { - const { disabled, name, placeholder, className } = this.props; + const { disabled, name, className } = this.props; const { selectedValue, open } = this.state; const cls = classNames(s.block, className, { [s.disabled]: disabled, @@ -179,7 +179,7 @@ export default class TextDropdown extends Component {
    (this._pivot = p)} onClick={this.handleToggleClick}>
    {this.displayText}
    - +
    {this.menu}
    diff --git a/ashes/src/components/customers/store-credits/new-store-credit.jsx b/ashes/src/components/customers/store-credits/new-store-credit.jsx index 5b146d5241..087071bb00 100644 --- a/ashes/src/components/customers/store-credits/new-store-credit.jsx +++ b/ashes/src/components/customers/store-credits/new-store-credit.jsx @@ -37,20 +37,22 @@ const actions = { ...CustomerActions, ...NewStoreCreditActions, ...ScTypesActions, - ...ReasonsActions + ...ReasonsActions, }; -@connect((state, props) => ({ - ...state.customers.details[props.params.customerId], - ...state.customers.newStoreCredit, - ...state.storeCreditTypes, - ...state.reasons -}), actions) +@connect( + (state, props) => ({ + ...state.customers.details[props.params.customerId], + ...state.customers.newStoreCredit, + ...state.storeCreditTypes, + ...state.reasons, + }), + actions +) export default class NewStoreCredit extends React.Component { - static propTypes = { params: PropTypes.shape({ - customerId: PropTypes.number.required + customerId: PropTypes.number.required, }), error: PropTypes.array, }; @@ -129,13 +131,11 @@ export default class NewStoreCredit extends React.Component { get balances() { return this.props.balances.map((balance, idx) => { const classes = classNames('fc-store-credit-form__balance-value', { - '_selected': this.props.form.amount === balance + _selected: this.props.form.amount === balance, }); return ( -
    this.props.changeScFormData('amount', balance)}> +
    this.props.changeScFormData('amount', balance)}> ${balance / 100}
    ); @@ -151,12 +151,7 @@ export default class NewStoreCredit extends React.Component {
    - +
    {this.storeCreditTypeError} @@ -194,37 +189,37 @@ export default class NewStoreCredit extends React.Component { get storeCreditForm() { const hiddenClass = classNames('fc-col-md-1-3', { - '_hidden': _.isEmpty(this.scSubtypes) + _hidden: _.isEmpty(this.scSubtypes), }); const { form, changeScFormData } = this.props; return ( - +
      {this.typeChangeField}
    • - +
      - - + +
    • @@ -233,7 +228,7 @@ export default class NewStoreCredit extends React.Component {
    • @@ -250,7 +245,7 @@ export default class NewStoreCredit extends React.Component { name="subReasonId" items={this.scSubtypes} value={form.subTypeId} - onChange={(value) => changeScFormData('subTypeId', value)} + onChange={value => changeScFormData('subTypeId', value)} />
    @@ -290,23 +285,29 @@ export default class NewStoreCredit extends React.Component { const { form, convertGiftCard } = this.props; return ( - convertGiftCard(this.customerId)}> + convertGiftCard(this.customerId)} + >
      {this.typeChangeField}
    • - - + + {this.giftCardConvertErrors}
    • @@ -320,7 +321,7 @@ export default class NewStoreCredit extends React.Component {
    • @@ -332,11 +333,7 @@ export default class NewStoreCredit extends React.Component { } render() { - const form = this.props.form.type === 'giftCardTransfer' - ? this.giftCardConvertForm - : this.storeCreditForm; - - console.log('this.props.form.type', this.props.form.type); + const form = this.props.form.type === 'giftCardTransfer' ? this.giftCardConvertForm : this.storeCreditForm; return (
      diff --git a/ashes/src/components/promotions/discounts/index.jsx b/ashes/src/components/promotions/discounts/index.jsx index 2fedfb1db6..cf6a077e46 100644 --- a/ashes/src/components/promotions/discounts/index.jsx +++ b/ashes/src/components/promotions/discounts/index.jsx @@ -1,7 +1,5 @@ // @todo this file not used anywhere -!@#$%^&*()_+ - import _ from 'lodash'; import React, { Component } from 'react'; import { autobind } from 'core-decorators'; From 02039ce71aec435f3c22b508a64d267f94cd36d2 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Fri, 23 Jun 2017 14:16:30 -0700 Subject: [PATCH 12/16] Add at-classnames --- .../addresses/address-form/address-form.jsx | 8 +++-- .../credit-cards/card-expiration-block.jsx | 12 ++++--- .../store-credits/new-store-credit.jsx | 11 +++++-- .../components/gift-cards/gift-cards-new.jsx | 4 ++- .../components/new-payment/new-payment.jsx | 1 + ashes/src/components/orders/order.jsx | 31 ++++++++----------- .../components/products/custom-property.jsx | 3 +- .../components/promotions/discount-attrs.jsx | 25 ++++++++------- .../promotions/discounts/discounts.css | 5 --- .../components/promotions/discounts/index.jsx | 23 ++------------ .../components/promotions/promotion-form.jsx | 12 +++---- 11 files changed, 62 insertions(+), 73 deletions(-) diff --git a/ashes/src/components/addresses/address-form/address-form.jsx b/ashes/src/components/addresses/address-form/address-form.jsx index 6bc3e9c1d9..39eabff0df 100644 --- a/ashes/src/components/addresses/address-form/address-form.jsx +++ b/ashes/src/components/addresses/address-form/address-form.jsx @@ -7,6 +7,7 @@ import { autobind } from 'core-decorators'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import classNames from 'classnames'; // components import FormField from '../../forms/formfield'; @@ -250,9 +251,9 @@ export default class AddressForm extends React.Component { this.handleCountryChange(Number(value))} + onChange={value => this.handleCountryChange(Number(value))} items={this.countryItems} /> @@ -275,9 +276,10 @@ export default class AddressForm extends React.Component {
    • this.handleStateChange(Number(value))} + onChange={value => this.handleStateChange(Number(value))} items={this.regionItems} /> diff --git a/ashes/src/components/credit-cards/card-expiration-block.jsx b/ashes/src/components/credit-cards/card-expiration-block.jsx index 2312543f79..10b43d1e82 100644 --- a/ashes/src/components/credit-cards/card-expiration-block.jsx +++ b/ashes/src/components/credit-cards/card-expiration-block.jsx @@ -11,11 +11,11 @@ import FormField from '../forms/formfield'; import * as CardUtils from 'lib/credit-card-utils'; type Props = { - month: string; - year: string; - onMonthChange: Function; - onYearChange: Function; -} + month: string, + year: string, + onMonthChange: Function, + onYearChange: Function, +}; export default class ExpirationBlock extends Component { props: Props; @@ -34,6 +34,7 @@ export default class ExpirationBlock extends Component {
      month} validationLabel="Month" required> year} validationLabel="Year" required>
      - +
      {this.storeCreditTypeError}
    • @@ -242,6 +248,7 @@ export default class NewStoreCredit extends React.Component {
      props.changeFormData('subTypeId', Number(value))} - items={props.subTypes.map(subType =>[subType.id, subType.title])} + items={props.subTypes.map(subType => [subType.id, subType.title])} />
      ); @@ -171,6 +172,7 @@ export default class NewGiftCard extends React.Component {
      changeFormData('originType', value)} items={types.map((entry, idx) => [entry.originType, typeTitles[entry.originType]])} diff --git a/ashes/src/components/new-payment/new-payment.jsx b/ashes/src/components/new-payment/new-payment.jsx index bb3c6df0de..cc8a5589d3 100644 --- a/ashes/src/components/new-payment/new-payment.jsx +++ b/ashes/src/components/new-payment/new-payment.jsx @@ -88,6 +88,7 @@ class NewPayment extends Component { label="Payment Type" > { + get orderStateDropdown(): Element { const order = this.order; const claims = getClaims(); - if (order.orderState === 'canceled' || - order.orderState === 'shipped') { + if (order.orderState === 'canceled' || order.orderState === 'shipped') { return ; } @@ -171,18 +170,15 @@ export default class Order extends React.Component { holdStates = [...holdStates, 'fraudHold']; } - const visibleAndSortedOrderStates = [ - ...holdStates, - 'fulfillmentStarted', - 'shipped', - 'canceled', - ].filter(state => { - return order.orderState in allowedStateTransitions && - allowedStateTransitions[order.orderState].indexOf(state) != -1; + const visibleAndSortedOrderStates = [...holdStates, 'fulfillmentStarted', 'shipped', 'canceled'].filter(state => { + return ( + order.orderState in allowedStateTransitions && allowedStateTransitions[order.orderState].indexOf(state) != -1 + ); }); return ( [state, states.order[state]])} placeholder="Order state" @@ -197,11 +193,10 @@ export default class Order extends React.Component { const order = this.order; const claims = getClaims(); const shippingState = isPermitted(shippingClaims, claims) - ? ( - - - - ) : null; + ? + + + : null; return (
      diff --git a/ashes/src/components/products/custom-property.jsx b/ashes/src/components/products/custom-property.jsx index d753944713..b6b5a45e8d 100644 --- a/ashes/src/components/products/custom-property.jsx +++ b/ashes/src/components/products/custom-property.jsx @@ -6,6 +6,7 @@ import React, { Component, Element } from 'react'; import { autobind } from 'core-decorators'; import _ from 'lodash'; +import classNames from 'classnames'; // components import Modal from 'components/core/modal'; @@ -113,7 +114,7 @@ export default class CustomProperty extends Component { labelClassName="fc-product-details__field-label" > {item.title}; - } + }, }; type Props = { - onChange: (attrs: Object) => any; - attr: string; - descriptions: Array; - discount: Object; - dropdownId: string; - blockId: string; + onChange: (attrs: Object) => any, + attr: string, + descriptions: Array, + discount: Object, + dropdownId: string, + blockId: string, }; const DiscountAttrs = (props: Props) => { @@ -66,9 +67,9 @@ const DiscountAttrs = (props: Props) => { }); }; const setType = (type: any) => { - const newDiscountParams = attrs[type] || _.find(props.descriptions, {type}).default || {}; + const newDiscountParams = attrs[type] || _.find(props.descriptions, { type }).default || {}; props.onChange({ - [type]: newDiscountParams + [type]: newDiscountParams, }); }; diff --git a/ashes/src/components/promotions/discounts/discounts.css b/ashes/src/components/promotions/discounts/discounts.css index 8510f82164..feff01d5cb 100644 --- a/ashes/src/components/promotions/discounts/discounts.css +++ b/ashes/src/components/promotions/discounts/discounts.css @@ -1,10 +1,5 @@ @import 'variables.css'; -.discount_qualifier :global .autowidth_dd { - width: auto; - margin-right: 10px; -} - .discount_qualifier :global .inline-container { display: inline-block; vertical-align: top; diff --git a/ashes/src/components/promotions/discounts/index.jsx b/ashes/src/components/promotions/discounts/index.jsx index cf6a077e46..5e4da565d3 100644 --- a/ashes/src/components/promotions/discounts/index.jsx +++ b/ashes/src/components/promotions/discounts/index.jsx @@ -138,12 +138,7 @@ export default class Discounts extends Component { @autobind renderDiscount() { return ( - + ); } @@ -152,14 +147,7 @@ export default class Discounts extends Component { let discountType = this.qualifier.discountType; let items = _.find(QUALIFIER_TYPES, i => i.scope == discountType).list; - return ( - - ); + return ; } @autobind @@ -255,12 +243,7 @@ export default class Discounts extends Component { onChange={this.toggleExGiftCardOffer} /> - +
      ); } diff --git a/ashes/src/components/promotions/promotion-form.jsx b/ashes/src/components/promotions/promotion-form.jsx index 417bbe7744..1a3ac54a5f 100644 --- a/ashes/src/components/promotions/promotion-form.jsx +++ b/ashes/src/components/promotions/promotion-form.jsx @@ -120,11 +120,11 @@ export default class PromotionForm extends ObjectDetails { const makeKey = prefix => `${prefix}-${disc.id || index}`; return [ ...acc, -
      Qualifier
      , +
      Qualifier promotion
      , Offer
      , + />, ]; }, []); From bafa73a679587638ad4f637fb92118d280ce7b26 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Fri, 23 Jun 2017 15:39:49 -0700 Subject: [PATCH 13/16] add font-dropdown --- .../search-dropdown/search-dropdown.jsx | 49 +++--- .../search-dropdown/search-dropdown.md | 16 +- .../dropdown/text-dropdown/text-dropdown.css | 1 + .../dropdown/text-dropdown/text-dropdown.jsx | 9 +- .../dropdown/text-dropdown/text-dropdown.md | 6 +- .../components/dropdown/dropdown-search.css | 25 --- .../components/dropdown/dropdown-search.jsx | 138 --------------- ashes/src/components/dropdown/index.js | 9 +- .../promotions/widgets/select-product.jsx | 3 +- .../rich-text-editor/font-dropdown.css | 37 ++++ .../rich-text-editor/font-dropdown.jsx | 161 ++++++++++++++++++ .../rich-text-editor/rich-text-editor.css | 7 + .../rich-text-editor/rich-text-editor.jsx | 22 ++- ashes/src/less/modules/_rich-text-editor.less | 12 -- 14 files changed, 258 insertions(+), 237 deletions(-) delete mode 100644 ashes/src/components/dropdown/dropdown-search.css delete mode 100644 ashes/src/components/dropdown/dropdown-search.jsx create mode 100644 ashes/src/components/rich-text-editor/font-dropdown.css create mode 100644 ashes/src/components/rich-text-editor/font-dropdown.jsx diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx index 013fde0a57..71c2e24b0f 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.jsx @@ -24,13 +24,13 @@ type Item = [string, string]; type Props = { /** An array of initial values which will be in a list */ - items: Array, + initialItems: Array, /** Input name which will be used by form */ name: string, // input name /** Input value */ - value: string | number | null, // input value + initialValue: string | number | null, // input value /** Initial display text to be shown (if no renderItem defined) */ - displayText: string, + initialDisplayText: string, /** Text which is visible when no value */ placeholder: string, /** Placeholder for search input */ @@ -57,30 +57,30 @@ type State = { }; /** - * Search Dropdown component. - * This component is for fetching and rendering fetched results as a list. - * There are initial value, displayText and items, but after mounting component handle these values internaly. + * This component is for fetching and rendering fetched results as a list of simple or user defined blocks. + * It is possible to define some initials: value, displayText and items. + * But after mounting component handle these values monopoly via state. */ export default class SearchDropdown extends Component { props: Props; static defaultProps = { name: '', - value: '', - displayText: '', + initialValue: '', + initialDisplayText: '', placeholder: '- Select -', searchbarPlaceholder: 'Start to type...', disabled: false, onChange: () => {}, - items: [], + initialItems: [], }; state: State = { open: false, - selectedValue: this.getValue(this.props.value), - displayText: this.getDisplayText(this.props.items), + selectedValue: this.getValue(this.props.initialValue), + displayText: this.getDisplayText(this.props.initialItems), token: '', - items: this.unifyItems(this.props.items), + items: this.unifyItems(this.props.initialItems), isLoading: false, }; @@ -101,10 +101,10 @@ export default class SearchDropdown extends Component { getDisplayText(dirtyItems: Array): string { const items = this.unifyItems(dirtyItems); - const value = this.getValue(this.props.value); + const value = this.getValue(this.props.initialValue); const item = items.find(item => item.value === value); - return item ? item.displayText : this.props.displayText; + return item ? item.displayText : this.props.initialDisplayText; } @autobind @@ -153,8 +153,15 @@ export default class SearchDropdown extends Component { @debounce(400) fetch(token: string) { - this.props - .fetch(token) + let promise; + + if (token) { + promise = this.props.fetch(token); + } else { + promise = Promise.resolve({ items: [], token }); + } + + promise .then(data => { if (data.token === token) { this.setState({ items: this.unifyItems(data.items), isLoading: false }); @@ -164,14 +171,8 @@ export default class SearchDropdown extends Component { } onTokenChange(token: string) { - this.setState({ token }); - - if (!token) { - this.setState({ items: [], isLoading: false }); - } else { - this.setState({ isLoading: true }); - this.fetch(token); - } + this.setState({ token, isLoading: true }); + this.fetch(token); } renderSearchBar() { diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md index 3c15e948ac..76b7dbe742 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md @@ -3,27 +3,25 @@ ```javascript import { SearchDropdown } from 'components/core/dropdown'; -const items = [ - { value: 'One' }, - { value: 'Two' }, - { value: 'Three', displayText: 'Three!' } -]; +function fetch(token) { + return new Promise(...); +}; - + new Promise(...)} /> ``` ``` -const items = [{ value: 'One' }, { value: 'Two' }, { value: 'Three', displayText: 'Three!' }]; - function fetch(token) { return new Promise(function(resolve, reject) { + const items = _.uniq(token.split('')).map(ch => ch); + setTimeout(() => resolve({ items, token }), 1000); }); };
      - +
      ``` diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css index ce69e62802..dce7c1db49 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css @@ -4,6 +4,7 @@ position: relative; font: var(--font-labels); text-align: left; /* in case if dropdown in centered stuff, e.g. Order State */ + user-select: none; &.open { z-index: 1; diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index fef17fd0be..57d422cd98 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -49,10 +49,9 @@ type State = { }; /** - * Text Dropdown component. - * This component is about to render simple text list and current value. + * This component could render simple text list and current value. * This component is not responsible for different skins, TextInputs, infinite lists, or any other complex stuff. - * If you need any functionality which is not exists here, try different Dropdown or build new one. + * If you need any functionality beyond existing, try different Dropdown or build new one. */ export default class TextDropdown extends Component { props: Props; @@ -139,11 +138,11 @@ export default class TextDropdown extends Component { renderItems() { const { detached, emptyMessage } = this.props; - let list = this.items.map(item => ( + let list = this.items.map(item =>
      this.handleItemClick(item)}> {item.displayText || item.value}
      - )); + ); if (!this.items.length) { list =
      {emptyMessage}
      ; diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md index 2bfb7e0c1b..e2fc3dc55a 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md @@ -17,14 +17,14 @@ const items = [{ value: 'One' }, { value: 'Two' }, { value: 'Three', displayText
      - console.log(e)} value="One" /> + console.log(e)} placeholder="- Change me -" />
      - +
      - +
      ``` diff --git a/ashes/src/components/dropdown/dropdown-search.css b/ashes/src/components/dropdown/dropdown-search.css deleted file mode 100644 index bc8fc58d01..0000000000 --- a/ashes/src/components/dropdown/dropdown-search.css +++ /dev/null @@ -1,25 +0,0 @@ -@import 'variables.css'; - -.searchbar-wrapper { - width: 100%; - height: 50px; -} - -.searchbar-input-wrapper { - margin: 10px; -} - -.searchbar-input { - width: 100%; - padding-left: 30px !important; -} - -.searchbar-icon-wrapper { - position: relative; - left: 15px; - top: -50px; - font-size: 20px; - width: 20px; - color: var(--color-additional-text); - line-height: 35px; -} diff --git a/ashes/src/components/dropdown/dropdown-search.jsx b/ashes/src/components/dropdown/dropdown-search.jsx deleted file mode 100644 index 71b114b6b7..0000000000 --- a/ashes/src/components/dropdown/dropdown-search.jsx +++ /dev/null @@ -1,138 +0,0 @@ -/* @flow */ - -// libs -import _ from 'lodash'; -import React, { Component } from 'react'; -import { autobind, debounce } from 'core-decorators'; - -// components -import GenericDropdown from './generic-dropdown'; -import DropdownItem from './dropdownItem'; -import TextInput from 'components/core/text-input'; -import Icon from 'components/core/icon'; - -// styles -import styles from './dropdown-search.css'; - -import type { Props as GenericProps } from './generic-dropdown'; - -type Props = GenericProps & { - searchbarPlaceholder?: string, - fetchOptions: Function, - renderOption?: Function, - omitSearchIfEmpty: boolean, -}; - -type State = { - token: string, - results: Array, -}; - -function doNothing(event: any) { - event.stopPropagation(); - event.preventDefault(); -} - -export default class DropdownSearch extends Component { - props: Props; - - state: State = { - token: '', - results: [], - }; - - mounted: boolean = false; - - componentDidMount() { - this.fetchOptions(this.state.token); - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - @debounce(400) - fetchOptions(token: string) { - if (!this.mounted) return null; - if (this.props.omitSearchIfEmpty && token == '') return null; - this.props.fetchOptions(this.state.token).then(data => { - this.setState({ results: data }); - }); - } - - @autobind - onTokenChange(token: string) { - this.setState({ token }, () => { - this.fetchOptions(token); - }); - } - - @autobind - searchBar() { - return ( -
      -
      -
      - -
      -
      - -
      -
      -
      - ); - } - - get results(): Array { - const results = this.state.results; - - if (_.isEmpty(results)) { - return []; - } - - return results; - } - - get searchResults(): any { - const { renderOption } = this.props; - return _.map(this.results, result => { - if (renderOption) { - return renderOption(result); - } - - const id = _.get(result, 'id', ''); - const name = _.get(result, 'name', ''); - - return ( - - {name} - - ); - }); - } - - shouldComponentUpdate(nextProps: Props, nextState: State): boolean { - return !_.eq(this.state, nextState); - } - - render() { - const restProps = _.omit(this.props, 'children'); - - return ( - - {this.searchResults} - - ); - } -} diff --git a/ashes/src/components/dropdown/index.js b/ashes/src/components/dropdown/index.js index f986bb55f8..3501c94baf 100644 --- a/ashes/src/components/dropdown/index.js +++ b/ashes/src/components/dropdown/index.js @@ -1,10 +1,3 @@ - import Dropdown from './dropdown'; -import DropdownItem from './dropdownItem'; -import DropdownSearch from './dropdown-search'; -export { - Dropdown, - DropdownItem, - DropdownSearch, -}; +export { Dropdown }; diff --git a/ashes/src/components/promotions/widgets/select-product.jsx b/ashes/src/components/promotions/widgets/select-product.jsx index 0a9de90fb5..99f8df3767 100644 --- a/ashes/src/components/promotions/widgets/select-product.jsx +++ b/ashes/src/components/promotions/widgets/select-product.jsx @@ -87,12 +87,13 @@ export default class SelectProduct extends Component { } render() { + /* @todo initialDisplayText */ return ( , + /** Input value */ + value: string | number | null, // input value + /** Additional root className */ + className?: string, + /** If true, you cant open dropdown or change its value from UI */ + disabled: boolean, + /** Callback which fires when the value has been changes */ + onChange: Function, +}; + +type State = { + open: boolean, // show or hide the menu + selectedValue: string, // current selected value of menu +}; + +/** + * This component could render simple text list and current value. + * This component is not responsible for different skins, TextInputs, infinite lists, or any other complex stuff. + * If you need any functionality beyond existing, try different Dropdown or build new one. + */ +export default class TextDropdown extends Component { + props: Props; + + static defaultProps = { + value: '', + disabled: false, + onChange: () => {}, + items: [], + }; + + state: State = { + open: false, + selectedValue: this.getValue(this.props.value), + }; + + _pivot: HTMLElement; + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.value !== this.props.value) { + this.setState({ selectedValue: this.getValue(nextProps.value) }); + } + } + + getValue(value: any) { + return value ? String(value) : ''; + } + + @autobind + handleToggleClick(event: any) { + event.preventDefault(); + + if (this.props.disabled) { + return; + } + + this.toggleMenu(); + } + + handleItemClick(item: InternalItem) { + let nextState = { open: false, selectedValue: this.state.selectedValue }; + + if (item.value !== this.state.selectedValue) { + nextState.selectedValue = item.value; + + this.props.onChange(item.value); + } + + this.setState(nextState); + } + + toggleMenu(nextOpen: ?boolean) { + const open = nextOpen != null ? nextOpen : !this.state.open; + + this.setState({ open }); + } + + get displayText(): string { + const item = _.find(this.items, item => item.value == this.state.selectedValue); // could be number == string + + return (item && item.displayText) || this.state.selectedValue; + } + + get items(): Array { + const { items } = this.props; + + if (Array.isArray(items[0])) { + return items.map(([value, displayText]) => ({ value, displayText })); + } else if (typeof items[0] === 'string') { + return items.map((value: string) => ({ value, displayText: value })); + } + + return items; + } + + renderItems() { + let list = this.items.map(item => +
      this.handleItemClick(item)}> + {item.displayText || item.value} +
      + ); + + return ( + this.toggleMenu(false)} pivot={this._pivot}> + {list} + + ); + } + + get menu(): ?Element { + if (!this.state.open) { + return; + } + + return this.renderItems(); + } + + render() { + const { disabled, className } = this.props; + const { open } = this.state; + const cls = classNames(s.block, className, { + [s.disabled]: disabled, + [s.open]: open, + }); + const arrow = this.state.open ? 'chevron-up' : 'chevron-down'; + + return ( +
      +
      (this._pivot = p)} onClick={this.handleToggleClick}> +
      {this.displayText}
      + +
      + {this.menu} +
      + ); + } +} diff --git a/ashes/src/components/rich-text-editor/rich-text-editor.css b/ashes/src/components/rich-text-editor/rich-text-editor.css index ffa04f5471..8d78f75d4f 100644 --- a/ashes/src/components/rich-text-editor/rich-text-editor.css +++ b/ashes/src/components/rich-text-editor/rich-text-editor.css @@ -1,3 +1,10 @@ .set { display: flex; + align-items: center; + flex: 1 1 auto; +} + +.commands { + display: flex; + flex: 1 1 auto; } diff --git a/ashes/src/components/rich-text-editor/rich-text-editor.jsx b/ashes/src/components/rich-text-editor/rich-text-editor.jsx index c3754e12eb..a42253ba04 100644 --- a/ashes/src/components/rich-text-editor/rich-text-editor.jsx +++ b/ashes/src/components/rich-text-editor/rich-text-editor.jsx @@ -13,7 +13,7 @@ import { stateToMarkdown } from 'draft-js-export-markdown'; // components import { ContentBlock, ContentState, Editor, EditorState, RichUtils } from 'draft-js'; -import { Dropdown } from '../dropdown'; // @todo replace with personal dropdown +import FontDropdown from './font-dropdown'; import ToggleButton from './toggle-button'; import s from './rich-text-editor.css'; import Icon from 'components/core/icon'; @@ -138,15 +138,13 @@ export default class RichTextEditor extends Component { const blockType = editorState.getCurrentContent().getBlockForKey(selection.getStartKey()).getType(); return ( -
      - } - onChange={this.handleBlockTypeChange} - value={blockType} - items={headerStyles.map(t => [t.value, t.label])} - /> -
      + [t.value, t.label])} + /> ); } @@ -271,7 +269,7 @@ export default class RichTextEditor extends Component { ); }); - return
      {buttons}
      ; + return
      {buttons}
      ; } shouldComponentUpdate(nextProps: Props, nextState: State): boolean { @@ -293,7 +291,7 @@ export default class RichTextEditor extends Component {
      {this.props.label &&
      {this.props.label}
      }
      -
      +
      {this.commandBarContent}
      diff --git a/ashes/src/less/modules/_rich-text-editor.less b/ashes/src/less/modules/_rich-text-editor.less index 2df3592f4b..31cdd4640c 100644 --- a/ashes/src/less/modules/_rich-text-editor.less +++ b/ashes/src/less/modules/_rich-text-editor.less @@ -12,18 +12,6 @@ padding: 0 10px; } - &__main-commands { - display: flex; - } - - &__command-set { - margin-right: 20px; - - &:last-child { - margin-right: 0; - } - } - &._content-type-html &__btn-markdown { visibility: hidden; } From b726cae0097506a358cb6f01de2e2dfa43ff5e2f Mon Sep 17 00:00:00 2001 From: Diokuz Date: Tue, 27 Jun 2017 19:07:35 +0300 Subject: [PATCH 14/16] cleanup --- .../core/dropdown/search-dropdown/search-dropdown.spec.jsx | 2 +- .../core/dropdown/text-dropdown/text-dropdown.jsx | 4 ++-- .../components/customers/store-credits/new-store-credit.jsx | 2 +- ashes/src/components/orders/order.jsx | 4 ++-- ashes/src/components/promotions/discount-attrs.jsx | 2 +- ashes/src/components/rich-text-editor/font-dropdown.jsx | 6 +++--- ashes/src/components/rich-text-editor/rich-text-editor.jsx | 1 - 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx index 3e3b6e5e40..a7399bff6b 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx @@ -4,7 +4,7 @@ import { mount } from 'enzyme'; import SearchDropdown from './search-dropdown'; -describe.only('SearchDropdown', function() { +describe('SearchDropdown', function() { // @todo sinon and promises // it.skip('should set list only for corresponding token', function() { // const clock = sinon.useFakeTimers(); diff --git a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx index 57d422cd98..5f37dc13c2 100644 --- a/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -138,11 +138,11 @@ export default class TextDropdown extends Component { renderItems() { const { detached, emptyMessage } = this.props; - let list = this.items.map(item => + let list = this.items.map(item => (
      this.handleItemClick(item)}> {item.displayText || item.value}
      - ); + )); if (!this.items.length) { list =
      {emptyMessage}
      ; diff --git a/ashes/src/components/customers/store-credits/new-store-credit.jsx b/ashes/src/components/customers/store-credits/new-store-credit.jsx index 5730d372af..98266c678c 100644 --- a/ashes/src/components/customers/store-credits/new-store-credit.jsx +++ b/ashes/src/components/customers/store-credits/new-store-credit.jsx @@ -274,7 +274,7 @@ export default class NewStoreCredit extends React.Component { case 'Open transactions should be canceled/completed': message = 'Gift card is already used in order and cannot be transfered to store credits.'; break; - case "cannot convert a gift card with state 'FullyRedeemed'": + case 'cannot convert a gift card with state \'FullyRedeemed\'': message = 'Gift card is fully redeemed.'; break; default: diff --git a/ashes/src/components/orders/order.jsx b/ashes/src/components/orders/order.jsx index 1e51fbb11d..5fc5b01d3e 100644 --- a/ashes/src/components/orders/order.jsx +++ b/ashes/src/components/orders/order.jsx @@ -193,9 +193,9 @@ export default class Order extends React.Component { const order = this.order; const claims = getClaims(); const shippingState = isPermitted(shippingClaims, claims) - ? + ? ( - + ) : null; return ( diff --git a/ashes/src/components/promotions/discount-attrs.jsx b/ashes/src/components/promotions/discount-attrs.jsx index 17c9d6f9c4..a21e2fb1d3 100644 --- a/ashes/src/components/promotions/discount-attrs.jsx +++ b/ashes/src/components/promotions/discount-attrs.jsx @@ -11,7 +11,7 @@ import { TextDropdown } from 'components/core/dropdown'; import type { ItemDesc, DiscountRow, DescriptionType, Context } from './types'; const renderers = { - type(item: ItemDesc, context: Context, dropdownId: Props) { + type(item: ItemDesc, context: Context, dropdownId: string) { const typeItems = _.map(context.root, entry => [entry.type, entry.title]); return ( diff --git a/ashes/src/components/rich-text-editor/font-dropdown.jsx b/ashes/src/components/rich-text-editor/font-dropdown.jsx index 9d8209b3c7..15d86bec30 100644 --- a/ashes/src/components/rich-text-editor/font-dropdown.jsx +++ b/ashes/src/components/rich-text-editor/font-dropdown.jsx @@ -49,7 +49,7 @@ export default class TextDropdown extends Component { static defaultProps = { value: '', disabled: false, - onChange: () => {}, + onChange: (value: string) => {}, items: [], }; @@ -118,11 +118,11 @@ export default class TextDropdown extends Component { } renderItems() { - let list = this.items.map(item => + let list = this.items.map(item => (
      this.handleItemClick(item)}> {item.displayText || item.value}
      - ); + )); return ( this.toggleMenu(false)} pivot={this._pivot}> diff --git a/ashes/src/components/rich-text-editor/rich-text-editor.jsx b/ashes/src/components/rich-text-editor/rich-text-editor.jsx index a42253ba04..a8f4e603fd 100644 --- a/ashes/src/components/rich-text-editor/rich-text-editor.jsx +++ b/ashes/src/components/rich-text-editor/rich-text-editor.jsx @@ -16,7 +16,6 @@ import { ContentBlock, ContentState, Editor, EditorState, RichUtils } from 'draf import FontDropdown from './font-dropdown'; import ToggleButton from './toggle-button'; import s from './rich-text-editor.css'; -import Icon from 'components/core/icon'; type Props = { label?: string, From caba7db0c44498df18bcd7934e23e468655de669 Mon Sep 17 00:00:00 2001 From: Diokuz Date: Tue, 27 Jun 2017 22:03:30 +0300 Subject: [PATCH 15/16] fix tests --- .../core/button-with-menu/button-with-menu.jsx | 2 +- .../dropdown/search-dropdown/search-dropdown.spec.jsx | 10 +++++----- ashes/src/components/images/edit-image.jsx | 2 +- ashes/test/acceptance/table/pagesize.jsx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ashes/src/components/core/button-with-menu/button-with-menu.jsx b/ashes/src/components/core/button-with-menu/button-with-menu.jsx index 481debb5b4..0694892f27 100644 --- a/ashes/src/components/core/button-with-menu/button-with-menu.jsx +++ b/ashes/src/components/core/button-with-menu/button-with-menu.jsx @@ -9,7 +9,7 @@ import Transition from 'react-transition-group/CSSTransitionGroup'; // components import { PrimaryButton } from 'components/core/button'; -import { DropdownItem } from 'components/dropdown'; +import DropdownItem from 'components/dropdown/dropdownItem'; // styles import s from './button-with-menu.css'; diff --git a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx index a7399bff6b..8111d29d67 100644 --- a/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx @@ -89,7 +89,7 @@ describe('SearchDropdown', function() { it('should not be spinner if loading and there is non-empty result', function() { const fetch = sinon.spy(() => Promise.resolve([])); - const searchDropdown = mount(); + const searchDropdown = mount(); searchDropdown.find('.pivot').simulate('click'); // open dropdown searchDropdown.find('.searchBarInput').simulate('change', { target: { value: 'foo' } }); // type `foo` @@ -100,7 +100,7 @@ describe('SearchDropdown', function() { it('should render value as displayText if no items with that value and no displayText', function() { const value = 'arfglauefah'; - const searchDropdown = mount(); + const searchDropdown = mount(); expect(searchDropdown.find('.pivot').text()).to.equal(value); }); @@ -109,14 +109,14 @@ describe('SearchDropdown', function() { const value = 'val2'; const displayText = 'displayText2'; const items = [['val1', 'displayText1'], ['val2', displayText]]; - const searchDropdown = mount(); + const searchDropdown = mount(); expect(searchDropdown.find('.pivot').text()).to.equal(displayText); }); it('should use renderItem to display value if passed and value exists', function() { const displayText = 'bfkyrgfoaui'; - const searchDropdown = mount( displayText} />); + const searchDropdown = mount( displayText} />); expect(searchDropdown.find('.pivot').text()).to.equal(displayText); }); @@ -131,7 +131,7 @@ describe('SearchDropdown', function() { it('should close dropdown after clicking the same item second time', function() { const items = ['val1', 'val2']; - const searchDropdown = mount(); + const searchDropdown = mount(); searchDropdown.find('.pivot').simulate('click'); // open expect(searchDropdown.state('open')).to.be.true; diff --git a/ashes/src/components/images/edit-image.jsx b/ashes/src/components/images/edit-image.jsx index 7c59daa91d..36024d79b8 100644 --- a/ashes/src/components/images/edit-image.jsx +++ b/ashes/src/components/images/edit-image.jsx @@ -6,7 +6,7 @@ import { autobind } from 'core-decorators'; import React, { Component } from 'react'; // components -import { DateTime } from 'components/common/datetime'; +import { DateTime } from 'components/utils/datetime'; import Modal from 'components/core/modal'; import { FormField } from '../forms'; import SaveCancel from 'components/core/save-cancel'; diff --git a/ashes/test/acceptance/table/pagesize.jsx b/ashes/test/acceptance/table/pagesize.jsx index b3eb53166b..3b6cebafab 100644 --- a/ashes/test/acceptance/table/pagesize.jsx +++ b/ashes/test/acceptance/table/pagesize.jsx @@ -15,6 +15,6 @@ describe('PageSize', function() { it(`should render selected page size`, function*() { pageSize = yield renderIntoDocument(); - expect(pageSize.container.querySelector('.fc-dropdown__value').textContent).to.equal('View 25'); + expect(pageSize.container.querySelector('.dropdown').textContent).to.equal('View 25'); }); }); From 882bebd049d7761056c984d33a52912c0643a5ef Mon Sep 17 00:00:00 2001 From: Diokuz Date: Wed, 28 Jun 2017 10:46:22 +0300 Subject: [PATCH 16/16] remove unused files --- ashes/Makefile | 1 - .../icon-input/append-icon-input.jsx | 0 .../icon-input/prepend-icon-input.jsx | 0 ashes/src/components/icon/icon.jsx | 0 ashes/src/components/toggle/toggle.jsx | 48 ------------------- 5 files changed, 49 deletions(-) delete mode 100644 ashes/src/components/icon-input/append-icon-input.jsx delete mode 100644 ashes/src/components/icon-input/prepend-icon-input.jsx delete mode 100644 ashes/src/components/icon/icon.jsx delete mode 100644 ashes/src/components/toggle/toggle.jsx diff --git a/ashes/Makefile b/ashes/Makefile index 10d783f41f..aaabe92093 100644 --- a/ashes/Makefile +++ b/ashes/Makefile @@ -1,7 +1,6 @@ include ../makelib header = $(call baseheader, $(1), ashes) - export PATH := $(CURDIR)/node_modules/.bin:$(PATH) SHELL := env PATH=$(PATH) /bin/sh diff --git a/ashes/src/components/icon-input/append-icon-input.jsx b/ashes/src/components/icon-input/append-icon-input.jsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ashes/src/components/icon-input/prepend-icon-input.jsx b/ashes/src/components/icon-input/prepend-icon-input.jsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ashes/src/components/icon/icon.jsx b/ashes/src/components/icon/icon.jsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ashes/src/components/toggle/toggle.jsx b/ashes/src/components/toggle/toggle.jsx deleted file mode 100644 index c2dfc7d842..0000000000 --- a/ashes/src/components/toggle/toggle.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -/** - * Toggle - */ -export default class Toggle extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { - on: 'yes', - off: 'no', - value: !!this.props.value - }; - } - - handleClick(event) { - event.preventDefault(); - this.setState({ - value: !this.state.value - }, () => { - if (this.props.onToggle) { - this.props.onToggle(this.state.value); - } - }); - } - - render() { - var classes = classNames({ - 'fc-ui-toggle': true, - 'fc-ui-toggle_on': this.state.value - }); - return ( -
      -
      -
      {this.state.on}
      -
      {this.state.off}
      -
      -
      - ); - } -} - -Toggle.propTypes = { - value: PropTypes.bool, - onToggle: PropTypes.func -};