From ddb1446aff684af501dd41d0387d203a5b6073af Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Mon, 9 May 2016 16:48:04 -0700 Subject: [PATCH 01/35] add 'right' and 'left' directions to ContextMenu --- src/components/ContextMenu/ContextMenu.jsx | 33 +++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index dfc178417..1218fb304 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -42,7 +42,7 @@ const ContextMenu = createClass({ /** * alignment of the FlyOut relative to Target. Defaults to `'down'`. */ - direction: oneOf(['down', 'up']), + direction: oneOf(['down', 'up', 'right', 'left']), /** * Indicates whether the FlyOut will render or not. Defaults to `true`. */ @@ -151,6 +151,33 @@ const ContextMenu = createClass({ }); }, + getFlyoutPosition(direction, targetRect, flyOutHeight/*, alignment */) { + + if (direction === ContextMenu.UP) { + return { + top: targetRect.top - flyOutHeight, + left: targetRect.left, + }; + } + if (direction === ContextMenu.DOWN) { + return { + top: targetRect.bottom, + left: targetRect.left, + }; + } + if (direction === ContextMenu.RIGHT) { + return { + top: targetRect.top, + left: targetRect.left + targetRect.width, + }; + } + return { + top: targetRect.top, + right: window.innerWidth - targetRect.left, + }; + + }, + render() { const { className, @@ -186,10 +213,8 @@ const ContextMenu = createClass({ portalId={portalId} style={_.assign({}, flyProps.style, { position: 'absolute', - left: targetRect.left, minWidth: targetRect.width, - top: (direction === ContextMenu.UP ? targetRect.top - flyOutHeight : targetRect.bottom) - })} + }, this.getFlyoutPosition(direction, targetRect, flyOutHeight))} > {flyProps.children} From fcc5e020f2f0fcdb57d009b0112437f26f725a32 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Tue, 10 May 2016 17:33:55 -0700 Subject: [PATCH 02/35] add right and left directions, and `alignment` prop --- src/components/ContextMenu/ContextMenu.jsx | 117 ++++++++++++------ .../ContextMenu/examples/3.menu-bar.jsx | 2 +- .../ContextMenu/examples/4.directions.jsx | 39 ++++++ 3 files changed, 121 insertions(+), 37 deletions(-) create mode 100644 src/components/ContextMenu/examples/4.directions.jsx diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index 1218fb304..faddc19ff 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -40,9 +40,13 @@ const ContextMenu = createClass({ */ style: object, /** - * alignment of the FlyOut relative to Target. Defaults to `'down'`. + * direction of the FlyOut relative to Target. Defaults to `'down'`. */ direction: oneOf(['down', 'up', 'right', 'left']), + /** + * alignment of the Flyout relative to Target. Defaults to `'start'` + */ + alignment: oneOf(['start', 'center', 'end']), /** * Indicates whether the FlyOut will render or not. Defaults to `true`. */ @@ -73,7 +77,8 @@ const ContextMenu = createClass({ getDefaultProps() { return { - direction: 'down', + direction: ContextMenu.DOWN, + alignment: ContextMenu.START, isExpanded: true, onClickOut: null, portalId: null @@ -126,6 +131,10 @@ const ContextMenu = createClass({ statics: { DOWN: 'down', + END: 'end', + LEFT: 'left', + RIGHT: 'right', + START: 'start', UP: 'up', }, @@ -143,7 +152,14 @@ const ContextMenu = createClass({ } const targetRect = getAbsoluteBoundingClientRect(target); - const flyOutEl = this.refs.flyOutPortal.portalElement.firstChild; + + if (!flyOutPortal) { + return this.setState({ + targetRect + }); + } + + const flyOutEl = flyOutPortal.portalElement.firstChild; this.setState({ targetRect, @@ -151,47 +167,76 @@ const ContextMenu = createClass({ }); }, - getFlyoutPosition(direction, targetRect, flyOutHeight/*, alignment */) { + getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight) { + + const { + DOWN, + END, + LEFT, + RIGHT, + START, + UP, + } = ContextMenu; - if (direction === ContextMenu.UP) { - return { - top: targetRect.top - flyOutHeight, - left: targetRect.left, - }; + const { + bottom, + left, + right, + top, + width, + height, + } = targetRect; + + const { + clientWidth, + } = document.body; + + const matcher = _.matches({ direction, alignment }); + + if (matcher({ direction: UP, alignment: START })) { + return { top: top - flyOutHeight, left }; } - if (direction === ContextMenu.DOWN) { - return { - top: targetRect.bottom, - left: targetRect.left, - }; + if (matcher({ direction: UP, alignment: END })) { + return { top: top - flyOutHeight, right: clientWidth - right }; } - if (direction === ContextMenu.RIGHT) { - return { - top: targetRect.top, - left: targetRect.left + targetRect.width, - }; + if (matcher({ direction: DOWN, alignment: START })) { + return { top: bottom, left }; + } + if (matcher({ direction: DOWN, alignment: END })) { + return { top: bottom, right: clientWidth - right }; + } + if (matcher({ direction: LEFT, alignment: START })) { + return { top, right: clientWidth - left }; + } + if (matcher({ direction: LEFT, alignment: END })) { + return { top: top - flyOutHeight + height, right: clientWidth - left }; + } + if (matcher({ direction: RIGHT, alignment: START })) { + return { top, left: left + width }; + } + if (matcher({ direction: RIGHT, alignment: END })) { + return { top: top - flyOutHeight + height, left: left + width }; } - return { - top: targetRect.top, - right: window.innerWidth - targetRect.left, - }; }, render() { const { - className, - style, - isExpanded, - direction, - ...passThroughs - } = this.props; - - const { - portalId, - targetRect, - flyOutHeight - } = this.state; + props: { + className, + style, + isExpanded, + direction, + alignment, + position, + ...passThroughs + }, + state: { + portalId, + targetRect, + flyOutHeight + } + } = this; const targetElement = _.first(findTypes(this.props, ContextMenu.Target)); const targetChildren = _.get(targetElement, 'props.children', null); @@ -214,7 +259,7 @@ const ContextMenu = createClass({ style={_.assign({}, flyProps.style, { position: 'absolute', minWidth: targetRect.width, - }, this.getFlyoutPosition(direction, targetRect, flyOutHeight))} + }, this.getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight))} > {flyProps.children} diff --git a/src/components/ContextMenu/examples/3.menu-bar.jsx b/src/components/ContextMenu/examples/3.menu-bar.jsx index 3ccf60bca..8aff9d180 100644 --- a/src/components/ContextMenu/examples/3.menu-bar.jsx +++ b/src/components/ContextMenu/examples/3.menu-bar.jsx @@ -51,7 +51,7 @@ export default React.createClass({ editDirection } = this.state; return ( -
+
+ + {_.map(['up', 'left', 'right', 'down'], direction => _.map(['start', 'end'], alignment => { + return ( +
+ + + Target + + + +
{`direction: ${direction}`}
+
{`alignment: ${alignment}`}
+
+
+
+ ); + }))} + +
+ ); + } +}); From fd38ac36945bed98e7943dba50ea913b4d3f46ce Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Wed, 11 May 2016 17:13:50 -0700 Subject: [PATCH 03/35] add alignment: center --- src/components/ContextMenu/ContextMenu.jsx | 54 +++++++++++++------ .../ContextMenu/examples/4.directions.jsx | 2 +- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index faddc19ff..3c69fc9d3 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -18,6 +18,16 @@ const { } } = React; +const CONSTANTS = { + CENTER: 'center', + DOWN: 'down', + END: 'end', + LEFT: 'left', + RIGHT: 'right', + START: 'start', + UP: 'up', +}; + /** * * {"categories": ["utility"], "madeFrom": ["Portal"]} @@ -77,8 +87,8 @@ const ContextMenu = createClass({ getDefaultProps() { return { - direction: ContextMenu.DOWN, - alignment: ContextMenu.START, + direction: CONSTANTS.DOWN, + alignment: CONSTANTS.START, isExpanded: true, onClickOut: null, portalId: null @@ -99,7 +109,8 @@ const ContextMenu = createClass({ height: 0, width: 0 }, - flyOutHeight: 0 + flyOutHeight: 0, + flyOutWidth: 0, }; }, @@ -129,14 +140,7 @@ const ContextMenu = createClass({ this.alignFlyOut(); }, - statics: { - DOWN: 'down', - END: 'end', - LEFT: 'left', - RIGHT: 'right', - START: 'start', - UP: 'up', - }, + statics: CONSTANTS, alignFlyOut() { @@ -160,16 +164,21 @@ const ContextMenu = createClass({ } const flyOutEl = flyOutPortal.portalElement.firstChild; - + const { + height, + width, + } = flyOutEl.getBoundingClientRect(); this.setState({ targetRect, - flyOutHeight: flyOutEl.getBoundingClientRect().height + flyOutHeight: height, + flyOutWidth: width }); }, - getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight) { + getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth) { const { + CENTER, DOWN, END, LEFT, @@ -199,24 +208,36 @@ const ContextMenu = createClass({ if (matcher({ direction: UP, alignment: END })) { return { top: top - flyOutHeight, right: clientWidth - right }; } + if (matcher({ direction: UP, alignment: CENTER })) { + return { top: top - flyOutHeight, left: left + (width / 2) - (flyOutWidth / 2) }; + } if (matcher({ direction: DOWN, alignment: START })) { return { top: bottom, left }; } if (matcher({ direction: DOWN, alignment: END })) { return { top: bottom, right: clientWidth - right }; } + if (matcher({ direction: DOWN, alignment: CENTER })) { + return { top: bottom, left: left + (width / 2) - (flyOutWidth / 2) }; + } if (matcher({ direction: LEFT, alignment: START })) { return { top, right: clientWidth - left }; } if (matcher({ direction: LEFT, alignment: END })) { return { top: top - flyOutHeight + height, right: clientWidth - left }; } + if (matcher({ direction: LEFT, alignment: CENTER })) { + return { top: top - (flyOutHeight / 2) + (height / 2), right: clientWidth - left }; + } if (matcher({ direction: RIGHT, alignment: START })) { return { top, left: left + width }; } if (matcher({ direction: RIGHT, alignment: END })) { return { top: top - flyOutHeight + height, left: left + width }; } + if (matcher({ direction: RIGHT, alignment: CENTER })) { + return { top: top - (flyOutHeight / 2) + (height / 2), left: left + width }; + } }, @@ -234,7 +255,8 @@ const ContextMenu = createClass({ state: { portalId, targetRect, - flyOutHeight + flyOutHeight, + flyOutWidth } } = this; @@ -259,7 +281,7 @@ const ContextMenu = createClass({ style={_.assign({}, flyProps.style, { position: 'absolute', minWidth: targetRect.width, - }, this.getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight))} + }, this.getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth))} > {flyProps.children} diff --git a/src/components/ContextMenu/examples/4.directions.jsx b/src/components/ContextMenu/examples/4.directions.jsx index 717f9d1e6..8dcf0de94 100644 --- a/src/components/ContextMenu/examples/4.directions.jsx +++ b/src/components/ContextMenu/examples/4.directions.jsx @@ -14,7 +14,7 @@ export default React.createClass({ return (
- {_.map(['up', 'left', 'right', 'down'], direction => _.map(['start', 'end'], alignment => { + {_.map(['up', 'left', 'right', 'down'], direction => _.map(['start', 'center', 'end'], alignment => { return (
Date: Wed, 11 May 2016 21:08:15 -0700 Subject: [PATCH 04/35] add directionOffset and alignmentOffset --- src/components/Banner/Banner.jsx | 2 +- src/components/ContextMenu/ContextMenu.jsx | 109 ++++++++++----- .../ContextMenu/examples/4.directions.jsx | 51 ++++--- src/components/ToolTip/ToolTip.jsx | 124 ++++++++++++++++++ src/components/ToolTip/ToolTip.less | 15 +++ src/components/ToolTip/examples/example.jsx | 49 +++++++ src/index.js | 2 + src/styles/components.less | 1 + 8 files changed, 304 insertions(+), 49 deletions(-) create mode 100644 src/components/ToolTip/ToolTip.jsx create mode 100644 src/components/ToolTip/ToolTip.less create mode 100644 src/components/ToolTip/examples/example.jsx diff --git a/src/components/Banner/Banner.jsx b/src/components/Banner/Banner.jsx index 1756c6b6e..ff47c9dd8 100644 --- a/src/components/Banner/Banner.jsx +++ b/src/components/Banner/Banner.jsx @@ -30,7 +30,7 @@ const defaultIcons = { /** * - * {"categories": ["controls", "banners"], "madeFrom": ["DangerIcon", "InfoIcon", "SuccessIcon", "WarningIcon"]} + * {"categories": ["controls", "communication"], "madeFrom": ["DangerIcon", "InfoIcon", "SuccessIcon", "WarningIcon"]} * * A basic Banner. Any props that are not explicitly called out below will be * passed through to the native `Banner` component. diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index 3c69fc9d3..3bd0f8d4c 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -12,22 +12,13 @@ const { bool, node, func, + number, object, oneOf, string } } = React; -const CONSTANTS = { - CENTER: 'center', - DOWN: 'down', - END: 'end', - LEFT: 'left', - RIGHT: 'right', - START: 'start', - UP: 'up', -}; - /** * * {"categories": ["utility"], "madeFrom": ["Portal"]} @@ -54,9 +45,17 @@ const ContextMenu = createClass({ */ direction: oneOf(['down', 'up', 'right', 'left']), /** - * alignment of the Flyout relative to Target. Defaults to `'start'` + * the px offset along the axis of the direction + */ + directonOffset: number, + /** + * alignment of the Flyout relative to Target in the cross axis from `direction` Defaults to `'start'` */ alignment: oneOf(['start', 'center', 'end']), + /** + * the px offset along the axis of the alignment + */ + alignmentOffset: number, /** * Indicates whether the FlyOut will render or not. Defaults to `true`. */ @@ -87,8 +86,10 @@ const ContextMenu = createClass({ getDefaultProps() { return { - direction: CONSTANTS.DOWN, - alignment: CONSTANTS.START, + direction: 'down', + directonOffset: 0, + alignment: 'start', + alignmentOffset: 0, isExpanded: true, onClickOut: null, portalId: null @@ -140,7 +141,15 @@ const ContextMenu = createClass({ this.alignFlyOut(); }, - statics: CONSTANTS, + statics: { + CENTER: 'center', + DOWN: 'down', + END: 'end', + LEFT: 'left', + RIGHT: 'right', + START: 'start', + UP: 'up', + }, alignFlyOut() { @@ -175,7 +184,7 @@ const ContextMenu = createClass({ }); }, - getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth) { + getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth, directonOffset, alignmentOffset) { const { CENTER, @@ -203,40 +212,76 @@ const ContextMenu = createClass({ const matcher = _.matches({ direction, alignment }); if (matcher({ direction: UP, alignment: START })) { - return { top: top - flyOutHeight, left }; + return { + top: top - flyOutHeight - directonOffset, + left: left - alignmentOffset + }; } if (matcher({ direction: UP, alignment: END })) { - return { top: top - flyOutHeight, right: clientWidth - right }; + return { + top: top - flyOutHeight - directonOffset, + right: clientWidth - right - alignmentOffset + }; } if (matcher({ direction: UP, alignment: CENTER })) { - return { top: top - flyOutHeight, left: left + (width / 2) - (flyOutWidth / 2) }; + return { + top: top - flyOutHeight - directonOffset, + left: left + (width / 2) - (flyOutWidth / 2) + }; } if (matcher({ direction: DOWN, alignment: START })) { - return { top: bottom, left }; + return { + top: bottom - directonOffset, + left: left - alignmentOffset + }; } if (matcher({ direction: DOWN, alignment: END })) { - return { top: bottom, right: clientWidth - right }; + return { + top: bottom - directonOffset, + right: clientWidth - right - alignmentOffset + }; } if (matcher({ direction: DOWN, alignment: CENTER })) { - return { top: bottom, left: left + (width / 2) - (flyOutWidth / 2) }; + return { + top: bottom - directonOffset, + left: left + (width / 2) - (flyOutWidth / 2) + }; } if (matcher({ direction: LEFT, alignment: START })) { - return { top, right: clientWidth - left }; + return { + top: top - alignmentOffset, + right: clientWidth - left + directonOffset + }; } if (matcher({ direction: LEFT, alignment: END })) { - return { top: top - flyOutHeight + height, right: clientWidth - left }; + return { + top: top - flyOutHeight + height + alignmentOffset, + right: clientWidth - left + directonOffset + }; } if (matcher({ direction: LEFT, alignment: CENTER })) { - return { top: top - (flyOutHeight / 2) + (height / 2), right: clientWidth - left }; + return { + top: top - (flyOutHeight / 2) + (height / 2), + right: clientWidth - left + directonOffset + }; } if (matcher({ direction: RIGHT, alignment: START })) { - return { top, left: left + width }; + return { + top: top - alignmentOffset, + left: left + width + directonOffset + }; } if (matcher({ direction: RIGHT, alignment: END })) { - return { top: top - flyOutHeight + height, left: left + width }; + return { + top: top - flyOutHeight + height + alignmentOffset, + left: left + width + directonOffset + }; } if (matcher({ direction: RIGHT, alignment: CENTER })) { - return { top: top - (flyOutHeight / 2) + (height / 2), left: left + width }; + return { + top: top - (flyOutHeight / 2) + (height / 2), + left: left + width + directonOffset + }; } }, @@ -244,12 +289,14 @@ const ContextMenu = createClass({ render() { const { props: { + alignment, + alignmentOffset, className, - style, - isExpanded, direction, - alignment, + directonOffset, + isExpanded, position, + style, ...passThroughs }, state: { @@ -281,7 +328,7 @@ const ContextMenu = createClass({ style={_.assign({}, flyProps.style, { position: 'absolute', minWidth: targetRect.width, - }, this.getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth))} + }, this.getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth, directonOffset, alignmentOffset))} > {flyProps.children} diff --git a/src/components/ContextMenu/examples/4.directions.jsx b/src/components/ContextMenu/examples/4.directions.jsx index 8dcf0de94..744bc461d 100644 --- a/src/components/ContextMenu/examples/4.directions.jsx +++ b/src/components/ContextMenu/examples/4.directions.jsx @@ -12,26 +12,43 @@ export default React.createClass({ }; return ( -
+
- {_.map(['up', 'left', 'right', 'down'], direction => _.map(['start', 'center', 'end'], alignment => { + {_.map(['left', 'up', 'down', 'right'], direction => { return ( -
- - - Target - +
+ {_.map(['start', 'center', 'end'], alignment => _.map([0, 15, -15], directonOffset => _.map([0, 15, -15], alignmentOffset => ( +
+ + + Target + - -
{`direction: ${direction}`}
-
{`alignment: ${alignment}`}
-
-
-
- ); - }))} + +
{`direction: ${direction}`}
+
{`directonOffset: ${directonOffset}`}
+
{`alignment: ${alignment}`}
+
{`alignmentOffset: ${alignmentOffset}`}
+
+ +
+ ))))} +
+ ); + })}
); diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx new file mode 100644 index 000000000..fa59f7304 --- /dev/null +++ b/src/components/ToolTip/ToolTip.jsx @@ -0,0 +1,124 @@ +import _ from 'lodash'; +import React from 'react'; +import ContextMenu from '../ContextMenu/ContextMenu'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass, findTypes } from '../../util/component-types'; + +const cx = lucidClassNames.bind('&-ToolTip'); + +const { + bool, + func, + node, + object, + oneOf, + string +} = React.PropTypes; + +const { + Target, + FlyOut +} = ContextMenu; + + +/** + * {"categories": ["communication"], "madeFrom": ["ContextMenu"]} + * + * `ToolTip` is a utility component to create a transient message anchored + * to another component + */ + +const ToolTip = createClass({ + displayName: 'ToolTip', + propTypes: { + /** + * `children` should include exactly one ToolTip.Target and one ToolTip.FlyOut. + */ + children: node, + /** + * Appended to the component-specific class names set on the root element. + */ + className: string, + /** + * Passed through to the root element. + */ + style: object, + /** + * direction of the FlyOut relative to Target. Defaults to `'down'`. + */ + direction: oneOf(['down', 'up', 'right', 'left']), + /** + * alignment of the Flyout relative to Target in the cross axis from `direction` Defaults to `'start'` + */ + alignment: oneOf(['start', 'center', 'end']), + /** + * Indicates whether the ToolTip will render or not. Defaults to `true`. + */ + isExpanded: bool, + /** + * The `id` of the FlyOut portal element that is appended to `document.body`. Defaults to a generated `id`. + */ + portalId: string + }, + + components: { + Target: createClass({ + displayName: 'ToolTip.Target', + propName: 'Target', + }), + Title: createClass({ + displayName: 'ToolTip.Title', + propName: 'Title', + }), + Body: createClass({ + displayName: 'ToolTip.Body', + propName: 'Body', + }), + }, + + getDefaultProps() { + return { + direction: ContextMenu.UP, + alignment: ContextMenu.CENTER, + portalId: null + }; + }, + + render() { + const { + className, + children, + style, + ...passThroughs + } = this.props; + + const target = _.chain(findTypes(this.props, ToolTip.Target)).map('props').first().get('children').value(); + const title = _.chain(findTypes(this.props, ToolTip.Title)).map('props').first().get('children').value(); + const body = _.chain(findTypes(this.props, ToolTip.Body)).map('props').first().get('children').value(); + + return ( +
+ + + {target} + + +

{title}

+ {body} +
+
+
+ ); + } +}); + +export default ToolTip; diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less new file mode 100644 index 000000000..91b9886a2 --- /dev/null +++ b/src/components/ToolTip/ToolTip.less @@ -0,0 +1,15 @@ +.lucid-ToolTip-flyout { + background-color: @color-white; + border: @border-standardBorder; + border-radius: @size-borderRadius; + padding: @size-standard; + font-size: @fontSize; + color: @color-darkGray; + + .lucid-ToolTip-title { + font-size: @size-L; + font-weight: bold; + margin: 0 0 @size-XXS; + } + +} diff --git a/src/components/ToolTip/examples/example.jsx b/src/components/ToolTip/examples/example.jsx new file mode 100644 index 000000000..e82d9fe41 --- /dev/null +++ b/src/components/ToolTip/examples/example.jsx @@ -0,0 +1,49 @@ +import _ from 'lodash'; +import React from 'react'; +import ToolTip from '../ToolTip'; + +const { + Target, + Title, + Body +} = ToolTip; + +export default React.createClass({ + getInitialState: () => ({ isExpanded: false }), + render() { + return ( +
+ {_.map(['right', 'up', 'down', 'left'], direction => +
+ {_.map(['start', 'center', 'end'], alignment => +
+ + + Title: {direction} {alignment} + + + ToolTip is a utility component to create a transient message anchored to another component. + My direction is "{direction}". + My alignment is "{alignment}". + + +
this.setState({ [direction]: { [alignment]: { isExpanded: true }}})} + onMouseOut={() => this.setState({ [direction]: { [alignment]: { isExpanded: false }}})} + > + Target {direction} {alignment} +
+
+
+
)} +
+ )} +
+ ); + } +}); diff --git a/src/index.js b/src/index.js index 7b7b879fa..3ea237373 100644 --- a/src/index.js +++ b/src/index.js @@ -43,6 +43,7 @@ import CheckboxLabeled from './components/CheckboxLabeled/CheckboxLabeled'; import RadioButtonLabeled from './components/RadioButtonLabeled/RadioButtonLabeled'; import SwitchLabeled from './components/SwitchLabeled/SwitchLabeled'; import LineChart from './components/LineChart/LineChart'; +import ToolTip from './components/ToolTip/ToolTip'; import MinusIcon from './components/Icon/MinusIcon/MinusIcon'; import Overlay from './components/Overlay/Overlay'; import Panel from './components/Panel/Panel'; @@ -109,6 +110,7 @@ export { RadioButtonLabeled, SwitchLabeled, LineChart, + ToolTip, MinusIcon, Overlay, PlusIcon, diff --git a/src/styles/components.less b/src/styles/components.less index 7da393144..dd40fe96a 100644 --- a/src/styles/components.less +++ b/src/styles/components.less @@ -37,6 +37,7 @@ @import '../components/Table/Table'; @import '../components/TextField/TextField'; @import '../components/TextFieldValidated/TextFieldValidated'; +@import '../components/ToolTip/ToolTip'; @import '../components/Validation/Validation'; @import '../components/Tabs/Tabs'; @import '../components/Overlay/Overlay'; From cae5f27084b369f79e0b7ddf1b74d321c824ac2a Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Thu, 12 May 2016 16:52:02 -0700 Subject: [PATCH 05/35] fix directionOffset direction for direcion: down --- src/components/ContextMenu/ContextMenu.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index 3bd0f8d4c..cf6f7abfc 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -231,19 +231,19 @@ const ContextMenu = createClass({ } if (matcher({ direction: DOWN, alignment: START })) { return { - top: bottom - directonOffset, + top: bottom + directonOffset, left: left - alignmentOffset }; } if (matcher({ direction: DOWN, alignment: END })) { return { - top: bottom - directonOffset, + top: bottom + directonOffset, right: clientWidth - right - alignmentOffset }; } if (matcher({ direction: DOWN, alignment: CENTER })) { return { - top: bottom - directonOffset, + top: bottom + directonOffset, left: left + (width / 2) - (flyOutWidth / 2) }; } From 3cc04b17df17f3aa7f2162d4152f99eb4097adcc Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Thu, 12 May 2016 17:13:32 -0700 Subject: [PATCH 06/35] improve ToolTip examples --- .../{example.jsx => 1.interactive.jsx} | 0 src/components/ToolTip/examples/2.static.jsx | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+) rename src/components/ToolTip/examples/{example.jsx => 1.interactive.jsx} (100%) create mode 100644 src/components/ToolTip/examples/2.static.jsx diff --git a/src/components/ToolTip/examples/example.jsx b/src/components/ToolTip/examples/1.interactive.jsx similarity index 100% rename from src/components/ToolTip/examples/example.jsx rename to src/components/ToolTip/examples/1.interactive.jsx diff --git a/src/components/ToolTip/examples/2.static.jsx b/src/components/ToolTip/examples/2.static.jsx new file mode 100644 index 000000000..f5511a4cb --- /dev/null +++ b/src/components/ToolTip/examples/2.static.jsx @@ -0,0 +1,42 @@ +import _ from 'lodash'; +import React from 'react'; +import ToolTip from '../ToolTip'; + +const { + Target, + Title, + Body +} = ToolTip; + +export default React.createClass({ + render() { + return ( +
+ {_.map(['right', 'up', 'down', 'left'], direction => _.map(['start', 'center', 'end'], alignment => +
+ + + Title: {direction} {alignment} + + + ToolTip is a utility component to create a transient message anchored to another component. + My direction is "{direction}". + My alignment is "{alignment}". + + +
+ Target {direction} {alignment} +
+
+
+
) + )} +
+ ); + } +}); From bc1864bee69754ba21d9009097cee550a27c3758 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Fri, 13 May 2016 15:30:53 -0700 Subject: [PATCH 07/35] add getAlignmentOffset prop --- src/components/ContextMenu/ContextMenu.jsx | 44 +++++-- .../examples/4.directions-interactive.jsx | 118 ++++++++++++++++++ .../ContextMenu/examples/4.directions.jsx | 56 --------- .../examples/5.directions-static.jsx | 58 +++++++++ 4 files changed, 212 insertions(+), 64 deletions(-) create mode 100644 src/components/ContextMenu/examples/4.directions-interactive.jsx delete mode 100644 src/components/ContextMenu/examples/4.directions.jsx create mode 100644 src/components/ContextMenu/examples/5.directions-static.jsx diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index cf6f7abfc..c0d9f503f 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -56,6 +56,10 @@ const ContextMenu = createClass({ * the px offset along the axis of the alignment */ alignmentOffset: number, + /** + * an alternative to `alignmentOffset`, a function that is applied with the width/height of the flyout. the result is used as the `alignmentOffset` + */ + getAlignmentOffset: func, /** * Indicates whether the FlyOut will render or not. Defaults to `true`. */ @@ -89,7 +93,8 @@ const ContextMenu = createClass({ direction: 'down', directonOffset: 0, alignment: 'start', - alignmentOffset: 0, + // no default alignmentOffset so it can default to result of `getAlignmentOffset` + getAlignmentOffset: _.constant(0), isExpanded: true, onClickOut: null, portalId: null @@ -134,7 +139,7 @@ const ContextMenu = createClass({ componentWillUnmount() { clearInterval(this.updateTargetRectangleIntervalId); - window.document.body.removeEventListener('click', this.onClickBodyEventListener); + document.body.removeEventListener('click', this.onClickBodyEventListener); }, componentWillReceiveProps() { @@ -184,7 +189,19 @@ const ContextMenu = createClass({ }); }, - getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth, directonOffset, alignmentOffset) { + getFlyoutPosition({ + direction, + alignment, + position, + targetRect, + flyOutHeight, + flyOutWidth, + directonOffset, + getAlignmentOffset, + alignmentOffset = alignment === ContextMenu.CENTER + ? getAlignmentOffset(_.includes([ContextMenu.UP, ContextMenu.DOWN], direction) ? flyOutWidth : flyOutHeight) + : 0, + }) { const { CENTER, @@ -226,7 +243,7 @@ const ContextMenu = createClass({ if (matcher({ direction: UP, alignment: CENTER })) { return { top: top - flyOutHeight - directonOffset, - left: left + (width / 2) - (flyOutWidth / 2) + left: left + (width / 2) - (flyOutWidth / 2) + alignmentOffset }; } if (matcher({ direction: DOWN, alignment: START })) { @@ -244,7 +261,7 @@ const ContextMenu = createClass({ if (matcher({ direction: DOWN, alignment: CENTER })) { return { top: bottom + directonOffset, - left: left + (width / 2) - (flyOutWidth / 2) + left: left + (width / 2) - (flyOutWidth / 2) + alignmentOffset }; } if (matcher({ direction: LEFT, alignment: START })) { @@ -261,7 +278,7 @@ const ContextMenu = createClass({ } if (matcher({ direction: LEFT, alignment: CENTER })) { return { - top: top - (flyOutHeight / 2) + (height / 2), + top: top - (flyOutHeight / 2) + (height / 2) + alignmentOffset, right: clientWidth - left + directonOffset }; } @@ -279,7 +296,7 @@ const ContextMenu = createClass({ } if (matcher({ direction: RIGHT, alignment: CENTER })) { return { - top: top - (flyOutHeight / 2) + (height / 2), + top: top - (flyOutHeight / 2) + (height / 2) + alignmentOffset, left: left + width + directonOffset }; } @@ -291,6 +308,7 @@ const ContextMenu = createClass({ props: { alignment, alignmentOffset, + getAlignmentOffset, className, direction, directonOffset, @@ -328,7 +346,17 @@ const ContextMenu = createClass({ style={_.assign({}, flyProps.style, { position: 'absolute', minWidth: targetRect.width, - }, this.getFlyoutPosition(direction, alignment, position, targetRect, flyOutHeight, flyOutWidth, directonOffset, alignmentOffset))} + }, this.getFlyoutPosition({ + direction, + alignment, + position, + targetRect, + flyOutHeight, + flyOutWidth, + directonOffset, + getAlignmentOffset, + alignmentOffset, + }))} > {flyProps.children} diff --git a/src/components/ContextMenu/examples/4.directions-interactive.jsx b/src/components/ContextMenu/examples/4.directions-interactive.jsx new file mode 100644 index 000000000..e19a230af --- /dev/null +++ b/src/components/ContextMenu/examples/4.directions-interactive.jsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { ContextMenu, Button, SingleSelect, TextField } from '../../../index'; + +const { + CENTER, + DOWN, + END, + LEFT, + RIGHT, + START, + UP, +} = ContextMenu; + +export default React.createClass({ + + render() { + + const style = { + background: 'white', + boxShadow: '1px 1px 4px black', + padding: 4 + }; + + const { + direction, + directonOffset, + alignment, + alignmentOffset, + getAlignmentOffset + } = this.state; + + const directions = [UP, DOWN, LEFT, RIGHT]; + const alignments = [START, CENTER, END]; + const alignmentOffsetArg = _.includes([UP, DOWN], direction) ? "flyOutHeight" : "flyOutWidth"; + + return ( +
+ +
+ this.setState({ direction: directions[i] })} > + Select a direction + {_.map(directions, direction => {direction})} + + + directonOffset: + this.setState({ directonOffset })} + /> + + this.setState({ alignment: alignments[i] })} > + Select an alignment + {_.map(alignments, alignment => {alignment})} + + + alignmentOffset: + this.setState({ alignmentOffset })} + /> + + getAlignmentOffset: + this.setState({ getAlignmentOffset })} + /> + {getAlignmentOffset || null} + +
+ +
+ + + + Target + + + +
{`direction: ${direction || 'default'}`}
+
{`directonOffset: ${directonOffset || 'default'}`}
+
{`alignment: ${alignment || 'default'}`}
+
{`alignmentOffset: ${alignmentOffset || 'default'}`}
+
{`getAlignmentOffset: ${getAlignmentOffset || 'default'}`}
+
+ +
+
+
+ ); + }, + + getInitialState: () => ({}), + +}); diff --git a/src/components/ContextMenu/examples/4.directions.jsx b/src/components/ContextMenu/examples/4.directions.jsx deleted file mode 100644 index 744bc461d..000000000 --- a/src/components/ContextMenu/examples/4.directions.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { ContextMenu, Button } from '../../../index'; - -export default React.createClass({ - - render() { - - const style = { - background: 'white', - boxShadow: '1px 1px 4px black', - padding: 4 - }; - - return ( -
- - {_.map(['left', 'up', 'down', 'right'], direction => { - return ( -
- {_.map(['start', 'center', 'end'], alignment => _.map([0, 15, -15], directonOffset => _.map([0, 15, -15], alignmentOffset => ( -
- - - Target - - - -
{`direction: ${direction}`}
-
{`directonOffset: ${directonOffset}`}
-
{`alignment: ${alignment}`}
-
{`alignmentOffset: ${alignmentOffset}`}
-
-
-
- ))))} -
- ); - })} - -
- ); - } -}); diff --git a/src/components/ContextMenu/examples/5.directions-static.jsx b/src/components/ContextMenu/examples/5.directions-static.jsx new file mode 100644 index 000000000..ae1243708 --- /dev/null +++ b/src/components/ContextMenu/examples/5.directions-static.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { ContextMenu, Button } from '../../../index'; + +export default React.createClass({ + + render() { + + return null; + + // const style = { + // background: 'white', + // boxShadow: '1px 1px 4px black', + // padding: 4 + // }; + + // return ( + //
+ + // {_.map(['left', 'up', 'down', 'right'], direction => { + // return ( + //
+ // {_.map(['start', 'center', 'end'], alignment => _.map([0, 15, -15], directonOffset => _.map([0, 15, -15], alignmentOffset => ( + //
+ // + // + // Target + // + + // + //
{`direction: ${direction}`}
+ //
{`directonOffset: ${directonOffset}`}
+ //
{`alignment: ${alignment}`}
+ //
{`alignmentOffset: ${alignmentOffset}`}
+ //
+ //
+ //
+ // ))))} + //
+ // ); + // })} + + //
+ // ); + } +}); From 9ec16e504621d8ac39831019353628911e2ee0a0 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Fri, 13 May 2016 15:31:09 -0700 Subject: [PATCH 08/35] position and style ToolTip --- src/components/ToolTip/ToolTip.jsx | 13 ++- src/components/ToolTip/ToolTip.less | 157 ++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 2 deletions(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index fa59f7304..9c774298b 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -89,28 +89,37 @@ const ToolTip = createClass({ className, children, style, + alignment, + direction, ...passThroughs } = this.props; const target = _.chain(findTypes(this.props, ToolTip.Target)).map('props').first().get('children').value(); const title = _.chain(findTypes(this.props, ToolTip.Title)).map('props').first().get('children').value(); const body = _.chain(findTypes(this.props, ToolTip.Body)).map('props').first().get('children').value(); + const getAlignmentOffset = n => alignment === ContextMenu.CENTER + ? 0 + : alignment === ContextMenu.START + ? n / 2 - 22.5 + : -(n / 2 - 22.5); return (
{target}

{title}

{body} diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index 91b9886a2..05f8bdd7e 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -1,3 +1,7 @@ +@tooltip-pointer-inner-width: 8px; +@tooltip-pointer-outer-width: 9px; +@tooltip-pointer-offset: 13px; + .lucid-ToolTip-flyout { background-color: @color-white; border: @border-standardBorder; @@ -12,4 +16,157 @@ margin: 0 0 @size-XXS; } + &:after, &:before { + content: ' '; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border: 7px solid transparent; + } + + &:after { + border-width: @tooltip-pointer-inner-width; + } + + &:before { + border-width: @tooltip-pointer-outer-width; + } + + &.up:after, + &.up:before { + top: 100%; + } + + &.up:after { + border-top-color: @color-white; + margin-left: -@tooltip-pointer-inner-width; + } + + &.up:before { + border-top-color: @color-borderColor; + } + + &.down:after, + &.down:before { + bottom: 100%; + } + + &.down:after { + border-bottom-color: @color-white; + } + + &.down:before { + border-bottom-color: @color-borderColor; + } + + &.up.center:after, + &.up.center:before, + &.down.center:after, + &.down.center:before { + left: 50%; + } + + &.up.center:before, + &.down.center:before { + margin-left: -@tooltip-pointer-outer-width; + } + + &.up.center:after, + &.down.center:after { + margin-left: -@tooltip-pointer-inner-width; + } + + &.up.start:after, + &.down.start:after { + margin-left: 1px; + } + + &.up.end:after, + &.down.end:after { + margin-right: 1px; + } + + &.down.start:after, + &.down.start:before, + &.up.start:after, + &.up.start:before { + left: @tooltip-pointer-offset; + } + + &.down.end:after, + &.down.end:before, + &.up.end:after, + &.up.end:before { + right: @tooltip-pointer-offset; + } + + &.left:after, + &.left:before { + left: 100%; + } + + &.left:after { + border-left-color: @color-white; + } + + &.left:before { + border-left-color: @color-borderColor; + } + + &.right:after, + &.right:before { + right: 100%; + } + + &.right:after { + border-right-color: @color-white; + } + + &.right:before { + border-right-color: @color-borderColor; + } + + &.left.center:after, + &.left.center:before, + &.right.center:after, + &.right.center:before { + top: 50%; + } + + &.left.center:before, + &.right.center:before { + margin-top: -@tooltip-pointer-outer-width; + } + + &.left.center:after, + &.right.center:after { + margin-top: -@tooltip-pointer-inner-width; + } + + &.left.start:after, + &.left.start:before, + &.right.start:after, + &.right.start:before { + top: @tooltip-pointer-offset; + } + + &.left.end:after, + &.left.end:before, + &.right.end:after, + &.right.end:before { + bottom: @tooltip-pointer-offset; + } + + &.left.start:after, + &.right.start:after { + margin-top: 1px; + } + + &.left.end:after, + &.right.end:after { + margin-bottom: 1px; + } + + } From a601e23711dd129744d97ba956d90ebb2a56e83e Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Fri, 13 May 2016 17:33:50 -0700 Subject: [PATCH 09/35] move getPosition out of component def --- src/components/ContextMenu/ContextMenu.jsx | 230 ++++++++++----------- 1 file changed, 114 insertions(+), 116 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index c0d9f503f..cb42a0932 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -189,120 +189,6 @@ const ContextMenu = createClass({ }); }, - getFlyoutPosition({ - direction, - alignment, - position, - targetRect, - flyOutHeight, - flyOutWidth, - directonOffset, - getAlignmentOffset, - alignmentOffset = alignment === ContextMenu.CENTER - ? getAlignmentOffset(_.includes([ContextMenu.UP, ContextMenu.DOWN], direction) ? flyOutWidth : flyOutHeight) - : 0, - }) { - - const { - CENTER, - DOWN, - END, - LEFT, - RIGHT, - START, - UP, - } = ContextMenu; - - const { - bottom, - left, - right, - top, - width, - height, - } = targetRect; - - const { - clientWidth, - } = document.body; - - const matcher = _.matches({ direction, alignment }); - - if (matcher({ direction: UP, alignment: START })) { - return { - top: top - flyOutHeight - directonOffset, - left: left - alignmentOffset - }; - } - if (matcher({ direction: UP, alignment: END })) { - return { - top: top - flyOutHeight - directonOffset, - right: clientWidth - right - alignmentOffset - }; - } - if (matcher({ direction: UP, alignment: CENTER })) { - return { - top: top - flyOutHeight - directonOffset, - left: left + (width / 2) - (flyOutWidth / 2) + alignmentOffset - }; - } - if (matcher({ direction: DOWN, alignment: START })) { - return { - top: bottom + directonOffset, - left: left - alignmentOffset - }; - } - if (matcher({ direction: DOWN, alignment: END })) { - return { - top: bottom + directonOffset, - right: clientWidth - right - alignmentOffset - }; - } - if (matcher({ direction: DOWN, alignment: CENTER })) { - return { - top: bottom + directonOffset, - left: left + (width / 2) - (flyOutWidth / 2) + alignmentOffset - }; - } - if (matcher({ direction: LEFT, alignment: START })) { - return { - top: top - alignmentOffset, - right: clientWidth - left + directonOffset - }; - } - if (matcher({ direction: LEFT, alignment: END })) { - return { - top: top - flyOutHeight + height + alignmentOffset, - right: clientWidth - left + directonOffset - }; - } - if (matcher({ direction: LEFT, alignment: CENTER })) { - return { - top: top - (flyOutHeight / 2) + (height / 2) + alignmentOffset, - right: clientWidth - left + directonOffset - }; - } - if (matcher({ direction: RIGHT, alignment: START })) { - return { - top: top - alignmentOffset, - left: left + width + directonOffset - }; - } - if (matcher({ direction: RIGHT, alignment: END })) { - return { - top: top - flyOutHeight + height + alignmentOffset, - left: left + width + directonOffset - }; - } - if (matcher({ direction: RIGHT, alignment: CENTER })) { - return { - top: top - (flyOutHeight / 2) + (height / 2) + alignmentOffset, - left: left + width + directonOffset - }; - } - - }, - render() { const { props: { @@ -346,10 +232,9 @@ const ContextMenu = createClass({ style={_.assign({}, flyProps.style, { position: 'absolute', minWidth: targetRect.width, - }, this.getFlyoutPosition({ + }, getFlyoutPosition({ direction, alignment, - position, targetRect, flyOutHeight, flyOutWidth, @@ -367,3 +252,116 @@ const ContextMenu = createClass({ }); export default ContextMenu; + +function getFlyoutPosition({ + direction, + alignment, + targetRect, + flyOutHeight, + flyOutWidth, + directonOffset, + getAlignmentOffset, + alignmentOffset = alignment === ContextMenu.CENTER + ? getAlignmentOffset(_.includes([ContextMenu.UP, ContextMenu.DOWN], direction) ? flyOutWidth : flyOutHeight) + : 0, +}) { + + const { + CENTER, + DOWN, + END, + LEFT, + RIGHT, + START, + UP, + } = ContextMenu; + + const { + bottom, + left, + right, + top, + width, + height, + } = targetRect; + + const { + clientWidth, + } = document.body; + + const matcher = _.matches({ direction, alignment }); + + if (matcher({ direction: UP, alignment: START })) { + return { + top: top - flyOutHeight - directonOffset, + left: left - alignmentOffset + }; + } + if (matcher({ direction: UP, alignment: END })) { + return { + top: top - flyOutHeight - directonOffset, + right: clientWidth - right - alignmentOffset + }; + } + if (matcher({ direction: UP, alignment: CENTER })) { + return { + top: top - flyOutHeight - directonOffset, + left: left + (width / 2) - (flyOutWidth / 2) + alignmentOffset + }; + } + if (matcher({ direction: DOWN, alignment: START })) { + return { + top: bottom + directonOffset, + left: left - alignmentOffset + }; + } + if (matcher({ direction: DOWN, alignment: END })) { + return { + top: bottom + directonOffset, + right: clientWidth - right - alignmentOffset + }; + } + if (matcher({ direction: DOWN, alignment: CENTER })) { + return { + top: bottom + directonOffset, + left: left + (width / 2) - (flyOutWidth / 2) + alignmentOffset + }; + } + if (matcher({ direction: LEFT, alignment: START })) { + return { + top: top - alignmentOffset, + right: clientWidth - left + directonOffset + }; + } + if (matcher({ direction: LEFT, alignment: END })) { + return { + top: top - flyOutHeight + height + alignmentOffset, + right: clientWidth - left + directonOffset + }; + } + if (matcher({ direction: LEFT, alignment: CENTER })) { + return { + top: top - (flyOutHeight / 2) + (height / 2) + alignmentOffset, + right: clientWidth - left + directonOffset + }; + } + if (matcher({ direction: RIGHT, alignment: START })) { + return { + top: top - alignmentOffset, + left: left + width + directonOffset + }; + } + if (matcher({ direction: RIGHT, alignment: END })) { + return { + top: top - flyOutHeight + height + alignmentOffset, + left: left + width + directonOffset + }; + } + if (matcher({ direction: RIGHT, alignment: CENTER })) { + return { + top: top - (flyOutHeight / 2) + (height / 2) + alignmentOffset, + left: left + width + directonOffset + }; + } + +} From 867eaf5d4c98548f1c85ec00ba5529c8fd632bb2 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Fri, 13 May 2016 17:35:01 -0700 Subject: [PATCH 10/35] add state wrapper to ToolTip --- src/components/ToolTip/ToolTip.jsx | 72 +++++++++++++++++-- src/components/ToolTip/ToolTip.reducers.js | 13 ++++ .../ToolTip/examples/1.interactive.jsx | 9 +-- src/components/ToolTip/examples/2.static.jsx | 8 +-- src/index.js | 27 +++---- 5 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 src/components/ToolTip/ToolTip.reducers.js diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 9c774298b..5ca555bd5 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -1,6 +1,7 @@ import _ from 'lodash'; import React from 'react'; import ContextMenu from '../ContextMenu/ContextMenu'; +import * as reducers from './ToolTip.reducers'; import { lucidClassNames } from '../../util/style-helpers'; import { createClass, findTypes } from '../../util/component-types'; @@ -30,6 +31,9 @@ const { const ToolTip = createClass({ displayName: 'ToolTip', + + reducers, + propTypes: { /** * `children` should include exactly one ToolTip.Target and one ToolTip.FlyOut. @@ -55,6 +59,14 @@ const ToolTip = createClass({ * Indicates whether the ToolTip will render or not. Defaults to `true`. */ isExpanded: bool, + /** + * Called when cursor moves over the target + */ + onMouseOver: func, + /** + * Called with cursor leaves the target and the tooltip + */ + onMouseOut: func, /** * The `id` of the FlyOut portal element that is appended to `document.body`. Defaults to a generated `id`. */ @@ -78,12 +90,58 @@ const ToolTip = createClass({ getDefaultProps() { return { - direction: ContextMenu.UP, alignment: ContextMenu.CENTER, - portalId: null + direction: ContextMenu.UP, + isExpanded: false, + onMouseOut: _.noop, + onMouseOver: _.noop, + portalId: null, + }; + }, + + getInitialState() { + return { + isMouseOverFlyout: false, + isMouseOverTarget: false, }; }, + handleMouseOut() { + setTimeout(() => { + const { + state: { + isMouseOverFlyout, + isMouseOverTarget + }, + props: { + onMouseOut + } + } = this; + if (!isMouseOverFlyout && !isMouseOverTarget) { + onMouseOut(); + } + }, 100); + }, + + handleMouseOverFlyout() { + this.setState({ isMouseOverFlyout: true }); + }, + + handleMouseOutFlyout() { + this.setState({ isMouseOverFlyout: false }); + this.handleMouseOut(); + }, + + handleMouseOverTarget() { + this.setState({ isMouseOverTarget: true }); + this.props.onMouseOver(); + }, + + handleMouseOutTarget() { + this.setState({ isMouseOverTarget: false }); + this.handleMouseOut(); + }, + render() { const { className, @@ -109,17 +167,23 @@ const ToolTip = createClass({ > - + {target}

{title}

{body} diff --git a/src/components/ToolTip/ToolTip.reducers.js b/src/components/ToolTip/ToolTip.reducers.js new file mode 100644 index 000000000..708805e5c --- /dev/null +++ b/src/components/ToolTip/ToolTip.reducers.js @@ -0,0 +1,13 @@ +export function onMouseOver(state) { + return { + ...state, + isExpanded: true + }; +} + +export function onMouseOut(state) { + return { + ...state, + isExpanded: false + } +} diff --git a/src/components/ToolTip/examples/1.interactive.jsx b/src/components/ToolTip/examples/1.interactive.jsx index e82d9fe41..66895a2c0 100644 --- a/src/components/ToolTip/examples/1.interactive.jsx +++ b/src/components/ToolTip/examples/1.interactive.jsx @@ -1,6 +1,6 @@ import _ from 'lodash'; import React from 'react'; -import ToolTip from '../ToolTip'; +import { ToolTip } from '../../../index'; const { Target, @@ -9,7 +9,6 @@ const { } = ToolTip; export default React.createClass({ - getInitialState: () => ({ isExpanded: false }), render() { return (
@@ -20,7 +19,6 @@ export default React.createClass({ @@ -32,10 +30,7 @@ export default React.createClass({ My alignment is "{alignment}". </Body> <Target> - <div - onMouseOver={() => this.setState({ [direction]: { [alignment]: { isExpanded: true }}})} - onMouseOut={() => this.setState({ [direction]: { [alignment]: { isExpanded: false }}})} - > + <div> Target {direction} {alignment} </div> </Target> diff --git a/src/components/ToolTip/examples/2.static.jsx b/src/components/ToolTip/examples/2.static.jsx index f5511a4cb..8f48df734 100644 --- a/src/components/ToolTip/examples/2.static.jsx +++ b/src/components/ToolTip/examples/2.static.jsx @@ -1,12 +1,12 @@ import _ from 'lodash'; import React from 'react'; -import ToolTip from '../ToolTip'; +import { ToolTipDumb } from '../../../index'; const { Target, Title, Body -} = ToolTip; +} = ToolTipDumb; export default React.createClass({ render() { @@ -14,7 +14,7 @@ export default React.createClass({ <section style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> {_.map(['right', 'up', 'down', 'left'], direction => _.map(['start', 'center', 'end'], alignment => <section key={`${direction}${alignment}`} style={{ margin: '90px' }}> - <ToolTip + <ToolTipDumb direction={direction} alignment={alignment} isExpanded={true} @@ -33,7 +33,7 @@ export default React.createClass({ Target {direction} {alignment} </div> </Target> - </ToolTip> + </ToolTipDumb> </section>) )} </section> diff --git a/src/index.js b/src/index.js index 3ea237373..9192aa096 100644 --- a/src/index.js +++ b/src/index.js @@ -5,10 +5,11 @@ import { default as AutocompleteDumb } from './components/Autocomplete/Autocompl import { default as ButtonGroupDumb } from './components/ButtonGroup/ButtonGroup'; import { default as DropMenuDumb } from './components/DropMenu/DropMenu'; import { default as ExpanderDumb } from './components/Expander/Expander'; +import { default as RadioGroupDumb } from './components/RadioGroup/RadioGroup'; import { default as SingleSelectDumb } from './components/SingleSelect/SingleSelect'; import { default as TabsDumb } from './components/Tabs/Tabs'; +import { default as ToolTipDumb } from './components/ToolTip/ToolTip'; import { default as VerticalListMenuDumb } from './components/VerticalListMenu/VerticalListMenu'; -import { default as RadioGroupDumb } from './components/RadioGroup/RadioGroup'; const Autocomplete = buildHybridComponent(AutocompleteDumb); const ButtonGroup = buildHybridComponent(ButtonGroupDumb); @@ -17,6 +18,7 @@ const Expander = buildHybridComponent(ExpanderDumb); const RadioGroup = buildHybridComponent(RadioGroupDumb); const SingleSelect = buildHybridComponent(SingleSelectDumb); const Tabs = buildHybridComponent(TabsDumb); +const ToolTip = buildHybridComponent(ToolTipDumb); const VerticalListMenu = buildHybridComponent(VerticalListMenuDumb); // dumb components @@ -27,6 +29,7 @@ import Button from './components/Button/Button'; import CaretIcon from './components/Icon/CaretIcon/CaretIcon'; import CheckIcon from './components/Icon/CheckIcon/CheckIcon'; import Checkbox from './components/Checkbox/Checkbox'; +import CheckboxLabeled from './components/CheckboxLabeled/CheckboxLabeled'; import ChevronIcon from './components/Icon/ChevronIcon/ChevronIcon'; import ContextMenu from './components/ContextMenu/ContextMenu'; import CrossIcon from './components/Icon/CrossIcon/CrossIcon'; @@ -39,23 +42,21 @@ import EligibilityIcon from './components/Icon/EligibilityIcon/EligibilityIcon'; import Grid from './components/Grid/Grid'; import Icon from './components/Icon/Icon'; import InfoIcon from './components/Icon/InfoIcon/InfoIcon'; -import CheckboxLabeled from './components/CheckboxLabeled/CheckboxLabeled'; -import RadioButtonLabeled from './components/RadioButtonLabeled/RadioButtonLabeled'; -import SwitchLabeled from './components/SwitchLabeled/SwitchLabeled'; import LineChart from './components/LineChart/LineChart'; -import ToolTip from './components/ToolTip/ToolTip'; import MinusIcon from './components/Icon/MinusIcon/MinusIcon'; import Overlay from './components/Overlay/Overlay'; import Panel from './components/Panel/Panel'; import PlusIcon from './components/Icon/PlusIcon/PlusIcon'; import Portal from './components/Portal/Portal'; import RadioButton from './components/RadioButton/RadioButton'; +import RadioButtonLabeled from './components/RadioButtonLabeled/RadioButtonLabeled'; import ResizeIcon from './components/Icon/ResizeIcon/ResizeIcon'; import ScrollTable from './components/ScrollTable/ScrollTable'; import SearchIcon from './components/Icon/SearchIcon/SearchIcon'; import StickySection from './components/StickySection/StickySection'; import SuccessIcon from './components/Icon/SuccessIcon/SuccessIcon'; import Switch from './components/Switch/Switch'; +import SwitchLabeled from './components/SwitchLabeled/SwitchLabeled'; import Table from './components/Table/Table'; import TextField from './components/TextField/TextField'; import TextFieldValidated from './components/TextFieldValidated/TextFieldValidated'; @@ -88,9 +89,9 @@ export { ButtonGroupDumb, CaretIcon, CheckIcon, - ChevronIcon, Checkbox, - Panel, + CheckboxLabeled, + ChevronIcon, ContextMenu, CrossIcon, DangerIcon, @@ -106,16 +107,14 @@ export { Grid, Icon, InfoIcon, - CheckboxLabeled, - RadioButtonLabeled, - SwitchLabeled, LineChart, - ToolTip, MinusIcon, Overlay, + Panel, PlusIcon, Portal, RadioButton, + RadioButtonLabeled, RadioGroup, RadioGroupDumb, ResizeIcon, @@ -123,17 +122,19 @@ export { SearchIcon, SingleSelect, SingleSelectDumb, - SuccessIcon, StickySection, + SuccessIcon, Switch, + SwitchLabeled, Table, Tabs, TabsDumb, TextField, TextFieldValidated, + ToolTip, + ToolTipDumb, Validation, VerticalListMenu, VerticalListMenuDumb, WarningIcon }; - From 4e703f88afcf01224aa268e010a12238632c32f6 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 11:26:20 -0700 Subject: [PATCH 11/35] move banner to `Communication` category --- src/components/Banner/Banner.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Banner/Banner.jsx b/src/components/Banner/Banner.jsx index ff47c9dd8..6b4a97908 100644 --- a/src/components/Banner/Banner.jsx +++ b/src/components/Banner/Banner.jsx @@ -30,7 +30,7 @@ const defaultIcons = { /** * - * {"categories": ["controls", "communication"], "madeFrom": ["DangerIcon", "InfoIcon", "SuccessIcon", "WarningIcon"]} + * {"categories": ["communication"], "madeFrom": ["DangerIcon", "InfoIcon", "SuccessIcon", "WarningIcon"]} * * A basic Banner. Any props that are not explicitly called out below will be * passed through to the native `Banner` component. From 9fc344c4dea78130e9e123e959d65d448fbe3ae1 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 11:33:25 -0700 Subject: [PATCH 12/35] remove unnecessary initial state --- .../ContextMenu/examples/4.directions-interactive.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/ContextMenu/examples/4.directions-interactive.jsx b/src/components/ContextMenu/examples/4.directions-interactive.jsx index e19a230af..fe713183f 100644 --- a/src/components/ContextMenu/examples/4.directions-interactive.jsx +++ b/src/components/ContextMenu/examples/4.directions-interactive.jsx @@ -27,7 +27,7 @@ export default React.createClass({ alignment, alignmentOffset, getAlignmentOffset - } = this.state; + } = this.state || {}; const directions = [UP, DOWN, LEFT, RIGHT]; const alignments = [START, CENTER, END]; @@ -113,6 +113,4 @@ export default React.createClass({ ); }, - getInitialState: () => ({}), - }); From 0325c711dd642ba1d72c5dec94c8469bb591c1b4 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 11:41:20 -0700 Subject: [PATCH 13/35] uncomment static directions example --- .../examples/5.directions-static.jsx | 109 ++++++++++-------- 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/src/components/ContextMenu/examples/5.directions-static.jsx b/src/components/ContextMenu/examples/5.directions-static.jsx index ae1243708..bd5e32f06 100644 --- a/src/components/ContextMenu/examples/5.directions-static.jsx +++ b/src/components/ContextMenu/examples/5.directions-static.jsx @@ -1,58 +1,69 @@ import React from 'react'; import { ContextMenu, Button } from '../../../index'; +const { + CENTER, + DOWN, + END, + LEFT, + RIGHT, + START, + UP, +} = ContextMenu; + +const directions = [UP, DOWN, LEFT, RIGHT]; +const alignments = [START, CENTER, END]; + export default React.createClass({ render() { - return null; - - // const style = { - // background: 'white', - // boxShadow: '1px 1px 4px black', - // padding: 4 - // }; - - // return ( - // <section - // style={{ - // display: 'flex', - // flexDirection: 'row', - // margin: '0 60px' - // }} - // > - - // {_.map(['left', 'up', 'down', 'right'], direction => { - // return ( - // <section key={direction} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flexGrow: 1 }}> - // {_.map(['start', 'center', 'end'], alignment => _.map([0, 15, -15], directonOffset => _.map([0, 15, -15], alignmentOffset => ( - // <div key={`${alignment}${alignmentOffset}${directonOffset}`} style={{marginTop: '120px'}}> - // <ContextMenu - // {...{ - // direction, - // directonOffset, - // alignment, - // alignmentOffset, - // }} - // > - // <ContextMenu.Target> - // Target - // </ContextMenu.Target> - - // <ContextMenu.FlyOut style={style}> - // <div>{`direction: ${direction}`}</div> - // <div>{`directonOffset: ${directonOffset}`}</div> - // <div>{`alignment: ${alignment}`}</div> - // <div>{`alignmentOffset: ${alignmentOffset}`}</div> - // </ContextMenu.FlyOut> - // </ContextMenu> - // </div> - // ))))} - // </section> - // ); - // })} - - // </section> - // ); + const style = { + background: 'white', + boxShadow: '1px 1px 4px black', + padding: 4 + }; + + return ( + <section + style={{ + display: 'flex', + flexDirection: 'row', + margin: '0 60px' + }} + > + + {_.map(directions, direction => { + return ( + <section key={direction} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flexGrow: 1 }}> + {_.map(alignments, alignment => _.map([0, 15, -15], directonOffset => _.map([0, 15, -15], alignmentOffset => ( + <div key={`${alignment}${alignmentOffset}${directonOffset}`} style={{marginTop: '120px'}}> + <ContextMenu + {...{ + direction, + directonOffset, + alignment, + alignmentOffset, + }} + > + <ContextMenu.Target> + Target + </ContextMenu.Target> + + <ContextMenu.FlyOut style={style}> + <div>{`direction: ${direction}`}</div> + <div>{`directonOffset: ${directonOffset}`}</div> + <div>{`alignment: ${alignment}`}</div> + <div>{`alignmentOffset: ${alignmentOffset}`}</div> + </ContextMenu.FlyOut> + </ContextMenu> + </div> + ))))} + </section> + ); + })} + + </section> + ); } }); From f2c16331420d51a18e6c3d02a4bc06c1ad99fbd2 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 11:44:15 -0700 Subject: [PATCH 14/35] fix typo --- src/components/ToolTip/ToolTip.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 5ca555bd5..938d8ff3e 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -64,7 +64,7 @@ const ToolTip = createClass({ */ onMouseOver: func, /** - * Called with cursor leaves the target and the tooltip + * Called when cursor leaves the target and the tooltip */ onMouseOut: func, /** From e7b9f317def1204163ebb1a63729cbf700872bf7 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 11:57:10 -0700 Subject: [PATCH 15/35] fix lint errors --- src/components/ContextMenu/ContextMenu.jsx | 1 - .../ContextMenu/examples/4.directions-interactive.jsx | 4 ++-- src/components/ContextMenu/examples/5.directions-static.jsx | 3 ++- src/components/ToolTip/ToolTip.jsx | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index cb42a0932..390b2804c 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -199,7 +199,6 @@ const ContextMenu = createClass({ direction, directonOffset, isExpanded, - position, style, ...passThroughs }, diff --git a/src/components/ContextMenu/examples/4.directions-interactive.jsx b/src/components/ContextMenu/examples/4.directions-interactive.jsx index fe713183f..581abd27f 100644 --- a/src/components/ContextMenu/examples/4.directions-interactive.jsx +++ b/src/components/ContextMenu/examples/4.directions-interactive.jsx @@ -1,5 +1,6 @@ +import _ from 'lodash'; import React from 'react'; -import { ContextMenu, Button, SingleSelect, TextField } from '../../../index'; +import { ContextMenu, SingleSelect, TextField } from '../../../index'; const { CENTER, @@ -31,7 +32,6 @@ export default React.createClass({ const directions = [UP, DOWN, LEFT, RIGHT]; const alignments = [START, CENTER, END]; - const alignmentOffsetArg = _.includes([UP, DOWN], direction) ? "flyOutHeight" : "flyOutWidth"; return ( <section> diff --git a/src/components/ContextMenu/examples/5.directions-static.jsx b/src/components/ContextMenu/examples/5.directions-static.jsx index bd5e32f06..42348cfd1 100644 --- a/src/components/ContextMenu/examples/5.directions-static.jsx +++ b/src/components/ContextMenu/examples/5.directions-static.jsx @@ -1,5 +1,6 @@ +import _ from 'lodash'; import React from 'react'; -import { ContextMenu, Button } from '../../../index'; +import { ContextMenu } from '../../../index'; const { CENTER, diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 938d8ff3e..a916ca221 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -145,7 +145,6 @@ const ToolTip = createClass({ render() { const { className, - children, style, alignment, direction, From 3ad9cd83cc9a96119bf59f433463dd0b3151f84d Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 11:57:17 -0700 Subject: [PATCH 16/35] add tooltip reducers tests --- .../ToolTip/ToolTip.reducers.spec.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/components/ToolTip/ToolTip.reducers.spec.js diff --git a/src/components/ToolTip/ToolTip.reducers.spec.js b/src/components/ToolTip/ToolTip.reducers.spec.js new file mode 100644 index 000000000..455b77568 --- /dev/null +++ b/src/components/ToolTip/ToolTip.reducers.spec.js @@ -0,0 +1,25 @@ +import assert from 'assert'; +import { + onMouseOver, + onMouseOut, +} from './ToolTip.reducers.js'; + +describe('ToolTip reducers', () => { + + describe('onMouseOver', () => { + it('should set `isExpanded` to `true`', () => { + const initialState = { isExpanded: false }; + const nextState = onMouseOver(initialState); + assert(nextState.isExpanded, 'isExpanded must be true'); + }); + }); + + describe('onMouseOut', () => { + it('should set `isExpanded` to `false`', () => { + const initialState = { isExpanded: true }; + const nextState = onMouseOut(initialState); + assert(!nextState.isExpanded, 'isExpanded must be false'); + }); + }); + +}); From acf895c6038a0323a8b06bf7e01728ad347fc1e2 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 12:21:41 -0700 Subject: [PATCH 17/35] refactor onX convention test for better error output --- src/util/generic-tests.jsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/util/generic-tests.jsx b/src/util/generic-tests.jsx index 5985d9dda..bd307f7b2 100644 --- a/src/util/generic-tests.jsx +++ b/src/util/generic-tests.jsx @@ -69,7 +69,8 @@ export function common(Component, { }); it('should only use onX convention for function proptypes', () => { - assert(_.every(Component.propTypes, (value, key) => { + + _.forEach(Component.propTypes, (value, key) => { // See the following tests from the React source code to figure out how // this works: https://github.com/facebook/react/blob/v0.14.7/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js let props = {}; @@ -87,17 +88,13 @@ export function common(Component, { // If it's probably a function, and it's not `any`, then we make sure // it starts with `on` - if ( - isProbablyFunction - && !isAny - && !_.startsWith(key, 'on') - && !_.includes(exemptFunctionProps, key) // we make exceptions to the rule - ) { - return false; + + if (isProbablyFunction) { + assert(isAny || _.startsWith(key, 'on') || _.includes(exemptFunctionProps, key), `${key} must follow onX convention`); } - return true; - })); + }); + }); // Only run this test if it's a public component From 4e805a4ca5ae40d0ee2d208244e73a46d2c162a5 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 12:24:12 -0700 Subject: [PATCH 18/35] exempt getAlignmentOffset from onX convention --- src/components/ContextMenu/ContextMenu.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ContextMenu/ContextMenu.spec.js b/src/components/ContextMenu/ContextMenu.spec.js index f542efe5b..0688057b7 100644 --- a/src/components/ContextMenu/ContextMenu.spec.js +++ b/src/components/ContextMenu/ContextMenu.spec.js @@ -9,6 +9,7 @@ import ContextMenu from './ContextMenu'; describe('ContextMenu', () => { common(ContextMenu, { + exemptFunctionProps: ['getAlignmentOffset'], getDefaultProps: () => ({ children: [ (<ContextMenu.Target>Test</ContextMenu.Target>), From 3500090114b2adc775779d0dde4459cb86121ad1 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 15:23:52 -0700 Subject: [PATCH 19/35] refactor scoped class test for better errors --- src/util/generic-tests.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/generic-tests.jsx b/src/util/generic-tests.jsx index bd307f7b2..2a88cd788 100644 --- a/src/util/generic-tests.jsx +++ b/src/util/generic-tests.jsx @@ -63,9 +63,9 @@ export function common(Component, { const allClasses = parentClasses.concat(childrenClasses); - assert(_.every(allClasses, (className) => { - return _.includes(className, 'lucid-' + Component.displayName); - })); + _.forEach(allClasses, className => { + assert(_.includes(className, `lucid-${Component.displayName}`), `${className} must be scoped`); + }); }); it('should only use onX convention for function proptypes', () => { From ef34797de37959266cb1ac25e1b16479a6aee67f Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 15:26:54 -0700 Subject: [PATCH 20/35] remove intermediate div --- src/components/ToolTip/ToolTip.jsx | 47 +++++++++++++----------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index a916ca221..c808fbec7 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -145,7 +145,6 @@ const ToolTip = createClass({ render() { const { className, - style, alignment, direction, ...passThroughs @@ -161,34 +160,30 @@ const ToolTip = createClass({ : -(n / 2 - 22.5); return ( - <div + <ContextMenu className={cx('&', '&-base', className)} + alignment={ContextMenu.CENTER} + direction={direction} + directonOffset={15} + getAlignmentOffset={getAlignmentOffset} + {...passThroughs} + onMouseOver={this.handleMouseOverTarget} + onMouseOut={this.handleMouseOutTarget} > - <ContextMenu - alignment={ContextMenu.CENTER} - direction={direction} - directonOffset={15} - getAlignmentOffset={getAlignmentOffset} - {...passThroughs} - onMouseOver={this.handleMouseOverTarget} - onMouseOut={this.handleMouseOutTarget} + <Target + className={cx('&', className)} > - <Target - className={cx('&', 'target', className)} - > - {target} - </Target> - <FlyOut - style={style} - className={cx('&-flyout', className, direction, alignment)} - onMouseOver={this.handleMouseOverFlyout} - onMouseOut={this.handleMouseOutFlyout} - > - <h2 className={cx('&-title')}>{title}</h2> - {body} - </FlyOut> - </ContextMenu> - </div> + {target} + </Target> + <FlyOut + className={cx('&-flyout', className, direction, alignment)} + onMouseOver={this.handleMouseOverFlyout} + onMouseOut={this.handleMouseOutFlyout} + > + <h2 className={cx('&-title')}>{title}</h2> + {body} + </FlyOut> + </ContextMenu> ); } }); From 445389c5b1667b457cca08f1246a01696c1453dd Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 15:27:20 -0700 Subject: [PATCH 21/35] scope direction/alignment classes --- src/components/ToolTip/ToolTip.jsx | 2 +- src/components/ToolTip/ToolTip.less | 113 ++++++++++++++-------------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index c808fbec7..da872a8b7 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -176,7 +176,7 @@ const ToolTip = createClass({ {target} </Target> <FlyOut - className={cx('&-flyout', className, direction, alignment)} + className={cx('&-flyout', className, `&-${direction}`, `&-${alignment}`)} onMouseOver={this.handleMouseOverFlyout} onMouseOut={this.handleMouseOutFlyout} > diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index 05f8bdd7e..068faf964 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -33,140 +33,139 @@ border-width: @tooltip-pointer-outer-width; } - &.up:after, - &.up:before { + &.lucid-ToolTip-up:after, + &.lucid-ToolTip-up:before { top: 100%; } - &.up:after { + &.lucid-ToolTip-up:after { border-top-color: @color-white; margin-left: -@tooltip-pointer-inner-width; } - &.up:before { + &.lucid-ToolTip-up:before { border-top-color: @color-borderColor; } - &.down:after, - &.down:before { + &.lucid-ToolTip-down:after, + &.lucid-ToolTip-down:before { bottom: 100%; } - &.down:after { + &.lucid-ToolTip-down:after { border-bottom-color: @color-white; } - &.down:before { + &.lucid-ToolTip-down:before { border-bottom-color: @color-borderColor; } - &.up.center:after, - &.up.center:before, - &.down.center:after, - &.down.center:before { + &.lucid-ToolTip-up.lucid-ToolTip-center:after, + &.lucid-ToolTip-up.lucid-ToolTip-center:before, + &.lucid-ToolTip-down.lucid-ToolTip-center:after, + &.lucid-ToolTip-down.lucid-ToolTip-center:before { left: 50%; } - &.up.center:before, - &.down.center:before { + &.lucid-ToolTip-up.lucid-ToolTip-center:before, + &.lucid-ToolTip-down.lucid-ToolTip-center:before { margin-left: -@tooltip-pointer-outer-width; } - &.up.center:after, - &.down.center:after { + &.lucid-ToolTip-up.lucid-ToolTip-center:after, + &.lucid-ToolTip-down.lucid-ToolTip-center:after { margin-left: -@tooltip-pointer-inner-width; } - &.up.start:after, - &.down.start:after { + &.lucid-ToolTip-up.lucid-ToolTip-start:after, + &.lucid-ToolTip-down.lucid-ToolTip-start:after { margin-left: 1px; } - &.up.end:after, - &.down.end:after { + &.lucid-ToolTip-up.lucid-ToolTip-end:after, + &.lucid-ToolTip-down.lucid-ToolTip-end:after { margin-right: 1px; } - &.down.start:after, - &.down.start:before, - &.up.start:after, - &.up.start:before { + &.lucid-ToolTip-down.lucid-ToolTip-start:after, + &.lucid-ToolTip-down.lucid-ToolTip-start:before, + &.lucid-ToolTip-up.lucid-ToolTip-start:after, + &.lucid-ToolTip-up.lucid-ToolTip-start:before { left: @tooltip-pointer-offset; } - &.down.end:after, - &.down.end:before, - &.up.end:after, - &.up.end:before { + &.lucid-ToolTip-down.lucid-ToolTip-end:after, + &.lucid-ToolTip-down.lucid-ToolTip-end:before, + &.lucid-ToolTip-up.lucid-ToolTip-end:after, + &.lucid-ToolTip-up.lucid-ToolTip-end:before { right: @tooltip-pointer-offset; } - &.left:after, - &.left:before { + &.lucid-ToolTip-left:after, + &.lucid-ToolTip-left:before { left: 100%; } - &.left:after { + &.lucid-ToolTip-left:after { border-left-color: @color-white; } - &.left:before { + &.lucid-ToolTip-left:before { border-left-color: @color-borderColor; } - &.right:after, - &.right:before { + &.lucid-ToolTip-right:after, + &.lucid-ToolTip-right:before { right: 100%; } - &.right:after { + &.lucid-ToolTip-right:after { border-right-color: @color-white; } - &.right:before { + &.lucid-ToolTip-right:before { border-right-color: @color-borderColor; } - &.left.center:after, - &.left.center:before, - &.right.center:after, - &.right.center:before { + &.lucid-ToolTip-left.lucid-ToolTip-center:after, + &.lucid-ToolTip-left.lucid-ToolTip-center:before, + &.lucid-ToolTip-right.lucid-ToolTip-center:after, + &.lucid-ToolTip-right.lucid-ToolTip-center:before { top: 50%; } - &.left.center:before, - &.right.center:before { + &.lucid-ToolTip-left.lucid-ToolTip-center:before, + &.lucid-ToolTip-right.lucid-ToolTip-center:before { margin-top: -@tooltip-pointer-outer-width; } - &.left.center:after, - &.right.center:after { + &.lucid-ToolTip-left.lucid-ToolTip-center:after, + &.lucid-ToolTip-right.lucid-ToolTip-center:after { margin-top: -@tooltip-pointer-inner-width; } - &.left.start:after, - &.left.start:before, - &.right.start:after, - &.right.start:before { + &.lucid-ToolTip-left.lucid-ToolTip-start:after, + &.lucid-ToolTip-left.lucid-ToolTip-start:before, + &.lucid-ToolTip-right.lucid-ToolTip-start:after, + &.lucid-ToolTip-right.lucid-ToolTip-start:before { top: @tooltip-pointer-offset; } - &.left.end:after, - &.left.end:before, - &.right.end:after, - &.right.end:before { + &.lucid-ToolTip-left.lucid-ToolTip-end:after, + &.lucid-ToolTip-left.lucid-ToolTip-end:before, + &.lucid-ToolTip-right.lucid-ToolTip-end:after, + &.lucid-ToolTip-right.lucid-ToolTip-end:before { bottom: @tooltip-pointer-offset; } - &.left.start:after, - &.right.start:after { + &.lucid-ToolTip-left.lucid-ToolTip-start:after, + &.lucid-ToolTip-right.lucid-ToolTip-start:after { margin-top: 1px; } - &.left.end:after, - &.right.end:after { + &.lucid-ToolTip-left.lucid-ToolTip-end:after, + &.lucid-ToolTip-right.lucid-ToolTip-end:after { margin-bottom: 1px; } - } From 8720841af2bbb4e50ba907ad7ed1a57af19eedd4 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 15:46:40 -0700 Subject: [PATCH 22/35] add default maxWidth to ToolTip flyout --- src/components/ToolTip/ToolTip.jsx | 22 +++++++++++++++---- .../ToolTip/examples/1.interactive.jsx | 1 - src/components/ToolTip/examples/2.static.jsx | 1 - 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index da872a8b7..375118be2 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -11,6 +11,7 @@ const { bool, func, node, + number, object, oneOf, string @@ -44,9 +45,17 @@ const ToolTip = createClass({ */ className: string, /** - * Passed through to the root element. + * Passed through to the root target element. */ style: object, + /** + * Passed through to the root FlyOut element. + */ + flyOutStyle: object, + /** + * maximum width of the ToolTip FlyOut. Defaults to 400px. + */ + flyOutMaxWidth: number, /** * direction of the FlyOut relative to Target. Defaults to `'down'`. */ @@ -92,6 +101,7 @@ const ToolTip = createClass({ return { alignment: ContextMenu.CENTER, direction: ContextMenu.UP, + flyOutStyle: {}, isExpanded: false, onMouseOut: _.noop, onMouseOver: _.noop, @@ -147,6 +157,8 @@ const ToolTip = createClass({ className, alignment, direction, + flyOutMaxWidth, + flyOutStyle, ...passThroughs } = this.props; @@ -170,12 +182,14 @@ const ToolTip = createClass({ onMouseOver={this.handleMouseOverTarget} onMouseOut={this.handleMouseOutTarget} > - <Target - className={cx('&', className)} - > + <Target className={cx('&', className)}> {target} </Target> <FlyOut + style={{ + ...flyOutStyle, + maxWidth: flyOutMaxWidth || flyOutStyle.maxWidth || 400 + }} className={cx('&-flyout', className, `&-${direction}`, `&-${alignment}`)} onMouseOver={this.handleMouseOverFlyout} onMouseOut={this.handleMouseOutFlyout} diff --git a/src/components/ToolTip/examples/1.interactive.jsx b/src/components/ToolTip/examples/1.interactive.jsx index 66895a2c0..365487b95 100644 --- a/src/components/ToolTip/examples/1.interactive.jsx +++ b/src/components/ToolTip/examples/1.interactive.jsx @@ -19,7 +19,6 @@ export default React.createClass({ <ToolTip direction={direction} alignment={alignment} - style={{ maxWidth: 400 }} > <Title> Title: {direction} {alignment} diff --git a/src/components/ToolTip/examples/2.static.jsx b/src/components/ToolTip/examples/2.static.jsx index 8f48df734..25f324da6 100644 --- a/src/components/ToolTip/examples/2.static.jsx +++ b/src/components/ToolTip/examples/2.static.jsx @@ -18,7 +18,6 @@ export default React.createClass({ direction={direction} alignment={alignment} isExpanded={true} - style={{ maxWidth: 400 }} > <Title> Title: {direction} {alignment} From 3baf1f3bbcffa85506624f414d7ca14adfac5246 Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 17:13:43 -0700 Subject: [PATCH 23/35] follow component class name convention --- src/components/ToolTip/ToolTip.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 375118be2..750c210a9 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -190,7 +190,7 @@ const ToolTip = createClass({ ...flyOutStyle, maxWidth: flyOutMaxWidth || flyOutStyle.maxWidth || 400 }} - className={cx('&-flyout', className, `&-${direction}`, `&-${alignment}`)} + className={cx('&-FlyOut', className, `&-${direction}`, `&-${alignment}`)} onMouseOver={this.handleMouseOverFlyout} onMouseOut={this.handleMouseOutFlyout} > From 86d9f60dd9d41bc750db21aaede23496041794ef Mon Sep 17 00:00:00 2001 From: Joe Moon <joe@xoxomoon.com> Date: Mon, 16 May 2016 17:13:56 -0700 Subject: [PATCH 24/35] add tests for ToolTip --- src/components/ToolTip/ToolTip.spec.js | 265 +++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 src/components/ToolTip/ToolTip.spec.js diff --git a/src/components/ToolTip/ToolTip.spec.js b/src/components/ToolTip/ToolTip.spec.js new file mode 100644 index 000000000..08907eaf9 --- /dev/null +++ b/src/components/ToolTip/ToolTip.spec.js @@ -0,0 +1,265 @@ +import _ from 'lodash'; +import React from 'react'; +import assert from 'assert'; +import sinon from 'sinon'; +import { mount, shallow } from 'enzyme'; +import describeWithDOM from '../../util/describe-with-dom'; +import { common } from '../../util/generic-tests'; +import ToolTip from './ToolTip'; +import ContextMenu from '../ContextMenu/ContextMenu'; + +const { + Target, + Title, + Body +} = ToolTip; + +describe('ToolTip', () => { + common(ToolTip); + + describe('render', () => { + it('should render a ContextMenu', () => { + const wrapper = shallow( + <ToolTip> + <Target>ToolTip Target</Target> + <Title>Title + Body + + ); + + assert.equal(wrapper.find(ContextMenu).length, 1); + }); + }); + + describe('props', () => { + describe('children', () => { + it('should not render any direct child elements which are not ToolTip-specific', () => { + const wrapper = shallow( + + + ToolTip Target + Title + Body +

header

+
+ ); + assert.equal(wrapper.find('button').length, 0, 'must not render button'); + assert.equal(wrapper.find('h1').length, 0, 'must not render h1'); + }); + }); + }); + + describe('className', () => { + describeWithDOM('FlyOut', () => { + let wrapper; + + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); + + it('should pass the className prop thru to the FlyOut (portal) element', () => { + wrapper = mount( + + Target + Body + + ); + + const flyOutClassName = document.querySelector('.lucid-ToolTip-FlyOut.lucid-ContextMenu-FlyOut').className; + + assert(_.includes(flyOutClassName, 'MyToolTip'), 'must include `MyToolTip`'); + }); + }); + }); + + describe('flyOutStyle', () => { + it('should pass flyOutStyle to the underlying ContextMenu FlyOut with a default maxWidth', () => { + const wrapper = shallow( + + Target + Body + + ); + const flyOutStyle = wrapper.find(ContextMenu.FlyOut).prop('style'); + assert.deepEqual(flyOutStyle, { flex: 2, maxWidth: 400 }, 'must have flex:2 and maxWidth: 400'); + }); + }); + + describe('flyOutMaxWidth', () => { + it('should pass maxWidth to the underlying ContextMenu FlyOut style', () => { + const wrapper = shallow( + + Target + Body + + ); + const flyOutStyle = wrapper.find(ContextMenu.FlyOut).prop('style'); + assert.deepEqual(flyOutStyle, { flex: 2, maxWidth: 100 }, 'must have flex:2 and maxWidth: 400'); + }); + }); + + describe('direction', () => { + it('should pass direction to the underlying ContextMenu', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(ContextMenu).prop('direction'), 'right', 'must be "right"'); + }); + }); + + describe('alignment', () => { + + it('should pass alignment center to the underlying ContextMenu', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(ContextMenu).prop('alignment'), 'center', 'must be "center"'); + }); + + describe('center', () => { + it('should pass getAlignmentOffset with correct closed over values', () => { + const wrapper = shallow( + + Target + Body + + ); + const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); + assert.equal(getAlignmentOffset(400), 0, 'must be 0'); + }); + }); + + describe('start', () => { + it('should pass getAlignmentOffset with correct closed over values', () => { + const wrapper = shallow( + + Target + Body + + ); + const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); + assert.equal(getAlignmentOffset(400), 177.5, 'must be 177.5'); + }); + }); + + describe('end', () => { + it('should pass getAlignmentOffset with correct closed over values', () => { + const wrapper = shallow( + + Target + Body + + ); + const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); + assert.equal(getAlignmentOffset(400), -177.5, 'must be -177.5'); + }); + }); + + }); + + describe('isExpanded', () => { + it('should pass isExpanded=true to the underlying ContextMenu component thru props', () => { + const wrapper = shallow( + + Target + Body + + ); + assert(wrapper.find(ContextMenu).prop('isExpanded'), 'isExpanded must be true') + }); + + it('should be false by default', () => { + const wrapper = shallow( + + Target + Body + + ); + assert(!wrapper.find(ContextMenu).prop('isExpanded'), 'isExpanded must be false') + }); + }); + + describe('onMouseOver', () => { + it('should call onMouseOver on target mouseover', () => { + const spy = sinon.spy(); + const wrapper = shallow( + + Target + Body + + ); + wrapper.find(ContextMenu).shallow().find('span').simulate('mouseOver'); + assert(spy.calledOnce, 'onMouseOver must be called once'); + }); + }); + + describe('onMouseOut', () => { + it('should call onMouseOut when cursor leaves target', done => { + const spy = sinon.spy(); + const wrapper = shallow( + + Target + Body + + ); + const root = wrapper.find('ContextMenu').shallow().find('span'); + root.simulate('mouseOver'); + root.simulate('mouseOut'); + // wait for timeout + setTimeout(() => { + try { + assert(spy.calledOnce, 'onMouseOut must be called once'); + done(); + } catch(e) { + done(e); + } + }, 100); + }); + + it('should not call onMouseOut if cursor enters FlyOut', done => { + const spy = sinon.spy(); + const wrapper = shallow( + + Target + Body + + ); + const root = wrapper.find(ContextMenu).shallow().find('span'); + root.simulate('mouseOver'); + // simulate click hover over FlyOut Portal + wrapper.find(ContextMenu.FlyOut).prop('onMouseOver')(); + root.simulate('mouseOut'); + // wait for timeout + setTimeout(() => { + try { + assert(!spy.called, 'onMouseOut must not be called'); + done(); + } catch(e) { + done(e); + } + }, 100); + }); + + }); + + describe('portalId', () => { + it('should pass portalId to underlying ContextMenu', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(ContextMenu).prop('portalId'), 'foo-portal-id', 'must equal "foo-portal-id"'); + }); + }); + +}); From 542c5ba42c9eeb9e79b0eeb6ef0b863c11b9e09e Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Tue, 17 May 2016 10:52:29 -0700 Subject: [PATCH 25/35] fix class name --- src/components/ToolTip/ToolTip.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index 068faf964..d6b98f761 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -2,7 +2,7 @@ @tooltip-pointer-outer-width: 9px; @tooltip-pointer-offset: 13px; -.lucid-ToolTip-flyout { +.lucid-ToolTip-FlyOut { background-color: @color-white; border: @border-standardBorder; border-radius: @size-borderRadius; From e4cb6653eb55b35ea37352c7800f4bb4ebf6ec9c Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Tue, 17 May 2016 15:12:55 -0700 Subject: [PATCH 26/35] add isCloseable/onClose props to ToolTip --- src/components/ToolTip/ToolTip.jsx | 22 +++++++++++++++++++++- src/components/ToolTip/ToolTip.spec.js | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 750c210a9..7c6ee0d31 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -1,6 +1,7 @@ import _ from 'lodash'; import React from 'react'; import ContextMenu from '../ContextMenu/ContextMenu'; +import CrossIcon from '../Icon/CrossIcon/CrossIcon'; import * as reducers from './ToolTip.reducers'; import { lucidClassNames } from '../../util/style-helpers'; import { createClass, findTypes } from '../../util/component-types'; @@ -44,6 +45,16 @@ const ToolTip = createClass({ * Appended to the component-specific class names set on the root element. */ className: string, + /** + * Set this to `true` if you want to have a `x` close icon. + */ + isCloseable: bool, + /** + * Called when the user closes the `Banner`. + * + * Signature: `({ event, props }) => {}` + */ + onClose: func, /** * Passed through to the root target element. */ @@ -102,7 +113,10 @@ const ToolTip = createClass({ alignment: ContextMenu.CENTER, direction: ContextMenu.UP, flyOutStyle: {}, + isCloseable: false, isExpanded: false, + kind: 'default', + onClose: _.noop, onMouseOut: _.noop, onMouseOver: _.noop, portalId: null, @@ -152,6 +166,10 @@ const ToolTip = createClass({ this.handleMouseOut(); }, + handleClose(event) { + this.props.onClose({ event, props: this.props }); + }, + render() { const { className, @@ -159,6 +177,7 @@ const ToolTip = createClass({ direction, flyOutMaxWidth, flyOutStyle, + isCloseable, ...passThroughs } = this.props; @@ -194,7 +213,8 @@ const ToolTip = createClass({ onMouseOver={this.handleMouseOverFlyout} onMouseOut={this.handleMouseOutFlyout} > -

{title}

+ {isCloseable ? : null} +

{title}

{body} diff --git a/src/components/ToolTip/ToolTip.spec.js b/src/components/ToolTip/ToolTip.spec.js index 08907eaf9..3f8c654be 100644 --- a/src/components/ToolTip/ToolTip.spec.js +++ b/src/components/ToolTip/ToolTip.spec.js @@ -74,6 +74,31 @@ describe('ToolTip', () => { }); }); + describe('isCloseable', () => { + describe('true', () => { + it('should render a `CrossIcon`', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(CrossIcon).length, 1 ,'must include a CrossIcon'); + }); + }); + describe('false', () => { + it('should not render a `CrossIcon`', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(CrossIcon).length, 0 ,'must not include a CrossIcon'); + }); + }); + }); + describe('flyOutStyle', () => { it('should pass flyOutStyle to the underlying ContextMenu FlyOut with a default maxWidth', () => { const wrapper = shallow( From 7b731bfa72fe57b88faa5ba8ada8511eca654553 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Tue, 17 May 2016 15:14:36 -0700 Subject: [PATCH 27/35] add `kind` prop to ToolTip and clean up less --- src/components/ToolTip/ToolTip.jsx | 17 +- src/components/ToolTip/ToolTip.less | 309 ++++++++++++++----- src/components/ToolTip/ToolTip.spec.js | 399 +++++++++++++------------ 3 files changed, 452 insertions(+), 273 deletions(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 7c6ee0d31..21b4a5f49 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -7,6 +7,7 @@ import { lucidClassNames } from '../../util/style-helpers'; import { createClass, findTypes } from '../../util/component-types'; const cx = lucidClassNames.bind('&-ToolTip'); +const flyOutCx = cx.bind('&-FlyOut'); const { bool, @@ -49,6 +50,17 @@ const ToolTip = createClass({ * Set this to `true` if you want to have a `x` close icon. */ isCloseable: bool, + /** + * Style variations of the `ToolTip`. + */ + kind: oneOf([ + 'primary', + 'success', + 'warning', + 'danger', + 'info', + 'default', + ]), /** * Called when the user closes the `Banner`. * @@ -64,7 +76,7 @@ const ToolTip = createClass({ */ flyOutStyle: object, /** - * maximum width of the ToolTip FlyOut. Defaults to 400px. + * maximum width of the ToolTip FlyOut. Defaults to 200px. */ flyOutMaxWidth: number, /** @@ -178,6 +190,7 @@ const ToolTip = createClass({ flyOutMaxWidth, flyOutStyle, isCloseable, + kind, ...passThroughs } = this.props; @@ -209,7 +222,7 @@ const ToolTip = createClass({ ...flyOutStyle, maxWidth: flyOutMaxWidth || flyOutStyle.maxWidth || 400 }} - className={cx('&-FlyOut', className, `&-${direction}`, `&-${alignment}`)} + className={flyOutCx('&', { [`${className}-FlyOut`]: className }, `&-${direction}`, `&-${alignment}`, `&-${kind}`)} onMouseOver={this.handleMouseOverFlyout} onMouseOut={this.handleMouseOutFlyout} > diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index d6b98f761..77dd82b8b 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -10,7 +10,177 @@ font-size: @fontSize; color: @color-darkGray; - .lucid-ToolTip-title { + &-default&-up:after { + border-top-color: @color-white; + } + &-default&-up:before { + border-top-color: @color-borderColor; + } + &-default&-left:after { + border-left-color: @color-white; + } + &-default&-left:before { + border-left-color: @color-borderColor; + } + &-default&-right:after { + border-right-color: @color-white; + } + &-default&-right:before { + border-right-color: @color-borderColor; + } + &-default&-down:after { + border-bottom-color: @color-white; + } + &-default&-down:before { + border-bottom-color: @color-borderColor; + } + + &-primary&-up:after { + border-top-color: @featured-color-primary-backgroundColor; + } + &-primary&-up:before { + border-top-color: @featured-color-primary-borderColorLite; + } + &-primary&-left:after { + border-left-color: @featured-color-primary-backgroundColor; + } + &-primary&-left:before { + border-left-color: @featured-color-primary-borderColorLite; + } + &-primary&-right:after { + border-right-color: @featured-color-primary-backgroundColor; + } + &-primary&-right:before { + border-right-color: @featured-color-primary-borderColorLite; + } + &-primary&-down:after { + border-bottom-color: @featured-color-primary-backgroundColor; + } + &-primary&-down:before { + border-bottom-color: @featured-color-primary-borderColorLite; + } + &-primary { + border-color: @featured-color-primary-borderColorLite; + background-color: @featured-color-primary-backgroundColor; + } + + &-success&-up:after { + border-top-color: @featured-color-success-backgroundColor; + } + &-success&-up:before { + border-top-color: @featured-color-success-borderColorLite; + } + &-success&-left:after { + border-left-color: @featured-color-success-backgroundColor; + } + &-success&-left:before { + border-left-color: @featured-color-success-borderColorLite; + } + &-success&-right:after { + border-right-color: @featured-color-success-backgroundColor; + } + &-success&-right:before { + border-right-color: @featured-color-success-borderColorLite; + } + &-success&-down:after { + border-bottom-color: @featured-color-success-backgroundColor; + } + &-success&-down:before { + border-bottom-color: @featured-color-success-borderColorLite; + } + &-success { + border-color: @featured-color-success-borderColorLite; + background-color: @featured-color-success-backgroundColor; + } + + &-info&-up:after { + border-top-color: @featured-color-info-backgroundColor; + } + &-info&-up:before { + border-top-color: @featured-color-info-borderColorLite; + } + &-info&-left:after { + border-left-color: @featured-color-info-backgroundColor; + } + &-info&-left:before { + border-left-color: @featured-color-info-borderColorLite; + } + &-info&-right:after { + border-right-color: @featured-color-info-backgroundColor; + } + &-info&-right:before { + border-right-color: @featured-color-info-borderColorLite; + } + &-info&-down:after { + border-bottom-color: @featured-color-info-backgroundColor; + } + &-info&-down:before { + border-bottom-color: @featured-color-info-borderColorLite; + } + &-info { + border-color: @featured-color-info-borderColorLite; + background-color: @featured-color-info-backgroundColor; + } + + &-warning&-up:after { + border-top-color: @featured-color-warning-backgroundColor; + } + &-warning&-up:before { + border-top-color: @featured-color-warning-borderColorLite; + } + &-warning&-left:after { + border-left-color: @featured-color-warning-backgroundColor; + } + &-warning&-left:before { + border-left-color: @featured-color-warning-borderColorLite; + } + &-warning&-right:after { + border-right-color: @featured-color-warning-backgroundColor; + } + &-warning&-right:before { + border-right-color: @featured-color-warning-borderColorLite; + } + &-warning&-down:after { + border-bottom-color: @featured-color-warning-backgroundColor; + } + &-warning&-down:before { + border-bottom-color: @featured-color-warning-borderColorLite; + } + &-warning { + border-color: @featured-color-warning-borderColorLite; + background-color: @featured-color-warning-backgroundColor; + } + + &-danger&-up:after { + border-top-color: @featured-color-danger-backgroundColor; + } + &-danger&-up:before { + border-top-color: @featured-color-danger-borderColorLite; + } + &-danger&-left:after { + border-left-color: @featured-color-danger-backgroundColor; + } + &-danger&-left:before { + border-left-color: @featured-color-danger-borderColorLite; + } + &-danger&-right:after { + border-right-color: @featured-color-danger-backgroundColor; + } + &-danger&-right:before { + border-right-color: @featured-color-danger-borderColorLite; + } + &-danger&-down:after { + border-bottom-color: @featured-color-danger-backgroundColor; + } + &-danger&-down:before { + border-bottom-color: @featured-color-danger-borderColorLite; + } + &-danger { + border-color: @featured-color-danger-borderColorLite; + background-color: @featured-color-danger-backgroundColor; + } + + &-title { font-size: @size-L; font-weight: bold; margin: 0 0 @size-XXS; @@ -33,139 +203,120 @@ border-width: @tooltip-pointer-outer-width; } - &.lucid-ToolTip-up:after, - &.lucid-ToolTip-up:before { + &-up:after, + &-up:before { top: 100%; } - &.lucid-ToolTip-up:after { - border-top-color: @color-white; + &-up:after { margin-left: -@tooltip-pointer-inner-width; } - &.lucid-ToolTip-up:before { - border-top-color: @color-borderColor; - } - - &.lucid-ToolTip-down:after, - &.lucid-ToolTip-down:before { + &-down:after, + &-down:before { bottom: 100%; } - &.lucid-ToolTip-down:after { - border-bottom-color: @color-white; - } - - &.lucid-ToolTip-down:before { - border-bottom-color: @color-borderColor; - } - - &.lucid-ToolTip-up.lucid-ToolTip-center:after, - &.lucid-ToolTip-up.lucid-ToolTip-center:before, - &.lucid-ToolTip-down.lucid-ToolTip-center:after, - &.lucid-ToolTip-down.lucid-ToolTip-center:before { + &-up&-center:after, + &-up&-center:before, + &-down&-center:after, + &-down&-center:before { left: 50%; } - &.lucid-ToolTip-up.lucid-ToolTip-center:before, - &.lucid-ToolTip-down.lucid-ToolTip-center:before { + &-up&-center:before, + &-down&-center:before { margin-left: -@tooltip-pointer-outer-width; } - &.lucid-ToolTip-up.lucid-ToolTip-center:after, - &.lucid-ToolTip-down.lucid-ToolTip-center:after { + &-up&-center:after, + &-down&-center:after { margin-left: -@tooltip-pointer-inner-width; } - &.lucid-ToolTip-up.lucid-ToolTip-start:after, - &.lucid-ToolTip-down.lucid-ToolTip-start:after { + &-up&-start:after, + &-down&-start:after { margin-left: 1px; } - &.lucid-ToolTip-up.lucid-ToolTip-end:after, - &.lucid-ToolTip-down.lucid-ToolTip-end:after { + &-up&-end:after, + &-down&-end:after { margin-right: 1px; } - &.lucid-ToolTip-down.lucid-ToolTip-start:after, - &.lucid-ToolTip-down.lucid-ToolTip-start:before, - &.lucid-ToolTip-up.lucid-ToolTip-start:after, - &.lucid-ToolTip-up.lucid-ToolTip-start:before { + &-down&-start:after, + &-down&-start:before, + &-up&-start:after, + &-up&-start:before { left: @tooltip-pointer-offset; } - &.lucid-ToolTip-down.lucid-ToolTip-end:after, - &.lucid-ToolTip-down.lucid-ToolTip-end:before, - &.lucid-ToolTip-up.lucid-ToolTip-end:after, - &.lucid-ToolTip-up.lucid-ToolTip-end:before { + &-down&-end:after, + &-down&-end:before, + &-up&-end:after, + &-up&-end:before { right: @tooltip-pointer-offset; } - &.lucid-ToolTip-left:after, - &.lucid-ToolTip-left:before { + &-left:after, + &-left:before { left: 100%; } - &.lucid-ToolTip-left:after { - border-left-color: @color-white; - } - - &.lucid-ToolTip-left:before { - border-left-color: @color-borderColor; - } - - &.lucid-ToolTip-right:after, - &.lucid-ToolTip-right:before { + &-right:after, + &-right:before { right: 100%; } - &.lucid-ToolTip-right:after { - border-right-color: @color-white; - } - - &.lucid-ToolTip-right:before { - border-right-color: @color-borderColor; - } - - &.lucid-ToolTip-left.lucid-ToolTip-center:after, - &.lucid-ToolTip-left.lucid-ToolTip-center:before, - &.lucid-ToolTip-right.lucid-ToolTip-center:after, - &.lucid-ToolTip-right.lucid-ToolTip-center:before { + &-left&-center:after, + &-left&-center:before, + &-right&-center:after, + &-right&-center:before { top: 50%; } - &.lucid-ToolTip-left.lucid-ToolTip-center:before, - &.lucid-ToolTip-right.lucid-ToolTip-center:before { + &-left&-center:before, + &-right&-center:before { margin-top: -@tooltip-pointer-outer-width; } - &.lucid-ToolTip-left.lucid-ToolTip-center:after, - &.lucid-ToolTip-right.lucid-ToolTip-center:after { + &-left&-center:after, + &-right&-center:after { margin-top: -@tooltip-pointer-inner-width; } - &.lucid-ToolTip-left.lucid-ToolTip-start:after, - &.lucid-ToolTip-left.lucid-ToolTip-start:before, - &.lucid-ToolTip-right.lucid-ToolTip-start:after, - &.lucid-ToolTip-right.lucid-ToolTip-start:before { + &-left&-start:after, + &-left&-start:before, + &-right&-start:after, + &-right&-start:before { top: @tooltip-pointer-offset; } - &.lucid-ToolTip-left.lucid-ToolTip-end:after, - &.lucid-ToolTip-left.lucid-ToolTip-end:before, - &.lucid-ToolTip-right.lucid-ToolTip-end:after, - &.lucid-ToolTip-right.lucid-ToolTip-end:before { + &-left&-end:after, + &-left&-end:before, + &-right&-end:after, + &-right&-end:before { bottom: @tooltip-pointer-offset; } - &.lucid-ToolTip-left.lucid-ToolTip-start:after, - &.lucid-ToolTip-right.lucid-ToolTip-start:after { + &-left&-start:after, + &-right&-start:after { margin-top: 1px; } - &.lucid-ToolTip-left.lucid-ToolTip-end:after, - &.lucid-ToolTip-right.lucid-ToolTip-end:after { + &-left&-end:after, + &-right&-end:after { margin-bottom: 1px; } + &-close { + position: absolute; + top: 6px; + right: 6px; + cursor: pointer; + fill: fade(@color-black, 50%); + &:hover { + fill: @color-black; + } + } } diff --git a/src/components/ToolTip/ToolTip.spec.js b/src/components/ToolTip/ToolTip.spec.js index 3f8c654be..0c9f92f0a 100644 --- a/src/components/ToolTip/ToolTip.spec.js +++ b/src/components/ToolTip/ToolTip.spec.js @@ -7,6 +7,7 @@ import describeWithDOM from '../../util/describe-with-dom'; import { common } from '../../util/generic-tests'; import ToolTip from './ToolTip'; import ContextMenu from '../ContextMenu/ContextMenu'; +import CrossIcon from '../Icon/CrossIcon/CrossIcon'; const { Target, @@ -36,255 +37,269 @@ describe('ToolTip', () => { it('should not render any direct child elements which are not ToolTip-specific', () => { const wrapper = shallow( - - ToolTip Target - Title - Body -

header

+ + ToolTip Target + Title + Body +

header

); assert.equal(wrapper.find('button').length, 0, 'must not render button'); assert.equal(wrapper.find('h1').length, 0, 'must not render h1'); }); }); - }); - describe('className', () => { - describeWithDOM('FlyOut', () => { - let wrapper; + describe('className', () => { + describeWithDOM('FlyOut', () => { + let wrapper; - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - } - }); + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); - it('should pass the className prop thru to the FlyOut (portal) element', () => { - wrapper = mount( - + it('should pass the className prop thru to the FlyOut (portal) element', () => { + wrapper = mount( + Target Body - - ); + + ); - const flyOutClassName = document.querySelector('.lucid-ToolTip-FlyOut.lucid-ContextMenu-FlyOut').className; + const flyOutClassName = document.querySelector('.lucid-ToolTip-FlyOut.lucid-ContextMenu-FlyOut').className; - assert(_.includes(flyOutClassName, 'MyToolTip'), 'must include `MyToolTip`'); + assert(_.includes(flyOutClassName, 'MyToolTip'), 'must include `MyToolTip`'); + }); }); }); - }); - - describe('isCloseable', () => { - describe('true', () => { - it('should render a `CrossIcon`', () => { - const wrapper = shallow( - - Target - Body - - ); - assert.equal(wrapper.find(CrossIcon).length, 1 ,'must include a CrossIcon'); - }); - }); - describe('false', () => { - it('should not render a `CrossIcon`', () => { - const wrapper = shallow( - - Target - Body - - ); - assert.equal(wrapper.find(CrossIcon).length, 0 ,'must not include a CrossIcon'); - }); - }); - }); - describe('flyOutStyle', () => { - it('should pass flyOutStyle to the underlying ContextMenu FlyOut with a default maxWidth', () => { - const wrapper = shallow( - - Target - Body - - ); - const flyOutStyle = wrapper.find(ContextMenu.FlyOut).prop('style'); - assert.deepEqual(flyOutStyle, { flex: 2, maxWidth: 400 }, 'must have flex:2 and maxWidth: 400'); + describe('isCloseable', () => { + describe('true', () => { + it('should render a `CrossIcon`', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(CrossIcon).length, 1 ,'must include a CrossIcon'); + }); + }); + describe('false', () => { + it('should not render a `CrossIcon`', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(CrossIcon).length, 0 ,'must not include a CrossIcon'); + }); + }); }); - }); - describe('flyOutMaxWidth', () => { - it('should pass maxWidth to the underlying ContextMenu FlyOut style', () => { - const wrapper = shallow( - - Target - Body - - ); - const flyOutStyle = wrapper.find(ContextMenu.FlyOut).prop('style'); - assert.deepEqual(flyOutStyle, { flex: 2, maxWidth: 100 }, 'must have flex:2 and maxWidth: 400'); + describe('flyOutStyle', () => { + it('should pass flyOutStyle to the underlying ContextMenu FlyOut with a default maxWidth', () => { + const wrapper = shallow( + + Target + Body + + ); + const flyOutStyle = wrapper.find(ContextMenu.FlyOut).prop('style'); + assert.deepEqual(flyOutStyle, { flex: 2, maxWidth: 200 }, 'must have flex:2 and maxWidth: 200'); + }); }); - }); - describe('direction', () => { - it('should pass direction to the underlying ContextMenu', () => { - const wrapper = shallow( - - Target - Body - - ); - assert.equal(wrapper.find(ContextMenu).prop('direction'), 'right', 'must be "right"'); + describe('flyOutMaxWidth', () => { + it('should pass maxWidth to the underlying ContextMenu FlyOut style', () => { + const wrapper = shallow( + + Target + Body + + ); + const flyOutStyle = wrapper.find(ContextMenu.FlyOut).prop('style'); + assert.deepEqual(flyOutStyle, { flex: 2, maxWidth: 100 }, 'must have flex:2 and maxWidth: 400'); + }); }); - }); - describe('alignment', () => { - - it('should pass alignment center to the underlying ContextMenu', () => { - const wrapper = shallow( - - Target - Body - - ); - assert.equal(wrapper.find(ContextMenu).prop('alignment'), 'center', 'must be "center"'); + describe('kind', () => { + it('should pass the correct className to the Flyout', () => { + const wrapper = shallow( + + Target + Body + + ); + const className = wrapper.find(ContextMenu.FlyOut).prop('className'); + assert(_.includes(className, 'lucid-ToolTip-FlyOut-primary'), 'must include className'); + }); }); - describe('center', () => { - it('should pass getAlignmentOffset with correct closed over values', () => { + describe('direction', () => { + it('should pass direction to the underlying ContextMenu', () => { const wrapper = shallow( - + Target Body ); - const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); - assert.equal(getAlignmentOffset(400), 0, 'must be 0'); + assert.equal(wrapper.find(ContextMenu).prop('direction'), 'right', 'must be "right"'); }); }); - describe('start', () => { - it('should pass getAlignmentOffset with correct closed over values', () => { + describe('alignment', () => { + + it('should pass alignment center to the underlying ContextMenu', () => { const wrapper = shallow( Target Body ); - const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); - assert.equal(getAlignmentOffset(400), 177.5, 'must be 177.5'); + assert.equal(wrapper.find(ContextMenu).prop('alignment'), 'center', 'must be "center"'); + }); + + describe('center', () => { + it('should pass getAlignmentOffset with correct closed over values', () => { + const wrapper = shallow( + + Target + Body + + ); + const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); + assert.equal(getAlignmentOffset(400), 0, 'must be 0'); + }); }); + + describe('start', () => { + it('should pass getAlignmentOffset with correct closed over values', () => { + const wrapper = shallow( + + Target + Body + + ); + const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); + assert.equal(getAlignmentOffset(400), 177.5, 'must be 177.5'); + }); + }); + + describe('end', () => { + it('should pass getAlignmentOffset with correct closed over values', () => { + const wrapper = shallow( + + Target + Body + + ); + const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); + assert.equal(getAlignmentOffset(400), -177.5, 'must be -177.5'); + }); + }); + }); - describe('end', () => { - it('should pass getAlignmentOffset with correct closed over values', () => { + describe('isExpanded', () => { + it('should pass isExpanded=true to the underlying ContextMenu component thru props', () => { const wrapper = shallow( - + Target Body ); - const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); - assert.equal(getAlignmentOffset(400), -177.5, 'must be -177.5'); + assert(wrapper.find(ContextMenu).prop('isExpanded'), 'isExpanded must be true') }); - }); - }); - - describe('isExpanded', () => { - it('should pass isExpanded=true to the underlying ContextMenu component thru props', () => { - const wrapper = shallow( - - Target - Body - - ); - assert(wrapper.find(ContextMenu).prop('isExpanded'), 'isExpanded must be true') + it('should be false by default', () => { + const wrapper = shallow( + + Target + Body + + ); + assert(!wrapper.find(ContextMenu).prop('isExpanded'), 'isExpanded must be false') + }); }); - it('should be false by default', () => { - const wrapper = shallow( - - Target - Body - - ); - assert(!wrapper.find(ContextMenu).prop('isExpanded'), 'isExpanded must be false') + describe('onMouseOver', () => { + it('should call onMouseOver on target mouseover', () => { + const spy = sinon.spy(); + const wrapper = shallow( + + Target + Body + + ); + wrapper.find(ContextMenu).shallow().find('span').simulate('mouseOver'); + assert(spy.calledOnce, 'onMouseOver must be called once'); + }); }); - }); - describe('onMouseOver', () => { - it('should call onMouseOver on target mouseover', () => { - const spy = sinon.spy(); - const wrapper = shallow( - - Target - Body - - ); - wrapper.find(ContextMenu).shallow().find('span').simulate('mouseOver'); - assert(spy.calledOnce, 'onMouseOver must be called once'); - }); - }); + describe('onMouseOut', () => { + it('should call onMouseOut when cursor leaves target', done => { + const spy = sinon.spy(); + const wrapper = shallow( + + Target + Body + + ); + const root = wrapper.find('ContextMenu').shallow().find('span'); + root.simulate('mouseOver'); + root.simulate('mouseOut'); + // wait for timeout + setTimeout(() => { + try { + assert(spy.calledOnce, 'onMouseOut must be called once'); + done(); + } catch(e) { + done(e); + } + }, 100); + }); - describe('onMouseOut', () => { - it('should call onMouseOut when cursor leaves target', done => { - const spy = sinon.spy(); - const wrapper = shallow( - - Target - Body - - ); - const root = wrapper.find('ContextMenu').shallow().find('span'); - root.simulate('mouseOver'); - root.simulate('mouseOut'); - // wait for timeout - setTimeout(() => { - try { - assert(spy.calledOnce, 'onMouseOut must be called once'); - done(); - } catch(e) { - done(e); - } - }, 100); - }); + it('should not call onMouseOut if cursor enters FlyOut', done => { + const spy = sinon.spy(); + const wrapper = shallow( + + Target + Body + + ); + const root = wrapper.find(ContextMenu).shallow().find('span'); + root.simulate('mouseOver'); + // simulate click hover over FlyOut Portal + wrapper.find(ContextMenu.FlyOut).prop('onMouseOver')(); + root.simulate('mouseOut'); + // wait for timeout + setTimeout(() => { + try { + assert(!spy.called, 'onMouseOut must not be called'); + done(); + } catch(e) { + done(e); + } + }, 100); + }); - it('should not call onMouseOut if cursor enters FlyOut', done => { - const spy = sinon.spy(); - const wrapper = shallow( - - Target - Body - - ); - const root = wrapper.find(ContextMenu).shallow().find('span'); - root.simulate('mouseOver'); - // simulate click hover over FlyOut Portal - wrapper.find(ContextMenu.FlyOut).prop('onMouseOver')(); - root.simulate('mouseOut'); - // wait for timeout - setTimeout(() => { - try { - assert(!spy.called, 'onMouseOut must not be called'); - done(); - } catch(e) { - done(e); - } - }, 100); }); - }); - - describe('portalId', () => { - it('should pass portalId to underlying ContextMenu', () => { - const wrapper = shallow( - - Target - Body - - ); - assert.equal(wrapper.find(ContextMenu).prop('portalId'), 'foo-portal-id', 'must equal "foo-portal-id"'); + describe('portalId', () => { + it('should pass portalId to underlying ContextMenu', () => { + const wrapper = shallow( + + Target + Body + + ); + assert.equal(wrapper.find(ContextMenu).prop('portalId'), 'foo-portal-id', 'must equal "foo-portal-id"'); + }); }); + }); }); From c30a68e2e68298734793e93a5893abd40ccc1f35 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Tue, 17 May 2016 15:14:54 -0700 Subject: [PATCH 28/35] change default maxWidth of ToolTip --- src/components/ToolTip/ToolTip.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 21b4a5f49..b30e36b3b 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -220,7 +220,7 @@ const ToolTip = createClass({ Date: Tue, 17 May 2016 15:15:33 -0700 Subject: [PATCH 29/35] add examples for `kind`, `isCloseable` --- .../ToolTip/examples/2.close-button.jsx | 34 +++++++++++++ .../ToolTip/examples/3.variants.jsx | 51 +++++++++++++++++++ .../examples/{2.static.jsx => 4.static.jsx} | 0 3 files changed, 85 insertions(+) create mode 100644 src/components/ToolTip/examples/2.close-button.jsx create mode 100644 src/components/ToolTip/examples/3.variants.jsx rename src/components/ToolTip/examples/{2.static.jsx => 4.static.jsx} (100%) diff --git a/src/components/ToolTip/examples/2.close-button.jsx b/src/components/ToolTip/examples/2.close-button.jsx new file mode 100644 index 000000000..66a5e7776 --- /dev/null +++ b/src/components/ToolTip/examples/2.close-button.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { ToolTipDumb } from '../../../index'; + +const { + Target, + Title, + Body +} = ToolTipDumb; + +export default React.createClass({ + getInitialState: () => ({ isExpanded: true }), + render() { + return ( +
+ this.setState({ isExpanded: false })} + isExpanded={this.state.isExpanded} + > + Title + + ToolTip is a utility component to create a transient message anchored to another component. + + +
+ Target +
+
+
+
+ ); + } +}); diff --git a/src/components/ToolTip/examples/3.variants.jsx b/src/components/ToolTip/examples/3.variants.jsx new file mode 100644 index 000000000..8fe0af129 --- /dev/null +++ b/src/components/ToolTip/examples/3.variants.jsx @@ -0,0 +1,51 @@ +import _ from 'lodash'; +import React from 'react'; +import { ToolTipDumb } from '../../../index'; + +const { + Target, + Title, + Body +} = ToolTipDumb; + +const kinds = ['primary', 'success', 'warning', 'danger', 'info', 'default']; +const rows = _.chunk(kinds, 3); + +export default React.createClass({ + getInitialState: () => ({ isExpanded: true }), + render() { + return ( +
+ {_.map(rows, row => +
+ {_.map(row, kind => + + {_.capitalize(kind)} + + ToolTip is a utility component to create a transient message anchored to another component. + + +
Target for {kind}
+
+
+ )} +
+ )} +
+ ); + } +}); diff --git a/src/components/ToolTip/examples/2.static.jsx b/src/components/ToolTip/examples/4.static.jsx similarity index 100% rename from src/components/ToolTip/examples/2.static.jsx rename to src/components/ToolTip/examples/4.static.jsx From 6a07f32ed1700484ae942403f53417a473f097f9 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Tue, 17 May 2016 15:22:17 -0700 Subject: [PATCH 30/35] style tweaks for ToolTip --- src/components/ToolTip/ToolTip.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index 77dd82b8b..61718ad78 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -183,7 +183,8 @@ &-title { font-size: @size-L; font-weight: bold; - margin: 0 0 @size-XXS; + margin: 0 0 @size-XS; + line-height: 1em; } &:after, &:before { From 0143d3ad4e2ae20d28bb9253520b492bbb009444 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Wed, 18 May 2016 14:28:23 -0700 Subject: [PATCH 31/35] code review feedback --- src/components/ToolTip/ToolTip.less | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index 61718ad78..189c039eb 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -1,6 +1,6 @@ -@tooltip-pointer-inner-width: 8px; -@tooltip-pointer-outer-width: 9px; -@tooltip-pointer-offset: 13px; +@Tooltip-pointer-inner-width: 8px; +@Tooltip-pointer-outer-width: 9px; +@Tooltip-pointer-offset: 13px; .lucid-ToolTip-FlyOut { background-color: @color-white; @@ -193,15 +193,15 @@ width: 0; position: absolute; pointer-events: none; - border: 7px solid transparent; + border: 7px solid @color-transparent; } &:after { - border-width: @tooltip-pointer-inner-width; + border-width: @Tooltip-pointer-inner-width; } &:before { - border-width: @tooltip-pointer-outer-width; + border-width: @Tooltip-pointer-outer-width; } &-up:after, @@ -210,7 +210,7 @@ } &-up:after { - margin-left: -@tooltip-pointer-inner-width; + margin-left: -@Tooltip-pointer-inner-width; } &-down:after, @@ -227,12 +227,12 @@ &-up&-center:before, &-down&-center:before { - margin-left: -@tooltip-pointer-outer-width; + margin-left: -@Tooltip-pointer-outer-width; } &-up&-center:after, &-down&-center:after { - margin-left: -@tooltip-pointer-inner-width; + margin-left: -@Tooltip-pointer-inner-width; } &-up&-start:after, @@ -249,14 +249,14 @@ &-down&-start:before, &-up&-start:after, &-up&-start:before { - left: @tooltip-pointer-offset; + left: @Tooltip-pointer-offset; } &-down&-end:after, &-down&-end:before, &-up&-end:after, &-up&-end:before { - right: @tooltip-pointer-offset; + right: @Tooltip-pointer-offset; } &-left:after, @@ -278,26 +278,26 @@ &-left&-center:before, &-right&-center:before { - margin-top: -@tooltip-pointer-outer-width; + margin-top: -@Tooltip-pointer-outer-width; } &-left&-center:after, &-right&-center:after { - margin-top: -@tooltip-pointer-inner-width; + margin-top: -@Tooltip-pointer-inner-width; } &-left&-start:after, &-left&-start:before, &-right&-start:after, &-right&-start:before { - top: @tooltip-pointer-offset; + top: @Tooltip-pointer-offset; } &-left&-end:after, &-left&-end:before, &-right&-end:after, &-right&-end:before { - bottom: @tooltip-pointer-offset; + bottom: @Tooltip-pointer-offset; } &-left&-start:after, @@ -315,7 +315,7 @@ top: 6px; right: 6px; cursor: pointer; - fill: fade(@color-black, 50%); + fill: @color-gray-50; &:hover { fill: @color-black; } From 43b3b8cd2bfd461ca9f6e4f2900152442f3f302a Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Wed, 18 May 2016 17:09:27 -0700 Subject: [PATCH 32/35] fix indents --- src/components/ToolTip/ToolTip.spec.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/ToolTip/ToolTip.spec.js b/src/components/ToolTip/ToolTip.spec.js index 0c9f92f0a..7b6946aa1 100644 --- a/src/components/ToolTip/ToolTip.spec.js +++ b/src/components/ToolTip/ToolTip.spec.js @@ -22,9 +22,9 @@ describe('ToolTip', () => { it('should render a ContextMenu', () => { const wrapper = shallow( - ToolTip Target - Title - Body + ToolTip Target + Title + Body ); @@ -37,11 +37,11 @@ describe('ToolTip', () => { it('should not render any direct child elements which are not ToolTip-specific', () => { const wrapper = shallow( - - ToolTip Target - Title - Body -

header

+ + ToolTip Target + Title + Body +

header

); assert.equal(wrapper.find('button').length, 0, 'must not render button'); @@ -62,8 +62,8 @@ describe('ToolTip', () => { it('should pass the className prop thru to the FlyOut (portal) element', () => { wrapper = mount( - Target - Body + Target + Body ); @@ -192,8 +192,8 @@ describe('ToolTip', () => { it('should pass getAlignmentOffset with correct closed over values', () => { const wrapper = shallow( - Target - Body + Target + Body ); const getAlignmentOffset = wrapper.find(ContextMenu).prop('getAlignmentOffset'); From 8ec5bcc14021e15e78874577321cb381b735c419 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Thu, 19 May 2016 11:38:48 -0700 Subject: [PATCH 33/35] className tweaks to ToolTip --- src/components/ContextMenu/ContextMenu.jsx | 5 +---- src/components/ToolTip/ToolTip.jsx | 10 +++++----- src/components/ToolTip/ToolTip.less | 16 ++++++++-------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.jsx b/src/components/ContextMenu/ContextMenu.jsx index 390b2804c..c0c1025c4 100644 --- a/src/components/ContextMenu/ContextMenu.jsx +++ b/src/components/ContextMenu/ContextMenu.jsx @@ -223,10 +223,7 @@ const ContextMenu = createClass({ - + {target} - {isCloseable ? : null} -

{title}

+ {isCloseable ? : null} +

{title}

{body}
diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index 189c039eb..02b6eea8c 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -180,13 +180,6 @@ background-color: @featured-color-danger-backgroundColor; } - &-title { - font-size: @size-L; - font-weight: bold; - margin: 0 0 @size-XS; - line-height: 1em; - } - &:after, &:before { content: ' '; height: 0; @@ -310,7 +303,14 @@ margin-bottom: 1px; } - &-close { + &-Title { + font-size: @size-L; + font-weight: bold; + margin: 0 0 @size-XS; + line-height: 1em; + } + + &-Close { position: absolute; top: 6px; right: 6px; From c8ad6fe85cf7eeef10aa05b65272bbb6445d8430 Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Thu, 19 May 2016 11:47:58 -0700 Subject: [PATCH 34/35] code review feedback on ToolTip --- src/components/ToolTip/ToolTip.jsx | 8 ++++---- src/components/ToolTip/ToolTip.less | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ToolTip/ToolTip.jsx b/src/components/ToolTip/ToolTip.jsx index 6f5159d35..64444db3e 100644 --- a/src/components/ToolTip/ToolTip.jsx +++ b/src/components/ToolTip/ToolTip.jsx @@ -205,7 +205,7 @@ const ToolTip = createClass({ return ( - + {target} - {isCloseable ? : null} + {isCloseable ? : null}

{title}

{body}
diff --git a/src/components/ToolTip/ToolTip.less b/src/components/ToolTip/ToolTip.less index 02b6eea8c..6bb13a04e 100644 --- a/src/components/ToolTip/ToolTip.less +++ b/src/components/ToolTip/ToolTip.less @@ -310,7 +310,7 @@ line-height: 1em; } - &-Close { + &-close { position: absolute; top: 6px; right: 6px; From d571cb2d7c24353dd3bfcb4087d79b57720a86da Mon Sep 17 00:00:00 2001 From: Joe Moon Date: Thu, 19 May 2016 12:02:11 -0700 Subject: [PATCH 35/35] fix test --- .../ContextMenu/ContextMenu.spec.js | 49 +++++++------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.spec.js b/src/components/ContextMenu/ContextMenu.spec.js index 0688057b7..1499528ef 100644 --- a/src/components/ContextMenu/ContextMenu.spec.js +++ b/src/components/ContextMenu/ContextMenu.spec.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import React from 'react'; import assert from 'assert'; import sinon from 'sinon'; @@ -60,39 +61,25 @@ describe('ContextMenu', () => { }); describe('direction', () => { - it('should apply the `lucid-ContextMenu-FlyOut-Down` className when `down`', () => { - wrapper = mount( - - - File - - - Open - - - ); - - const flyOutPortalDomNode = document.getElementById('ContextMenu-test123'); - - assert(flyOutPortalDomNode.querySelector('.lucid-ContextMenu-FlyOut-Down')); + _.forEach(['up', 'down', 'left', 'right'], direction => { + it(`should apply the 'lucid-ContextMenu-FlyOut-${direction}' className when '${direction}'`, () => { + wrapper = mount( + + + File + + + Open + + + ); + + const flyOutPortalDomNode = document.getElementById('ContextMenu-test123'); + + assert(flyOutPortalDomNode.querySelector(`.lucid-ContextMenu-FlyOut-${direction}`)); + }); }); - it('should apply the `lucid-ContextMenu-FlyOut-Up` className when `up`', () => { - wrapper = mount( - - - File - - - Open - - - ); - - const flyOutPortalDomNode = document.getElementById('ContextMenu-test123'); - - assert(flyOutPortalDomNode.querySelector('.lucid-ContextMenu-FlyOut-Up')); - }); }); describe('portalId', () => {