diff --git a/README.md b/README.md index cff4172b0..a1c1bb5a1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,28 @@ To build the dist version: $ npm run prepublish ``` +## Developer notes + A PlotlyEditor widgets is composed of 3 layers: + +### Layer 1: Base Component +``` + +``` + +### Layer 2: Container Components: +One or more nested Container Components with one and only one connected by a connectToPlot function (connectLayoutToPlot, connectTraceToPlot). +``` + ,
, +``` + +### Layer 3: Attribute Widgets +Each connected by a `connectContainerToPlot` function + , , and remaining UI Controls +``` + +Data flows via `context` downward and is augmented with additional information at each layer boundary. +The Base Components aggregate references to the graphDiv objects (data, fullData, layout...), grid Data sources, locale, update functions etc. One of the Container Components uses its knowledge about which container to target (traces, layout, ...) to generate fewer but more specific containers and updaters which are passed down the hierarchy. The Attribute widgets are higher-order wrappers around dumb UI controls. The higher-order wrapper uses the container contexts and specific attributes information to provide specific plot update functions and other behaviours for the inner UI control. + ## See also - [plotlyjs-react](https://github.com/plotly/plotlyjs-react) diff --git a/src/DefaultEditor.js b/src/DefaultEditor.js index 6b7db7bb7..df421feab 100644 --- a/src/DefaultEditor.js +++ b/src/DefaultEditor.js @@ -1,24 +1,38 @@ -import ColorPicker from './components/Color'; -import Dropdown from './components/Dropdown'; -import DataSelector from './components/DataSelector'; -import TraceSelector from './components/TraceSelector'; -import Flaglist from './components/Flaglist'; -import Numeric from './components/Numeric'; -import Panel from './components/Panel'; -import PanelMenuWrapper from './components/PanelMenuWrapper'; -import PropTypes from 'prop-types'; -import Radio from './components/Radio'; import React, {Component} from 'react'; -import Section from './components/Section'; -import TraceAccordion from './components/TraceAccordion'; -import {localize} from './lib'; +import PropTypes from 'prop-types'; +import { + SubPanel, + ColorPicker, + DataSelector, + Dropdown, + Flaglist, + Fold, + Info, + Numeric, + Panel, + PanelMenuWrapper, + Radio, + Section, + TraceAccordion, + TraceSelector, +} from './components'; +import {DEFAULT_FONTS} from './constants'; +import {localize, connectLayoutToPlot} from './lib'; + +const LayoutPanel = connectLayoutToPlot(Panel); class DefaultEditor extends Component { constructor(props, context) { super(props, context); const capitalize = s => s.charAt(0).toUpperCase() + s.substring(1); - const traceTypes = Object.keys(context.plotSchema.traces); + + // Filter out Polar "area" type (it is fairly broken and we want to present + // scatter with fill as an "area" chart type for convenience. + const traceTypes = Object.keys(context.plotSchema.traces).filter( + t => t !== 'area' + ); + const labels = traceTypes.map(capitalize); this.traceOptions = traceTypes.map((t, i) => ({ label: labels[i], @@ -39,7 +53,7 @@ class DefaultEditor extends Component { return ( - + - + -
- +
+
-
+
-
+
-
+
- + - +
-
- +
+ @@ -164,13 +161,127 @@ class DefaultEditor extends Component { label={_('Connect Gaps')} attr="connectgaps" options={[ - {value: true, label: 'Connect'}, - {value: false, label: 'Blank'}, + {label: _('Connect'), value: true}, + {label: _('Blank'), value: false}, ]} />
+ + + + + + + + + +
+ +
+
+ + + +
+
+ + + +
+
+ +
+ + {_( + 'The positioning inputs are relative to the ' + + 'anchor points on the text box' + )} + + + +
+
+ + +
+
+ +
+
+ +
+
+
); } diff --git a/src/PlotlyEditor.css b/src/PlotlyEditor.css index 67d5b7a6b..41922a6f4 100644 --- a/src/PlotlyEditor.css +++ b/src/PlotlyEditor.css @@ -7,7 +7,7 @@ overflow: hidden; } -.plotly-editor__mode-menu { +.plotly-editor__sidebar { user-select: none; height: 100%; width: 100px; @@ -18,20 +18,20 @@ border-right: 1px solid #f3f6fa; } -.plotly-editor__mode-menu-section--is-active { +.plotly-editor__sidebar-group--is-active { color: #2a3f5f; font-weight: 600; cursor: default; border-left: 3px solid #119DFF; } -.plotly-editor__mode-menu-section { +.plotly-editor__sidebar-group { background-color: #fff; cursor: pointer; width: 100%; } -.plotly-editor__mode-menu-section__title { +.plotly-editor__sidebar-group__title { color: #506784; font-size: 14px; font-weight: 400; @@ -40,7 +40,7 @@ text-align: left; } -.plotly-editor__mode-menu-section__title:before { +.plotly-editor__sidebar-group__title:before { width: 0; height: 0; display: inline-block; @@ -54,16 +54,16 @@ transition: all 0.15s ease-in-out; } -.plotly-editor__mode-menu-section--is-expanded .plotly-editor__mode-menu-section__title:before { +.plotly-editor__sidebar-group--is-expanded .plotly-editor__sidebar-group__title:before { transform: rotate(90deg); } -.plotly-editor__mode-menu-section--is-expanded .plotly-editor__mode-menu-section__title { +.plotly-editor__sidebar-group--is-expanded .plotly-editor__sidebar-group__title { box-shadow: 0px 3px 4px rgba(80,103,132,0.2); position: relative; } -.plotly-editor__mode-menu-item { +.plotly-editor__sidebar-item { color: #69738a; cursor: pointer; font-size: 14px; @@ -77,7 +77,7 @@ text-align: left; } -.plotly-editor__mode-menu-item--is-active { +.plotly-editor__sidebar-item--is-active { border-left: 3px solid #119DFF; color: #2a3f5f; font-weight: 600; @@ -99,11 +99,17 @@ } .plotly-editor__field { - width: 100%; - padding: 6px 0; - display: block; + Align-items: center; + border-top: 1px solid #f8f8f9; color: #6D6D6D; + display: flex; font-size: 13px; + font-weight: 300; + justify-content: flex-start; + line-height: 13px; + min-height: 32px; + padding: 6px 0; + width: 100%; } .plotly-editor__field:not(:last-child) { @@ -115,6 +121,10 @@ padding: 0 12px; } +.plotly-editor__field__no-title--center { + text-align: center; +} + .plotly-editor__field__title { padding-left: 0.5em; color: #69738a; @@ -123,21 +133,19 @@ display: inline-block; } -.plotly-editor__field__control { - width: 60%; - display: inline-block; - padding-right: 0.5em; -} - .plotly-editor__field__widget { - width: 70%; + width: 64%; padding-left: 18px; display: inline-block; padding-right: 12px; } +.plotly-editor__field__widget--postfix { + width: 50%; + padding-right: 0px; +} -.plotly-editor__trace-panel__panel { +.plotly-editor__accordion-panel__panel { background: #ffffff; border: 1px solid #bec8d9; border-width: 0 1px 1px 1px; @@ -146,7 +154,11 @@ margin-bottom: 6px; } -.plotly-editor__trace-panel__top { +.plotly-editor__accordion-panel__panel--noheader { + border-width: 1px 1px 1px 1px; +} + +.plotly-editor__accordion-panel__top { clear: both; padding: 6px 12px; color: #ffffff; @@ -163,26 +175,22 @@ transition: background-color 0.1s ease-in-out,color 0.1s ease-in-out,border 0.1s ease-in-out; } -.plotly-editor__trace-panel__top--active { +.plotly-editor__accordion-panel__top--active { color: #fff; background-color: #506784; border: 1px solid #506784; border-radius: 5px 5px 0 0; } -.plotly-editor__field { - color: #6D6D6D; - font-size: 13px; - line-height: 13px; - padding: 6px 0; - border-top: 1px solid #f8f8f9; - min-height: 32px; - font-weight: 300; - display: flex; - align-items: center; - justify-content: flex-start; +.plotly-editor__accordion-panel__delete { + float: right; + display: inline-block; + color: white; + text-decoration: none; + font-size: 1.5em; } + .plotly-editor__field__title { width: 30%; padding-left: 12px; @@ -377,11 +385,3 @@ border-bottom-left-radius: 3px; border-left: 1px solid #c8d4e3; } - -.plotly-editor__trace-panel__delete { - float: right; - display: inline-block; - color: white; - text-decoration: none; - font-size: 1.5em; -} diff --git a/src/PlotlyEditor.js b/src/PlotlyEditor.js index 21d3d2542..94e7b7a38 100644 --- a/src/PlotlyEditor.js +++ b/src/PlotlyEditor.js @@ -1,16 +1,20 @@ -import React, {Component} from 'react'; +import DefaultEditor from './DefaultEditor'; import PropTypes from 'prop-types'; - -import constants from './lib/constants'; -import {bem} from './lib'; +import React, {Component} from 'react'; import dictionaries from './locales'; - -import DefaultEditor from './DefaultEditor'; +import {bem} from './lib'; class PlotlyEditor extends Component { + constructor(props, context) { + super(props, context); + + // we only need to compute this once. + this.plotSchema = this.props.plotly.PlotSchema.get(); + } + getChildContext() { - var gd = this.props.graphDiv || {}; - var dataSourceNames = Object.keys(this.props.dataSources || {}); + const gd = this.props.graphDiv || {}; + const dataSourceNames = Object.keys(this.props.dataSources || {}); return { data: gd.data, dataSourceNames: dataSourceNames, @@ -22,14 +26,14 @@ class PlotlyEditor extends Component { layout: gd.layout, locale: this.props.locale, onUpdate: this.updateProp.bind(this), - plotSchema: this.props.plotly.PlotSchema.get(), + plotSchema: this.plotSchema, plotly: this.props.plotly, }; } - updateProp(updates, traces, type) { - this.props.onUpdate && - this.props.onUpdate(this.props.graphDiv, updates, traces, type); + updateProp(event) { + const {graphDiv} = this.props; + this.props.onUpdate && this.props.onUpdate({graphDiv, ...event}); } render() { @@ -42,6 +46,14 @@ class PlotlyEditor extends Component { } } +PlotlyEditor.propTypes = { + onUpdate: PropTypes.func, + plotly: PropTypes.object.isRequired, + graphDiv: PropTypes.object, + locale: PropTypes.string, + dataSources: PropTypes.object, +}; + PlotlyEditor.defaultProps = { locale: 'en', }; diff --git a/src/PlotlyEditor.scss b/src/PlotlyEditor.scss deleted file mode 100644 index 4872ccbf4..000000000 --- a/src/PlotlyEditor.scss +++ /dev/null @@ -1,380 +0,0 @@ -.plotly-editor { - font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; - border: 1px solid #aaa; - display: inline-block; - width: 425px; - min-height: 600px; - overflow: hidden; -} - -.plotly-editor__mode-menu { - user-select: none; - height: 100%; - width: 100px; - text-align: center; - background: #fff; - overflow-y: scroll; - float: left; - border-right: 1px solid #f3f6fa; -} - -.plotly-editor__mode-menu-section--is-active { - color: #2a3f5f; - font-weight: 600; - cursor: default; - border-left: 3px solid #119DFF; -} - -.plotly-editor__mode-menu-section { - background-color: #fff; - cursor: pointer; - width: 100%; -} - -.plotly-editor__mode-menu-section__title { - color: #506784; - font-size: 14px; - font-weight: 400; - padding: 12px 0; - text-transform: capitalize; - text-align: left; -} - -.plotly-editor__mode-menu-section__title:before { - width: 0; - height: 0; - display: inline-block; - margin-left: 8px; - margin-right: 8px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - - border-left: 5px solid #506784; - content: ''; - transition: all 0.15s ease-in-out; -} - -.plotly-editor__mode-menu-section--is-expanded .plotly-editor__mode-menu-section__title:before { - transform: rotate(90deg); -} - -.plotly-editor__mode-menu-section--is-expanded .plotly-editor__mode-menu-section__title { - box-shadow: 0px 3px 4px rgba(80,103,132,0.2); - position: relative; -} - -.plotly-editor__mode-menu-item { - color: #69738a; - cursor: pointer; - font-size: 14px; - font-weight: 300; - line-height: 14px; - text-transform: capitalize; - background-color: #f3f6fa; - padding-top: 8px; - padding-bottom: 8px; - padding-left: 25%; - text-align: left; -} - -.plotly-editor__mode-menu-item--is-active { - border-left: 3px solid #119DFF; - color: #2a3f5f; - font-weight: 600; - cursor: default; -} - -.plotly-editor__panel { - position: absolute; - top: 21px; - left: 0px; - bottom: 5px; - right: 1px; - margin-left: 100px; - background-color: #f3f6fa; - overflow-y: scroll; - padding-left: 12px; - padding-right: 12px; - padding-top: 12px; -} - -.plotly-editor__field { - width: 100%; - padding: 6px 0; - display: block; - color: #6D6D6D; - font-size: 13px; -} - -.plotly-editor__field:not(:last-child) { - border-bottom: 1px solid #f8f8f9; -} - -.plotly-editor__field__no-title { - width: 100%; - padding: 0 12px; -} - -.plotly-editor__field__title { - padding-left: 0.5em; - color: #69738a; - font-size: 14px; - width: 36%; - display: inline-block; -} - -.plotly-editor__field__control { - width: 60%; - display: inline-block; - padding-right: 0.5em; -} - -.plotly-editor__field__widget { - width: 70%; - padding-left: 18px; - display: inline-block; - padding-right: 12px; -} - - -.plotly-editor__trace-panel__panel { - background: #ffffff; - border: 1px solid #bec8d9; - border-width: 0 1px 1px 1px; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; - margin-bottom: 6px; -} - -.plotly-editor__trace-panel__top { - clear: both; - padding: 6px 12px; - color: #ffffff; - font-size: 13px; - line-height: 13px; - border: 1px solid #a2b1c6; - background-color: #a2b1c6; - height: 15px; - border-radius: 5px; - text-shadow: 0px 1px 2px rgba(42,63,95,0.7); - -moz-transition: background-color 0.1s ease-in-out,color 0.1s ease-in-out,border 0.1s ease-in-out; - -o-transition: background-color 0.1s ease-in-out,color 0.1s ease-in-out,border 0.1s ease-in-out; - -webkit-transition: background-color 0.1s ease-in-out,color 0.1s ease-in-out,border 0.1s ease-in-out; - transition: background-color 0.1s ease-in-out,color 0.1s ease-in-out,border 0.1s ease-in-out; -} - -.plotly-editor__trace-panel__top--active { - color: #fff; - background-color: #506784; - border: 1px solid #506784; - border-radius: 5px 5px 0 0; -} - -.plotly-editor__field { - color: #6D6D6D; - font-size: 13px; - line-height: 13px; - padding: 6px 0; - border-top: 1px solid #f8f8f9; - min-height: 32px; - font-weight: 300; - display: flex; - align-items: center; - justify-content: flex-start; -} - -.plotly-editor__field__title { - width: 30%; - padding-left: 12px; - display: inline-block; - line-height: 18px; - text-transform: capitalize; -} - -.plotly-editor__field__title-text { - text-transform: capitalize; - cursor: default; -} - -.plotly-editor__section__heading { - font-size: 13px; - color: #69738a; - font-weight: 400; - cursor: default; - background-color: #F4FAFF; - padding: 6px 12px; - clear: both; - text-transform: capitalize; -} - -.numeric-input__wrapper { - line-height: 20px; - max-width: 100%; -} - -.numeric-input__number { - display: inline-block; - border: 1px solid #bec8d9; - background: white; - cursor: text; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: left; - border-radius: 2px; - padding: 6px 6px 6px 12px; - width: 80px; - vertical-align: middle; - font-size: inherit; - color: inherit; - font-family: inherit; -} - -.numeric-input__caret-box { - display: inline-block; - max-height: 32px; - margin-left: 6px; - margin-right: 12px; - vertical-align: middle; - box-sizing: border-box; -} - -.numeric-input__caret:first-child { - margin-bottom: 2px; -} - -.numeric-input__caret { - cursor: pointer; - background-color: #f8f8f9; - border: 1px solid #bec8d9; - border-radius: 1px; - width: 13px; - height: 13px; - line-height: 12px; - text-align: center; -} - -.icon-caret-down:before { - font-size: 8px; - content: '\25bc'; -} - -.icon-caret-up:before { - font-size: 8px; - content: '\25b2'; -} - -.colorpicker-container { - display: inline-block; - line-height: 2; - position: relative; -} - -.colorpicker { - display: inline-block; - width: 32px; - height: 32px; - border-radius: 50%; - border: 1px solid #dcdddd; - vertical-align: middle; - padding-top: 3px; - padding-left: 3px; - box-sizing: border-box; -} - -.colorpicker-swatch { - border-radius: 50%; - width: 24px; - height: 24px; - vertical-align: middle; -} - -.colorpicker-selected-color { - margin-left: 12px; - color: #777; - font-weight: 300; - font-size: 12px; - display: inline-block; - vertical-align: middle; - text-transform: uppercase; -} - -.checkbox__group { - padding-left: 12px; - text-align: left; -} - -.checkbox__item { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - cursor: default; - padding-top: 3px; - padding-bottom: 3px; -} - -.checkbox__item--vertical { - display: block; - clear: both; -} - -.checkbox__box { - height: 18px; - width: 18px; - border: 1px solid #a9b3c7; - border-radius: 3px; - cursor: pointer; - display: inline-block; - vertical-align: middle; - text-align: center; -} - -.checkbox__check { - color: #6D6D6D; - font-size: 12px; -} - -.icon-check-mark:before { - content: "\2714"; -} - -.checkbox__label { - padding-left: 6px; - font-size: 12px; - display: inline-block; - line-height: 20px; - text-align: left; - vertical-align: middle; -} - -.radio-block__option { - padding: 6px 12px; - background-color: #fff; - border-top: 1px solid #c8d4e3; - border-bottom: 1px solid #c8d4e3; - border-right: 1px solid #c8d4e3; - display: inline-block; - cursor: pointer; -} - -.radio-block__option--active { - background-color: #119DFF; - color: #fff; - border: 1px solid #0D76BF; - margin-left: -1px; - cursor: default; -} - -.radio-block__option:last-child { - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; -} - -.radio-block__option:first-child { - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; - border-left: 1px solid #c8d4e3; -} - diff --git a/src/__tests__/connectLayoutToPlot-test.js b/src/__tests__/connectLayoutToPlot-test.js new file mode 100644 index 000000000..8b18bff0a --- /dev/null +++ b/src/__tests__/connectLayoutToPlot-test.js @@ -0,0 +1,71 @@ +import {Numeric} from '../components/fields'; +import {Fold, Panel, Section} from '../components/containers'; +import NumericInput from '../components/widgets/NumericInputStatefulWrapper'; +import React from 'react'; +import {EDITOR_ACTIONS} from '../constants'; +import {TestEditor, fixtures, plotly} from '../lib/test-utils'; +import {connectLayoutToPlot} from '../lib'; +import {mount} from 'enzyme'; + +const Layouts = [Panel, Fold, Section].map(connectLayoutToPlot); +const Editor = props => ( + +); + +Layouts.forEach(Layout => { + describe(`<${Layout.displayName}>`, () => { + it(`wraps container with fullValue pointing to gd._fullLayout`, () => { + const wrapper = mount( + + + + + + ) + .find('[attr="width"]') + .find(NumericInput); + + expect(wrapper.prop('value')).toBe(100); + }); + + it(`sends updates to gd._layout`, () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="width"]') + .find(NumericInput); + + wrapper.prop('onChange')(200); + const event = onUpdate.mock.calls[0][0]; + expect(event.type).toBe(EDITOR_ACTIONS.UPDATE_LAYOUT); + expect(event.payload).toEqual({update: {width: 200}}); + }); + + it(`automatically computes min and max defaults`, () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="legend.x"]') + .find(NumericInput); + + expect(wrapper.prop('min')).toBe(-2); + expect(wrapper.prop('max')).toBe(3); + }); + }); +}); diff --git a/src/__tests__/connectTraceToPlot-test.js b/src/__tests__/connectTraceToPlot-test.js new file mode 100644 index 000000000..82c8788b4 --- /dev/null +++ b/src/__tests__/connectTraceToPlot-test.js @@ -0,0 +1,70 @@ +import {Numeric} from '../components/fields'; +import {Fold, Panel, Section} from '../components/containers'; +import NumericInput from '../components/widgets/NumericInputStatefulWrapper'; +import React from 'react'; +import {EDITOR_ACTIONS} from '../constants'; +import {TestEditor, fixtures, plotly} from '../lib/test-utils'; +import {connectTraceToPlot} from '../lib'; +import {mount} from 'enzyme'; + +const Traces = [Panel, Fold, Section].map(connectTraceToPlot); +const Editor = props => ( + +); + +const defaultMarkerSize = 6; + +Traces.forEach(Trace => { + describe(`<${Trace.displayName}>`, () => { + it('wraps container with fullValue pointing to gd._fullData[i]', () => { + const wrapper = mount( + + + + + + ) + .find('[attr="marker.size"]') + .find(NumericInput); + + expect(wrapper.prop('value')).toBe(defaultMarkerSize); + }); + + it('sends updates to gd.data', () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="marker.size"]') + .find(NumericInput); + + wrapper.prop('onChange')(200); + const event = onUpdate.mock.calls[0][0]; + expect(event.type).toBe(EDITOR_ACTIONS.UPDATE_TRACES); + expect(event.payload).toEqual({ + update: {'marker.size': 200}, + traceIndexes: [0], + }); + }); + + it('automatically computes min and max defaults', () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="marker.size"]') + .find(NumericInput); + + expect(wrapper.prop('min')).toBe(0); + expect(wrapper.prop('max')).toBe(undefined); + }); + }); +}); diff --git a/src/components/Base.js b/src/components/Base.js deleted file mode 100644 index 7d507fe16..000000000 --- a/src/components/Base.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { _ } from "../lib"; - -class Base extends Component { - constructor(props, context) { - super(props, context); - - this._ = function(key) { - return _(context.dictionaries, context.locale, key); - }; - } -} - -Base.contextTypes = { - locale: PropTypes.string, - dictionaries: PropTypes.object, -}; - -export default Base; diff --git a/src/components/Color.js b/src/components/Color.js deleted file mode 100644 index 120f2ca23..000000000 --- a/src/components/Color.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, {Component} from 'react'; -import ColorPicker from './widgets/ColorPicker'; -import {bem, connectToPlot} from '../lib'; - -class Color extends Component { - render() { - return ( -
-
-
{this.props.label}
-
-
- -
-
- ); - } -} - -export default connectToPlot(Color); diff --git a/src/components/DataSelector.js b/src/components/DataSelector.js deleted file mode 100644 index c0919e88c..000000000 --- a/src/components/DataSelector.js +++ /dev/null @@ -1,74 +0,0 @@ -import DropdownWidget from './widgets/Dropdown'; -import PropTypes from 'prop-types'; -import React, {Component} from 'react'; -import nestedProperty from 'plotly.js/src/lib/nested_property'; -import {bem, connectToPlot} from '../lib'; - -function attributeIsData(meta = {}) { - return meta.valType === 'data_array' || meta.arrayOk; -} - -class DataSelector extends Component { - constructor(props, context) { - super(props, context); - - this.setLocals(props, context); - this.updatePlot = this.updatePlot.bind(this); - } - - static unpackPlotProps(props, context, plotProps) { - if (attributeIsData(plotProps.attrMeta)) { - plotProps.isVisible = true; - } - } - - componentWillReceiveProps(nextProps, nextContext) { - this.setLocals(nextProps, nextContext); - } - - setLocals(props, context) { - this.dataSrcExists = false; - if (attributeIsData(props.attrMeta)) { - this.dataSrcExists = true; - this.srcAttr = props.attr + 'src'; - this.srcProperty = nestedProperty(props.trace, this.srcAttr); - } - } - - fullValue() { - if (this.dataSrcExists) { - return this.srcProperty.get(); - } - return this.props.fullValue(); - } - - updatePlot(value) { - const attr = this.dataSrcExists ? this.srcAttr : this.props.attr; - const update = {[attr]: [value]}; - this.props.onUpdate && this.props.onUpdate(update, [this.props.traceIndex]); - } - - render() { - return ( -
-
-
{this.props.label}
-
-
- -
-
- ); - } -} - -DataSelector.contextTypes = { - plotSchema: PropTypes.object, -}; - -export default connectToPlot(DataSelector); diff --git a/src/components/Dropdown.js b/src/components/Dropdown.js deleted file mode 100644 index b0225f714..000000000 --- a/src/components/Dropdown.js +++ /dev/null @@ -1,25 +0,0 @@ -import React, {Component} from 'react'; -import DropdownWidget from './widgets/Dropdown'; -import {bem, connectToPlot} from '../lib'; - -export class UnconnectedDropdown extends Component { - render() { - return ( -
-
-
{this.props.label}
-
-
- -
-
- ); - } -} - -export default connectToPlot(UnconnectedDropdown); diff --git a/src/components/Flaglist.js b/src/components/Flaglist.js deleted file mode 100644 index 3d4217e80..000000000 --- a/src/components/Flaglist.js +++ /dev/null @@ -1,21 +0,0 @@ -import React, {Component} from 'react'; -import FlaglistCheckboxGroup from './widgets/FlaglistCheckboxGroup'; -import {bem, connectToPlot} from '../lib'; - -class Flaglist extends Component { - render() { - return ( -
-
- -
-
- ); - } -} - -export default connectToPlot(Flaglist); diff --git a/src/components/Heading.js b/src/components/Heading.js deleted file mode 100644 index 367126a06..000000000 --- a/src/components/Heading.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { bem } from "../lib"; - -class Heading extends Component { - constructor(props, context) { - super(props); - this.section = context.section; - } - - componentWillReceiveProps(nextProps, nextContext) { - this.section = nextContext.section; - } - - renderField() { - return ( -
-
{this.props.label}
- {this.props.children} -
- ); - } -} - -Heading.contextTypes = { - label: PropTypes.string, -}; - -export default Heading; diff --git a/src/components/ModeMenu.js b/src/components/ModeMenu.js deleted file mode 100644 index 5dd8d1936..000000000 --- a/src/components/ModeMenu.js +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Component } from "react"; -import { bem } from "../lib"; - -import ModeMenuSection from "./ModeMenuSection"; - -export default class ModeMenu extends Component { - render() { - return ( -
- {this.props.sections.map(this.renderSection)} -
- ); - } -} diff --git a/src/components/ModeMenuItem.js b/src/components/ModeMenuItem.js deleted file mode 100644 index 9d592f168..000000000 --- a/src/components/ModeMenuItem.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; -import { bem } from "../lib"; - -export default class ModeMenuItem extends Component { - render() { - return ( -
- {this.props.label} -
- ); - } -} diff --git a/src/components/ModeMenuSection.js b/src/components/ModeMenuSection.js deleted file mode 100644 index 321b3e563..000000000 --- a/src/components/ModeMenuSection.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from "react"; -import { bem } from "../lib"; - -import ModeMenuItem from "./ModeMenuItem"; - -export default class ModeMenuSection extends Component { - constructor(props) { - super(props); - - this.state = { - expanded: this.props.section === this.props.selectedSection, - }; - - this.toggleExpanded = this.toggleExpanded.bind(this); - this.onChangeSection = this.onChangeSection.bind(this); - this.renderSubItem = this.renderSubItem.bind(this); - } - - toggleExpanded() { - this.setState({ expanded: !this.state.expanded }); - } - - onChangeSection(panel) { - this.props.onChangeSection(this.props.section, panel); - } - - renderSubItem(panel, i) { - const isActive = - this.props.selectedPanel === panel && - this.props.section === this.props.selectedSection; - - return ( - this.onChangeSection(panel)} - label={panel} - /> - ); - } - - render() { - return ( -
-
- {this.props.section} -
- {this.state.expanded && this.props.panels.map(this.renderSubItem)} -
- ); - } -} - -ModeMenuSection.defaultProps = { - expanded: false, -}; diff --git a/src/components/Numeric.js b/src/components/Numeric.js deleted file mode 100644 index ead1baf81..000000000 --- a/src/components/Numeric.js +++ /dev/null @@ -1,28 +0,0 @@ -import React, {Component} from 'react'; -import NumericInput from './widgets/NumericInputStatefulWrapper'; -import {bem, connectToPlot} from '../lib'; - -class Numeric extends Component { - render() { - return ( -
-
-
{this.props.label}
-
-
- -
-
- ); - } -} - -export default connectToPlot(Numeric); diff --git a/src/components/PanelMenuWrapper.js b/src/components/PanelMenuWrapper.js index b1f49c15a..8d51c707b 100644 --- a/src/components/PanelMenuWrapper.js +++ b/src/components/PanelMenuWrapper.js @@ -1,84 +1,84 @@ +import SidebarGroup from './sidebar/SidebarGroup'; import React, {cloneElement, Component} from 'react'; import {bem, localize} from '../lib'; -import ModeMenuSection from './ModeMenuSection'; -class PanelsWithModeMenu extends Component { +class PanelsWithSidebar extends Component { constructor(props) { super(props); var opts = this.computeMenuOptions(props); this.state = { - section: opts[0].name, + group: opts[0].name, panel: opts[0].panels[0], }; this.setPanel = this.setPanel.bind(this); - this.renderSection = this.renderSection.bind(this); + this.renderGroup = this.renderGroup.bind(this); } - setPanel(section, panel) { - this.setState({section, panel}); + setPanel(group, panel) { + this.setState({group, panel}); } - renderSection(section, i) { + renderGroup(group, i) { return ( - ); } computeMenuOptions(props) { - var obj, child, section, name; + var obj, child, group, name; var children = props.children; if (!Array.isArray(children)) { children = [children]; } - var sectionLookup = {}; - var sectionIndex; - var sections = []; + var groupLookup = {}; + var groupIndex; + var groups = []; for (var i = 0; i < children.length; i++) { child = children[i]; - section = child.props.section; + group = child.props.group; name = child.props.name; - if (sectionLookup.hasOwnProperty(section)) { - sectionIndex = sectionLookup[section]; - obj = sections[sectionIndex]; + if (groupLookup.hasOwnProperty(group)) { + groupIndex = groupLookup[group]; + obj = groups[groupIndex]; } else { - sectionLookup[section] = sections.length; - obj = {name: section, panels: []}; - sections.push(obj); + groupLookup[group] = groups.length; + obj = {name: group, panels: []}; + groups.push(obj); } obj.panels.push(name); } - return sections; + return groups; } render() { var menuOpts = this.computeMenuOptions(this.props); - var children = Array.isArray(this.props.children) ? this.props.children : [this.props.children]; + var children = Array.isArray(this.props.children) + ? this.props.children + : [this.props.children]; return (
-
- {menuOpts.map(this.renderSection)} -
+
{menuOpts.map(this.renderGroup)}
{children.map((child, i) => cloneElement(child, { key: i, visible: - this.state.section === child.props.section && + this.state.group === child.props.group && this.state.panel === child.props.name, }) )} @@ -87,4 +87,4 @@ class PanelsWithModeMenu extends Component { } } -export default localize(PanelsWithModeMenu); +export default localize(PanelsWithSidebar); diff --git a/src/components/PlotlyEditorBase.js b/src/components/PlotlyEditorBase.js deleted file mode 100644 index a89ae191a..000000000 --- a/src/components/PlotlyEditorBase.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from "react"; -import Base from "./Base"; -import PropTypes from "prop-types"; - -class PlotlyEditorBase extends Component { - constructor(props, context) { - super(props, context); - this.dataSources = context.dataSources; - this.dataSourceNames = context.dataSourceNames; - } -} - -// It's not enough for Base to specify which context it accepts. This component -// must manually pull Base's defined context types into its own. -PlotlyEditorBase.contextTypes = Object.assign( - { - dataSources: PropTypes.object, - dataSourceNames: PropTypes.array, - }, - Base.contextTypes -); - -export default PlotlyEditorBase; diff --git a/src/components/Radio.js b/src/components/Radio.js deleted file mode 100644 index bc538fa11..000000000 --- a/src/components/Radio.js +++ /dev/null @@ -1,24 +0,0 @@ -import React, {Component} from 'react'; -import RadioBlocks from './widgets/RadioBlocks'; -import {bem, connectToPlot} from '../lib'; - -class Radio extends Component { - render() { - return ( -
-
-
{this.props.label}
-
-
- -
-
- ); - } -} - -export default connectToPlot(Radio); diff --git a/src/components/Section.js b/src/components/Section.js deleted file mode 100644 index fa6923422..000000000 --- a/src/components/Section.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, {Component, cloneElement} from 'react'; -import PropTypes from 'prop-types'; -import {bem} from '../lib'; -import unpackPlotProps from '../lib/unpackPlotProps'; - -function childIsVisible(child) { - return Boolean((child.props.plotProps || {}).isVisible); -} - -class Section extends Component { - constructor(props, context) { - super(props, context); - - this.children = this.processChildren(context); - } - - componentWillReceiveProps(nextProps, nextContext) { - this.children = this.processChildren(nextContext); - } - - processChildren(context) { - let children = this.props.children; - if (!Array.isArray(children)) { - children = [children]; - } - children = children.filter(c => Boolean(c)); - - for (let i = 0; i < children.length; i++) { - let child = children[i]; - - let isAttr = !!child.props.attr; - let plotProps = isAttr - ? unpackPlotProps(child.props, context, child.constructor) - : {}; - let childProps = Object.assign({plotProps}, child.props); - childProps.key = i; - - children[i] = cloneElement(child, childProps, child.children); - } - - return children; - } - - render() { - const hasVisibleChildren = this.children.some(childIsVisible); - - return hasVisibleChildren ? ( -
-
{this.props.heading}
- {this.children} -
- ) : null; - } -} - -Section.contextTypes = { - data: PropTypes.array, - fullData: PropTypes.array, - fullLayout: PropTypes.object, - fullTraceIndex: PropTypes.number, - graphDiv: PropTypes.any, - layout: PropTypes.object, - onUpdate: PropTypes.func, - plotSchema: PropTypes.object, - traceIndex: PropTypes.number, -}; - -export default Section; diff --git a/src/components/Select.js b/src/components/Select.js deleted file mode 100644 index f8d27a722..000000000 --- a/src/components/Select.js +++ /dev/null @@ -1,46 +0,0 @@ -import React, {Component} from 'react'; -import {bem, connectToPlot} from '../lib'; - -class Select extends Component { - renderOption(attrs, i) { - return ( - - ); - } - - renderField() { - var options = (this.props.options || []).slice(); - - for (let i = 0; i < options.length; i++) { - let opt = options[i]; - if (typeof opt !== 'object') { - options[i] = { - label: opt, - value: opt, - }; - } - } - - if (this.props.hasBlank) { - options.unshift({label: '', value: ''}); - } - - return ( - - ); - } -} - -export default connectToPlot(Select); diff --git a/src/components/Trace.js b/src/components/Trace.js deleted file mode 100644 index 3a0f22fbb..000000000 --- a/src/components/Trace.js +++ /dev/null @@ -1,57 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import {bem, findFullTraceIndex} from '../lib'; - -export default class Trace extends Component { - constructor(props) { - super(props); - this.deleteTrace = this.deleteTrace.bind(this); - } - - getChildContext() { - return { - traceIndex: this.props.traceIndex, - fullTraceIndex: findFullTraceIndex( - this.context.fullData, - this.props.traceIndex - ), - }; - } - - deleteTrace() { - this.props.onUpdate && - this.props.onUpdate(null, [this.props.traceIndex], 'deleteTraces'); - } - - render() { - return ( -
-
- Trace {this.props.traceIndex} - - × - -
-
{this.props.children}
-
- ); - } -} - -Trace.propTypes = { - traceIndex: PropTypes.number.isRequired, - onUpdate: PropTypes.func, -}; - -Trace.contextTypes = { - fullData: PropTypes.array, -}; - -Trace.childContextTypes = { - fullTraceIndex: PropTypes.number, - traceIndex: PropTypes.number, -}; diff --git a/src/components/__tests__/Section-test.js b/src/components/__tests__/Section-test.js deleted file mode 100644 index 9e6c43df1..000000000 --- a/src/components/__tests__/Section-test.js +++ /dev/null @@ -1,70 +0,0 @@ -import Section from '../Section'; -import Flaglist from '../Flaglist'; -import Numeric from '../Numeric'; -import Trace from '../Trace'; -import React from 'react'; -import {TestEditor, fixtures, plotly} from '../../lib/test-utils'; -import {mount} from 'enzyme'; - -describe('Section', () => { - it('is visible if it contains any visible children', () => { - // mode is visible with scatter. Hole is not visible. Section should show. - const wrapper = mount( - - -
- - -
-
-
- ).find('[heading="test-section"]'); - - expect(wrapper.children().length).toBe(1); - expect( - wrapper - .find(Flaglist) - .childAt(0) // unwrap higher-level component - .exists() - ).toBe(true); - expect( - wrapper - .find(Numeric) - .childAt(0) // unwrap higher-level component - .exists() - ).toBe(false); - }); - - it('is not visible if it contains no visible children', () => { - // pull and hole are not scatter attrs. Section should not show. - const wrapper = mount( - - -
- - -
-
-
- ).find('[heading="test-section"]'); - - expect(wrapper.find(Flaglist).exists()).toBe(false); - expect(wrapper.find(Numeric).exists()).toBe(false); - - expect(wrapper.children().length).toBe(0); - }); -}); diff --git a/src/components/containers/Fold.js b/src/components/containers/Fold.js new file mode 100644 index 000000000..d6cd510a1 --- /dev/null +++ b/src/components/containers/Fold.js @@ -0,0 +1,45 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import {bem} from '../../lib'; + +export default class Fold extends Component { + renderHeader() { + const {deleteContainer} = this.context; + const {canDelete, name} = this.props; + const doDelete = canDelete && typeof deleteContainer === 'function'; + return ( +
+ {this.props.name} + {doDelete ? ( + + × + + ) : null} +
+ ); + } + + render() { + const modifiers = this.props.hideHeader ? ['noheader'] : null; + return ( +
+ {this.props.hideHeader ? null : this.renderHeader()} +
{this.props.children}
+
+ ); + } +} + +Fold.propTypes = { + canDelete: PropTypes.bool, + hideHeader: PropTypes.bool, + name: PropTypes.string, +}; + +Fold.contextTypes = { + deleteContainer: PropTypes.func, +}; diff --git a/src/components/Panel.js b/src/components/containers/Panel.js similarity index 70% rename from src/components/Panel.js rename to src/components/containers/Panel.js index 9caf49739..8c1c94235 100644 --- a/src/components/Panel.js +++ b/src/components/containers/Panel.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; -import {bem} from '../lib'; +import {bem} from '../../lib'; class Panel extends Component { render() { @@ -12,4 +12,12 @@ class Panel extends Component { } } +Panel.propTypes = { + visible: PropTypes.bool, +}; + +Panel.defaultProps = { + visible: true, +}; + export default Panel; diff --git a/src/components/containers/Section.js b/src/components/containers/Section.js new file mode 100644 index 000000000..5b681598a --- /dev/null +++ b/src/components/containers/Section.js @@ -0,0 +1,85 @@ +import SubPanel from './SubPanel'; +import React, {Component, cloneElement} from 'react'; +import PropTypes from 'prop-types'; +import {icon} from '../../lib'; +import unpackPlotProps from '../../lib/unpackPlotProps'; + +function childIsVisible(child) { + return Boolean((child.props.plotProps || {}).isVisible); +} + +class Section extends Component { + constructor(props, context) { + super(props, context); + + this.children = null; + this.subPanel = null; + + this.processAndSetChildren(context); + } + + componentWillReceiveProps(nextProps, nextContext) { + this.processAndSetChildren(nextContext); + } + + processAndSetChildren(context) { + let children = this.props.children; + if (!Array.isArray(children)) { + children = [children]; + } + + const attrChildren = []; + let subPanel = null; + + for (let i = 0; i < children.length; i++) { + let child = children[i]; + if (!child) { + continue; + } + if (child.type === SubPanel) { + // Process the first subPanel. Ignore the rest. + if (subPanel) { + continue; + } + subPanel = child; + continue; + } + + let isAttr = !!child.props.attr; + let plotProps = isAttr + ? unpackPlotProps(child.props, context, child.constructor) + : {isVisible: true}; + let childProps = Object.assign({plotProps}, child.props); + childProps.key = i; + attrChildren.push(cloneElement(child, childProps)); + } + + this.children = attrChildren.length ? attrChildren : null; + this.subPanel = subPanel; + } + + render() { + const hasVisibleChildren = + (this.children && this.children.some(childIsVisible)) || + Boolean(this.subPanel); + + return hasVisibleChildren ? ( +
+
+ {this.props.name} + {this.subPanel} +
+ {this.children} +
+ ) : null; + } +} + +Section.contextTypes = { + container: PropTypes.object, + fullContainer: PropTypes.object, + getValObject: PropTypes.func, + updateContainer: PropTypes.func, +}; + +export default Section; diff --git a/src/components/containers/SubPanel.js b/src/components/containers/SubPanel.js new file mode 100644 index 000000000..e20e9a5fc --- /dev/null +++ b/src/components/containers/SubPanel.js @@ -0,0 +1,42 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + +export default class SubPanel extends Component { + constructor() { + super(); + this.state = {isVisible: false}; + + this.toggleVisibility = this.toggleVisibility.bind(this); + } + + toggleVisibility() { + this.setState({isVisible: !this.state.isVisible}); + } + + render() { + const toggleClass = `subpanel__toggle ${this.props.toggleIconClass}`; + const isVisible = this.props.show || this.state.isVisible; + return ( + + + + + {isVisible ? ( +
+
+
{this.props.children}
+
+ ) : null} + + ); + } +} + +SubPanel.propTypes = { + toggleIconClass: PropTypes.string.isRequired, + show: PropTypes.bool, +}; + +SubPanel.defaultProps = { + toggleIconClass: 'plotlyjs_editor__icon-cog', +}; diff --git a/src/components/TraceAccordion.js b/src/components/containers/TraceAccordion.js similarity index 61% rename from src/components/TraceAccordion.js rename to src/components/containers/TraceAccordion.js index fc340ed1d..ae9cddfae 100644 --- a/src/components/TraceAccordion.js +++ b/src/components/containers/TraceAccordion.js @@ -1,42 +1,44 @@ +import Fold from './Fold'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import Trace from './Trace'; +import {EDITOR_ACTIONS} from '../../constants'; +import {connectTraceToPlot} from '../../lib'; + +const TraceFold = connectTraceToPlot(Fold); export default class TraceAccordion extends Component { constructor(props, context) { super(props); - this.data = context.data || []; - this.renderPanel = this.renderPanel.bind(this); - this.addTrace = this.addTrace.bind(this); - this.onUpdate = context.onUpdate; - } - - componentWillUpdate(nextProps, nextState, nextContext) { - this.data = (nextContext && nextContext.data) || []; + this.addTrace = this.addTrace.bind(this); + this.renderPanel = this.renderPanel.bind(this); } renderPanel(d, i) { return ( - + {this.props.children} - + ); } addTrace() { - this.onUpdate && this.onUpdate(null, [], 'addTrace'); + this.context.onUpdate && + this.context.onUpdate({ + type: EDITOR_ACTIONS.ADD_TRACE, + }); } render() { + const data = this.context.data || []; return ( -
+
{this.props.canAdd && ( Add )} - {this.data.map(this.renderPanel)} + {data.map(this.renderPanel)}
); } diff --git a/src/components/containers/__tests__/Layout-test.js b/src/components/containers/__tests__/Layout-test.js new file mode 100644 index 000000000..3597717fc --- /dev/null +++ b/src/components/containers/__tests__/Layout-test.js @@ -0,0 +1,52 @@ +import {Numeric} from '../../fields'; +import {Fold, Panel, Section} from '..'; +import NumericInput from '../../widgets/NumericInputStatefulWrapper'; +import React from 'react'; +import {EDITOR_ACTIONS} from '../../../constants'; +import {TestEditor, fixtures, plotly} from '../../../lib/test-utils'; +import {connectLayoutToPlot} from '../../../lib'; +import {mount} from 'enzyme'; + +const Layouts = [Panel, Fold, Section].map(connectLayoutToPlot); +const Editor = props => ( + +); + +Layouts.forEach(Layout => { + describe(`<${Layout.displayName}>`, () => { + it(`wraps container with fullValue pointing to gd._fullLayout`, () => { + const wrapper = mount( + + + + + + ) + .find('[attr="width"]') + .find(NumericInput); + + expect(wrapper.prop('value')).toBe(100); + }); + + it(`sends updates to gd._layout`, () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="width"]') + .find(NumericInput); + + wrapper.prop('onChange')(200); + const event = onUpdate.mock.calls[0][0]; + expect(event.type).toBe(EDITOR_ACTIONS.UPDATE_LAYOUT); + expect(event.payload).toEqual({update: {width: 200}}); + }); + }); +}); diff --git a/src/components/containers/__tests__/Section-test.js b/src/components/containers/__tests__/Section-test.js new file mode 100644 index 000000000..702d9e9d9 --- /dev/null +++ b/src/components/containers/__tests__/Section-test.js @@ -0,0 +1,115 @@ +import React from 'react'; +import Section from '../Section'; +import SubPanel from '../SubPanel'; +import {Flaglist, Info, Numeric} from '../../fields'; +import {TestEditor, fixtures, plotly} from '../../../lib/test-utils'; +import {connectTraceToPlot} from '../../../lib'; +import {mount} from 'enzyme'; + +const TraceSection = connectTraceToPlot(Section); + +describe('Section', () => { + it('is visible if it contains any visible children', () => { + // mode is visible with scatter. Hole is not visible. Section should show. + const wrapper = mount( + + + + + + + ) + .find('[name="test-section"]') + // we use last to select the Section within the higher-level component + .last(); + + expect(wrapper.children().length).toBe(1); + expect( + wrapper + .find(Flaglist) + .childAt(0) // unwrap higher-level component + .exists() + ).toBe(true); + expect( + wrapper + .find(Numeric) + .childAt(0) // unwrap higher-level component + .exists() + ).toBe(false); + }); + + it('is visible if it contains any non attr children', () => { + const wrapper = mount( + +
+ INFO +
+
+ ).find('[name="test-section"]'); + + expect(wrapper.children().length).toBe(1); + expect(wrapper.find(Info).exists()).toBe(true); + expect(wrapper.find(Info).text()).toBe('INFO'); + }); + + it('is not visible if it contains no visible children', () => { + // pull and hole are not scatter attrs. Section should not show. + const wrapper = mount( + + + + + + + ) + .find('[name="test-section"]') + // we use last to select the Section within the higher-level component + .last(); + + // childAt(0) unwraps higher-level component + expect(wrapper.children().length).toBe(0); + + expect(wrapper.find(Flaglist).exists()).toBe(false); + expect(wrapper.find(Numeric).exists()).toBe(false); + }); + + it('will render first subPanel even with no visible attrs', () => { + const wrapper = mount( + +
+ + INFO + + + MISINFORMATION + +
+
+ ).find('[name="test-section"]'); + + expect(wrapper.find(Info).length).toBe(1); + expect(wrapper.find(Info).text()).toBe('INFO'); + }); +}); diff --git a/src/components/containers/index.js b/src/components/containers/index.js new file mode 100644 index 000000000..af6ee56e2 --- /dev/null +++ b/src/components/containers/index.js @@ -0,0 +1,7 @@ +import SubPanel from './SubPanel'; +import Fold from './Fold'; +import Panel from './Panel'; +import Section from './Section'; +import TraceAccordion from './TraceAccordion'; + +export {SubPanel, Fold, Panel, Section, TraceAccordion}; diff --git a/src/components/fields/Color.js b/src/components/fields/Color.js new file mode 100644 index 000000000..0b24eb07a --- /dev/null +++ b/src/components/fields/Color.js @@ -0,0 +1,26 @@ +import ColorPicker from '../widgets/ColorPicker'; +import Field from './Field'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {bem, connectToContainer} from '../../lib'; + +class Color extends Component { + render() { + return ( + + + + ); + } +} + +Color.propTypes = { + fullValue: PropTypes.func, + updatePlot: PropTypes.func, + ...Field.propTypes, +}; + +export default connectToContainer(Color); diff --git a/src/components/fields/DataSelector.js b/src/components/fields/DataSelector.js new file mode 100644 index 000000000..d5ce33f7b --- /dev/null +++ b/src/components/fields/DataSelector.js @@ -0,0 +1,73 @@ +import DropdownWidget from '../widgets/Dropdown'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import Field from './Field'; +import nestedProperty from 'plotly.js/src/lib/nested_property'; +import {bem, connectToContainer} from '../../lib'; + +function attributeIsData(meta = {}) { + return meta.valType === 'data_array' || meta.arrayOk; +} + +class DataSelector extends Component { + static unpackPlotProps(props, context, plotProps) { + if (attributeIsData(plotProps.attrMeta)) { + plotProps.isVisible = true; + } + } + + constructor(props, context) { + super(props); + + this.setLocals(props); + this.updatePlot = this.updatePlot.bind(this); + } + + componentWillReceiveProps(nextProps) { + this.setLocals(nextProps); + } + + setLocals(props) { + this.dataSrcExists = false; + if (attributeIsData(props.attrMeta)) { + this.dataSrcExists = true; + this.srcAttr = props.attr + 'src'; + this.srcProperty = nestedProperty(props.container, this.srcAttr); + } + } + + fullValue() { + if (this.dataSrcExists) { + return this.srcProperty.get(); + } + return this.props.fullValue(); + } + + updatePlot(value) { + const attr = this.dataSrcExists ? this.srcAttr : this.props.attr; + this.props.updateContainer && this.props.updateContainer({[attr]: value}); + } + + render() { + return ( + + + + ); + } +} + +DataSelector.propTypes = { + fullValue: PropTypes.func, + options: PropTypes.array.isRequired, + updatePlot: PropTypes.func, + clearable: PropTypes.bool, + ...Field.propTypes, +}; + +export default connectToContainer(DataSelector); diff --git a/src/components/fields/Dropdown.js b/src/components/fields/Dropdown.js new file mode 100644 index 000000000..8a0e679f2 --- /dev/null +++ b/src/components/fields/Dropdown.js @@ -0,0 +1,30 @@ +import DropdownWidget from '../widgets/Dropdown'; +import Field from './Field'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {bem, connectToContainer} from '../../lib'; + +export class UnconnectedDropdown extends Component { + render() { + return ( + + + + ); + } +} + +UnconnectedDropdown.propTypes = { + fullValue: PropTypes.func, + options: PropTypes.array.isRequired, + updatePlot: PropTypes.func, + clearable: PropTypes.bool, + ...Field.propTypes, +}; + +export default connectToContainer(UnconnectedDropdown); diff --git a/src/components/fields/Field.js b/src/components/fields/Field.js new file mode 100644 index 000000000..4a5c2bdee --- /dev/null +++ b/src/components/fields/Field.js @@ -0,0 +1,53 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import {bem} from '../../lib'; + +export default class Field extends Component { + render() { + let postfix = null; + if (this.props.postfix) { + postfix = ( +
+
+ {this.props.postfix} +
+
+ ); + } + + if (!this.props.label) { + const noTitleModifier = this.props.center ? ['center'] : null; + return ( +
+
+ {this.props.children} +
+ {postfix} +
+ ); + } + + const widgetModifier = this.props.postfix ? ['postfix'] : null; + return ( +
+
+
{this.props.label}
+
+
+ {this.props.children} +
+ {postfix} +
+ ); + } +} + +Field.propTypes = { + center: PropTypes.bool, + label: PropTypes.string, + postfix: PropTypes.string, +}; + +Field.defaultProps = { + center: false, +}; diff --git a/src/components/fields/Flaglist.js b/src/components/fields/Flaglist.js new file mode 100644 index 000000000..49ebbd8be --- /dev/null +++ b/src/components/fields/Flaglist.js @@ -0,0 +1,28 @@ +import Field from './Field'; +import FlaglistCheckboxGroup from '../widgets/FlaglistCheckboxGroup'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {bem, connectToContainer} from '../../lib'; + +class Flaglist extends Component { + render() { + return ( + + + + ); + } +} + +Flaglist.propTypes = { + fullValue: PropTypes.func, + options: PropTypes.array.isRequired, + updatePlot: PropTypes.func, + ...Field.propTypes, +}; + +export default connectToContainer(Flaglist); diff --git a/src/components/fields/Info.js b/src/components/fields/Info.js new file mode 100644 index 000000000..0a431c1bb --- /dev/null +++ b/src/components/fields/Info.js @@ -0,0 +1,13 @@ +import Field from './Field'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; + +export default class Info extends Component { + render() { + return {this.props.children}; + } +} + +Info.propTypes = { + ...Field.propTypes, +}; diff --git a/src/components/fields/Numeric.js b/src/components/fields/Numeric.js new file mode 100644 index 000000000..fa1222368 --- /dev/null +++ b/src/components/fields/Numeric.js @@ -0,0 +1,34 @@ +import Field from './Field'; +import NumericInput from '../widgets/NumericInputStatefulWrapper'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {bem, connectToContainer} from '../../lib'; + +class Numeric extends Component { + render() { + return ( + + + + ); + } +} + +Numeric.propTypes = { + fullValue: PropTypes.func, + min: PropTypes.number, + max: PropTypes.number, + step: PropTypes.number, + updatePlot: PropTypes.func, + ...Field.propTypes, +}; + +export default connectToContainer(Numeric); diff --git a/src/components/fields/Radio.js b/src/components/fields/Radio.js new file mode 100644 index 000000000..d803905c2 --- /dev/null +++ b/src/components/fields/Radio.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import RadioBlocks from '../widgets/RadioBlocks'; +import Field from './Field'; +import {bem, connectToContainer} from '../../lib'; + +class Radio extends Component { + render() { + return ( + + + + ); + } +} + +Radio.propTypes = { + center: PropTypes.bool, + fullValue: PropTypes.func, + options: PropTypes.array.isRequired, + updatePlot: PropTypes.func, + ...Field.propTypes, +}; + +// for better appearance overrides {center: false} +// default prop. This can be overridden manually using props for . +Radio.defaultProps = { + center: true, +}; + +export default connectToContainer(Radio); diff --git a/src/components/TraceSelector.js b/src/components/fields/TraceSelector.js similarity index 80% rename from src/components/TraceSelector.js rename to src/components/fields/TraceSelector.js index a11fbbb5b..2032d6901 100644 --- a/src/components/TraceSelector.js +++ b/src/components/fields/TraceSelector.js @@ -2,7 +2,7 @@ import {UnconnectedDropdown} from './Dropdown'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; import nestedProperty from 'plotly.js/src/lib/nested_property'; -import {connectToPlot} from '../lib'; +import {connectToContainer} from '../../lib'; class TraceSelector extends Component { constructor(props, context) { @@ -17,16 +17,16 @@ class TraceSelector extends Component { updatePlot(value) { let update; if (value === 'line') { - update = {type: ['scatter'], mode: ['lines'], fill: ['none']}; + update = {type: 'scatter', mode: 'lines', fill: 'none'}; } else if (value === 'scatter') { - update = {type: ['scatter'], mode: ['markers'], fill: ['none']}; + update = {type: 'scatter', mode: 'markers', fill: 'none'}; } else if (value === 'area') { - update = {type: ['scatter'], fill: ['tozeroy']}; + update = {type: 'scatter', fill: 'tozeroy'}; } else { - update = {type: [value]}; + update = {type: value}; } - this.props.onUpdate && this.props.onUpdate(update, [this.props.traceIndex]); + this.props.updateContainer && this.props.updateContainer(update); } fullValue() { @@ -64,4 +64,4 @@ TraceSelector.contextTypes = { plotSchema: PropTypes.object, }; -export default connectToPlot(TraceSelector); +export default connectToContainer(TraceSelector); diff --git a/src/components/__tests__/DataSelector-test.js b/src/components/fields/__tests__/DataSelector-test.js similarity index 85% rename from src/components/__tests__/DataSelector-test.js rename to src/components/fields/__tests__/DataSelector-test.js index 0578aed04..1f190da77 100644 --- a/src/components/__tests__/DataSelector-test.js +++ b/src/components/fields/__tests__/DataSelector-test.js @@ -1,7 +1,6 @@ -import DataSelector from '../DataSelector'; -import DropdownWidget from '../widgets/Dropdown'; +import DropdownWidget from '../../widgets/Dropdown'; import React from 'react'; -import {TestEditor, fixtures, plotly} from '../../lib/test-utils'; +import {TestEditor, fixtures, plotly} from '../../../lib/test-utils'; import {mount} from 'enzyme'; function render(overrides = {}) { @@ -33,7 +32,10 @@ describe('DataSelector', () => { const onUpdate = jest.fn(); const wrapper = render({onUpdate}).find(DropdownWidget); wrapper.prop('onChange')('y2'); - expect(onUpdate.mock.calls[0][1]).toEqual({xsrc: ['y2']}); + expect(onUpdate.mock.calls[0][0].payload).toEqual({ + update: {xsrc: 'y2'}, + traceIndexes: [0], + }); }); it('is invisible when a data src does not exist for trace type', () => { diff --git a/src/components/fields/__tests__/Radio-test.js b/src/components/fields/__tests__/Radio-test.js new file mode 100644 index 000000000..631646cb2 --- /dev/null +++ b/src/components/fields/__tests__/Radio-test.js @@ -0,0 +1,50 @@ +import React from 'react'; +import Field from '../Field'; +import Radio from '../Radio'; +import {Section} from '../../containers'; +import {TestEditor, fixtures, plotly} from '../../../lib/test-utils'; +import {connectTraceToPlot} from '../../../lib'; +import {mount} from 'enzyme'; + +const Trace = connectTraceToPlot(Section); + +describe('', () => { + it('enables centering by default', () => { + const wrapper = mount( + + + + + + ).find(Field); + + expect(wrapper.prop('center')).toBe(true); + }); + + it('permits centering to be disabled', () => { + const wrapper = mount( + + + + + + ).find(Field); + + expect(wrapper.prop('center')).toBe(false); + }); +}); diff --git a/src/components/fields/index.js b/src/components/fields/index.js new file mode 100644 index 000000000..a18b3f12f --- /dev/null +++ b/src/components/fields/index.js @@ -0,0 +1,19 @@ +import ColorPicker from './Color'; +import Dropdown from './Dropdown'; +import Flaglist from './Flaglist'; +import Info from './Info'; +import Radio from './Radio'; +import DataSelector from './DataSelector'; +import Numeric from './Numeric'; +import TraceSelector from './TraceSelector'; + +export { + ColorPicker, + Dropdown, + Flaglist, + Info, + Radio, + DataSelector, + Numeric, + TraceSelector, +}; diff --git a/src/components/index.js b/src/components/index.js index 3f837706e..4bd7f548c 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,29 +1,30 @@ -import Base from './Base'; -import ColorPicker from './Color'; -import DataSelector from './DataSelector'; -import Dropdown from './Dropdown'; -import Flaglist from './Flaglist'; -import Numeric from './Numeric'; -import Panel from './Panel'; +import { + ColorPicker, + Dropdown, + Flaglist, + Info, + Radio, + DataSelector, + Numeric, + TraceSelector, +} from './fields'; + +import {SubPanel, Fold, Panel, Section, TraceAccordion} from './containers'; import PanelMenuWrapper from './PanelMenuWrapper'; -import Radio from './Radio'; -import Section from './Section'; -import Select from './Select'; -import TraceAccordion from './TraceAccordion'; -import TraceSelector from './TraceSelector'; export { - Base, + SubPanel, ColorPicker, DataSelector, Dropdown, Flaglist, + Fold, + Info, Numeric, Panel, PanelMenuWrapper, Radio, Section, - Select, TraceAccordion, TraceSelector, }; diff --git a/src/components/sidebar/SidebarGroup.js b/src/components/sidebar/SidebarGroup.js new file mode 100644 index 000000000..59ae5e6f4 --- /dev/null +++ b/src/components/sidebar/SidebarGroup.js @@ -0,0 +1,63 @@ +import React, {Component} from 'react'; +import {bem} from '../../lib'; + +import SidebarItem from './SidebarItem'; + +export default class SidebarGroup extends Component { + constructor(props) { + super(props); + + this.state = { + expanded: this.props.group === this.props.selectedGroup, + }; + + this.toggleExpanded = this.toggleExpanded.bind(this); + this.onChangeGroup = this.onChangeGroup.bind(this); + this.renderSubItem = this.renderSubItem.bind(this); + } + + toggleExpanded() { + this.setState({expanded: !this.state.expanded}); + } + + onChangeGroup(panel) { + this.props.onChangeGroup(this.props.group, panel); + } + + renderSubItem(panel, i) { + const isActive = + this.props.selectedPanel === panel && + this.props.group === this.props.selectedGroup; + + return ( + this.onChangeGroup(panel)} + label={panel} + /> + ); + } + + render() { + return ( +
+
+ {this.props.group} +
+ {this.state.expanded && this.props.panels.map(this.renderSubItem)} +
+ ); + } +} + +SidebarGroup.defaultProps = { + expanded: false, +}; diff --git a/src/components/sidebar/SidebarItem.js b/src/components/sidebar/SidebarItem.js new file mode 100644 index 000000000..fa3a119fc --- /dev/null +++ b/src/components/sidebar/SidebarItem.js @@ -0,0 +1,17 @@ +import React, {Component} from 'react'; +import {bem} from '../../lib'; + +export default class SidebarItem extends Component { + render() { + return ( +
+ {this.props.label} +
+ ); + } +} diff --git a/src/components/widgets/AccordionMenu.js b/src/components/widgets/AccordionMenu.js deleted file mode 100644 index 22d5e69fb..000000000 --- a/src/components/widgets/AccordionMenu.js +++ /dev/null @@ -1,405 +0,0 @@ -import AccordionMenuItem from "./AccordionMenuItem"; -import Dropdown from "./widgets/Dropdown"; -import R from "ramda"; -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; -import { dropdownOptionShape } from "@workspace/utils/customPropTypes"; - -export class SimpleAddMenu extends Component { - render() { - const { addNewMenuClass, disableAddMenu, addNewMenuHandler } = this.props; - - const className = classnames( - "btnbase", - "btn--primary", - "js-test-add-new-layer", - "+float-right", - addNewMenuClass, - { "--disabled": disableAddMenu } - ); - const buttonText = this.props.addMenuText || ""; - - return ( -
- {"+ " + buttonText} -
- ); - } -} - -/* - * See the main AccordionMenu component PropTypes section for more information. - */ -SimpleAddMenu.propTypes = { - addNewMenuClass: PropTypes.string, - addNewMenuHandler: PropTypes.func, - disableAddMenu: PropTypes.bool, - addMenuText: PropTypes.string, -}; - -export class DropdownAddMenu extends Component { - constructor(props) { - super(props); - } - - render() { - const { title, options } = this.props.addNewMenuConfig; - - const simpleAddMenuPlaceholder = ; - - /* - * Always display `+` (SimpleAddMenu) and show a - * dropdown when you click on it. - * We're always displaying the `+` by returning SimpleAddMenu in - * the valueRenderer and the placeholder - */ - return ( -
- simpleAddMenuPlaceholder} - /> -
- ); - } -} - -/* - * See the main AccordionMenu component PropTypes section for more information. - */ -DropdownAddMenu.propTypes = { - addNewMenuConfig: PropTypes.shape({ - title: PropTypes.string.isRequired, - options: PropTypes.arrayOf(dropdownOptionShape).isRequired, - }), - addNewMenuClass: PropTypes.string, - addNewMenuHandler: PropTypes.func, -}; - -function idOrIndex(id, index) { - if (typeof id === "string") { - return id; - } - - return String(index); -} - -/* - * Each AccordionMenuItem is passed in as an object and its content - * as an element - * The expand and collapse buttons set the state of each submenu - */ -export default class AccordionMenu extends Component { - constructor(props) { - super(props); - this.state = this.initialState(props); - - this.toggleAllHandler = this.toggleAll.bind(this); - - // Used to construct a collapse/expand handler for each sub-menu. - const makeHandler = index => this.toggleSubMenu(index); - this.toggleSubMenuHandlerConstructor = makeHandler.bind(this); - } - - componentWillReceiveProps(newProps) { - const newSubmenuStates = this.initialState(newProps).subMenuStates; - - /* - * Only update openess state when adding a new AccordionMenuItem - * e.g for +Trace / +Filter / +Note - */ - if ( - this.props.assignOpenState && - newProps.subMenus.length !== Object.keys(this.state.subMenuStates).length - ) { - this.setState({ - subMenuStates: newSubmenuStates, - }); - return; - } - - // Only update sub-menu states of those that are newly added. - this.setState({ - subMenuStates: R.merge(newSubmenuStates, this.state.subMenuStates), - }); - } - - initialState(props) { - const subMenuStates = {}; - props.subMenus.forEach((subMenu, i) => { - const id = idOrIndex(subMenu.id, i); - subMenuStates[id] = subMenu.isOpen; - }); - - return { subMenuStates }; - } - - /** - * Check if any sub-menu is open. - * @return {Boolean} True if all sub-menus are closed. - */ - anySubMenusOpen() { - return R.any(R.identity, this.getMenuStates()); - } - - /** - * Set all the sub-menus to a given state. - * @param {Boolean} state the collapse state for all sub-menus - */ - setMenuStates(state) { - const subMenuStates = {}; - const setState = (menu, index) => { - const id = idOrIndex(menu.id, index); - subMenuStates[id] = state; - }; - this.props.subMenus.forEach(setState); - this.setState({ subMenuStates }); - } - - /** - * Get all the sub-menu collapse states. - * @return {Boolean[]} state: the collapse state for all sub-menus - */ - getMenuStates() { - const getState = id => this.state.subMenuStates[id]; - const menuStates = Object.keys(this.state.subMenuStates).map(getState); - return menuStates; - } - - /** - * Given the state of all menus, expand or collapse all menus at once. - */ - toggleAll() { - /* - * If any sub-menu is open, collapse all sub-menus - * Else expand all sub-menus - */ - - if (this.anySubMenusOpen()) { - this.setMenuStates(false); - } else { - this.setMenuStates(true); - } - } - - /** - * Toggle an individual submenu - * @param {Number} id: The sub-menu ref id. - */ - toggleSubMenu(id) { - this.setState({ - subMenuStates: R.merge(this.state.subMenuStates, { - [id]: !this.state.subMenuStates[id], - }), - }); - } - - /** - * Renders each sub-menu in the accordion menu. - * @return {AccordionMenuItem[]} sub-menu DOM elements - */ - renderSubmenuItems() { - return this.props.subMenus.map((subMenuConfig, i) => { - const { title, titleColor, iconClass, content } = subMenuConfig; - - const isRemovable = R.pathOr( - R.has("removeMenuHandler", this.props), - ["isRemovable"], - subMenuConfig - ); - - const id = idOrIndex(subMenuConfig.id, i); - - return ( - this.toggleSubMenuHandlerConstructor(id)} - accordionMenuModifier={this.props.accordionMenuModifier || null} - > - {content} - - ); - }); - } - - render() { - const { addNewMenuHandler, addNewMenuConfig, header } = this.props; - - /* - * If the developer passes in a handler we are going to set up - * an add new menu UI. - */ - const doAddMenu = typeof addNewMenuHandler === "function"; - - /* - * if the developer doesn't pass in options we just call the handler - * when the user clicks on a "+" button. - */ - const doSimpleAddMenu = doAddMenu && !addNewMenuConfig; - - /* - * if the developer passes in options we are going to set up a - * standalone box with a dropdown and a "+" button. - */ - const doDropdownAddMenu = doAddMenu && Boolean(addNewMenuConfig); - - /* - * Only render the components when needed. - */ - const simpleAddMenu = doSimpleAddMenu ? ( - - ) : null; - - const dropdownAddMenu = doDropdownAddMenu ? ( - - ) : null; - - let collapseButtonText; - if (this.anySubMenusOpen()) { - collapseButtonText = "Collapse All"; - } else { - collapseButtonText = "Expand All"; - } - - const collapseButton = - this.props.subMenus.length > 0 ? ( -
- {this.anySubMenusOpen() ? ( - - ) : ( - - )} - {collapseButtonText} -
- ) : null; - - const accordionClassName = classnames( - "accordion-menu__button-area", - "+clearfix" - ); - - return ( -
-
- {collapseButton} - {simpleAddMenu} - {dropdownAddMenu} -
- {header} -
- {this.props.subMenus && this.props.subMenus.length > 0 ? ( - this.renderSubmenuItems() - ) : ( - this.props.placeholder && this.props.placeholder() - )} -
-
- ); - } -} - -AccordionMenu.defaultProps = { - assignOpenState: false, -}; - -AccordionMenu.propTypes = { - subMenus: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - titleColor: PropTypes.string, - isOpen: PropTypes.bool, - iconClass: PropTypes.string, - content: PropTypes.element, - id: PropTypes.string, - isRemovable: PropTypes.bool, - }) - ), - - /* - * If this handler is passed in as a prop the Accordion menu - * will have a "+" button in the top right with a click handler - * set to the this function. - */ - addNewMenuHandler: PropTypes.func, - - // Make add layer button unclickable - disableAddMenu: PropTypes.bool, - - /* - * A class in which to style the "+" button (if it is shown) - */ - addNewMenuClass: PropTypes.string, - - /* - * Text to include in the "+" button (if it is shown) - */ - addMenuText: PropTypes.string, - - /* - * If this object is present the accordian menu will show a "create - * new menu" box with a dropdown consisting of the options - * given in the list instead of the simple "+" button. The - * addNewMenuClass handler will be passed the value in the dropdown - * once the user clicks the plus button. - */ - addNewMenuConfig: PropTypes.shape({ - title: PropTypes.string.isRequired, - options: PropTypes.arrayOf(dropdownOptionShape).isRequired, - }), - - /* - * If this handler is passed in submenus will show an 'x' - * which when called evokes this function with the id - * of the submenu. - */ - removeMenuHandler: PropTypes.func, - - /* - * Optionally display a generic header item at the top of the AccordionMenu - */ - - header: PropTypes.element, - - parentClassName: PropTypes.string, - - /* - * On first render get open state from parent Component - */ - assignOpenState: PropTypes.bool, - - /* - * Class to modify default look of accoridon menu - */ - accordionMenuModifier: PropTypes.string, - - /* - * Optional method to render a placeholder when no subMenus present - */ - placeholder: PropTypes.func, -}; diff --git a/src/components/widgets/AccordionMenuItem.js b/src/components/widgets/AccordionMenuItem.js deleted file mode 100644 index fea9b13d7..000000000 --- a/src/components/widgets/AccordionMenuItem.js +++ /dev/null @@ -1,126 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -class AccordionMenuItem extends Component { - constructor(props) { - super(props); - this.handleRemoveMenu = this.handleRemoveMenu.bind(this); - this.renderMenuContent = this.renderMenuContent.bind(this); - } - - handleRemoveMenu(event) { - const { removeMenuHandler, id } = this.props; - - event.stopPropagation(); - - if (typeof removeMenuHandler === "function") { - removeMenuHandler(id); - } - } - - renderMenuContent() { - const { isOpen, children, id, accordionMenuModifier } = this.props; - - const subMenuDisplay = classnames( - "accordion-item__content", - `js-accordion-menu-content-${id}`, - accordionMenuModifier - ); - - if (!isOpen) { - return ( -
- {children} -
- ); - } - - return
{children}
; - } - - render() { - const { - isOpen, - removeMenuHandler, - titleColor, - onToggle, - iconClass, - title, - isRemovable, - } = this.props; - - const subMenuOpen = classnames( - ["accordion-item__top", "js-test-menu-item-click"], - { - "accordion-item__top--active": isOpen, - } - ); - - const iconDirection = classnames("accordion-item__top__arrow", { - "+rotate-90 ": !isOpen, - }); - - const topIcon = classnames("accordion-item__top__icon", { - "accordion-item__top__icon_active": isOpen, - }); - - const titleClass = classnames( - "js-test-title", - "accordion-item__top__title", - { - "accordion-item__top__title_active": isOpen, - } - ); - - const closeClass = classnames( - "icon-close-thin", - "accordion-item__top__close", - "js-accordion-menu-item-close" - ); - - let closeIcon = null; - if (isRemovable && typeof removeMenuHandler === "function") { - closeIcon = ; - } - - const titleStyling = { color: titleColor || "" }; - - return ( -
-
- -
- -
- - {iconClass ? ( -
- -
- ) : null} - -
{title}
-
- {closeIcon} -
- {this.renderMenuContent()} -
- ); - } -} - -AccordionMenuItem.propTypes = { - children: PropTypes.element.isRequired, - iconClass: PropTypes.string, - id: PropTypes.string.isRequired, - isOpen: PropTypes.bool, - onToggle: PropTypes.func, - removeMenuHandler: PropTypes.func, - title: PropTypes.string.isRequired, - titleColor: PropTypes.string, - accordionMenuModifier: PropTypes.string, - isRemovable: PropTypes.bool, -}; - -export default AccordionMenuItem; diff --git a/src/components/widgets/Anchor.js b/src/components/widgets/Anchor.js deleted file mode 100644 index dce4ec1ff..000000000 --- a/src/components/widgets/Anchor.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import Dropdown from "./widgets/Dropdown"; -import connectWorkspacePlot from "@workspace/utils/connectWorkspacePlot"; - -/* - * The Anchor component is a control for specifing the `anchor` axis property - * in plotly.js: https://plot.ly/javascript/reference/#layout-xaxis-anchor - */ - -const Anchor = ({ options, onChange, value }) => ( - -); - -Anchor.propTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - options: PropTypes.array.isRequired, - axisLetter: PropTypes.oneOf(["x", "y"]), -}; - -function mapPlotToProps(plot, props) { - const { axisLetter } = props; - const options = plot - .keysAtPath(["_fullLayout"]) - .filter(key => key.startsWith(`${axisLetter}axis`)) - .map(axisName => ({ - label: axisName, - value: axisName.replace("axis", ""), - })); - options.unshift({ label: "Unanchored", value: "free" }); - return { options }; -} - -export default connectWorkspacePlot(mapPlotToProps)(Anchor); diff --git a/src/components/widgets/AnchorPositioning.js b/src/components/widgets/AnchorPositioning.js deleted file mode 100644 index a4a98a335..000000000 --- a/src/components/widgets/AnchorPositioning.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import hat from "hat"; - -/* - * Anchor Positioning. - * Positions: top left, top center, top right, middle left, middle center, - * middle right, bottom left, bottom center, bottom right - */ - -export default class AnchorPositioning extends Component { - constructor(props) { - super(props); - this.state = { - uid: hat(), - }; - - this.renderRadio = this.renderRadio.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - handleChange(e) { - const newActiveOption = e.target.value; - this.props.onOptionChange(newActiveOption); - } - - renderRadio(value) { - const label = value; - const activeRadio = this.props.activeOption - ? this.props.activeOption - : "middle center"; - const defaultActive = activeRadio === value; - - return ( - - -