diff --git a/packages/@nucleus/package.json b/packages/@nucleus/package.json index 335d8032..86c5bde9 100644 --- a/packages/@nucleus/package.json +++ b/packages/@nucleus/package.json @@ -27,6 +27,7 @@ "@freshworks/inline-banner": "^0.5.4", "@freshworks/modal": "^0.5.4", "@freshworks/toast-message": "^0.6.4", + "@freshworks/tabs": "^0.1.0", "ember-cli-autoprefixer": "^0.8.1", "ember-cli-babel": "^7.11.1", "ember-cli-htmlbars": "^4.0.0", diff --git a/packages/@nucleus/tests/dummy/app/components/nucleus-tabs/demo-1.js b/packages/@nucleus/tests/dummy/app/components/nucleus-tabs/demo-1.js new file mode 100644 index 00000000..54b22ed0 --- /dev/null +++ b/packages/@nucleus/tests/dummy/app/components/nucleus-tabs/demo-1.js @@ -0,0 +1,17 @@ +// BEGIN-SNIPPET nucleus-tabs-2.js +import Component from '@ember/component'; +import { inject } from '@ember/service'; +import { get } from '@ember/object'; + +export default Component.extend({ + flashMessages: inject(), + actions: { + onChange(changedTo) { // changedTo, ChangedFrom, event params accepted + const flashMessages = get(this, 'flashMessages'); + flashMessages.info(`Custom action invoked. '${changedTo}' tab selected!`, { + timeout: 1500 + }); + } + } +}); +// END-SNIPPET diff --git a/packages/@nucleus/tests/dummy/app/components/nucleus-tabs/demo-2.js b/packages/@nucleus/tests/dummy/app/components/nucleus-tabs/demo-2.js new file mode 100644 index 00000000..ea26f50c --- /dev/null +++ b/packages/@nucleus/tests/dummy/app/components/nucleus-tabs/demo-2.js @@ -0,0 +1,29 @@ +// BEGIN-SNIPPET nucleus-tabs-3.js +import Component from '@ember/component'; +import { inject } from '@ember/service'; +import RSVP from 'rsvp'; +import { get } from '@ember/object'; + +export default Component.extend({ + flashMessages: inject(), + actions: { + beforeChange() { // changedTo, changedFrom, event params accepted + const flashMessages = get(this, 'flashMessages'); + flashMessages.info(`Performing asyncronous operation..`, { + timeout: 1500 + }); + return new RSVP.Promise(function(resolve) { + setTimeout(function () { + resolve(); + }, 2000); + }); + }, + onChange() { // changedTo, ChangedFrom, event params accepted + const flashMessages = get(this, 'flashMessages'); + flashMessages.info(`Tab changed.`, { + timeout: 1500 + }); + } + } +}); +// END-SNIPPET diff --git a/packages/@nucleus/tests/dummy/app/router.js b/packages/@nucleus/tests/dummy/app/router.js index 9de255c1..9823652c 100644 --- a/packages/@nucleus/tests/dummy/app/router.js +++ b/packages/@nucleus/tests/dummy/app/router.js @@ -20,6 +20,7 @@ Router.map(function() { this.route("nucleus-modal"); this.route("nucleus-toast-message"); this.route("nucleus-banner"); + this.route("nucleus-tabs"); }); this.route('not-found', { path: '/*path' }); diff --git a/packages/@nucleus/tests/dummy/app/templates/components/nucleus-tabs/demo-1.hbs b/packages/@nucleus/tests/dummy/app/templates/components/nucleus-tabs/demo-1.hbs new file mode 100644 index 00000000..14e98cdc --- /dev/null +++ b/packages/@nucleus/tests/dummy/app/templates/components/nucleus-tabs/demo-1.hbs @@ -0,0 +1,20 @@ +{{#docs-demo as |demo|}} + {{#demo.example name="nucleus-tabs-2.hbs" }} + {{#nucleus-tabs + description="site-navigation" + onChange=(action "onChange") as |tabs| }} + {{#tabs.panel name="I want apples" }} +
This is apples section
+ {{/tabs.panel}} + {{#tabs.panel name="I want oranges" }} +
This is oranges section
+ {{/tabs.panel}} + {{#tabs.panel name="I want grapes" }} +
This is grapes section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + {{/demo.example}} + + {{demo.snippet "nucleus-tabs-2.hbs"}} + {{demo.snippet "nucleus-tabs-2.js" label="component.js"}} +{{/docs-demo}} diff --git a/packages/@nucleus/tests/dummy/app/templates/components/nucleus-tabs/demo-2.hbs b/packages/@nucleus/tests/dummy/app/templates/components/nucleus-tabs/demo-2.hbs new file mode 100644 index 00000000..c6098491 --- /dev/null +++ b/packages/@nucleus/tests/dummy/app/templates/components/nucleus-tabs/demo-2.hbs @@ -0,0 +1,22 @@ +{{#docs-demo as |demo|}} + {{#demo.example name="nucleus-tabs-3.hbs" }} + {{#nucleus-tabs + description="site-navigation" + select="I want apples" + beforeChange=(action "beforeChange") + onChange=(action "onChange") as |tabs| }} + {{#tabs.panel name="I want apples" }} +
This is apples section
+ {{/tabs.panel}} + {{#tabs.panel name="I want oranges" }} +
This is oranges section
+ {{/tabs.panel}} + {{#tabs.panel name="I want grapes" }} +
This is grapes section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + {{/demo.example}} + + {{demo.snippet "nucleus-tabs-3.hbs"}} + {{demo.snippet "nucleus-tabs-3.js" label="component.js"}} +{{/docs-demo}} diff --git a/packages/@nucleus/tests/dummy/app/templates/docs.hbs b/packages/@nucleus/tests/dummy/app/templates/docs.hbs index e9e64633..ec66d07d 100644 --- a/packages/@nucleus/tests/dummy/app/templates/docs.hbs +++ b/packages/@nucleus/tests/dummy/app/templates/docs.hbs @@ -11,6 +11,7 @@ {{nav.item "Inline Banner" "docs.components.nucleus-inline-banner"}} {{nav.item "Modal" "docs.components.nucleus-modal"}} {{nav.item "Toast message" "docs.components.nucleus-toast-message"}} + {{nav.item "Tabs" "docs.components.nucleus-tabs"}} {{/viewer.nav}} {{#viewer.main}} diff --git a/packages/@nucleus/tests/dummy/app/templates/docs/components/nucleus-tabs.md b/packages/@nucleus/tests/dummy/app/templates/docs/components/nucleus-tabs.md new file mode 100644 index 00000000..a88bb093 --- /dev/null +++ b/packages/@nucleus/tests/dummy/app/templates/docs/components/nucleus-tabs.md @@ -0,0 +1,166 @@ +# Tabs + +```sh +yarn add @freshworks/tabs +``` + +Tabs are used to organise content under each section. Tabs are most helpful when there is a lot of content to show in a page. Tabs can help in showing content which are under the same level of hierarchy, under each section inside the same page. + +## Usage + +#### 1. Categorisation + +It's easy for the user to quickly distinguish which tab belongs to which content. + +{{#docs-demo as |demo|}} + {{#demo.example name="nucleus-tabs.hbs"}} + {{#nucleus-tabs + customClasses="sample-tab sample-tab-simple" + description="site-navigation" + select="I want apples" + variant="line" as |tabs| }} + {{#tabs.panel name="I want apples" }} +
This is apples section
+ {{/tabs.panel}} + {{#tabs.panel name="I want oranges" }} +
This is oranges section
+ {{/tabs.panel}} + {{#tabs.panel name="I want grapes" }} +
This is grapes section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + {{/demo.example}} + {{demo.snippet 'nucleus-tabs.hbs'}} +{{/docs-demo}} + +#### 2. Custom action 'on' changing tab + +{{nucleus-tabs/demo-1}} + +#### 3. Custom action 'before' changing tab + +{{nucleus-tabs/demo-2}} + +#### 4. Disabled tab + +{{#docs-demo as |demo|}} + {{#demo.example name="nucleus-tabs-4.hbs"}} + {{#nucleus-tabs + description="site-navigation" + variant="line" as |tabs| }} + {{#tabs.panel name="I want apples" }} +
This is apples section
+ {{/tabs.panel}} + {{#tabs.panel name="I want oranges" }} +
This is oranges section
+ {{/tabs.panel}} + {{#tabs.panel name="I want grapes" disabled=true }} +
This is grapes section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + {{/demo.example}} + {{demo.snippet 'nucleus-tabs-4.hbs'}} +{{/docs-demo}} + +## Styles + +#### 1. Default + +{{#docs-demo as |demo|}} + {{#demo.example name="nucleus-tabs-variant1.hbs"}} + {{#nucleus-tabs + description="site-navigation" + select="I want apples" + variant="line" as |tabs| }} + {{#tabs.panel name="I want apples" }} +
This is apples section
+ {{/tabs.panel}} + {{#tabs.panel name="I want oranges" }} +
This is oranges section
+ {{/tabs.panel}} + {{#tabs.panel name="I want grapes" }} +
This is grapes section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + {{/demo.example}} + {{demo.snippet 'nucleus-tabs-variant1.hbs'}} +{{/docs-demo}} + + + +#### 2. With Background +Pass 'variant' property as 'background'. + +{{#docs-demo as |demo|}} + {{#demo.example name="nucleus-tabs-variant2.hbs"}} + {{#nucleus-tabs + description="site-navigation" + select="I want apples" + variant="background" as |tabs| }} + {{#tabs.panel name="I want apples" }} +
This is apples section
+ {{/tabs.panel}} + {{#tabs.panel name="I want oranges" }} +
This is oranges section
+ {{/tabs.panel}} + {{#tabs.panel name="I want grapes" }} +
This is grapes section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + {{/demo.example}} + {{demo.snippet 'nucleus-tabs-variant2.hbs'}} +{{/docs-demo}} + + +## Guidelines + +✅ **Do's** + +1. Tabs should be placed in a single row over the content + +2. Include all interactive states for the tabs + + +🚫 **Dont's** + +1. Dont use tabs for sequential content. Users can navigate to any tab at any time and cannot be expected to do it sequentially. + +2. Dont use tabs for content in different levels of hierarchy. + +## Accessibility + +__role=tablist__ + +Indicates that the element serves as a container for a set of tabs. + +__aria-label=Entertainment__ + +Provides a label that describes the purpose of the set of tabs. + + +__role=tab__ + +Indicates the element serves as a tab control. + +__aria-select=true__ + +Indicates the tab control is activated and its associated panel is displayed. + +__aria-select=false__ + +Indicates the tab control is not active and its associated panel is NOT displayed. + +__aria-controls=IDREF__ + +Refers to the tabpanel element associated with the tab. + + +__role=tabpanel__ + +Indicates the element serves as a container for tab panel content. + +__aria-labelledby=IDREF__ + +Refers to the tab element that controls the panel. + +{{docs-note}} \ No newline at end of file diff --git a/packages/tabs/CHANGELOG.md b/packages/tabs/CHANGELOG.md new file mode 100644 index 00000000..71b67641 --- /dev/null +++ b/packages/tabs/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.0] - 2019-03-06 +### Added +- {{nucleus-tabs}} Initial imlpementation. \ No newline at end of file diff --git a/packages/tabs/LICENSE.md b/packages/tabs/LICENSE.md new file mode 100644 index 00000000..f8d1edb3 --- /dev/null +++ b/packages/tabs/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/tabs/README.md b/packages/tabs/README.md new file mode 100644 index 00000000..19854beb --- /dev/null +++ b/packages/tabs/README.md @@ -0,0 +1,22 @@ +@freshworks/tabs +============================================================================== + +``` +yarn add @freshworks/tabs +``` + +Tabs are used to organise content under each section. + +Scenario +------------------------------------------------------------------------------ +Tabs are most helpful when there is a lot of content to show in a page. Tabs can help in showing content which are under the same level of hierarchy, under each section inside the same page. + +Guidelines +------------------------------------------------------------------------------ +**Do's** +1. Tabs should be placed in a single row over the content +2. Include all interactive states for the tabs + + +**Dont's** +1. Dont use tabs for sequential content. Users can navigate to any tab at any time and cannot be expected to do it sequentially. diff --git a/packages/tabs/addon/components/nucleus-tabs.js b/packages/tabs/addon/components/nucleus-tabs.js new file mode 100644 index 00000000..3e439137 --- /dev/null +++ b/packages/tabs/addon/components/nucleus-tabs.js @@ -0,0 +1,169 @@ +import Component from '@ember/component'; +import { classNames, classNameBindings, layout as templateLayout } from '@ember-decorators/component'; +import layout from '../templates/components/nucleus-tabs'; +import { set, get, getProperties, computed, action } from '@ember/object'; +import defaultProp from '@freshworks/core/utils/default-decorator'; +import { A } from '@ember/array'; +import { oneWay }from '@ember/object/computed'; + +/** + __Usage:__ + + [Refer component page](/docs/components/nucleus-tabs) + + @class Nucleus Tabs + @namespace Components + @extends Ember.Component + @public +*/ +@templateLayout(layout) +@classNames('nucleus-tabs') +@classNameBindings('variantClass', 'customClasses') +class NucleusTabs extends Component { + /** + * description + * + * @field description + * @description to add aria label + * @type string|null + * @default null + * @readonly + * @public + */ + @defaultProp + description = null; + + /** + * customClasses + * + * @field customClasses + * @description to add custom class to the tabs component + * @type string + * @readonly + * @public + */ + @defaultProp + customClasses = ''; + + /** + * select + * + * @field select + * @description default open tab + * @type string|null + * @default null + * @readonly + * @public + */ + @defaultProp + select = null; + + /** + * variant + * + * @field variant + * @description tab styles, line/background + * @type string + * @default 'line' + * @readonly + * @public + */ + @defaultProp + variant = 'line'; + + /** + * tabPanels + * + * @field tabPanels + * @description Collection of all tab panels + * @type array + * @public + */ + tabPanels = A([]); + + /** + * tabListItems + * + * @field tabListItems + * @description Collection of all tab list items + * @type array + * @public + */ + tabListItems = A([]); + + /** + * selected + * + * @field selected + * @description takes intial value from select + * @type string|null + * @public + */ + @oneWay('select') + selected; + + /** + * variantClass + * + * @field variantClass + * @type string + * @public + */ + @computed('variant', function() { + return 'nucleus-tabs--' + get(this, 'variant'); + }) + variantClass; + + /** + * registerPanel + * + * @method registerPanel + * @param {Object} tab + * @public + * + */ + @action + registerPanel(tab) { + if(!get(this, 'selected') && (tab.disabled === 'false')) { + set(this, 'selected', tab.name); // set selected if not initially set + } + get(this, 'tabPanels').pushObject(tab); + } + + /** + * registerTabListItem + * + * @method registerTabListItem + * @param {Object} tab + * @public + * + */ + @action + registerTabListItem(tabList) { + get(this, 'tabListItems').pushObject(tabList); + } + + /** + * activateTab + * + * @method activateTab + * @description Handler that will be called when a tab is clicked + * @param {string} name + * @param {any} event + * @public + * + */ + @action + async activateTab(changedTo, event) { + const { beforeChange, onChange, currentTab } = getProperties(this, 'beforeChange', 'onChange', 'currentTab'); + if(beforeChange) { + await beforeChange.call(this, changedTo, currentTab, event); + } + set(this, 'selected', changedTo); + if(onChange) { + await onChange.call(this, changedTo, currentTab, event); + } + } +} + +export default NucleusTabs; diff --git a/packages/tabs/addon/components/nucleus-tabs/tab-list-item.js b/packages/tabs/addon/components/nucleus-tabs/tab-list-item.js new file mode 100644 index 00000000..21966c89 --- /dev/null +++ b/packages/tabs/addon/components/nucleus-tabs/tab-list-item.js @@ -0,0 +1,293 @@ +import Component from '@ember/component'; +import { classNames, attributeBindings, classNameBindings, tagName, layout as templateLayout } from '@ember-decorators/component'; +import layout from '../../templates/components/nucleus-tabs/tab-list-item'; +import { get, set, computed } from '@ember/object'; +import defaultProp from '@freshworks/core/utils/default-decorator'; +import { TABS_KEY_CODE } from '../../constants/nucleus-tabs' + +/** + __Usage:__ + + [Refer component page](/docs/components/nucleus-tabs) + + @class Nucleus Tab List Item + @namespace Components + @extends Ember.Component + @private +*/ +@tagName('div') +@templateLayout(layout) +@classNames('nucleus-tabs__list__item') +@classNameBindings('isActive:is-active', 'isDisabled:is-disabled', 'isPressed:is-pressed') +@attributeBindings('isDisabled:disabled', 'tabindex', 'title', 'role', 'aria-controls', 'aria-selected') +class TabListItem extends Component { + + /** + * disabled + * + * @field disabled + * @type string + * @default null + * @readonly + * @public + */ + @defaultProp + disabled = null; + + /** + * controls + * + * @field controls + * @description idref to what panel this tab item controls + * @type string|null + * @readonly + * @public + */ + @defaultProp + controls; + + /** + * selected + * + * @field selected + * @description currently selected tab + * @readonly + * @public + */ + @defaultProp + selected; + + /** + * tabOrder + * + * @field tabOrder + * @description order in which tabs are displayed. Only first tabs will not have tabIndex value + * @type number + * @readonly + * @public + */ + @defaultProp + tabOrder; + + /** + * role + * + * @field role + * @type string + * @default 'tab' + * @public + */ + role = 'tab'; + + /** + * isPressed + * + * @field isPressed + * @description to solve focus on click styling + * @type boolean + * @default false + * @public + */ + isPressed = false; + + /** + * tabindex + * + * @field tabindex + * @type string|null + * @public + */ + @computed('tabOrder', function() { + let tabIndex = null; + if(!get(this, 'isDisabled')) { + tabIndex = (get(this, 'tabOrder') === 0)? '0' : '-1'; + } + return tabIndex; + }) + tabindex; + + /** + * isActive + * + * @field isActive + * @type boolean + * @public + */ + @computed('selected', function() { + return (get(this, 'selected') === get(this, 'name')); + }) + isActive; + + /** + * isDisabled + * + * @field isDisabled + * @type boolean + * @public + */ + @computed('disabled', function() { + return (get(this, 'disabled').toString() === 'true')? true : false; + }) + isDisabled; + + /** + * title + * + * @field title + * @type string + * @public + */ + @computed('name', function() { + return get(this, 'name'); + }) + title; + + /** + * aria-controls + * + * @field aria-controls + * @type string + * @public + */ + @computed('controls', function() { + return get(this, 'controls'); + }) + 'aria-controls'; + + /** + * aria-selected + * + * @field aria-selected + * @type boolean + * @public + */ + @computed('selected', function() { + return (get(this, 'selected') === get(this, 'name')).toString(); + }) + 'aria-selected'; + + /** + * didInsertElement + * + * @method didInsertElement + * @description lifecycle event + * @public + * + */ + didInsertElement() { + get(this, 'registerTabListItem').call(this, { + id: get(this, 'elementId'), + name: get(this, 'name') + }); + } + + /** + * mouseDown + * + * @method mouseDown + * @description event handler : We are negating the style applied on click-focus + * @public + * + */ + mouseDown() { + if(get(this, 'isDisabled') === false) { + set(this, 'isPressed', true); + } + } + + /** + * focusOut + * + * @method focusOut + * @description event handler : Removing style that was applied during press to negate focus + * @public + * + */ + focusOut() { + if(get(this, 'isPressed') === true) { + set(this, 'isPressed', false); + } + } + + /** + * click + * + * @method click + * @description event handler + * @public + * + */ + click(event) { + if(get(this, 'isDisabled') === false) { + get(this, 'handleActivateTab').call(this, get(this, 'name'), event); + } + } + + /** + * keyDown + * + * @method keyDown + * @description event handler + * @public + * + */ + keyDown(event) { + event.stopPropagation(); + const targetElement = event.target; + const firstElement = targetElement.parentElement.firstElementChild; + const lastElement = targetElement.parentElement.lastElementChild; + + const keyCode = TABS_KEY_CODE; + switch (event.keyCode) { + case keyCode.ENTER: + case keyCode.SPACE: + event.preventDefault(); + targetElement.click(); + break; + case keyCode.END: + lastElement.focus(); + break; + case keyCode.HOME: + firstElement.focus(); + break; + case keyCode.LEFT: + get(this, '_changeTabFocus').call(this, 'previous', targetElement); + break; + case keyCode.RIGHT: + get(this, '_changeTabFocus').call(this, 'next', targetElement); + break; + default: + break; + } + } + + /** + * _changeTabFocus + * + * @method _changeTabFocus + * @description Change focus based on direction + * @param {string} direction takes either 'next' or 'previous' + * @param {Object} element + * @param {Object} [elementInFocus] + * @private + * + */ + _changeTabFocus(direction, element, elementInFocus) { + let adjacentElement; + if(direction === 'previous') { + adjacentElement = (element.previousElementSibling)? element.previousElementSibling : element.parentElement.lastElementChild; + } else { + adjacentElement = (element.nextElementSibling)? element.nextElementSibling : element.parentElement.firstElementChild; + } + + if(elementInFocus && (elementInFocus.id === adjacentElement.id)) { + return; + } else if(adjacentElement.getAttribute('tabindex') === null) { + get(this, '_changeTabFocus').call(this, direction, adjacentElement, element); + } else { + adjacentElement.focus(); + } + } +} + +export default TabListItem; diff --git a/packages/tabs/addon/components/nucleus-tabs/tab-panel.js b/packages/tabs/addon/components/nucleus-tabs/tab-panel.js new file mode 100644 index 00000000..9303c6c5 --- /dev/null +++ b/packages/tabs/addon/components/nucleus-tabs/tab-panel.js @@ -0,0 +1,98 @@ +import Component from '@ember/component'; +import { classNames, attributeBindings, classNameBindings, tagName, layout as templateLayout } from '@ember-decorators/component'; +import layout from '../../templates/components/nucleus-tabs/tab-panel'; +import { get, computed } from '@ember/object'; +import defaultProp from '@freshworks/core/utils/default-decorator'; + +/** + __Usage:__ + + [Refer component page](/docs/components/nucleus-tabs) + + @class Nucleus Tab Panel + @namespace Components + @extends Ember.Component + @public +*/ +@tagName('div') +@templateLayout(layout) +@classNames('nucleus-tabs__panel') +@classNameBindings('isActive:is-active') +@attributeBindings('tabindex', 'role', 'aria-labelledby') +class TabPanel extends Component { + + /** + * disabled + * + * @field disabled + * @type string + * @default 'false' + * @readonly + * @public + */ + @defaultProp + disabled = 'false'; + + /** + * tabindex + * + * @field tabindex + * @default '0' + * @type string + * @public + */ + tabindex = '0'; + + /** + * role + * + * @field role + * @type string + * @public + */ + role = 'tabpanel' + + /** + * isActive + * + * @field isActive + * @type boolean + * @public + */ + @computed('selected', function() { + return (get(this, 'selected') === get(this, 'name')); + }) + isActive; + + /** + * aria-labelledby + * + * @field aria-labelledby + * @type string + * @public + */ + @computed('tabListItems.[]', function() { + const tabListItems = get(this, 'tabListItems'); + const tabList = tabListItems.findBy('name', get(this, 'name')); + return (tabList)? tabList.id : ''; + }) + 'aria-labelledby'; + + /** + * didInsertElement + * + * @method didInsertElement + * @description lifecycle event + * @public + * + */ + didInsertElement() { + get(this, 'registerPanel').call(this, { + id: get(this, 'elementId'), + name: get(this, 'name'), + disabled: get(this, 'disabled') + }); + } +} + +export default TabPanel; diff --git a/packages/tabs/addon/constants/nucleus-tabs.js b/packages/tabs/addon/constants/nucleus-tabs.js new file mode 100644 index 00000000..8f8692a1 --- /dev/null +++ b/packages/tabs/addon/constants/nucleus-tabs.js @@ -0,0 +1,10 @@ +const TABS_KEY_CODE = { + ENTER: 13, + SPACE: 32, + END: 35, + HOME: 36, + LEFT: 37, + RIGHT: 39 +}; + +export { TABS_KEY_CODE }; diff --git a/packages/tabs/addon/styles/addon.scss b/packages/tabs/addon/styles/addon.scss new file mode 100644 index 00000000..1cc72022 --- /dev/null +++ b/packages/tabs/addon/styles/addon.scss @@ -0,0 +1,3 @@ +@import "nucleus/variables"; +@import "nucleus/animations"; +@import "./components/nucleus-tabs"; diff --git a/packages/tabs/addon/styles/components/_nucleus-tabs.scss b/packages/tabs/addon/styles/components/_nucleus-tabs.scss new file mode 100644 index 00000000..4387ad74 --- /dev/null +++ b/packages/tabs/addon/styles/components/_nucleus-tabs.scss @@ -0,0 +1,135 @@ +.nucleus-tabs { + + $self: &; + + display: flex; + flex-direction: column; + + &__list { + width: 100%; + overflow: scroll hidden; + + &__container { + display: flex; + flex-wrap: nowrap; + width: 100%; + padding: 0 12px; + border-bottom: 1px solid $color-smoke-50; + } + + &__item { + flex-shrink: 0; + position: relative; + text-align: center; + line-height: 20px; + padding: 10px 8px; + margin: 0 4px; + font-size: 14px; + color: $color-smoke-700; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + + &::before { + display: block; + content: attr(title); + font-weight: 500; + height: 0; + overflow: hidden; + visibility: hidden; + } + + &::after { + content: ''; + position: absolute; + left: 0; + bottom: -1px; + width: 100%; + box-sizing: border-box; + } + + &:hover { + outline: none; + border: 0; + box-shadow: none; + cursor: pointer; + + &::after { + border-bottom: 2px solid $color-smoke-100; + } + } + + &:focus { + outline: none; + border: 0; + box-shadow: none; + + &::after { + height: calc(100% + 1px); + border: 2px solid $color-azure-800; + border-radius: 4px; + } + } + + &.is-pressed { + &::after { + border: 0; + border-radius: 0; + border-bottom: 0; + } + + &:hover::after { + border-bottom: 2px solid $color-smoke-100; + } + + &.is-active::after { + border-bottom: 2px solid $color-azure-800; + } + } + + &.is-active { + color: $color-azure-800; + font-weight: 500; + + &::after { + border-bottom: 2px solid $color-azure-800; + } + } + + &.is-disabled { + color: $color-smoke-300; + + &:hover { + cursor: default; + + &::after { + display: none; + } + } + } + } + } + + &__panel { + width: 100%; + display: none; + padding: 8px 20px; + + &.is-active { + display: block; + } + } + + &--background { + #{ $self }__list { + background: $color-smoke-25; + border: 1px solid $color-smoke-50; + border-radius: 4px; + } + } +} diff --git a/packages/tabs/addon/templates/components/nucleus-tabs.hbs b/packages/tabs/addon/templates/components/nucleus-tabs.hbs new file mode 100644 index 00000000..e0ba1844 --- /dev/null +++ b/packages/tabs/addon/templates/components/nucleus-tabs.hbs @@ -0,0 +1,20 @@ +
+
+ {{#each tabPanels key="id" as |tab index|}} + {{#nucleus-tabs/tab-list-item + registerTabListItem=(action registerTabListItem) + name=tab.name + disabled=tab.disabled + controls=tab.id + tabOrder=index + selected=selected + handleActivateTab=(action activateTab)}} + {{tab.name}} + {{/nucleus-tabs/tab-list-item}} + {{/each}} +
+
+ +{{yield (hash + panel=(component "nucleus-tabs/tab-panel" registerPanel=(action "registerPanel") selected=selected tabListItems=tabListItems) +)}} diff --git a/packages/tabs/addon/templates/components/nucleus-tabs/tab-list-item.hbs b/packages/tabs/addon/templates/components/nucleus-tabs/tab-list-item.hbs new file mode 100644 index 00000000..889d9eea --- /dev/null +++ b/packages/tabs/addon/templates/components/nucleus-tabs/tab-list-item.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/packages/tabs/addon/templates/components/nucleus-tabs/tab-panel.hbs b/packages/tabs/addon/templates/components/nucleus-tabs/tab-panel.hbs new file mode 100644 index 00000000..889d9eea --- /dev/null +++ b/packages/tabs/addon/templates/components/nucleus-tabs/tab-panel.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/packages/tabs/app/.gitkeep b/packages/tabs/app/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/app/components/nucleus-tabs.js b/packages/tabs/app/components/nucleus-tabs.js new file mode 100644 index 00000000..f51454bb --- /dev/null +++ b/packages/tabs/app/components/nucleus-tabs.js @@ -0,0 +1 @@ +export { default } from '@freshworks/tabs/components/nucleus-tabs'; diff --git a/packages/tabs/app/components/nucleus-tabs/tab-list-item.js b/packages/tabs/app/components/nucleus-tabs/tab-list-item.js new file mode 100644 index 00000000..87c6a9c1 --- /dev/null +++ b/packages/tabs/app/components/nucleus-tabs/tab-list-item.js @@ -0,0 +1 @@ +export { default } from '@freshworks/tabs/components/nucleus-tabs/tab-list-item'; diff --git a/packages/tabs/app/components/nucleus-tabs/tab-panel.js b/packages/tabs/app/components/nucleus-tabs/tab-panel.js new file mode 100644 index 00000000..8279bd3a --- /dev/null +++ b/packages/tabs/app/components/nucleus-tabs/tab-panel.js @@ -0,0 +1 @@ +export { default } from '@freshworks/tabs/components/nucleus-tabs/tab-panel'; diff --git a/packages/tabs/app/styles/app.scss b/packages/tabs/app/styles/app.scss new file mode 100644 index 00000000..f281a636 --- /dev/null +++ b/packages/tabs/app/styles/app.scss @@ -0,0 +1,3 @@ +// Dummy file + +// Ember cli 3.4 expects styles/app.scss file to be found inside app folder. diff --git a/packages/tabs/config/deploy.js b/packages/tabs/config/deploy.js new file mode 100644 index 00000000..b5581aed --- /dev/null +++ b/packages/tabs/config/deploy.js @@ -0,0 +1,29 @@ +/* eslint-env node */ +'use strict'; + +module.exports = function(deployTarget) { + let ENV = { + build: {} + // include other plugin configuration that applies to all deploy targets here + }; + + if (deployTarget === 'development') { + ENV.build.environment = 'development'; + // configure other plugins for development deploy target here + } + + if (deployTarget === 'staging') { + ENV.build.environment = 'production'; + // configure other plugins for staging deploy target here + } + + if (deployTarget === 'production') { + ENV.build.environment = 'production'; + // configure other plugins for production deploy target here + } + + // Note: if you need to build some configuration asynchronously, you can return + // a promise that resolves with the ENV object instead of returning the + // ENV object synchronously. + return ENV; +}; diff --git a/packages/tabs/config/environment.js b/packages/tabs/config/environment.js new file mode 100644 index 00000000..defadf30 --- /dev/null +++ b/packages/tabs/config/environment.js @@ -0,0 +1,60 @@ +'use strict'; + +/* eslint-env node */ +module.exports = function(environment) { + let ENV = { + modulePrefix: 'nucleus', + environment, + rootURL: '/', + locationType: 'auto', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false + } + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + } + }; + + ENV['ember-a11y-testing'] = { + componentOptions: { + turnAuditOff: true, // Change to true to disable the audit in development + visualNoiseLevel: 2, + axeViolationClassNames: ['alert-box', 'alert-box--a11y'] + } + } + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/packages/tabs/ember-backstop/backstop.js b/packages/tabs/ember-backstop/backstop.js new file mode 100644 index 00000000..09bad864 --- /dev/null +++ b/packages/tabs/ember-backstop/backstop.js @@ -0,0 +1,37 @@ +/* eslint-env node */ +module.exports = { + id: `ember-backstop test`, + viewports: [ + { + label: 'webview', + width: 1440, + height: 900, + }, + ], + onBeforeScript: `puppet/onBefore.js`, + onReadyScript: `puppet/onReady.js`, + scenarios: [ + { + label: '{testName}', + cookiePath: 'backstop_data/engine_scripts/cookies.json', + url: '{origin}/backstop/dview/{testId}/{scenarioId}', + delay: 500, + }, + ], + paths: { + bitmaps_reference: 'backstop_data/bitmaps_reference', + bitmaps_test: 'backstop_data/bitmaps_test', + engine_scripts: 'backstop_data/engine_scripts', + html_report: 'backstop_data/html_report', + ci_report: 'backstop_data/ci_report', + }, + report: [], + engine: 'puppet', + engineOptions: { + args: ['--no-sandbox'], + }, + asyncCaptureLimit: 10, + asyncCompareLimit: 50, + debug: false, + debugWindow: false, +}; diff --git a/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_background_style_tabs__assert0_0_document_0_webview.png b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_background_style_tabs__assert0_0_document_0_webview.png new file mode 100644 index 00000000..578e1a07 Binary files /dev/null and b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_background_style_tabs__assert0_0_document_0_webview.png differ diff --git a/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_border_style_tabs__assert0_0_document_0_webview.png b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_border_style_tabs__assert0_0_document_0_webview.png new file mode 100644 index 00000000..eeb77a3a Binary files /dev/null and b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_border_style_tabs__assert0_0_document_0_webview.png differ diff --git a/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_default_style_tabs__assert0_0_document_0_webview.png b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_default_style_tabs__assert0_0_document_0_webview.png new file mode 100644 index 00000000..aff3e31d Binary files /dev/null and b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_default_style_tabs__assert0_0_document_0_webview.png differ diff --git a/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_disabled_tabs__assert0_0_document_0_webview.png b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_disabled_tabs__assert0_0_document_0_webview.png new file mode 100644 index 00000000..5a7f6d94 Binary files /dev/null and b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_disabled_tabs__assert0_0_document_0_webview.png differ diff --git a/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_the_different_variants_-_default_background_disabled__assert0_0_document_0_webview.png b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_the_different_variants_-_default_background_disabled__assert0_0_document_0_webview.png new file mode 100644 index 00000000..4285e2d4 Binary files /dev/null and b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_the_different_variants_-_default_background_disabled__assert0_0_document_0_webview.png differ diff --git a/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_the_different_variants__assert0_0_document_0_webview.png b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_the_different_variants__assert0_0_document_0_webview.png new file mode 100644 index 00000000..e74c1121 Binary files /dev/null and b/packages/tabs/ember-backstop/backstop_data/bitmaps_reference/ember-backstoptest_Integration__Component__nucleus-tabs__visual_regression_for_the_different_variants__assert0_0_document_0_webview.png differ diff --git a/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/clickAndHoverHelper.js b/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/clickAndHoverHelper.js new file mode 100644 index 00000000..d6a16e0c --- /dev/null +++ b/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/clickAndHoverHelper.js @@ -0,0 +1,41 @@ +/* eslint-env browser, node */ + +module.exports = async (page, scenario) => { + const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector; + const clickSelector = scenario.clickSelectors || scenario.clickSelector; + const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector; + const scrollToSelector = scenario.scrollToSelector; + const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int] + + if (keyPressSelector) { + for (const keyPressSelectorItem of [].concat(keyPressSelector)) { + await page.waitFor(keyPressSelectorItem.selector); + await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress); + } + } + + if (hoverSelector) { + for (const hoverSelectorIndex of [].concat(hoverSelector)) { + await page.waitFor(hoverSelectorIndex); + await page.hover(hoverSelectorIndex); + } + } + + if (clickSelector) { + for (const clickSelectorIndex of [].concat(clickSelector)) { + await page.waitFor(clickSelectorIndex); + await page.click(clickSelectorIndex); + } + } + + if (postInteractionWait) { + await page.waitFor(postInteractionWait); + } + + if (scrollToSelector) { + await page.waitFor(scrollToSelector); + await page.evaluate(scrollToSelector => { + document.querySelector(scrollToSelector).scrollIntoView(); + }, scrollToSelector); + } +}; diff --git a/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/onReady.js b/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/onReady.js new file mode 100644 index 00000000..94c95fe8 --- /dev/null +++ b/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/onReady.js @@ -0,0 +1,11 @@ +/* eslint-env browser, node */ + +const debug = require('debug')('BackstopJS'); + +module.exports = async (page, scenario) => { + debug('SCENARIO > ' + scenario.label); + await require('./overrideCSS')(page, scenario); + await require('./clickAndHoverHelper')(page, scenario); + + // add more ready handlers here... +}; diff --git a/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/overrideCSS.js b/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/overrideCSS.js new file mode 100644 index 00000000..29cd0e20 --- /dev/null +++ b/packages/tabs/ember-backstop/backstop_data/engine_scripts/puppet/overrideCSS.js @@ -0,0 +1,13 @@ +/* eslint-env browser, node */ + +module.exports = function(page) { + // inject arbitrary css to override styles + page.evaluate(() => { + const BACKSTOP_TEST_CSS_OVERRIDE = `#ember-testing {width: 100% !important; height: 100% !important; -webkit-transform: scale(1) !important; transform: scale(1) !important;}`; + let style = document.createElement('style'); + style.type = 'text/css'; + let styleNode = document.createTextNode(BACKSTOP_TEST_CSS_OVERRIDE); + style.appendChild(styleNode); + document.head.appendChild(style); + }); +}; diff --git a/packages/tabs/ember-cli-build.js b/packages/tabs/ember-cli-build.js new file mode 100644 index 00000000..b908d9d4 --- /dev/null +++ b/packages/tabs/ember-cli-build.js @@ -0,0 +1,14 @@ +'use strict'; + +// eslint-disable-next-line node/no-unpublished-require +const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); + +module.exports = function(defaults) { + let app = new EmberAddon(defaults, + { + hinting: false + } + ); + + return app.toTree(); +}; diff --git a/packages/tabs/index.js b/packages/tabs/index.js new file mode 100644 index 00000000..5b337fac --- /dev/null +++ b/packages/tabs/index.js @@ -0,0 +1,34 @@ +/* eslint-env node */ +'use strict'; +// eslint-disable-next-line node/no-unpublished-require +const mergeTrees = require('broccoli-merge-trees'); +// eslint-disable-next-line node/no-unpublished-require +const Funnel = require('broccoli-funnel'); +const path = require('path'); + +module.exports = { + name: '@freshworks/tabs', + + isDevelopingAddon() { + return true; + }, + + included(app, parentAddon) { + let target = (parentAddon || app); + target.options = target.options || {}; + target.options.babel = target.options.babel || { includePolyfill: true }; + return this._super.included.apply(this, arguments); + }, + + treeForAddonStyles(tree) { + let coreStyleTree = new Funnel(this.getCoreStylesPath(), { + destDir: 'nucleus' + }); + return mergeTrees([coreStyleTree, tree]); + }, + + getCoreStylesPath() { + let pkgPath = path.dirname(require.resolve(`@freshworks/core/package.json`)); + return path.join(pkgPath, 'app/styles'); + } +}; diff --git a/packages/tabs/package.json b/packages/tabs/package.json new file mode 100644 index 00000000..456afef5 --- /dev/null +++ b/packages/tabs/package.json @@ -0,0 +1,82 @@ +{ + "name": "@freshworks/tabs", + "version": "0.1.0", + "description": "tabs component in Nucleus", + "keywords": [ + "ember-addon" + ], + "repository": "https://github.com/freshdesk/nucleus", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build", + "start": "ember serve -p 4003", + "deploy": "ember deploy production", + "test": "ember backstop-remote & COVERAGE=TRUE ember test --test-port=1509", + "posttest": "ember backstop-stop", + "test:dev": "COVERAGE=TRUE ember test --server -launch=false" + }, + "dependencies": { + "@freshworks/core": "^0.1.9", + "@freshworks/icon": "^0.5.0", + "ember-cli-babel": "^7.11.1", + "ember-cli-htmlbars": "^4.0.0", + "ember-cli-sass": "^10.0.0", + "ember-css-transitions": "^0.1.16", + "ember-decorators": "^6.1.1", + "ember-decorators-polyfill": "shibulijack-fd/ember-decorators-polyfill#master", + "ember-truth-helpers": "^2.1.0" + }, + "devDependencies": { + "@ember/optional-features": "^1.0.0", + "broccoli-asset-rev": "^3.0.0", + "broccoli-funnel": "^2.0.2", + "broccoli-merge-trees": "^3.0.2", + "ember-a11y-testing": "^1.0.0", + "ember-backstop": "^1.3.4", + "ember-cli": "~3.13.1", + "ember-cli-autoprefixer": "^0.8.1", + "ember-cli-code-coverage": "^1.0.0-beta.8", + "ember-cli-dependency-checker": "^3.1.0", + "ember-cli-deploy": "^1.0.2", + "ember-cli-deploy-build": "^1.1.1", + "ember-cli-deploy-git": "^1.3.3", + "ember-cli-deploy-git-ci": "^1.0.1", + "ember-cli-flash": "^1.7.2", + "ember-cli-inject-live-reload": "^2.0.1", + "ember-cli-sri": "^2.1.1", + "ember-cli-uglify": "^3.0.0", + "ember-disable-prototype-extensions": "^1.1.3", + "ember-export-application-global": "^2.0.0", + "ember-load-initializers": "^2.1.0", + "ember-maybe-import-regenerator": "^0.1.6", + "ember-qunit": "^4.5.1", + "ember-resolver": "^5.3.0", + "ember-sinon": "^4.0.0", + "ember-sinon-qunit": "^3.4.0", + "ember-source": "~3.13.0", + "ember-test-selectors": "^2.1.0", + "loader.js": "^4.7.0", + "qunit-dom": "^0.9.0", + "sass": "^1.23.0" + }, + "engines": { + "node": "8.* || >= 10.*" + }, + "ember-addon": { + "configPath": "tests/dummy/config", + "before": [ + "ember-cli-htmlbars", + "ember-svg-jar" + ] + }, + "publishConfig": { + "access": "public" + }, + "homepage": "https://freshdesk.github.io/nucleus", + "gitHead": "272f136386ba4d45348c6498f55a1dabe873d1fc" +} diff --git a/packages/tabs/testem.js b/packages/tabs/testem.js new file mode 100644 index 00000000..e23f1188 --- /dev/null +++ b/packages/tabs/testem.js @@ -0,0 +1,30 @@ +module.exports = { + test_page: 'tests/index.html?hidepassed', + disable_watching: true, + launch_in_ci: [ + 'Chrome' + ], + launch_in_dev: [ + 'Chrome' + ], + browser_args: { + Chrome: { + ci: [ + // --no-sandbox is needed when running Chrome inside a container + process.env.CI ? '--no-sandbox' : null, + '--headless', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--mute-audio', + '--remote-debugging-port=0', + '--window-size=1440,900' + ].filter(Boolean) + } + }, + proxies: { + '/backstop': { + target: 'http://localhost:1509', + secure: false + } + } +} diff --git a/packages/tabs/tests/.eslintrc.js b/packages/tabs/tests/.eslintrc.js new file mode 100644 index 00000000..fbf25552 --- /dev/null +++ b/packages/tabs/tests/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + embertest: true + } +}; diff --git a/packages/tabs/tests/dummy/app/app.js b/packages/tabs/tests/dummy/app/app.js new file mode 100644 index 00000000..b3b2bd67 --- /dev/null +++ b/packages/tabs/tests/dummy/app/app.js @@ -0,0 +1,14 @@ +import Application from '@ember/application'; +import Resolver from './resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from './config/environment'; + +const App = Application.extend({ + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + Resolver +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/packages/tabs/tests/dummy/app/components/.gitkeep b/packages/tabs/tests/dummy/app/components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/tests/dummy/app/controllers/.gitkeep b/packages/tabs/tests/dummy/app/controllers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/tests/dummy/app/helpers/.gitkeep b/packages/tabs/tests/dummy/app/helpers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/tests/dummy/app/index.html b/packages/tabs/tests/dummy/app/index.html new file mode 100644 index 00000000..1f84141c --- /dev/null +++ b/packages/tabs/tests/dummy/app/index.html @@ -0,0 +1,25 @@ + + + + + + Nucleus - The Freshworks Design System + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/packages/tabs/tests/dummy/app/models/.gitkeep b/packages/tabs/tests/dummy/app/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/tests/dummy/app/resolver.js b/packages/tabs/tests/dummy/app/resolver.js new file mode 100644 index 00000000..2fb563d6 --- /dev/null +++ b/packages/tabs/tests/dummy/app/resolver.js @@ -0,0 +1,3 @@ +import Resolver from 'ember-resolver'; + +export default Resolver; diff --git a/packages/tabs/tests/dummy/app/router.js b/packages/tabs/tests/dummy/app/router.js new file mode 100644 index 00000000..1202e6cf --- /dev/null +++ b/packages/tabs/tests/dummy/app/router.js @@ -0,0 +1,12 @@ +import EmberRouter from '@ember/routing/router'; +import config from './config/environment'; + +const Router = EmberRouter.extend({ + location: config.locationType, + rootURL: config.rootURL +}); + +Router.map(function() { +}); + +export default Router; \ No newline at end of file diff --git a/packages/tabs/tests/dummy/app/routes/.gitkeep b/packages/tabs/tests/dummy/app/routes/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/tests/dummy/app/styles/app.scss b/packages/tabs/tests/dummy/app/styles/app.scss new file mode 100644 index 00000000..f281a636 --- /dev/null +++ b/packages/tabs/tests/dummy/app/styles/app.scss @@ -0,0 +1,3 @@ +// Dummy file + +// Ember cli 3.4 expects styles/app.scss file to be found inside app folder. diff --git a/packages/tabs/tests/dummy/config/environment.js b/packages/tabs/tests/dummy/config/environment.js new file mode 100644 index 00000000..3dda71a8 --- /dev/null +++ b/packages/tabs/tests/dummy/config/environment.js @@ -0,0 +1,54 @@ +'use strict'; + +/* eslint-env node */ +module.exports = function(environment) { + let ENV = { + modulePrefix: 'dummy', + environment, + rootURL: '/', + locationType: 'auto', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false + } + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + } + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // Allow ember-cli-addon-docs to update the rootURL in compiled assets + ENV.rootURL = 'ADDON_DOCS_ROOT_URL'; + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/packages/tabs/tests/dummy/config/optional-features.json b/packages/tabs/tests/dummy/config/optional-features.json new file mode 100644 index 00000000..b1902623 --- /dev/null +++ b/packages/tabs/tests/dummy/config/optional-features.json @@ -0,0 +1,3 @@ +{ + "jquery-integration": false +} diff --git a/packages/tabs/tests/dummy/config/targets.js b/packages/tabs/tests/dummy/config/targets.js new file mode 100644 index 00000000..ebacf95b --- /dev/null +++ b/packages/tabs/tests/dummy/config/targets.js @@ -0,0 +1,19 @@ +'use strict'; + +/* eslint-env node */ +const browsers = [ + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions' +]; + +const isCI = !!process.env.CI; +const isProduction = process.env.EMBER_ENV === 'production'; + +if (isCI || isProduction) { + browsers.push('ie 11'); +} + +module.exports = { + browsers +}; diff --git a/packages/tabs/tests/dummy/public/robots.txt b/packages/tabs/tests/dummy/public/robots.txt new file mode 100644 index 00000000..f5916452 --- /dev/null +++ b/packages/tabs/tests/dummy/public/robots.txt @@ -0,0 +1,3 @@ +# http://www.robotstxt.org +User-agent: * +Disallow: diff --git a/packages/tabs/tests/helpers/.gitkeep b/packages/tabs/tests/helpers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/tests/index.html b/packages/tabs/tests/index.html new file mode 100644 index 00000000..5209b852 --- /dev/null +++ b/packages/tabs/tests/index.html @@ -0,0 +1,33 @@ + + + + + + Dummy Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/packages/tabs/tests/integration/.gitkeep b/packages/tabs/tests/integration/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/tabs/tests/integration/components/nucleus-tabs-test.js b/packages/tabs/tests/integration/components/nucleus-tabs-test.js new file mode 100644 index 00000000..69918558 --- /dev/null +++ b/packages/tabs/tests/integration/components/nucleus-tabs-test.js @@ -0,0 +1,204 @@ +import { module } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import test from 'ember-sinon-qunit/test-support/test'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { render, click } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import backstop from 'ember-backstop/test-support/backstop'; + +let sampleTabsTemplate = hbs` + {{#nucleus-tabs description="site-navigation" select="home" variant="background" as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{#tabs.panel name="about" }} +
This is about us section
+ {{/tabs.panel}} + {{#tabs.panel name="contact" }} +
This is the contact section
+ {{/tabs.panel}} + {{/nucleus-tabs}} +`; + +module('Integration | Component | nucleus-tabs', function(hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function() { + this.actions = {}; + this.send = (actionName, ...args) => this.actions[actionName].apply(this, args); + }); + + test('it should yield tab-list items and tab-panels', async function(assert) { + await render(sampleTabsTemplate); + assert.dom('.nucleus-tabs').exists({ count: 1 }, 'Tabs component exists.'); + assert.dom('.nucleus-tabs .nucleus-tabs__list').exists({ count: 1 }, 'Tabs component has a Tab list'); + assert.dom('.nucleus-tabs .nucleus-tabs__list__item').exists({ count: 3 }, 'Tabs component has 3 Tab list items'); + assert.dom('.nucleus-tabs .nucleus-tabs__panel').exists({ count: 3 }, 'Tabs component has right number of Tab panels'); + }); + + test('it should have only selected panel as active', async function(assert) { + await render(sampleTabsTemplate); + assert.dom('.nucleus-tabs .nucleus-tabs__panel.is-active').exists({ count: 1 }, 'Only one active panel at a time'); + assert.dom('.nucleus-tabs .nucleus-tabs__panel.is-active').hasText('This is the home section'); + }); + + test('it should have only selected tab list item as active', async function(assert) { + await render(sampleTabsTemplate); + assert.dom('.nucleus-tabs .nucleus-tabs__list__item.is-active').exists({ count: 1 }, 'Only one active panel at a time'); + assert.dom('.nucleus-tabs .nucleus-tabs__list__item.is-active').hasText('home'); + }); + + test('it should attach appropriate background class for the background variant', async function(assert) { + await render(sampleTabsTemplate); + assert.dom('.nucleus-tabs.nucleus-tabs--background').exists({ count: 1 }, 'Has one appropriate class when passing variant as prop'); + }); + + test('it should attach appropriate line class for the line variant', async function(assert) { + await render(hbs` + {{#nucleus-tabs description="site-navigation" select="home" as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + `); + assert.dom('.nucleus-tabs.nucleus-tabs--line').exists({ count: 1 }, 'Has line class when no variant passed as prop'); + }); + + test('it should attach disable class to disabled tab list item', async function(assert) { + await render(hbs` + {{#nucleus-tabs description="site-navigation" select="home" as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{#tabs.panel name="about" disabled="true" }} +
This is the home section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + `); + assert.dom('.nucleus-tabs .nucleus-tabs__list__item.is-disabled').exists({ count: 1 }, 'Has disabled class when disabled prop is passed'); + }); + + test('it should not enable tab when tab list item is disabled', async function(assert) { + await render(hbs` + {{#nucleus-tabs description="site-navigation" select="home" as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{#tabs.panel name="about" disabled="true" }} +
This is the home section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + `); + await click('.nucleus-tabs .nucleus-tabs__list__item:not(.is-active)'); + assert.dom('.nucleus-tabs .nucleus-tabs__list__item.is-active').hasText('home'); + }); + + test('it should yeilds onChange action', async function(assert) { + let onChangeAction = this.spy(); + this.actions.onChange = onChangeAction; + await render(hbs` + {{#nucleus-tabs description="site-navigation" select="home" onChange=(action "onChange") as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{#tabs.panel name="about" }} +
This is the about section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + `); + + await click('.nucleus-tabs .nucleus-tabs__list__item:not(.is-active)'); + assert.ok(onChangeAction.calledOnce, 'onChange action has been called.'); + }); + + test('it should yeilds beforeChange action', async function(assert) { + let beforeChangeAction = this.spy(); + this.actions.beforeChange = beforeChangeAction; + await render(hbs` + {{#nucleus-tabs description="site-navigation" select="home" beforeChange=(action "beforeChange") as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{#tabs.panel name="about" }} +
This is the about section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + `); + + await click('.nucleus-tabs .nucleus-tabs__list__item:not(.is-active)'); + assert.ok(beforeChangeAction.calledOnce, 'beforeChange action has been called.'); + }); + + test('it should call beforeChange action ahead of onChange action', async function(assert) { + let beforeChangeAction = this.spy(function() { + assert.step('beforeChange'); + }); + this.actions.beforeChange = beforeChangeAction; + let onChangeAction = this.spy(function() { + assert.step('onChange'); + }); + this.actions.onChange = onChangeAction; + await render(hbs` + {{#nucleus-tabs description="site-navigation" select="home" beforeChange=(action "beforeChange") onChange=(action "onChange") as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{#tabs.panel name="about" }} +
This is the about section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + `); + + await click('.nucleus-tabs .nucleus-tabs__list__item:not(.is-active)'); + assert.ok(beforeChangeAction.calledOnce, 'beforeChange action has been called.'); + assert.ok(onChangeAction.calledOnce, 'beforeChange action has been called.'); + assert.verifySteps(['beforeChange', 'onChange']); + }); + + test('it has accessibility attributes', async function(assert) { + await render(sampleTabsTemplate); + assert.dom('.nucleus-tabs .nucleus-tabs__list').hasAttribute('role', 'tablist'); + assert.dom('.nucleus-tabs .nucleus-tabs__list').hasAttribute('aria-label', 'site-navigation'); + assert.dom('.nucleus-tabs .nucleus-tabs__list .nucleus-tabs__list__item').hasAttribute('role', 'tab'); + assert.dom('.nucleus-tabs .nucleus-tabs__list .nucleus-tabs__list__item.is-active').hasAttribute('aria-selected', 'true'); + assert.dom('.nucleus-tabs .nucleus-tabs__list .nucleus-tabs__list__item:not(.is-active)').hasAttribute('aria-selected', 'false'); + assert.dom('.nucleus-tabs .nucleus-tabs__list .nucleus-tabs__list__item').hasAttribute('aria-controls'); + assert.dom('.nucleus-tabs .nucleus-tabs__panel').hasAttribute('role', 'tabpanel'); + assert.dom('.nucleus-tabs .nucleus-tabs__panel').hasAttribute('aria-labelledby'); + }); + + test('it passes a11y tests', async function(assert) { + await render(sampleTabsTemplate); + + return a11yAudit(this.element).then(() => { + assert.ok(true, 'no a11y errors found!'); + }); + }); + + test('visual regression for the different variants - default, background, disabled', async function(assert) { + await render(hbs` +
Default:
+ {{#nucleus-tabs description="site-navigation" select="home" as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{/nucleus-tabs}} +
With background:
+ {{#nucleus-tabs description="site-navigation" variant="background" select="home" as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{/nucleus-tabs}} +
With disabled:
+ {{#nucleus-tabs description="site-navigation" variant="background" select="home" as |tabs|}} + {{#tabs.panel name="home" }} +
This is the home section
+ {{/tabs.panel}} + {{#tabs.panel name="about" disabled="true" }} +
This is the home section
+ {{/tabs.panel}} + {{/nucleus-tabs}} + `); + await backstop(assert, {scenario:{misMatchThreshold: 0.001}}); + }); +}); diff --git a/packages/tabs/tests/test-helper.js b/packages/tabs/tests/test-helper.js new file mode 100644 index 00000000..0382a848 --- /dev/null +++ b/packages/tabs/tests/test-helper.js @@ -0,0 +1,8 @@ +import Application from '../app'; +import config from '../config/environment'; +import { setApplication } from '@ember/test-helpers'; +import { start } from 'ember-qunit'; + +setApplication(Application.create(config.APP)); + +start(); diff --git a/packages/tabs/tests/unit/.gitkeep b/packages/tabs/tests/unit/.gitkeep new file mode 100644 index 00000000..e69de29b