Skip to content

Commit

Permalink
refactor(hx-menu): surf-1497 use positionable mixin
Browse files Browse the repository at this point in the history
  • Loading branch information
100stacks committed Dec 11, 2018
1 parent 320251b commit ff11e04
Show file tree
Hide file tree
Showing 2 changed files with 13 additions and 172 deletions.
5 changes: 3 additions & 2 deletions docs/elements/hx-menu/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<ul class="hxList">
<li><code>close</code></li>
<li><code>open</code></li>
<li><code>reposition</code></li>
</ul>
</dd>
</div>
Expand All @@ -44,10 +45,10 @@

{% block attributes %}
<dl>
<dt>open</dt>
<dt>open <i>(optional)</i></dt>
<dd>Opens the menu</dd>

<dt>position</dt>
<dt>position <i>(optional)</i></dt>
<dd>Positions the menu</dd>

<dt>relative-to <i>(optional)</i></dt>
Expand Down
180 changes: 10 additions & 170 deletions src/helix-ui/elements/HXMenuElement.js
Original file line number Diff line number Diff line change
@@ -1,193 +1,33 @@
import { HXElement } from './HXElement';
import { getPosition } from '../utils/position';
import debounce from 'lodash/debounce';

const DEFAULT_POSITION = 'bottom-start';
import { mix } from '../utils';
import { Positionable } from '../mixins/Positionable';

/**
* Fires when the element is concealed.
*
* @event Menu:close
* @since 0.6.0
* @type {CustomEvent}
*/

/**
* Fires when the element is revealed.
*
* @event Menu:open
* @since 0.6.0
* @type {CustomEvent}
*/
class _ProtoClass extends mix(HXElement, Positionable) {}

/**
* Defines behavior for the `<hx-menu>` element.
*
* @emits Menu:close
* @emits Menu:open
* @extends HXElement
* @extends Positionable
* @hideconstructor
* @since 0.2.0
*/
export class HXMenuElement extends HXElement {
export class HXMenuElement extends _ProtoClass {
static get is () {
return 'hx-menu';
}

/** @override */
$onCreate () {
this._onDocumentClick = this._onDocumentClick.bind(this);
this._onDocumentScroll = this._onDocumentScroll.bind(this);
this._reposition = this._reposition.bind(this);

this._onWindowResize = debounce(this._reposition, 50);
super.$onCreate();
this.DEFAULT_POSITION = 'bottom-start';
}

/** @override */
$onConnect () {
this.$upgradeProperty('open');
this.$upgradeProperty('position');
this.$upgradeProperty('relativeTo');

this.$defaultAttribute('position', DEFAULT_POSITION);
super.$onConnect();
this.$defaultAttribute('role', 'menu');

this.setAttribute('aria-hidden', !this.open);
this.setAttribute('aria-expanded', this.open);
}

static get $observedAttributes () {
return [ 'open' ];
}

$onAttributeChange (attr, oldVal, newVal) {
if (attr === 'open') {
this._attrOpenChange(oldVal, newVal);
}
}

/**
* External element that controls menu visibility.
* This is commonly a `<hx-disclosure>`.
*
* @readonly
* @type {HTMLElement}
*/
get controlElement () {
return this.getRootNode().querySelector(`[aria-controls="${this.id}"]`);
}

/**
* Determines if the menu is revealed.
*
* @default false
* @type {Boolean}
*/
get open () {
return this.hasAttribute('open');
}
set open (value) {
if (value) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
}

// TODO: Need to re-evaluate how we handle positioning when scrolling
/**
* Where to position the open menu in relation to its reference element.
*
* @default 'bottom-start'
* @type {PositionString}
*/
get position () {
return this.getAttribute('position') || DEFAULT_POSITION;
}
set position (value) {
this.setAttribute('position', value);
}

/**
* Reference element used to calculate open menu position.
*
* @readonly
* @type {HTMLElement}
*/
get relativeElement () {
if (this.relativeTo) {
return this.getRootNode().getElementById(this.relativeTo);
} else {
return this.controlElement;
}
}

/**
* ID of the element to position the menu.
*
* @type {String}
*/
get relativeTo () {
return this.getAttribute('relative-to');
}
set relativeTo (value) {
this.setAttribute('relative-to', value);
}

/** @private */
_addOpenListeners () {
document.addEventListener('click', this._onDocumentClick);
document.addEventListener('scroll', this._onDocumentScroll);
window.addEventListener('resize', this._onWindowResize);
}

/** @private */
_attrOpenChange (oldVal, newVal) {
let isOpen = (newVal !== null);
this.setAttribute('aria-hidden', !isOpen);
this.setAttribute('aria-expanded', isOpen);
this.$emit(isOpen ? 'open' : 'close');

if (isOpen) {
this._addOpenListeners();
this._reposition();
} else {
this._removeOpenListeners();
}
}

/** @private */
_onDocumentClick (evt) {
let isDescendant = this.contains(evt.target);
let withinControl = this.controlElement.contains(evt.target);
let isBackground = (!isDescendant && !withinControl);

if (this.open && isBackground) {
this.open = false;
}
}

/** @private */
_onDocumentScroll () {
this._reposition();
}

/** @private */
_removeOpenListeners () {
document.removeEventListener('click', this._onDocumentClick);
document.removeEventListener('scroll', this._onDocumentScroll);
window.removeEventListener('resize', this._onWindowResize);
}

/** @private */
_reposition () {
if (this.relativeElement) {
let { x, y } = getPosition({
element: this,
reference: this.relativeElement,
position: this.position,
});

this.style.top = `${y}px`;
this.style.left = `${x}px`;
}
}
}

0 comments on commit ff11e04

Please sign in to comment.