mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-22 07:08:02 -05:00
first take on toggleable open close state for info messages
make info button have large, invisible boundary fix info button ref setting WIP: experiment with more sophisticated mouseout handling add lodash debounce use lodash debounce to reduce info message flickering tweak info message position per added padding remove leftover function update info button tests, add a bunch more
This commit is contained in:
parent
ae04860160
commit
5805d8a0fe
6 changed files with 155 additions and 23 deletions
|
@ -99,6 +99,7 @@
|
||||||
"keymirror": "0.1.1",
|
"keymirror": "0.1.1",
|
||||||
"lodash.bindall": "4.4.0",
|
"lodash.bindall": "4.4.0",
|
||||||
"lodash.clone": "3.0.3",
|
"lodash.clone": "3.0.3",
|
||||||
|
"lodash.debounce": "4.0.8",
|
||||||
"lodash.defaultsdeep": "3.10.0",
|
"lodash.defaultsdeep": "3.10.0",
|
||||||
"lodash.isarray": "3.0.4",
|
"lodash.isarray": "3.0.4",
|
||||||
"lodash.merge": "3.3.2",
|
"lodash.merge": "3.3.2",
|
||||||
|
|
|
@ -2,6 +2,7 @@ const bindAll = require('lodash.bindall');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const MediaQuery = require('react-responsive').default;
|
const MediaQuery = require('react-responsive').default;
|
||||||
|
const debounce = require('lodash.debounce');
|
||||||
|
|
||||||
const frameless = require('../../lib/frameless');
|
const frameless = require('../../lib/frameless');
|
||||||
|
|
||||||
|
@ -11,18 +12,48 @@ class InfoButton extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleHideMessage',
|
'handleClick',
|
||||||
'handleShowMessage'
|
'handleMouseOut',
|
||||||
|
'handleShowMessage',
|
||||||
|
'setButtonRef'
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
requireClickToClose: false, // default to closing on mouseout
|
||||||
visible: false
|
visible: false
|
||||||
};
|
};
|
||||||
|
this.setVisibleWithDebounce = debounce(this.setVisible, 100);
|
||||||
}
|
}
|
||||||
handleHideMessage () {
|
componentWillMount () {
|
||||||
this.setState({visible: false});
|
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 () {
|
handleShowMessage () {
|
||||||
this.setState({visible: true});
|
this.setVisibleWithDebounce(true);
|
||||||
|
}
|
||||||
|
setButtonRef (element) {
|
||||||
|
this.buttonRef = element;
|
||||||
|
}
|
||||||
|
setVisible (newVisibleState) {
|
||||||
|
this.setState({visible: newVisibleState});
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
const messageJsx = this.state.visible && (
|
const messageJsx = this.state.visible && (
|
||||||
|
@ -34,8 +65,8 @@ class InfoButton extends React.Component {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className="info-button"
|
className="info-button"
|
||||||
onClick={this.handleShowMessage}
|
ref={this.setButtonRef}
|
||||||
onMouseOut={this.handleHideMessage}
|
onMouseOut={this.handleMouseOut}
|
||||||
onMouseOver={this.handleShowMessage}
|
onMouseOver={this.handleShowMessage}
|
||||||
>
|
>
|
||||||
<MediaQuery minWidth={frameless.desktop}>
|
<MediaQuery minWidth={frameless.desktop}>
|
||||||
|
|
|
@ -4,23 +4,21 @@
|
||||||
.info-button {
|
.info-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1rem;
|
width: 2rem;
|
||||||
height: 1rem;
|
height: 2rem;
|
||||||
margin-left: .375rem;
|
margin-left: -.125rem;
|
||||||
margin-top: -.25rem;
|
margin-top: -.75rem;
|
||||||
border-radius: 50%;
|
|
||||||
background-color: $type-gray-60percent;
|
|
||||||
background-image: url("/svgs/info-button/info-button.svg");
|
background-image: url("/svgs/info-button/info-button.svg");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
top: .1875rem;
|
top: .6875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-button-message {
|
.info-button-message {
|
||||||
$arrow-border-width: 1rem;
|
$arrow-border-width: 1rem;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: .375rem;
|
||||||
left: 0;
|
left: .5rem;
|
||||||
transform: translate(1rem, -1rem);
|
transform: translate(1rem, -1rem);
|
||||||
width: 16.5rem;
|
width: 16.5rem;
|
||||||
min-height: 1rem;
|
min-height: 1rem;
|
||||||
|
@ -66,6 +64,7 @@
|
||||||
we need to center this element within its width. */
|
we need to center this element within its width. */
|
||||||
margin: 0 calc((100% - 16.5rem) / 2);;
|
margin: 0 calc((100% - 16.5rem) / 2);;
|
||||||
top: .125rem;
|
top: .125rem;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -127,10 +127,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-flow-privacy-message {
|
.join-flow-privacy-message {
|
||||||
margin: 1rem auto;
|
margin: .5rem auto 1rem;
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: $type-gray-60percent;
|
color: $type-gray-60percent;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-flow-inner-username-step {
|
.join-flow-inner-username-step {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"><g fill-rule="evenodd"><path d="M10 0a10 10 0 1 0 10 10A10 10 0 0 0 10 0" fill="#9a9eac"/><path d="M10 13.39a1.33 1.33 0 1 1-1.33 1.33A1.33 1.33 0 0 1 10 13.39zm2.68-8.77a3 3 0 0 1 1.42 2.31 3.15 3.15 0 0 1-.95 2.56 8.37 8.37 0 0 1-1.59 1.1c-.7.39-.7.41-.7.77 0 .55 0 1-1 1s-1-.45-1-1a2.65 2.65 0 0 1 1.72-2.52A6.61 6.61 0 0 0 11.79 8a1.22 1.22 0 0 0 .3-.91 1 1 0 0 0-.5-.79 2.8 2.8 0 0 0-2.87.2c-.98.64-.81 1.55-1.72 1.3s-.92-1-.53-1.7a3.94 3.94 0 0 1 1.9-1.66 4.67 4.67 0 0 1 4.3.18z" fill="#fff"/></g></svg>
|
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="40" height="40"><g fill-rule="evenodd"><path d="M20 10a10 10 0 1 0 10 10 10 10 0 0 0-10-10" fill="#9a9eac"/><path d="M20 23.39a1.33 1.33 0 1 1-1.33 1.33A1.34 1.34 0 0 1 20 23.39zm2.68-8.77a3 3 0 0 1 1.42 2.31 3.14 3.14 0 0 1-1 2.56 8.2 8.2 0 0 1-1.59 1.1c-.7.39-.7.41-.7.77 0 .55 0 1-1 1s-1-.45-1-1a2.64 2.64 0 0 1 1.72-2.52 6.71 6.71 0 0 0 1.26-.84 1.22 1.22 0 0 0 .3-.91 1 1 0 0 0-.5-.79 2.8 2.8 0 0 0-2.87.2c-1 .64-.81 1.55-1.72 1.3s-.92-1-.53-1.7a3.9 3.9 0 0 1 1.9-1.66 4.67 4.67 0 0 1 4.3.18z" fill="#fff"/></g></svg>
|
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 589 B |
|
@ -3,6 +3,15 @@ import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||||
import InfoButton from '../../../src/components/info-button/info-button';
|
import InfoButton from '../../../src/components/info-button/info-button';
|
||||||
|
|
||||||
describe('InfoButton', () => {
|
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', () => {
|
test('Info button defaults to not visible', () => {
|
||||||
const component = mountWithIntl(
|
const component = mountWithIntl(
|
||||||
<InfoButton
|
<InfoButton
|
||||||
|
@ -11,33 +20,124 @@ describe('InfoButton', () => {
|
||||||
);
|
);
|
||||||
expect(component.find('div.info-button-message').exists()).toEqual(false);
|
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(
|
const component = mountWithIntl(
|
||||||
<InfoButton
|
<InfoButton
|
||||||
message="Here is some info about something!"
|
message="Here is some info about something!"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// mouseOver info button
|
||||||
component.find('div.info-button').simulate('mouseOver');
|
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', () => {
|
test('clicking on info button makes info message visible', () => {
|
||||||
const component = mountWithIntl(
|
const component = mountWithIntl(
|
||||||
<InfoButton
|
<InfoButton
|
||||||
message="Here is some info about something!"
|
message="Here is some info about something!"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
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);
|
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(
|
const component = mountWithIntl(
|
||||||
<InfoButton
|
<InfoButton
|
||||||
message="Here is some info about something!"
|
message="Here is some info about something!"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
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);
|
expect(component.find('div.info-button-message').exists()).toEqual(true);
|
||||||
|
|
||||||
|
// mouseOut from info button
|
||||||
component.find('div.info-button').simulate('mouseOut');
|
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(
|
||||||
|
<InfoButton
|
||||||
|
message="Here is some info about something!"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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);
|
expect(component.find('div.info-button-message').exists()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('clicking on info button, then clicking somewhere else', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<InfoButton
|
||||||
|
message="Here is some info about something!"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<InfoButton
|
||||||
|
message="Here is some info about something!"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue