Skip to content

Instantly share code, notes, and snippets.

@nbibler
Created August 5, 2016 16:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nbibler/b1e795ee06962b080fee17dee8cc142a to your computer and use it in GitHub Desktop.
Save nbibler/b1e795ee06962b080fee17dee8cc142a to your computer and use it in GitHub Desktop.
tabs
import Ember from 'ember';
import layout from '../templates/components/ivy-tab-list';
/**
* @module ivy-tabs
*/
/**
* @class IvyTabListComponent
* @namespace IvyTabs
* @extends Ember.Component
*/
export default Ember.Component.extend({
layout: layout,
tagName: 'ul',
attributeBindings: ['aria-multiselectable'],
classNames: ['ivy-tab-list'],
init() {
this._super(...arguments);
Ember.run.once(this, this._registerWithTabsContainer);
},
willDestroy() {
this._super(...arguments);
Ember.run.once(this, this._unregisterWithTabsContainer);
},
/**
* Tells screenreaders that only one tab can be selected at a time.
*
* @property aria-multiselectable
* @type String
* @default 'false'
*/
'aria-multiselectable': 'false',
/**
* The `role` attribute of the tab list element.
*
* See http://www.w3.org/TR/wai-aria/roles#tablist
*
* @property ariaRole
* @type String
* @default 'tablist'
*/
ariaRole: 'tablist',
/**
* Gives focus to the selected tab.
*
* @method focusSelectedTab
*/
focusSelectedTab() {
this.get('selectedTab').$().focus();
},
/**
* Event handler for navigating tabs via arrow keys. The left (or up) arrow
* selects the previous tab, while the right (or down) arrow selects the next
* tab.
*
* @method navigateOnKeyDown
* @param {Event} event
*/
navigateOnKeyDown: Ember.on('keyDown', function(event) {
switch (event.keyCode) {
case 37: /* left */
case 38: /* up */
this.selectPreviousTab();
break;
case 39: /* right */
case 40: /* down */
this.selectNextTab();
break;
default:
return;
}
event.preventDefault();
Ember.run.scheduleOnce('afterRender', this, this.focusSelectedTab);
}),
/**
* Adds a tab to the `tabs` array.
*
* @method registerTab
* @param {IvyTabs.IvyTabComponent} tab
*/
registerTab(tab) {
this.get('tabs').pushObject(tab);
},
/**
* Selects the next tab in the list, if any.
*
* @method selectNextTab
*/
selectNextTab() {
let index = this.get('selected-index') + 1;
if (index === this.get('tabs.length')) { index = 0; }
this.selectTabByIndex(index);
},
/**
* Selects the previous tab in the list, if any.
*
* @method selectPreviousTab
*/
selectPreviousTab() {
let index = this.get('selected-index') - 1;
// Previous from the first tab should select the last tab.
if (index < 0) { index = this.get('tabs.length') - 1; }
// This would only happen if there are no tabs, so stay at 0.
if (index < 0) { index = 0; }
this.selectTabByIndex(index);
},
'selected-index': Ember.computed.alias('tabsContainer.selected-index'),
/**
* The currently-selected `ivy-tab` instance.
*
* @property selectedTab
* @type IvyTabs.IvyTabComponent
*/
selectedTab: Ember.computed('selected-index', 'tabs.[]', function() {
return this.get('tabs').objectAt(this.get('selected-index'));
}),
/**
* Select the given tab.
*
* @method selectTab
* @param {IvyTabs.IvyTabComponent} tab
*/
selectTab(tab) {
this.selectTabByIndex(this.get('tabs').indexOf(tab));
},
/**
* Select the tab at `index`.
*
* @method selectTabByIndex
* @param {Number} index
*/
selectTabByIndex(index) {
this.sendAction('on-select', index);
},
tabs: Ember.computed(function() {
return Ember.A();
}).readOnly(),
/**
* The `ivy-tabs` component.
*
* @property tabsContainer
* @type IvyTabs.IvyTabsComponent
* @default null
*/
tabsContainer: null,
/**
* Removes a tab from the `tabs` array.
*
* @method unregisterTab
* @param {IvyTabs.IvyTabComponent} tab
*/
unregisterTab(tab) {
const index = tab.get('index');
this.get('tabs').removeObject(tab);
if (index < this.get('selected-index')) {
this.selectPreviousTab();
} else if (tab.get('isSelected')) {
if (index !== 0) {
this.selectPreviousTab();
}
}
},
_registerWithTabsContainer() {
this.get('tabsContainer').registerTabList(this);
},
_unregisterWithTabsContainer() {
this.get('tabsContainer').unregisterTabList(this);
}
});
import Ember from 'ember';
/**
* @module ivy-tabs
*/
/**
* @class IvyTabPanelComponent
* @namespace IvyTabs
* @extends Ember.Component
*/
export default Ember.Component.extend({
attributeBindings: ['aria-hidden', 'aria-labelledby'],
classNames: ['ivy-tab-panel'],
classNameBindings: ['active'],
init() {
this._super(...arguments);
Ember.run.once(this, this._registerWithTabsContainer);
},
willDestroy() {
this._super(...arguments);
Ember.run.once(this, this._unregisterWithTabsContainer);
},
/**
* Tells screenreaders whether or not the panel is visible.
*
* See http://www.w3.org/TR/wai-aria/states_and_properties#aria-hidden
*
* @property aria-hidden
* @type Boolean
* @readOnly
*/
'aria-hidden': Ember.computed.not('isSelected').readOnly(),
/**
* Tells screenreaders which tab labels this panel.
*
* See http://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby
*
* @property aria-labelledby
* @type String
* @readOnly
*/
'aria-labelledby': Ember.computed.readOnly('tab.elementId'),
/**
* See http://www.w3.org/TR/wai-aria/roles#tabpanel
*
* @property ariaRole
* @type String
* @default 'tabpanel'
*/
ariaRole: 'tabpanel',
/**
* Accessed as a className binding to apply the panel's `activeClass` CSS
* class to the element when the panel's `isSelected` property is true.
*
* @property active
* @type String
* @readOnly
*/
active: Ember.computed('isSelected', function() {
if (this.get('isSelected')) { return this.get('activeClass'); }
}),
/**
* The CSS class to apply to a panel's element when its `isSelected` property
* is `true`.
*
* @property activeClass
* @type String
* @default 'active'
*/
activeClass: 'active',
/**
* The index of this panel in the `ivy-tabs` component.
*
* @property index
* @type Number
*/
index: Ember.computed('tabPanels.[]', function() {
return this.get('tabPanels').indexOf(this);
}),
/**
* Whether or not this panel's associated tab is selected.
*
* @property isSelected
* @type Boolean
* @readOnly
*/
isSelected: Ember.computed.readOnly('tab.isSelected'),
/**
* If `false`, this panel will appear hidden in the DOM. This is an alias to
* `isSelected`.
*
* @property isVisible
* @type Boolean
* @readOnly
*/
isVisible: Ember.computed.readOnly('isSelected'),
/**
* The `ivy-tab` associated with this panel.
*
* @property tab
* @type IvyTabs.IvyTabComponent
*/
tab: Ember.computed('tabs.[]', 'index', function() {
const tabs = this.get('tabs');
if (tabs) { return tabs.objectAt(this.get('index')); }
}),
/**
* The `ivy-tab-list` component this panel belongs to.
*
* @property tabList
* @type IvyTabs.IvyTabListComponent
* @readOnly
*/
tabList: Ember.computed.readOnly('tabsContainer.tabList'),
/**
* The array of all `ivy-tab-panel` instances within the `ivy-tabs`
* component.
*
* @property tabPanels
* @type Array | IvyTabs.IvyTabPanelComponent
* @readOnly
*/
tabPanels: Ember.computed.readOnly('tabsContainer.tabPanels'),
/**
* The array of all `ivy-tab` instances within the `ivy-tab-list` component.
*
* @property tabs
* @type Array | IvyTabs.IvyTabComponent
* @readOnly
*/
tabs: Ember.computed.readOnly('tabList.tabs'),
/**
* The `ivy-tabs` component.
*
* @property tabsContainer
* @type IvyTabs.IvyTabsComponent
* @default null
*/
tabsContainer: null,
_registerWithTabsContainer() {
this.get('tabsContainer').registerTabPanel(this);
},
_unregisterWithTabsContainer() {
this.get('tabsContainer').unregisterTabPanel(this);
}
});
import Ember from 'ember';
/**
* @module ivy-tabs
*/
/**
* @class IvyTabComponent
* @namespace IvyTabs
* @extends Ember.Component
*/
export default Ember.Component.extend({
tagName: 'li',
attributeBindings: ['aria-controls', 'aria-expanded', 'aria-selected', 'selected', 'tabindex'],
classNames: ['ivy-tab'],
classNameBindings: ['active'],
init() {
this._super(...arguments);
Ember.run.once(this, this._registerWithTabList);
},
willDestroy() {
this._super(...arguments);
Ember.run.once(this, this._unregisterWithTabList);
},
/**
* Tells screenreaders which panel this tab controls.
*
* See http://www.w3.org/TR/wai-aria/states_and_properties#aria-controls
*
* @property aria-controls
* @type String
* @readOnly
*/
'aria-controls': Ember.computed.readOnly('tabPanel.elementId'),
/**
* Tells screenreaders whether or not this tab's panel is expanded.
*
* See http://www.w3.org/TR/wai-aria/states_and_properties#aria-expanded
*
* @property aria-expanded
* @type String
* @readOnly
*/
'aria-expanded': Ember.computed.readOnly('aria-selected'),
/**
* Tells screenreaders whether or not this tab is selected.
*
* See http://www.w3.org/TR/wai-aria/states_and_properties#aria-selected
*
* @property aria-selected
* @type String
*/
'aria-selected': Ember.computed('isSelected', function() {
return this.get('isSelected') + ''; // coerce to 'true' or 'false'
}),
/**
* The `role` attribute of the tab element.
*
* See http://www.w3.org/TR/wai-aria/roles#tab
*
* @property ariaRole
* @type String
* @default 'tab'
*/
ariaRole: 'tab',
/**
* The `selected` attribute of the tab element. If the tab's `isSelected`
* property is `true` this will be the literal string 'selected', otherwise
* it will be `undefined`.
*
* @property selected
* @type String
*/
selected: Ember.computed('isSelected', function() {
if (this.get('isSelected')) { return 'selected'; }
}),
/**
* Makes the selected tab keyboard tabbable, and prevents tabs from getting
* focus when clicked with a mouse.
*
* @property tabindex
* @type Number
*/
tabindex: Ember.computed('isSelected', function() {
if (this.get('isSelected')) { return 0; }
}),
/**
* Accessed as a className binding to apply the tab's `activeClass` CSS class
* to the element when the tab's `isSelected` property is true.
*
* @property active
* @type String
* @readOnly
*/
active: Ember.computed('isSelected', function() {
if (this.get('isSelected')) { return this.get('activeClass'); }
}),
/**
* The CSS class to apply to a tab's element when its `isSelected` property
* is `true`.
*
* @property activeClass
* @type String
* @default 'active'
*/
activeClass: 'active',
/**
* The index of this tab in the `ivy-tab-list` component.
*
* @property index
* @type Number
*/
index: Ember.computed('tabs.[]', function() {
return this.get('tabs').indexOf(this);
}),
/**
* Whether or not this tab is selected.
*
* @property isSelected
* @type Boolean
*/
isSelected: Ember.computed('tabList.selectedTab', function() {
return this.get('tabList.selectedTab') === this;
}),
/**
* Called when the user clicks on the tab. Selects this tab.
*
* @method select
*/
select: Ember.on('click', 'touchEnd', function() {
this.get('tabList').selectTab(this);
}),
/**
* The `ivy-tab-list` component this tab belongs to.
*
* @property tabList
* @type IvyTabs.IvyTabListComponent
* @default null
*/
tabList: null,
/**
* The `ivy-tab-panel` associated with this tab.
*
* @property tabPanel
* @type IvyTabs.IvyTabPanelComponent
*/
tabPanel: Ember.computed('tabPanels.[]', 'index', function() {
return this.get('tabPanels').objectAt(this.get('index'));
}),
/**
* The array of all `ivy-tab-panel` instances within the `ivy-tabs`
* component.
*
* @property tabPanels
* @type Array | IvyTabs.IvyTabPanelComponent
* @readOnly
*/
tabPanels: Ember.computed.readOnly('tabsContainer.tabPanels'),
/**
* The array of all `ivy-tab` instances within the `ivy-tab-list` component.
*
* @property tabs
* @type Array | IvyTabs.IvyTabComponent
* @readOnly
*/
tabs: Ember.computed.readOnly('tabList.tabs'),
/**
* The `ivy-tabs` component.
*
* @property tabsContainer
* @type IvyTabs.IvyTabsComponent
* @readOnly
*/
tabsContainer: Ember.computed.readOnly('tabList.tabsContainer'),
_registerWithTabList() {
this.get('tabList').registerTab(this);
},
_unregisterWithTabList() {
this.get('tabList').unregisterTab(this);
}
});
import Ember from 'ember';
import layout from '../templates/components/ivy-tabs';
/**
* @module ivy-tabs
*/
/**
* @class IvyTabsComponent
* @namespace IvyTabs
* @extends Ember.Component
*/
export default Ember.Component.extend({
layout: layout,
classNames: ['ivy-tabs'],
/**
* Set this to the index of the tab you'd like to be selected. Usually it is
* bound to a controller property that is used as a query parameter, but can
* be bound to anything.
*
* @property selected-index
* @type Number
* @default 0
*/
'selected-index': 0,
/**
* Registers the `ivy-tab-list` instance.
*
* @method registerTabList
* @param {IvyTabs.IvyTabListComponent} tabList
*/
registerTabList(tabList) {
this.set('tabList', tabList);
Ember.run.once(this, this._selectTabByIndex);
},
/**
* Adds a panel to the `tabPanels` array.
*
* @method registerTabPanel
* @param {IvyTabs.IvyTabPanelComponent} tabPanel
*/
registerTabPanel(tabPanel) {
this.get('tabPanels').pushObject(tabPanel);
},
tabPanels: Ember.computed(function() {
return Ember.A();
}).readOnly(),
/**
* Removes the `ivy-tab-list` component.
*
* @method unregisterTabList
* @param {IvyTabs.IvyTabListComponent} tabList
*/
unregisterTabList(/* tabList */) {
this.set('tabList', null);
},
/**
* Removes a panel from the `tabPanels` array.
*
* @method unregisterTabPanel
* @param {IvyTabs.IvyTabPanelComponent} tabPanel
*/
unregisterTabPanel(tabPanel) {
this.get('tabPanels').removeObject(tabPanel);
},
_selectTabByIndex() {
let selectedIndex = this.get('selected-index');
if (Ember.isNone(selectedIndex)) { selectedIndex = 0; }
this.get('tabList').selectTabByIndex(selectedIndex);
}
});
import Ember from 'ember';
import IvyTabListComponent from './ivy-tab-list';
export default IvyTabListComponent.extend({
tagName: 'ul',
classNames: ['nav', 'nav-tabs']
});
import Ember from 'ember';
import IvyTabComponent from './ivy-tab';
export default IvyTabComponent.extend({
tagName: 'li',
actions: {
select() {
this.select();
}
},
panelHref: Ember.computed('index', function() {
return '#' + this.get('index');
})
});
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
init() {
this._super(...arguments);
console.log('hi');
this.set('selectedIndex', document.location.hash);
}
});
{{#ivy-tabs on-select=(action (mut selectedIndex)) selected-index=selectedIndex as |tabs|}}
{{#tabs.tablist as |tablist|}}
{{#tablist.tab}}Tab A{{/tablist.tab}}
{{#tablist.tab}}Tab B{{/tablist.tab}}
{{#tablist.tab}}Tab C{{/tablist.tab}}
{{/tabs.tablist}}
<div class="tab-content">
{{#tabs.tabpanel}}
<h2>Tab A</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
{{/tabs.tabpanel}}
{{#tabs.tabpanel}}
<h2>Tab B</h2>
<p>
Tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
{{/tabs.tabpanel}}
{{#tabs.tabpanel}}
<h2>Tab C</h2>
<p>
Veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
Tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim.
</p>
{{/tabs.tabpanel}}
</div>
{{/ivy-tabs}}
{{yield (hash tab=(component "ivy-tab" tabList=this))}}
{{yield (hash tablist=(component "my-ivy-tab-list" on-select=on-select tabsContainer=this) tabpanel=(component "ivy-tab-panel" tabsContainer=this))}}
{{yield (hash tab=(component "my-ivy-tab" tabList=this))}}
<a {{action "select"}} href="{{panelHref}}">{{yield}}</a>
{
"version": "0.10.4",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.7.0",
"ember-data": "2.7.0",
"ember-template-compiler": "2.7.0"
},
"addons": {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment