Skip to content

Commit

Permalink
Chore: Refactor controls for mobile (#174)
Browse files Browse the repository at this point in the history
* Chore: Remove dependency on CSS hover

* Chore: Refactor controls for mobile

* Chore: Responding to comments
  • Loading branch information
Jeremy Press authored Jun 26, 2017
1 parent 3b585d8 commit 066a3d4
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 20 deletions.
11 changes: 11 additions & 0 deletions src/lib/Browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,17 @@ class Browser {
return document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
}

/**
* Returns true if the browser supports touch.
* taken from Modernizr: https://github.com/Modernizr/Modernizr/blob/5eea7e2a213edc9e83a47b6414d0250468d83471/feature-detects/touchevents.js#L40
*
* @public
* @return {boolean} Is touch supported
*/
static hasTouch() {
return 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
}

/**
* Returns whether the browser is a mobile browser.
*
Expand Down
57 changes: 51 additions & 6 deletions src/lib/Controls.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import throttle from 'lodash.throttle';
import Browser from './Browser';
import { CLASS_HIDDEN } from './constants';

const SHOW_PREVIEW_CONTROLS_CLASS = 'box-show-preview-controls';
const CONTROLS_BUTTON_CLASS = 'bp-controls-btn';
const CONTROLS_AUTO_HIDE_TIMEOUT_IN_MILLIS = 1500;

class Controls {
/**
* Indicates if the control bar should be hidden or not
*
* @property {boolean}
*/
shouldHide = true;

/**
* Indicates if an element in the controls is focused
*
* @property {boolean}
*/
isFocused = false;

/**
* [constructor]
*
Expand All @@ -29,6 +45,11 @@ class Controls {
this.controlsEl.addEventListener('mouseleave', this.mouseleaveHandler);
this.controlsEl.addEventListener('focusin', this.focusinHandler);
this.controlsEl.addEventListener('focusout', this.focusoutHandler);

if (Browser.hasTouch()) {
this.containerEl.addEventListener('touchstart', this.mousemoveHandler);
this.controlsEl.addEventListener('click', this.clickHandler);
}
}

/**
Expand All @@ -42,6 +63,11 @@ class Controls {
this.controlsEl.removeEventListener('focusin', this.focusinHandler);
this.controlsEl.removeEventListener('focusout', this.focusoutHandler);

if (Browser.hasTouch()) {
this.containerEl.removeEventListener('touchstart', this.mousemoveHandler);
this.controlsEl.removeEventListener('click', this.clickHandler);
}

this.buttonRefs.forEach((ref) => {
ref.button.removeEventListener('click', ref.handler);
});
Expand All @@ -55,7 +81,11 @@ class Controls {
* @return {boolean} true if element is a preview control button
*/
isPreviewControlButton(element) {
return !!element && element.classList.contains('bp-controls-btn');
return (
!!element &&
(element.classList.contains(CONTROLS_BUTTON_CLASS) ||
element.parentNode.classList.contains(CONTROLS_BUTTON_CLASS))
);
}

/**
Expand All @@ -67,7 +97,7 @@ class Controls {
this.controlDisplayTimeoutId = setTimeout(() => {
clearTimeout(this.controlDisplayTimeoutId);

if (this.blockHiding) {
if (!this.shouldHide) {
this.resetTimeout();
} else {
this.containerEl.classList.remove(SHOW_PREVIEW_CONTROLS_CLASS);
Expand Down Expand Up @@ -97,7 +127,7 @@ class Controls {
* @return {void}
*/
mouseenterHandler = () => {
this.blockHiding = true;
this.shouldHide = false;
};

/**
Expand All @@ -107,7 +137,7 @@ class Controls {
* @return {void}
*/
mouseleaveHandler = () => {
this.blockHiding = false;
this.shouldHide = true;
};

/**
Expand All @@ -120,6 +150,8 @@ class Controls {
// When we focus onto a preview control button, show controls
if (this.isPreviewControlButton(event.target)) {
this.containerEl.classList.add(SHOW_PREVIEW_CONTROLS_CLASS);
this.isFocused = true;
this.shouldHide = false;
}
};

Expand All @@ -132,10 +164,23 @@ class Controls {
focusoutHandler = (event) => {
// When we focus out of a control button and aren't focusing onto another control button, hide the controls
if (this.isPreviewControlButton(event.target) && !this.isPreviewControlButton(event.relatedTarget)) {
this.containerEl.classList.remove(SHOW_PREVIEW_CONTROLS_CLASS);
this.isFocused = false;
this.shouldHide = true;
}
};

/**
* Handles click events for the control bar.
*
* @param {Event} event - A DOM-normalized event object.
* @return {void}
*/
clickHandler = (event) => {
event.preventDefault();
// If we are not focused in on the page num input, allow hiding after timeout
this.shouldHide = !this.isFocused;
};

/**
* Adds buttons to controls
*
Expand All @@ -153,7 +198,7 @@ class Controls {
const button = document.createElement('button');
button.setAttribute('aria-label', text);
button.setAttribute('title', text);
button.className = `bp-controls-btn ${classList}`;
button.className = `${CONTROLS_BUTTON_CLASS} ${classList}`;
button.addEventListener('click', handler);

if (buttonContent) {
Expand Down
4 changes: 1 addition & 3 deletions src/lib/Controls.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
transition: opacity .5s;
}

.box-show-preview-controls .bp-controls,
.bp-controls:hover,
.bp-controls:focus {
.box-show-preview-controls .bp-controls {
opacity: 1;
}

Expand Down
46 changes: 35 additions & 11 deletions src/lib/__tests__/Controls-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ describe('lib/Controls', () => {

describe('isPreviewControlButton()', () => {
it('should determine whether the element is a preview control button', () => {
let parent = null;
let element = null;
expect(controls.isPreviewControlButton(element)).to.be.false;

parent = document.createElement('div');
element = document.createElement('div');
element.className = 'bp-controls-btn';
parent.appendChild(element);

expect(controls.isPreviewControlButton(element)).to.be.true;

element.className = '';
Expand All @@ -104,8 +108,8 @@ describe('lib/Controls', () => {
expect(clearTimeoutStub).to.be.calledTwice;
});

it('should call resetTimeout again if block hiding is true', () => {
controls.blockHiding = true;
it('should call resetTimeout again if should hide is false', () => {
controls.shouldHide = false;
controls.resetTimeout();

const resetTimeoutStub = sandbox.stub(controls, 'resetTimeout');
Expand All @@ -114,8 +118,8 @@ describe('lib/Controls', () => {
expect(resetTimeoutStub).to.be.called;
});

it('should not remove the preview controls class if block hiding is true', () => {
controls.blockHiding = true;
it('should not remove the preview controls class if should hide is false', () => {
controls.shouldHide = false;
controls.containerEl.className = SHOW_PREVIEW_CONTROLS_CLASS;

controls.resetTimeout();
Expand All @@ -124,8 +128,8 @@ describe('lib/Controls', () => {
expect(controls.containerEl.classList.contains(SHOW_PREVIEW_CONTROLS_CLASS)).to.be.true;
});

it('should remove the preview controls class if block hiding is false', () => {
controls.blockHiding = false;
it('should remove the preview controls class if should hide is true', () => {
controls.shouldHide = true;
controls.containerEl.className = SHOW_PREVIEW_CONTROLS_CLASS;

controls.resetTimeout();
Expand All @@ -135,7 +139,7 @@ describe('lib/Controls', () => {
});

it('should blur the controls if they are active', () => {
controls.blockHiding = false;
controls.shouldHide = true;
const containsStub = sandbox.stub(controls.controlsEl, 'contains').returns(true);
const blurStub = sandbox.stub(document.activeElement, 'blur');

Expand All @@ -151,25 +155,27 @@ describe('lib/Controls', () => {
it('should make block hiding true', () => {
controls.mouseenterHandler();

expect(controls.blockHiding).to.be.true;
expect(controls.shouldHide).to.be.false;
});
});

describe('mouseleaveHandler()', () => {
it('should make block hiding false', () => {
controls.mouseleaveHandler();

expect(controls.blockHiding).to.be.false;
expect(controls.shouldHide).to.be.true;
});
});

describe('focusinHandler()', () => {
it('should add the controls class if the element is a preview control button', () => {
it('should add the controls class, block hiding, and set the controls to be focused if the element is a preview control button', () => {
const isControlButtonStub = sandbox.stub(controls, 'isPreviewControlButton').returns(true);

controls.focusinHandler('event');
expect(isControlButtonStub).to.be.called;
expect(controls.containerEl.classList.contains(SHOW_PREVIEW_CONTROLS_CLASS)).to.be.true;
expect(controls.shouldHide).to.be.false;
expect(controls.isFocused).to.be.true;
});

it('should not add the controls class if the element is not a preview control button', () => {
Expand All @@ -190,7 +196,8 @@ describe('lib/Controls', () => {

controls.focusoutHandler('event');
expect(isControlButtonStub).to.be.called;
expect(controls.containerEl.classList.contains(SHOW_PREVIEW_CONTROLS_CLASS)).to.be.false;
expect(controls.shouldHide).to.be.true;
expect(controls.isFocused).to.be.false;
});

it('should not remove the controls class if the element is not a preview control button and the related target is not', () => {
Expand Down Expand Up @@ -227,6 +234,23 @@ describe('lib/Controls', () => {
});
});

describe('clickHandler()', () => {
const event = {
preventDefault: sandbox.stub()
};

it('should prevent default', () => {
controls.clickHandler(event);
expect(event.preventDefault).to.be.called;
});

it('should stop block hiding if the controls are not focused', () => {
controls.isFocused = false;
controls.clickHandler(event);
expect(controls.shouldHide).to.be.true;
});
});

describe('add()', () => {
it('should create a button with the right attributes', () => {
const btn = controls.add('test button', sandbox.stub(), 'test1', 'test content');
Expand Down

0 comments on commit 066a3d4

Please sign in to comment.