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/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..39eabff0df 100644 --- a/ashes/src/components/addresses/address-form/address-form.jsx +++ b/ashes/src/components/addresses/address-form/address-form.jsx @@ -7,13 +7,14 @@ 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'; 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'; @@ -248,10 +249,9 @@ export default class AddressForm extends React.Component {
  • - this.handleCountryChange(Number(value))} items={this.countryItems} @@ -275,8 +275,8 @@ export default class AddressForm extends React.Component {
  • - this.handleStateChange(Number(value))} 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 e2a622cfc3..529b834a2e 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, 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,27 @@ 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); + ReactDOM.unstable_renderSubtreeIntoContainer(this,
    {this.props.children}
    , this._target); + + this.props.getRef(this._target); } render() { - return this.props.active ? null : this.props.children; + const { active, className, children, getRef } = this.props; + + if (active) { + return null; // see renderContent() + } + + return ( +
    + {children} +
    + ); } } 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..da0bd65f54 100644 --- a/ashes/src/components/bulk-actions/actions-dropdown.jsx +++ b/ashes/src/components/bulk-actions/actions-dropdown.jsx @@ -4,7 +4,7 @@ import React from 'react'; import PropTypes from 'prop-types'; // components -import { Dropdown } from '../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; // styles import s from './actions-dropdown.css'; @@ -18,20 +18,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/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)} /> - - + , + /** Input name which will be used by form */ + name: string, // input name + /** Input value */ + initialValue: string | number | null, // input value + /** Initial display text to be shown (if no renderItem defined) */ + initialDisplayText: string, + /** 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 */ + disabled: 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, + /** 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, + token: string, + items: Array, + isLoading: boolean, +}; + +/** + * 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: '', + initialValue: '', + initialDisplayText: '', + placeholder: '- Select -', + searchbarPlaceholder: 'Start to type...', + disabled: false, + onChange: () => {}, + initialItems: [], + }; + + state: State = { + open: false, + selectedValue: this.getValue(this.props.initialValue), + displayText: this.getDisplayText(this.props.initialItems), + token: '', + items: this.unifyItems(this.props.initialItems), + isLoading: false, + }; + + _pivot: HTMLElement; + _input: React$Component; + + componentDidUpdate(prevProps: Props, prevState: State) { + const input = ReactDOM.findDOMNode(this._input); + + if (this.state.open && !prevState.open && input instanceof HTMLElement) { + input.focus(); + } + } + + getValue(value: any): string { + return value ? String(value) : ''; + } + + getDisplayText(dirtyItems: Array): string { + const items = this.unifyItems(dirtyItems); + const value = this.getValue(this.props.initialValue); + const item = items.find(item => item.value === value); + + return item ? item.displayText : this.props.initialDisplayText; + } + + @autobind + handleToggleClick(event: any) { + event.preventDefault(); + + if (this.props.disabled) { + return; + } + + this.toggleMenu(); + } + + handleItemClick(item: InternalItem) { + const nextState = { + open: false, + selectedValue: item.value, + displayText: item.displayText || item.value, + }; + + if (item.value !== this.state.selectedValue) { + this.props.onChange(item.value); + this.setState(nextState); + } else { + this.setState({ open: false }); + } + } + + toggleMenu(nextOpen: ?boolean) { + const open = nextOpen != null ? nextOpen : !this.state.open; + + this.setState({ open }); + } + + unifyItems(dirtyItems: Array): 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) { + 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 }); + } + }) + .catch(() => this.setState({ isLoading: false })); + } + + onTokenChange(token: string) { + this.setState({ token, isLoading: true }); + this.fetch(token); + } + + renderSearchBar() { + return ( +
    + + (this._input = i)} + placeholder={this.props.searchbarPlaceholder} + className={s.searchBarInput} + value={this.state.token} + onChange={value => this.onTokenChange(value)} + /> +
    + ); + } + + renderItems() { + let after = null; + 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 = null; + after = ; + } + + return ( + this.toggleMenu(false)} + pivot={this._pivot} + before={this.renderSearchBar()} + after={after} + > + {content} + + ); + } + + get menu(): ?Element { + if (!this.state.open) { + return; + } + + return this.renderItems(); + } + + render() { + const { disabled, name, placeholder, className, renderItem } = 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'; + let displayText = this.state.displayText; + + if (renderItem && selectedValue) { + displayText = renderItem(selectedValue); + } + + return ( +
    +
    (this._pivot = p)} onClick={this.handleToggleClick}> +
    {displayText || selectedValue || placeholder}
    + + +
    + {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..76b7dbe742 --- /dev/null +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.md @@ -0,0 +1,27 @@ +#### Basic usage + +```javascript +import { SearchDropdown } from 'components/core/dropdown'; + +function fetch(token) { + return new Promise(...); +}; + + new Promise(...)} /> +``` + +``` +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/search-dropdown/search-dropdown.spec.jsx b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx new file mode 100644 index 0000000000..8111d29d67 --- /dev/null +++ b/ashes/src/components/core/dropdown/search-dropdown/search-dropdown.spec.jsx @@ -0,0 +1,146 @@ +import React from 'react'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; + +import SearchDropdown from './search-dropdown'; + +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)); + + // 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 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(fetch.calledOnce).to.be.true; + expect(searchDropdown.state('token')).to.equal('foo'); + + clock.restore(); + }); + + 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(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/core/dropdown/smart-list/smart-list.css b/ashes/src/components/core/dropdown/smart-list/smart-list.css new file mode 100644 index 0000000000..cb51177d66 --- /dev/null +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.css @@ -0,0 +1,27 @@ +@import 'variables.css'; + +.block { + position: absolute; + 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; + left: 0; + + /* define `top` in the hoist component */ +} + +.item { + &.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..d1daef7af2 --- /dev/null +++ b/ashes/src/components/core/dropdown/smart-list/smart-list.jsx @@ -0,0 +1,237 @@ +/* @flow */ + +// libs +import _ from 'lodash'; +import React, { Element, Component } from 'react'; +import { autobind } from 'core-decorators'; +import classNames from 'classnames'; + +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, + /** 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 = { + 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', + onEsc: () => {}, + detached: false, + }; + + state: State = { + pointedValueIndex: -1, + }; + + _items: HTMLElement; + _block: HTMLElement; + + 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(): 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 + + if (!pivot || !this._block) { + return; + } + + const pivotDim = pivot.getBoundingClientRect(); + + 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 = 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; + // esc + case 27: + this.props.onEsc(); + + 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 } = 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 }), + }; + + return React.cloneElement(item, props); + }); + } + + render() { + const { before, after, className, detached } = 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..dce7c1db49 --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.css @@ -0,0 +1,62 @@ +@import 'variables.css'; + +.block { + 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; + } +} + +.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); +} + +.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; + } +} 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..5f37dc13c2 --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.jsx @@ -0,0 +1,187 @@ +/* @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 './text-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, + /** 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: 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 */ + 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 +}; + +/** + * 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 = { + name: '', + value: '', + placeholder: '- Select -', + emptyMessage: '- Empty -', + disabled: false, + detached: 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() { + 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.toggleMenu(false)} detached={detached} pivot={this._pivot}> + {list} + + ); + } + + get menu(): ?Element { + if (!this.state.open) { + return; + } + + return this.renderItems(); + } + + render() { + const { disabled, name, 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/text-dropdown/text-dropdown.md b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md new file mode 100644 index 0000000000..e2fc3dc55a --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.md @@ -0,0 +1,30 @@ +#### 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)} placeholder="- Change me -" /> +
    +
    + +
    +
    + +
    + +
    +``` 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..9f9fe0d1db --- /dev/null +++ b/ashes/src/components/core/dropdown/text-dropdown/text-dropdown.spec.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; + +import TextDropdown from './text-dropdown'; + +describe('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'); + }); + + 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; + }); +}); 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/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..10b43d1e82 100644 --- a/ashes/src/components/credit-cards/card-expiration-block.jsx +++ b/ashes/src/components/credit-cards/card-expiration-block.jsx @@ -4,18 +4,18 @@ import React, { Component, Element } from 'react'; // components -import Dropdown from '../dropdown/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import FormField from '../forms/formfield'; // utils 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; @@ -33,23 +33,27 @@ export default class ExpirationBlock extends Component {
    month} validationLabel="Month" required> - +
    year} validationLabel="Year" required> - +
    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 ( - 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/customers/store-credits/new-store-credit.jsx b/ashes/src/components/customers/store-credits/new-store-credit.jsx index 8131fd6953..98266c678c 100644 --- a/ashes/src/components/customers/store-credits/new-store-credit.jsx +++ b/ashes/src/components/customers/store-credits/new-store-credit.jsx @@ -15,7 +15,7 @@ import { transitionTo, transitionToLazy } from 'browserHistory'; import { PageTitle } from '../../section-title'; import FormField from '../../forms/formfield'; import Form from '../../forms/form'; -import Dropdown from '../../dropdown/dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import Currency from 'components/utils/currency'; import SaveCancel from 'components/core/save-cancel'; import TextInput from 'components/core/text-input'; @@ -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,13 @@ export default class NewStoreCredit extends React.Component {
    - +
    {this.storeCreditTypeError}
  • @@ -194,37 +195,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 +234,7 @@ export default class NewStoreCredit extends React.Component {
    • @@ -246,12 +247,13 @@ export default class NewStoreCredit extends React.Component {
    - changeScFormData('subTypeId', value)} /> + changeScFormData('subTypeId', value)} + />
    @@ -290,23 +292,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 +328,7 @@ export default class NewStoreCredit extends React.Component {
    • @@ -332,9 +340,7 @@ export default class NewStoreCredit extends React.Component { } render() { - const form = this.props.form.type === 'giftCardTransfer' - ? this.giftCardConvertForm - : this.storeCreditForm; + const form = this.props.form.type === 'giftCardTransfer' ? this.giftCardConvertForm : this.storeCreditForm; 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/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 930f77b3e2..0000000000 Binary files a/ashes/src/components/dropdown/.dropdown.jsx.swo and /dev/null differ 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/dropdownItem.jsx b/ashes/src/components/dropdown/dropdownItem.jsx index 2c09134712..1baa0d7396 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, diff --git a/ashes/src/components/dropdown/generic-dropdown.jsx b/ashes/src/components/dropdown/generic-dropdown.jsx index de482d9dfb..433904a1ae 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/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/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 b8de79887e..47aa51f1ec 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..84d73eaf47 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,8 +137,8 @@ export default class NewGiftCard extends React.Component { return (
    - props.changeFormData('subTypeId', Number(value))} items={props.subTypes.map(subType => [subType.id, subType.title])} @@ -171,8 +171,8 @@ 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/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/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/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/new-payment/new-payment.jsx b/ashes/src/components/new-payment/new-payment.jsx index 6ae9732172..cc8a5589d3 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,8 @@ class NewPayment extends Component { labelClassName="fc-new-order-payment__payment-type-label" label="Payment Type" > -
    {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(); - if (order.orderState === 'canceled' || - order.orderState === 'shipped') { + if (order.orderState === 'canceled' || order.orderState === 'shipped') { return ; } @@ -171,32 +170,21 @@ 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'} + 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 /> ); } @@ -205,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 75fd492c7d..b6b5a45e8d 100644 --- a/ashes/src/components/products/custom-property.jsx +++ b/ashes/src/components/products/custom-property.jsx @@ -6,10 +6,11 @@ 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'; -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,9 +113,8 @@ export default class CustomProperty extends Component { label="Field Type" labelClassName="fc-product-details__field-label" > - [entry.type, entry.title]); return ( - {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) => { @@ -67,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 2e22041e1d..5e4da565d3 100644 --- a/ashes/src/components/promotions/discounts/index.jsx +++ b/ashes/src/components/promotions/discounts/index.jsx @@ -1,7 +1,9 @@ +// @todo this file not used anywhere + import _ from 'lodash'; import React, { Component } from 'react'; import { autobind } from 'core-decorators'; -import { Dropdown } from '../../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import Currency from 'components/utils/currency'; import Counter from './counter'; @@ -136,12 +138,7 @@ export default class Discounts extends Component { @autobind renderDiscount() { return ( - + ); } @@ -149,14 +146,8 @@ export default class Discounts extends Component { renderQualifier() { let discountType = this.qualifier.discountType; let items = _.find(QUALIFIER_TYPES, i => i.scope == discountType).list; - return ( - - ); + + return ; } @autobind @@ -252,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
    , + />, ]; }, []); 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..99f8df3767 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,56 @@ 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, - }]); + this.updateSearches([ + { + productSearchId: value, + }, + ]); } render() { + /* @todo initialDisplayText */ return ( - ); } diff --git a/ashes/src/components/promotions/widgets/select-products.jsx b/ashes/src/components/promotions/widgets/select-products.jsx index e0b52bb605..b64c43cf3d 100644 --- a/ashes/src/components/promotions/widgets/select-products.jsx +++ b/ashes/src/components/promotions/widgets/select-products.jsx @@ -10,7 +10,7 @@ import { bindActionCreators } from 'redux'; import styles from './select-products.css'; import SelectVertical from '../../select-verical/select-vertical'; -import { Dropdown } from '../../dropdown'; +import { TextDropdown } from 'components/core/dropdown'; import { actions } from '../../../modules/products/list'; import type { Context } from '../types'; @@ -103,7 +103,7 @@ class ProductsQualifier extends Component { const initialValue = references.length && references[0].productSearchId || void 0; return ( - {this.props.label} - {this.productReferences}
    diff --git a/ashes/src/components/rich-text-editor/font-dropdown.css b/ashes/src/components/rich-text-editor/font-dropdown.css new file mode 100644 index 0000000000..a65ee8e1f5 --- /dev/null +++ b/ashes/src/components/rich-text-editor/font-dropdown.css @@ -0,0 +1,37 @@ +@import 'variables.css'; + +.block { + position: relative; + font: var(--font-labels); + user-select: none; + + &.open { + z-index: 1; + } +} + +.pivot { + display: flex; + cursor: pointer; +} + +.displayText { + margin-right: 10px; +} + +.menu { + top: 30px; + left: -10px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, .2); +} + +.item { + padding: 12px 20px; + background-color: white; + font: var(--font-nav); + cursor: pointer; + + &:hover { + background-color: var(--bg-grey-headers); + } +} diff --git a/ashes/src/components/rich-text-editor/font-dropdown.jsx b/ashes/src/components/rich-text-editor/font-dropdown.jsx new file mode 100644 index 0000000000..15d86bec30 --- /dev/null +++ b/ashes/src/components/rich-text-editor/font-dropdown.jsx @@ -0,0 +1,161 @@ +/* @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 './font-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 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: (value: string) => {}, + 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 801a4ac3c1..a8f4e603fd 100644 --- a/ashes/src/components/rich-text-editor/rich-text-editor.jsx +++ b/ashes/src/components/rich-text-editor/rich-text-editor.jsx @@ -13,10 +13,9 @@ import { stateToMarkdown } from 'draft-js-export-markdown'; // components import { ContentBlock, ContentState, Editor, EditorState, RichUtils } from 'draft-js'; -import { Dropdown } from '../dropdown'; +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, @@ -138,15 +137,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 +268,7 @@ export default class RichTextEditor extends Component { ); }); - return
    {buttons}
    ; + return
    {buttons}
    ; } shouldComponentUpdate(nextProps: Props, nextState: State): boolean { @@ -293,7 +290,7 @@ export default class RichTextEditor extends Component {
    {this.props.label &&
    {this.props.label}
    }
    -
    +
    {this.commandBarContent}
    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/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 -}; 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 /> [ path.resolve(__dirname, '../src/components/docs/colors/text-colors.jsx'), path.resolve(__dirname, '../src/components/docs/colors/bg-colors.jsx'), @@ -80,6 +82,14 @@ 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'), + path.resolve(__dirname, '../src/components/core/dropdown/search-dropdown/search-dropdown.jsx'), + ], + }, { name: 'Navigation', components: () => [path.resolve(__dirname, '../src/components/core/page-nav/page-nav.jsx')], @@ -135,19 +145,16 @@ module.exports = { }, { name: 'Other', - components: () => ([ + components: () => [ path.resolve(__dirname, '../src/components/utils/change/change.jsx'), path.resolve(__dirname, '../src/components/utils/currency/currency.jsx'), path.resolve(__dirname, '../src/components/utils/datetime/datetime.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')], }; 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'); }); });