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|}}
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