diff --git a/package.json b/package.json index eea303d4c..4177833f5 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "keymirror": "0.1.1", "lodash.bindall": "4.4.0", "lodash.clone": "3.0.3", + "lodash.debounce": "4.0.8", "lodash.defaultsdeep": "3.10.0", "lodash.isarray": "3.0.4", "lodash.merge": "3.3.2", diff --git a/src/components/info-button/info-button.jsx b/src/components/info-button/info-button.jsx index f79890c05..80e2e0490 100644 --- a/src/components/info-button/info-button.jsx +++ b/src/components/info-button/info-button.jsx @@ -2,6 +2,7 @@ const bindAll = require('lodash.bindall'); const PropTypes = require('prop-types'); const React = require('react'); const MediaQuery = require('react-responsive').default; +const debounce = require('lodash.debounce'); const frameless = require('../../lib/frameless'); @@ -11,18 +12,48 @@ class InfoButton extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleHideMessage', - 'handleShowMessage' + 'handleClick', + 'handleMouseOut', + 'handleShowMessage', + 'setButtonRef' ]); this.state = { + requireClickToClose: false, // default to closing on mouseout visible: false }; + this.setVisibleWithDebounce = debounce(this.setVisible, 100); } - handleHideMessage () { - this.setState({visible: false}); + componentWillMount () { + window.addEventListener('mousedown', this.handleClick, false); + } + componentWillUnmount () { + window.removeEventListener('mousedown', this.handleClick, false); + } + handleClick (e) { + if (this.buttonRef) { // only handle click if we can tell whether it happened in this component + let newVisibleState = false; // for most clicks, hide the info message + if (this.buttonRef.contains(e.target)) { // if the click was inside the info icon... + newVisibleState = !this.state.requireClickToClose; // toggle it + } + this.setState({ + requireClickToClose: newVisibleState, + visible: newVisibleState + }); + } + } + handleMouseOut () { + if (this.state.visible && !this.state.requireClickToClose) { + this.setVisibleWithDebounce(false); + } } handleShowMessage () { - this.setState({visible: true}); + this.setVisibleWithDebounce(true); + } + setButtonRef (element) { + this.buttonRef = element; + } + setVisible (newVisibleState) { + this.setState({visible: newVisibleState}); } render () { const messageJsx = this.state.visible && ( @@ -34,8 +65,8 @@ class InfoButton extends React.Component {
diff --git a/src/components/info-button/info-button.scss b/src/components/info-button/info-button.scss index f2d93303d..2e9e3e4f4 100644 --- a/src/components/info-button/info-button.scss +++ b/src/components/info-button/info-button.scss @@ -4,23 +4,21 @@ .info-button { position: relative; display: inline-block; - width: 1rem; - height: 1rem; - margin-left: .375rem; - margin-top: -.25rem; - border-radius: 50%; - background-color: $type-gray-60percent; + width: 2rem; + height: 2rem; + margin-left: -.125rem; + margin-top: -.75rem; background-image: url("/svgs/info-button/info-button.svg"); background-size: cover; - top: .1875rem; + top: .6875rem; } .info-button-message { $arrow-border-width: 1rem; display: block; position: absolute; - top: 0; - left: 0; + top: .375rem; + left: .5rem; transform: translate(1rem, -1rem); width: 16.5rem; min-height: 1rem; @@ -66,6 +64,7 @@ we need to center this element within its width. */ margin: 0 calc((100% - 16.5rem) / 2);; top: .125rem; + left: 0; &:before { display: none; diff --git a/src/components/join-flow/join-flow-steps.scss b/src/components/join-flow/join-flow-steps.scss index 3d7e1df30..6caa86d50 100644 --- a/src/components/join-flow/join-flow-steps.scss +++ b/src/components/join-flow/join-flow-steps.scss @@ -127,10 +127,11 @@ } .join-flow-privacy-message { - margin: 1rem auto; + margin: .5rem auto 1rem; font-size: .75rem; font-weight: 500; color: $type-gray-60percent; + text-align: center; } .join-flow-inner-username-step { diff --git a/static/svgs/info-button/info-button.svg b/static/svgs/info-button/info-button.svg index 76edc37d8..a5bbdbd01 100644 --- a/static/svgs/info-button/info-button.svg +++ b/static/svgs/info-button/info-button.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/unit/components/info-button.test.jsx b/test/unit/components/info-button.test.jsx index e7e48231d..84f4fcb26 100644 --- a/test/unit/components/info-button.test.jsx +++ b/test/unit/components/info-button.test.jsx @@ -3,6 +3,15 @@ import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; import InfoButton from '../../../src/components/info-button/info-button'; describe('InfoButton', () => { + // mock window.addEventListener + // for more on this technique, see discussion at https://github.com/airbnb/enzyme/issues/426#issuecomment-253515886 + const mockedAddEventListener = {}; + /* eslint-disable no-undef */ + window.addEventListener = jest.fn((event, cb) => { + mockedAddEventListener[event] = cb; + }); + /* eslint-enable no-undef */ + test('Info button defaults to not visible', () => { const component = mountWithIntl( { ); expect(component.find('div.info-button-message').exists()).toEqual(false); }); - test('mouseOver on info button makes info message visible', () => { + + test('mouseOver on info button makes info message visible', done => { const component = mountWithIntl( ); + + // mouseOver info button component.find('div.info-button').simulate('mouseOver'); - expect(component.find('div.info-button-message').exists()).toEqual(true); + setTimeout(function () { // necessary because mouseover uses debounce + // crucial: if we don't call update(), then find() below looks through an OLD + // version of the DOM! see https://github.com/airbnb/enzyme/issues/1233#issuecomment-358915200 + component.update(); + expect(component.find('div.info-button-message').exists()).toEqual(true); + done(); + }, 500); }); + test('clicking on info button makes info message visible', () => { const component = mountWithIntl( ); - component.find('div.info-button').simulate('click'); + const buttonRef = component.instance().buttonRef; + + // click on info button + mockedAddEventListener.mousedown({target: buttonRef}); + component.update(); + expect(component.find('div.info-button').exists()).toEqual(true); expect(component.find('div.info-button-message').exists()).toEqual(true); }); - test('after message is visible, mouseOut makes it vanish', () => { + + test('clicking on info button, then mousing out makes info message still appear', done => { const component = mountWithIntl( ); - component.find('div.info-button').simulate('mouseOver'); + const buttonRef = component.instance().buttonRef; + + // click on info button + mockedAddEventListener.mousedown({target: buttonRef}); + component.update(); + expect(component.find('div.info-button').exists()).toEqual(true); expect(component.find('div.info-button-message').exists()).toEqual(true); + + // mouseOut from info button component.find('div.info-button').simulate('mouseOut'); + setTimeout(function () { // necessary because mouseover uses debounce + component.update(); + expect(component.find('div.info-button-message').exists()).toEqual(true); + done(); + }, 500); + }); + + test('clicking on info button, then clicking on it again makes info message go away', () => { + const component = mountWithIntl( + + ); + const buttonRef = component.instance().buttonRef; + + // click on info button + mockedAddEventListener.mousedown({target: buttonRef}); + component.update(); + expect(component.find('div.info-button').exists()).toEqual(true); + expect(component.find('div.info-button-message').exists()).toEqual(true); + + // click on info button again + mockedAddEventListener.mousedown({target: buttonRef}); + component.update(); expect(component.find('div.info-button-message').exists()).toEqual(false); }); + + test('clicking on info button, then clicking somewhere else', () => { + const component = mountWithIntl( + + ); + const buttonRef = component.instance().buttonRef; + + // click on info button + mockedAddEventListener.mousedown({target: buttonRef}); + component.update(); + expect(component.find('div.info-button').exists()).toEqual(true); + expect(component.find('div.info-button-message').exists()).toEqual(true); + + // click on some other target + mockedAddEventListener.mousedown({target: null}); + component.update(); + expect(component.find('div.info-button-message').exists()).toEqual(false); + }); + + test('after message is visible, mouseOut makes it vanish', done => { + const component = mountWithIntl( + + ); + + // mouseOver info button + component.find('div.info-button').simulate('mouseOver'); + setTimeout(function () { // necessary because mouseover uses debounce + component.update(); + expect(component.find('div.info-button-message').exists()).toEqual(true); + + // mouseOut away from info button + component.find('div.info-button').simulate('mouseOut'); + setTimeout(function () { // necessary because mouseover uses debounce + component.update(); + expect(component.find('div.info-button-message').exists()).toEqual(false); + done(); + }, 500); + + }, 500); + }); });