diff --git a/addon/components/-focusable.js b/addon/components/-focusable.js new file mode 100644 index 000000000..16fd249fd --- /dev/null +++ b/addon/components/-focusable.js @@ -0,0 +1,178 @@ +/** + * @module ember-paper + */ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +/** + * @class Focusable + * @extends @glimmer/component + * + * When extending from Focusable it is expected that md-focused be implemented + * on the top level tag along with setting tabindex and disabled. This component + * listens to a large number of events, therefore render listener register + * functions have been created to ease usage. Clearly, this is non-optimal and + * only the listeners that are required should be added using the `on` modifier. + * + * Use the following as a base: + * ```hbs + * + * + * ``` + */ +export default class Focusable extends Component { + @tracked pressed = false; + @tracked active = false; + @tracked focused = false; + @tracked hover = false; + + // classNameBindings: ['focused:md-focused'], + // attributeBindings: ['tabindex', 'disabledAttr:disabled'], + + get disabled() { + return this.args.disabled || false; + } + + toggle = false; + + // Only render the "focused" state if the element gains focus due to + // keyboard navigation. + get focusOnlyOnKey() { + return this.args.focusOnlyOnKey || false; + } + + @action registerListeners(element) { + element.addEventListener('focusin', this.handleFocusIn); + element.addEventListener('focusout', this.handleFocusOut); + element.addEventListener('mousedown', this.handleMouseDown); + element.addEventListener('mouseenter', this.handleMouseEnter); + element.addEventListener('mouseleave', this.handleMouseLeave); + element.addEventListener('mousemove', this.handleMouseMove); + element.addEventListener('mouseup', this.handleMouseUp); + element.addEventListener('pointermove', this.handlePointerMove); + // Set all touch events as passive listeners to remove scroll jank on + // mobile devices. + // refer: https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md + element.addEventListener('touchcancel', this.handleTouchCancel, { + passive: true, + }); + element.addEventListener('touchend', this.handleTouchEnd, { + passive: true, + }); + element.addEventListener('touchmove', this.handleTouchMove, { + passive: true, + }); + element.addEventListener('touchstart', this.handleTouchStart, { + passive: true, + }); + } + + @action unregisterListeners(element) { + element.removeEventListener('focusin', this.handleFocusIn); + element.removeEventListener('focusout', this.handleFocusOut); + element.removeEventListener('mousedown', this.handleMouseDown); + element.removeEventListener('mouseenter', this.handleMouseEnter); + element.removeEventListener('mouseleave', this.handleMouseLeave); + element.removeEventListener('mousemove', this.handleMouseMove); + element.removeEventListener('mouseup', this.handleMouseUp); + element.removeEventListener('pointermove', this.handlePointerMove); + element.removeEventListener('touchcancel', this.handleTouchCancel); + element.removeEventListener('touchend', this.handleTouchEnd); + element.removeEventListener('touchmove', this.handleTouchMove); + element.removeEventListener('touchstart', this.handleTouchStart); + } + + /* + * Listen to `focusIn` and `focusOut` events instead of `focus` and `blur`. + * This way we don't need to explicitly bubble the events. + * They bubble by default. + */ + @action handleFocusIn(e) { + if ((!this.disabled && !this.focusOnlyOnKey) || !this.pressed) { + this.focused = true; + if (this.args.onFocusIn) { + this.args.onFocusIn(e); + } + } + } + + @action handleFocusOut(e) { + this.focused = false; + if (this.args.onFocusOut) { + this.args.onFocusOut(e); + } + } + + @action handleMouseDown(e) { + this.down(e); + if (this.args.onMouseDown) { + this.args.onMouseDown(e); + } + } + + @action handleMouseEnter(e) { + this.hover = true; + if (this.args.onMouseEnter) { + this.args.onMouseEnter(e); + } + } + + @action handleMouseLeave(e) { + this.hover = false; + this.up(e); + if (this.args.onMouseLeave) { + this.args.onMouseLeave(e); + } + } + + @action handleMouseMove(e) { + return this.move(e); + } + + @action handleMouseUp(e) { + return this.up(e); + } + + @action handlePointerMove(e) { + return this.move(e); + } + + @action handleTouchCancel(e) { + return this.up(e); + } + + @action handleTouchEnd(e) { + return this.up(e); + } + + @action handleTouchMove(e) { + return this.move(e); + } + + @action handleTouchStart(e) { + return this.down(e); + } + + @action up() { + this.pressed = false; + if (!this.toggle) { + this.active = false; + } + } + + @action down() { + this.pressed = true; + if (this.toggle) { + this.active = !this.active; + } else { + this.active = true; + } + } + + move(e) { + if (this.args.move) { + this.args.move(e); + } + } +} diff --git a/addon/components/paper-button.hbs b/addon/components/paper-button.hbs index 79afe4288..2b9f2b85f 100644 --- a/addon/components/paper-button.hbs +++ b/addon/components/paper-button.hbs @@ -1,11 +1,39 @@ -{{! template-lint-disable no-curly-component-invocation }} -{{#if (has-block)}} - {{yield}} -{{else}} - {{@label}} -{{/if}} +{{#let (element this.tag) as |Tag|}} + + {{#if (has-block)}} + {{yield}} + {{else}} + {{@label}} + {{/if}} - + + +{{/let}} \ No newline at end of file diff --git a/addon/components/paper-button.js b/addon/components/paper-button.js index 3314b6963..efa94f574 100644 --- a/addon/components/paper-button.js +++ b/addon/components/paper-button.js @@ -1,59 +1,153 @@ -/* eslint-disable ember/no-classic-components, ember/no-mixins, ember/require-tagless-components */ /** * @module ember-paper */ -import { reads } from '@ember/object/computed'; - -import Component from '@ember/component'; -import FocusableMixin from 'ember-paper/mixins/focusable-mixin'; -import ProxiableMixin from 'ember-paper/mixins/proxiable-mixin'; -import { invokeAction } from 'ember-paper/utils/invoke-action'; +import Focusable from './-focusable'; +import { action } from '@ember/object'; +import { assert } from '@ember/debug'; /** * @class PaperButton - * @extends Ember.Component - * @uses FocusableMixin - * @uses ProxiableMixin + * @extends Focusable */ -export default Component.extend(FocusableMixin, ProxiableMixin, { - tagName: 'button', - classNames: ['md-default-theme', 'md-button'], - raised: false, - iconButton: false, - - // circular button - fab: reads('mini'), - - mini: false, - type: 'button', - href: null, - target: null, - - attributeBindings: ['type', 'href', 'target', 'title', 'download', 'rel'], - - classNameBindings: [ - 'raised:md-raised', - 'iconButton:md-icon-button', - 'fab:md-fab', - 'mini:md-mini', - 'warn:md-warn', - 'accent:md-accent', - 'primary:md-primary', - ], - - init() { - this._super(...arguments); - if (this.href) { - this.setProperties({ - tagName: 'a', - type: null, - }); - } - }, - - click(e) { - invokeAction(this, 'onClick', e); +export default class PaperButton extends Focusable { + /** + * Reference to the component's DOM element + * @type {HTMLElement} + */ + element; + /** + * The parent this component is bound to. + * @type {PaperForm|PaperItem|PaperTabs} + */ + parent; + /** + * Marks whether the component should register itself to the supplied parent + * @type {Boolean} + */ + shouldRegister; + /** + * Marks whether the component should skip being proxied. + * @type {Boolean} + */ + skipProxy; + + constructor(owner, args) { + super(owner, args); + + this.shouldRegister = this.args.shouldRegister || false; + this.skipProxy = this.args.skipProxy || false; + if (this.shouldRegister) { + assert( + 'A parent component should be supplied to when shouldRegister=true', + this.args.parentComponent + ); + this.parent = this.args.parentComponent; + } + } + + /** + * Performs any required DOM setup. + * @param element + */ + @action didInsertNode(element) { + this.element = element; + this.registerListeners(element); + + if (this.shouldRegister) { + this.parent.registerChild(this); + } + } + + /** + * Performs DOM updates based on tracked args. + */ + @action didUpdateNode() { + // noop + } + + /** + * Performs any required DOM teardown. + * @param element + */ + @action willDestroyNode(element) { + this.unregisterListeners(element); + } + + willDestroy() { + super.willDestroy(...arguments); + + if (this.shouldRegister) { + this.parent.unregisterChild(this); + } + } + + get tag() { + if (this.args.href) { + return 'a'; + } + + return 'button'; + } + + get type() { + if (this.args.type) { + return this.args.type; + } + + return 'button'; + } + + get fab() { + return this.args.fab || this.args.mini; + } + + get bubbles() { + return this.args.bubbles === undefined || this.args.bubbles; + } + + @action handleClick(e) { + if (this.args.onClick) { + this.args.onClick(e); + } + // Prevent bubbling, if specified. If undefined, the event will bubble. - return this.bubbles; - }, -}); + if (!this.bubbles && e) { + e.stopPropagation(); + } + } + + // Proxiable Handlers + + @action handleMouseDown(e) { + super.handleMouseDown(e); + + let parentComponent = this.parentComponent; + if (parentComponent) { + parentComponent.mouseActive = true; + setTimeout(() => { + if (parentComponent.isDestroyed) { + return; + } + parentComponent.mouseActive = false; + }, 100); + } + } + + @action handleFocusIn(e) { + super.handleFocusIn(e); + + let parentComponent = this.parent; + if (parentComponent && !parentComponent.mouseActive) { + parentComponent.focused = true; + } + } + + @action focusOut(e) { + super.focusOut(e); + + let parentComponent = this.parent; + if (parentComponent) { + parentComponent.focused = false; + } + } +} diff --git a/addon/components/paper-item.js b/addon/components/paper-item.js index 99254e445..c40c678bc 100644 --- a/addon/components/paper-item.js +++ b/addon/components/paper-item.js @@ -38,7 +38,7 @@ export default class PaperItem extends Component.extend(ParentMixin) { tabindex = '-1'; @filter('childComponents', function (c) { - return !c.get('skipProxy'); + return !c.skipProxy; }) proxiedComponents; @@ -86,8 +86,8 @@ export default class PaperItem extends Component.extend(ParentMixin) { this.proxiedComponents.forEach((component) => { if ( component.processProxy && - !component.get('disabled') && - component.get('bubbles') | !this.hasPrimaryAction + !component.disabled && + component.bubbles | !this.hasPrimaryAction ) { component.processProxy(); } diff --git a/tests/dummy/app/templates/catalog.hbs b/tests/dummy/app/templates/catalog.hbs index 2b603fe04..ff02d6e21 100644 --- a/tests/dummy/app/templates/catalog.hbs +++ b/tests/dummy/app/templates/catalog.hbs @@ -1297,7 +1297,7 @@ Toggle left sidenav diff --git a/tests/dummy/app/templates/demo/divider.hbs b/tests/dummy/app/templates/demo/divider.hbs index 0900087b1..c3e599eab 100644 --- a/tests/dummy/app/templates/demo/divider.hbs +++ b/tests/dummy/app/templates/demo/divider.hbs @@ -51,7 +51,7 @@ {{! BEGIN-SNIPPET divider.inset }} - + {{#each this.messages as |item index|}} {{item.who}} diff --git a/tests/dummy/app/templates/demo/sidenav.hbs b/tests/dummy/app/templates/demo/sidenav.hbs index cde678cfe..741d4f4bb 100644 --- a/tests/dummy/app/templates/demo/sidenav.hbs +++ b/tests/dummy/app/templates/demo/sidenav.hbs @@ -45,7 +45,7 @@

- + Toggle left sidenav