diff --git a/addon/components/paper-tabs.hbs b/addon/components/paper-tabs.hbs
index 5df13b113..f835f7c25 100644
--- a/addon/components/paper-tabs.hbs
+++ b/addon/components/paper-tabs.hbs
@@ -1,31 +1,76 @@
-{{! template-lint-disable no-action }}
-
+
+
+ {{#if this.shouldPaginate}}
+
+
+
+
+ {{!
+ md-next-button has a css rule that transforms the icon 180 degrees
+ to point the right way
+ }}
+
+
+ {{/if}}
- {{#if this.shouldPaginate}}
-
- {{paper-icon "keyboard-arrow-left"}}
-
-
- {{paper-icon "keyboard-arrow-left"}}
-
- {{/if}}
+
+
+ {{yield
+ (hash
+ tab=(component
+ 'paper-tab'
+ noInk=this.noInk
+ parentComponent=this
+ selected=this.selected
+ onSelect=this.localOnChange
+ )
+ )
+ }}
-
-
-
- {{yield (hash
- tab=(component "paper-tab"
- noInk=this.noInk
- selected=this.selected
- onSelect=(action "localOnChange")
- )
- )}}
-
- {{#if this.inkBar}}
-
- {{/if}}
-
-
-
-
-
\ No newline at end of file
+ {{#if this.inkBar}}
+ {{#let this.inkBar as |inkBar|}}
+
+ {{/let}}
+ {{/if}}
+
+
+
+
\ No newline at end of file
diff --git a/addon/components/paper-tabs.js b/addon/components/paper-tabs.js
index 5f46e1532..6aa617a81 100644
--- a/addon/components/paper-tabs.js
+++ b/addon/components/paper-tabs.js
@@ -1,93 +1,281 @@
-/* eslint-disable ember/classic-decorator-no-classic-methods, ember/no-classic-components, ember/no-computed-properties-in-native-classes, ember/no-get */
-import {
- classNames,
- classNameBindings,
- attributeBindings,
- tagName,
-} from '@ember-decorators/component';
-import { action, computed } from '@ember/object';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { A } from '@ember/array';
+import { action } from '@ember/object';
import { inject as service } from '@ember/service';
-import { gt } from '@ember/object/computed';
-import Component from '@ember/component';
import { htmlSafe } from '@ember/template';
-import { scheduleOnce, join } from '@ember/runloop';
-import { ParentMixin } from 'ember-composability-tools';
-import { invokeAction } from 'ember-paper/utils/invoke-action';
-
-@tagName('md-tabs')
-@classNames('md-no-tab-content', 'md-default-theme')
-@classNameBindings('warn:md-warn', 'accent:md-accent', 'primary:md-primary')
-@attributeBindings('borderBottom:md-border-bottom')
-export default class PaperTabs extends Component.extend(ParentMixin) {
- @service
- constants;
-
- selected = 0; // select first tab by default
- noInkBar = false;
- noInk = false;
- ariaLabel = null;
- stretch = 'sm';
- movingRight = true;
-
- @computed('noInkBar', '_selectedTab.{width,left}', 'wrapperWidth')
+
+export default class PaperTabs extends Component {
+ @service constants;
+ @service fastboot;
+
+ /**
+ * Reference to the component's DOM element.
+ *
+ * @type {HTMLElement}
+ * @private
+ */
+ @tracked element;
+ /**
+ * Reference to the component's md-tabs-canvas DOM element.
+ *
+ * @type {HTMLElement}
+ * @private
+ */
+ @tracked elementTabsCanvas;
+ /**
+ * Reference to the component's md-pagination-wrapper DOM element.
+ *
+ * @type {HTMLElement}
+ * @private
+ */
+ @tracked elementPaginationWrapper;
+ /**
+ * Array of tab components.
+ *
+ * @type {A}
+ */
+ @tracked children;
+ /**
+ * tracks the outer width of the element displaying the tabs.
+ *
+ * `>[[ tab ][ tab ] ]<`
+ *
+ * @type {number}
+ */
+ @tracked canvasWidth;
+ /**
+ * tracks the width of the tabs within the tab canvas. See {@link canvasWidth}.
+ *
+ * `[>[ tab ][ tab ]< ]`
+ *
+ * @type {number}
+ */
+ @tracked wrapperWidth;
+ /**
+ * used to offset tabs based on pagination.
+ *
+ * `>[ tab ][ tab ]< [ [<-] [ tab ] [->] ]`
+ *
+ * @type {number}
+ */
+ @tracked currentOffset;
+ /**
+ * holds a reference to the currently selected tab.
+ *
+ * @type {PaperTab}
+ * @private
+ */
+ @tracked selectedTab = null;
+ /**
+ * tracks the direction in which inkbar should be animated.
+ *
+ * @type {boolean}
+ */
+ @tracked movingRight = true;
+ /**
+ * set to true if the tab bar should enable paginating through tabs via
+ * left/right navigation arrows.
+ *
+ * @type {boolean}
+ */
+ @tracked shouldPaginate = false;
+ /**
+ * _selected provides auto-tracking of selected tabs, this is kept private, so
+ * that {@link selected} can output either the auto-tracked number, or a
+ * user supplied value.
+ *
+ * @type {number|any}
+ * @private
+ * @default 0 - the first tab when using auto-numbering.
+ */
+ @tracked _selected;
+
+ constructor(owner, args) {
+ super(owner, args);
+
+ this.children = A([]);
+ this.canvasWidth = 0;
+ this.wrapperWidth = 0;
+ this.currentOffset = 0;
+ this._selected = 0;
+ }
+
+ /**
+ * Performs any required DOM setup.
+ * @param element
+ */
+ @action didInsertNode(element) {
+ this.element = element;
+ this.elementTabsCanvas = element.querySelector('md-tabs-canvas');
+ this.elementPaginationWrapper = element.querySelector(
+ 'md-pagination-wrapper'
+ );
+
+ window.addEventListener('resize', this.updateCanvasWidth);
+ window.addEventListener('orientationchange', this.updateCanvasWidth);
+
+ // Do an initial sizing computation.
+ this.didUpdateNode(element);
+ }
+
+ @action didUpdateNode() {
+ // this makes sure that the tabs react to stretch and center changes
+ // this method is also called whenever one of the tab is re-rendered (content changes)
+ this.updateCanvasWidth();
+ this.updateSelectedTab();
+ this.fixOffsetIfNeeded();
+ }
+
+ /**
+ * Performs any required DOM teardown.
+ * @param {HTMLElement} element
+ */
+ @action willDestroyNode() {
+ window.removeEventListener('resize', this.updateCanvasWidth);
+ window.removeEventListener('orientationchange', this.updateCanvasWidth);
+ }
+
+ /**
+ * Registers a child form component
+ * @param {PaperTab} child - The paper tab component to register
+ */
+ @action registerChild(child) {
+ // automatically set value if not manually set
+ if (child.value === undefined) {
+ child.value = this.children.length;
+ }
+
+ this.children.pushObject(child);
+ }
+ /**
+ * Removes a registered child form component
+ * @param {Component} child - The form component to unregister
+ */
+ @action unregisterChild(child) {
+ this.children.removeObject(child);
+ }
+
+ /**
+ * whether we can allow paginating to a previous number of tabs to display.
+ *
+ * @returns {boolean}
+ */
+ get canPageBack() {
+ return this.currentOffset > 0;
+ }
+
+ /**
+ * whether we can allow paginating to the next number of tabs to display.
+ * @returns {boolean}
+ */
+ get canPageForward() {
+ return this.wrapperWidth - this.currentOffset > this.canvasWidth;
+ }
+
+ /**
+ * disables the animated bar that sits at the bottom of the currently
+ * selected tab.
+ *
+ * @type {boolean}
+ */
+ get noInkBar() {
+ return this.args.noInkBar || false;
+ }
+
+ /**
+ * passes down to the individual tabs.
+ *
+ * @type {boolean}
+ */
+ get noInk() {
+ return this.args.noInk || false;
+ }
+
get inkBar() {
if (this.noInkBar) {
return null;
}
- let selectedTab = this._selectedTab;
- if (!selectedTab || selectedTab.get('left') === undefined) {
+ let selectedTab = this.selectedTab;
+ if (!selectedTab || selectedTab.left === undefined) {
return null;
}
return {
- left: selectedTab.get('left'),
- right:
- this.wrapperWidth - selectedTab.get('left') - selectedTab.get('width'),
+ left: selectedTab.left,
+ right: this.wrapperWidth - selectedTab.left - selectedTab.width,
};
}
- @computed('currentOffset')
+ /**
+ * returns a 3d translate based on the number of tabs to offset by depending
+ * on the current pagination.
+ *
+ * @returns {string}
+ */
get paginationStyle() {
return htmlSafe(
`transform: translate3d(-${this.currentOffset}px, 0px, 0px);`
);
}
- shouldPaginate = true;
+ /**
+ * Returns a user supplied value, or the auto-tracked tab selection via
+ * {@link _selected}.
+ *
+ * @returns {any|number}
+ */
+ get selected() {
+ return this.args.selected || this._selected;
+ }
- @computed('shouldPaginate', 'center')
+ /**
+ * returns true if the tabs should be centered.
+ *
+ * Only applicable if the number of tabs don't overflow, causing pagination
+ * to be enabled.
+ *
+ * @returns {boolean}
+ */
get shouldCenter() {
- return !this.shouldPaginate && this.center;
+ let center = this.args.center ?? false;
+ return !this.shouldPaginate && center;
}
- @computed('shouldPaginate', 'currentStretch')
+ /**
+ * returns true if the tabs should be stretched based on either matching a
+ * provided media query {e.g. sm, md} or being set to true.
+ *
+ * Only applicable if the number of tabs don't overflow, causing pagination
+ * to be enabled.
+ *
+ * @returns {boolean}
+ */
get shouldStretch() {
- return !this.shouldPaginate && this.currentStretch;
- }
-
- didInsertElement() {
- super.didInsertElement(...arguments);
+ let stretch = this.args.stretch ?? 'sm';
+ let shouldStretch;
- this.updateCanvasWidth = () => {
- join(() => {
- this.updateDimensions();
- this.updateStretchTabs();
- });
- };
-
- window.addEventListener('resize', this.updateCanvasWidth);
- window.addEventListener('orientationchange', this.updateCanvasWidth);
+ // if `true` or `false` is specified, always/never "stretch tabs"
+ // otherwise proceed with normal matchMedia test
+ if (typeof stretch === 'boolean') {
+ shouldStretch = stretch;
+ } else {
+ let mediaQuery = this.constants.MEDIA[stretch] || stretch;
+ shouldStretch = !this.fastboot.isFastBoot
+ ? window.matchMedia(mediaQuery).matches
+ : false;
+ }
- scheduleOnce('afterRender', this, this.fixOffsetIfNeeded);
+ return !this.shouldPaginate && shouldStretch;
}
- didRender() {
- super.didRender(...arguments);
- // this makes sure that the tabs react to stretch and center changes
- // this method is also called whenever one of the tab is re-rendered (content changes)
- this.updateSelectedTab();
- this.updateCanvasWidth();
+ /**
+ * forces re-computation of element widths and tab offset.
+ */
+ @action updateCanvasWidth() {
+ this.updateDimensions();
+ this.fixOffsetIfNeeded();
}
/**
@@ -97,53 +285,39 @@ export default class PaperTabs extends Component.extend(ParentMixin) {
* nested because we pass the 'selected' property to them that will
* invalidate their 'isSelected' property.
*/
- updateSelectedTab() {
- let selectedTab = this.childComponents.findBy('isSelected');
- let previousSelectedTab = this._selectedTab;
-
+ @action updateSelectedTab() {
+ let selectedTab = this.children.findBy('isSelected');
+ let previousSelectedTab = this.selectedTab;
if (selectedTab === previousSelectedTab) {
return;
}
- this.set(
- 'movingRight',
+ this.movingRight =
!selectedTab ||
- !previousSelectedTab ||
- previousSelectedTab.get('left') < selectedTab.get('left')
- );
- this.set('_selectedTab', selectedTab);
-
- scheduleOnce('afterRender', this, this.fixOffsetIfNeeded);
+ !previousSelectedTab ||
+ previousSelectedTab.left < selectedTab.left;
+ this.selectedTab = selectedTab;
}
- willDestroyElement() {
- super.willDestroyElement(...arguments);
- window.removeEventListener('resize', this.updateCanvasWidth);
- window.removeEventListener('orientationchange', this.updateCanvasWidth);
- }
-
- registerChild(childComponent) {
- super.registerChild(...arguments);
- // automatically set value if not manually set
- if (childComponent.get('value') === undefined) {
- let length = this.childComponents.get('length');
- childComponent.set('value', length - 1);
- }
- }
-
- fixOffsetIfNeeded() {
- if (this.isDestroying || this.isDestroyed) {
+ /**
+ * updates the pagination tab offset if needed.
+ */
+ @action fixOffsetIfNeeded() {
+ if (this.isDestroying || this.isDestroyed || !this.selectedTab) {
+ // Don't attempt to compute if elements have not been added or are being
+ // removed from the DOM.
return;
}
let canvasWidth = this.canvasWidth;
let currentOffset = this.currentOffset;
- let tabLeftOffset = this.get('_selectedTab.left');
- let tabRightOffset = tabLeftOffset + this.get('_selectedTab.width');
+ let { left, width } = this.selected;
+ let tabLeftOffset = left;
+ let tabRightOffset = tabLeftOffset + width;
let newOffset;
- if (canvasWidth < this.get('_selectedTab.width')) {
+ if (canvasWidth < width) {
// align with selectedTab if canvas smaller than selected tab
newOffset = tabLeftOffset;
} else if (tabRightOffset - currentOffset > canvasWidth) {
@@ -160,81 +334,71 @@ export default class PaperTabs extends Component.extend(ParentMixin) {
return;
}
- this.set('currentOffset', newOffset);
+ this.currentOffset = newOffset;
}
- updateDimensions() {
- let canvasWidth = this.element.querySelector('md-tabs-canvas').offsetWidth;
- let wrapperWidth = this.element.querySelector(
- 'md-pagination-wrapper'
- ).offsetWidth;
- this.childComponents.invoke('updateDimensions');
- this.set('canvasWidth', canvasWidth);
- this.set('wrapperWidth', wrapperWidth);
- this.set('shouldPaginate', wrapperWidth > canvasWidth);
- }
-
- updateStretchTabs() {
- let stretch = this.stretch;
- let currentStretch;
-
- // if `true` or `false` is specified, always/never "stretch tabs"
- // otherwise proceed with normal matchMedia test
- if (typeof stretch === 'boolean') {
- currentStretch = stretch;
- } else {
- let mediaQuery = this.constants.MEDIA[stretch] || stretch;
- currentStretch = window.matchMedia(mediaQuery).matches;
+ /**
+ * sets widths based on the current tab canvas and pagination wrapper
+ * elements.
+ */
+ @action updateDimensions() {
+ if (!this.element) {
+ // node not added to the DOM yet...
+ return;
}
- this.set('currentStretch', currentStretch);
- }
-
- currentOffset = 0;
-
- @gt('currentOffset', 0)
- canPageBack;
-
- @computed('wrapperWidth', 'currentOffset', 'canvasWidth')
- get canPageForward() {
- return this.wrapperWidth - this.currentOffset > this.canvasWidth;
+ let canvasWidth = this.elementTabsCanvas.offsetWidth;
+ let wrapperWidth = this.elementPaginationWrapper.offsetWidth;
+ this.children.forEach((c) => c.updateDimensions());
+ this.canvasWidth = canvasWidth;
+ this.wrapperWidth = wrapperWidth;
+ this.shouldPaginate = wrapperWidth > canvasWidth;
}
- @action
- previousPage() {
- let tab = this.childComponents.find((t) => {
- // ensure we are no stuck because of a tab with a width > canvasWidth
- return t.get('left') + t.get('width') >= this.currentOffset;
+ /**
+ * computes if a tab offset is required when paginating backwards.
+ */
+ @action previousPage() {
+ let tab = this.children.find((t) => {
+ // ensure we are not stuck because of a tab with a width > canvasWidth
+ return t.left + t.width >= this.currentOffset;
});
if (tab) {
- let left = Math.max(0, tab.get('left') - this.canvasWidth);
- this.set('currentOffset', left);
+ this.currentOffset = Math.max(0, tab.left - this.canvasWidth);
}
}
- @action
- nextPage() {
- let tab = this.childComponents.find((t) => {
- // ensure tab's offset is greater than current
- // otherwise if the tab's width is greater than canvas we cannot paginate through it
+ /**
+ * computes if a tab offset is required when paginating forwards.
+ */
+ @action nextPage() {
+ let tab = this.children.find((t) => {
+ // ensure tab's offset is greater than current otherwise if the tab's
+ // width is greater than canvas we cannot paginate through it.
return (
- t.get('left') > this.currentOffset &&
+ t.left > this.currentOffset &&
// paginate until the first partially hidden tab
- t.get('left') + t.get('width') - this.currentOffset > this.canvasWidth
+ t.left + t.width - this.currentOffset > this.canvasWidth
);
});
if (tab) {
- this.set('currentOffset', tab.get('left'));
+ this.currentOffset = tab.left;
}
}
- @action
- localOnChange(selected) {
+ /**
+ * sets the current selected tab value, or passes the value up to the user to
+ * pass back down.
+ *
+ * @param {PaperTab} selected
+ */
+ @action localOnChange(selected) {
// support non DDAU scenario
- if (this.onChange) {
- invokeAction(this, 'onChange', selected.get('value'));
+ if (this.args.onChange) {
+ this.args.onChange(selected.value);
} else {
- this.set('selected', selected.get('value'));
+ // update our private reference
+ this._selected = selected.value;
}
}
}