-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tabs component #169
base: master
Are you sure you want to change the base?
Tabs component #169
Changes from all commits
42f81fb
b3c2768
72768b2
81f209c
06ddc00
ed2b046
07a9267
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<div data-test-ember-headlessui-tabs-wrapper ...attributes> | ||
{{yield (hash | ||
Title=(component 'tabs-group/-title' registerTabNames=this.registerTabNames selectTab=this.selectTab selectedTab=this.selectedTab) | ||
Content=(component 'tabs-group/-content' selectedContent=this.selectedContent registerContent=this.registerContent))}} | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { action } from '@ember/object'; | ||
export default class TABSGROUP extends Component { | ||
tabNames = []; | ||
Contents = []; | ||
@tracked currentTab = null; | ||
@action | ||
registerTabNames(e) { | ||
this.tabNames = [...this.tabNames, e]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of using immutability patterns, can we switch to a |
||
} | ||
@action | ||
registerContent(e) { | ||
this.Contents = [...this.Contents, e]; | ||
this.currentTab = 0; | ||
} | ||
@action | ||
selectTab(changedTo, changedFrom) { | ||
this.currentTab = this.tabNames.indexOf(changedTo.target); | ||
this.pastTab = this.tabNames.indexOf(changedFrom); | ||
if (this.args.onChange) { | ||
return this.args.onChange( | ||
this.currentTab, | ||
this.tabNames[this.currentTab], | ||
this.tabNames[this.pastTab] | ||
); | ||
} | ||
} | ||
|
||
get selectedTab() { | ||
return this.tabNames[this.currentTab]; | ||
} | ||
get selectedContent() { | ||
return this.Contents[this.currentTab]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<div class="{{if this.renderContent '' 'hidden'}}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we can assume a "hidden" class exists |
||
data-test-headlessui-tabs-content='test-tabs-content-{{@content-index}}' | ||
{{this.registerContent}} ...attributes> | ||
{{#if this.renderContent}} | ||
{{yield}} | ||
{{/if}} | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
|
||
import { modifier } from 'ember-modifier'; | ||
|
||
export default class Content extends Component { | ||
@tracked element = null; | ||
get renderContent() { | ||
return this.element === this.args.selectedContent; | ||
} | ||
registerContent = modifier((e) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we not have this side-effect here? I know this pattern is common in headlessui, but it's a problematic pattern, and we don't want to propagate it. Derived data will be more performant, have fewer layout shifts, and will in general be way easier to debug. |
||
this.element = e; | ||
this.args.registerContent(e); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<button | ||
type='button' | ||
data-test-headlessui-tabs-title='test-tabs-title-btn-{{@index}}' | ||
id={{guid}} | ||
disabled={{if @disable true false}} | ||
{{on 'click' this.selectTab}} | ||
{{this.registerTabs}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this modifier please <3 |
||
...attributes | ||
> | ||
{{yield}} | ||
</button> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { action } from '@ember/object'; | ||
import { guidFor } from '@ember/object/internals'; | ||
|
||
import { modifier } from 'ember-modifier'; | ||
|
||
export default class Title extends Component { | ||
guid = `${guidFor(this)}-tailwindui-tabs-title`; | ||
@tracked element = null; | ||
|
||
registerTabs = modifier((e) => { | ||
this.element = e; | ||
this.args.registerTabNames(e); | ||
}); | ||
|
||
@action | ||
selectTab(changedTo) { | ||
let changedFrom = this.args.selectedTab; | ||
return this.args.selectTab(changedTo, changedFrom); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from 'ember-headlessui/components/tabs-group'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from 'ember-headlessui/components/tabs-group/-content'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from 'ember-headlessui/components/tabs-group/-title'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { tracked } from '@glimmer/tracking'; | ||
import Controller from '@ember/controller'; | ||
import { action } from '@ember/object'; | ||
|
||
export default class TabsTabsBasicController extends Controller { | ||
@tracked activatedIndex = 0; | ||
titles = ['cars', 'motor cycles', 'boats', 'planes']; | ||
contents = [ | ||
'cars content', | ||
'motor cycles content', | ||
'boats content', | ||
'planes content', | ||
]; | ||
@action | ||
onChange(activeIndex) { | ||
this.activatedIndex = activeIndex; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { tracked } from '@glimmer/tracking'; | ||
import Controller from '@ember/controller'; | ||
import { action } from '@ember/object'; | ||
|
||
export default class TabsTabsBasicController extends Controller { | ||
disabledToBeIndex = 2; | ||
@tracked activatedIndex = 0; | ||
titles = ['cars', 'motor cycles', 'boats', 'planes']; | ||
contents = [ | ||
'cars content', | ||
'motor cycles content', | ||
'boats content', | ||
'planes content', | ||
]; | ||
@action | ||
onChange(activeIndex) { | ||
this.activatedIndex = activeIndex; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { tracked } from '@glimmer/tracking'; | ||
import Controller from '@ember/controller'; | ||
import { action } from '@ember/object'; | ||
|
||
export default class TabsTabsBasicController extends Controller { | ||
@tracked activatedIndex = 0; | ||
titles = ['cars', 'motor cycles', 'boats', 'planes']; | ||
contents = [ | ||
'cars content', | ||
'motor cycles content', | ||
'boats content', | ||
'planes content', | ||
]; | ||
@action | ||
onChange(activeIndex, changedTo, changedFrom) { | ||
alert( | ||
`sure you want to change to ${changedTo.textContent.trim()} from ${changedFrom.textContent.trim()}?` | ||
); | ||
this.activatedIndex = activeIndex; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import Route from '@ember/routing/route'; | ||
|
||
export default class TabsBasicRoute extends Route {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<div class='w-full max-w-md px-2 py-16 mx-auto'> | ||
<TabsGroup class="flex flex-col space-x-1 rounded-xl p-1" @onChange = {{this.onChange}} as |tabs|> | ||
<div class='flex space-x-1 rounded-xl bg-sky-600/20 p-1'> | ||
{{#each this.titles as |tabName index|}} | ||
<tabs.Title | ||
@index={{index}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4 pointer-events-auto {{if (eq index this.activatedIndex) "bg-white shadow"}}' | ||
> | ||
{{tabName}} | ||
</tabs.Title> | ||
{{/each}} | ||
</div> | ||
<div class="mx-auto w-full max-w-md shadow-md px-4 py-4 rounded-xl bg-white mt-4"> | ||
{{#each this.contents as |content index|}} | ||
<tabs.Content class="content" @content-index={{index}}> | ||
<p>{{content}}</p> | ||
</tabs.Content> | ||
{{/each}} | ||
</div> | ||
</TabsGroup> | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<div class='w-full max-w-md px-2 py-16 mx-auto'> | ||
<TabsGroup class="flex flex-col space-x-1 rounded-xl p-1" @onChange = {{this.onChange}} as |tabs|> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are your thoughts on internalizing the onChange behavior by default? can we abstract away the need for the consumer to configure this? (I think so!) |
||
<div class='flex space-x-1 rounded-xl bg-sky-600/20 p-1'> | ||
{{#each this.titles as |tabName index|}} | ||
<tabs.Title | ||
@index={{index}} | ||
@disable={{if (eq index this.disabledToBeIndex) true}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4 pointer-events-auto {{if (eq index this.activatedIndex) "bg-white shadow"}}' | ||
> | ||
{{tabName}} | ||
</tabs.Title> | ||
{{/each}} | ||
</div> | ||
<div class="mx-auto w-full max-w-md shadow-md px-4 py-4 rounded-xl bg-white mt-4"> | ||
{{#each this.contents as |content index|}} | ||
<tabs.Content class="content" @content-index={{index}}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we remove the index? and compare via object identity or some other way? using indexes makes looping inefficient / invalidate more frequently than we need it to |
||
<p>{{content}}</p> | ||
</tabs.Content> | ||
{{/each}} | ||
</div> | ||
</TabsGroup> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<div class='w-full max-w-md px-2 py-16 mx-auto'> | ||
<TabsGroup class="flex flex-col space-x-1 rounded-xl p-1" @onChange = {{this.onChange}} as |tabs|> | ||
<div class='flex space-x-1 rounded-xl bg-sky-600/20 p-1'> | ||
{{#each this.titles as |tabName index|}} | ||
<tabs.Title | ||
@index={{index}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4 pointer-events-auto {{if (eq index this.activatedIndex) "bg-white shadow"}}' | ||
> | ||
{{tabName}} | ||
</tabs.Title> | ||
{{/each}} | ||
</div> | ||
<div class="mx-auto w-full max-w-md shadow-md px-4 py-4 rounded-xl bg-white mt-4"> | ||
{{#each this.contents as |content index|}} | ||
<tabs.Content class="content" @content-index={{index}}> | ||
<p>{{content}}</p> | ||
</tabs.Content> | ||
{{/each}} | ||
</div> | ||
</TabsGroup> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { click, render } from '@ember/test-helpers'; | ||
import { hbs } from 'ember-cli-htmlbars'; | ||
import { module, test } from 'qunit'; | ||
import { setupRenderingTest } from 'ember-qunit'; | ||
|
||
module('Integration | Component | <Tabs>', function (hooks) { | ||
setupRenderingTest(hooks); | ||
test('Should render the tabs component', async function (assert) { | ||
await render(hbs` | ||
<div class='w-full max-w-md px-2 py-16 mx-auto'> | ||
<TabsGroup class="flex flex-col space-x-1 rounded-xl p-1" @onChange = {{this.onChange}} as |tabs|> | ||
<div class='flex space-x-1 rounded-xl bg-sky-600/20 p-1'> | ||
<tabs.Title | ||
@index={{1}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4' | ||
> | ||
hello | ||
</tabs.Title> | ||
<tabs.Title | ||
@index={{2}} | ||
@disable={{true}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4'> | ||
hell02 | ||
</tabs.Title> | ||
<tabs.Title | ||
@index={{3}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4'> | ||
hell03 | ||
</tabs.Title> | ||
<tabs.Title | ||
@index={{4}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4'> | ||
hell04 | ||
</tabs.Title> | ||
</div> | ||
<div class="mx-auto w-full max-w-md shadow-md px-4 py-4 rounded-xl bg-white mt-4"> | ||
<tabs.Content class="content"> | ||
<p>hello world</p> | ||
</tabs.Content> | ||
<tabs.Content class="content"> | ||
<p>hello world 2</p> | ||
</tabs.Content> | ||
<tabs.Content class="content"> | ||
<p>hello world 3</p> | ||
</tabs.Content> | ||
<tabs.Content class="content"> | ||
<p>hello world 4</p> | ||
</tabs.Content> | ||
</div> | ||
</TabsGroup> | ||
</div>`); | ||
assert.dom('[data-test-ember-headlessui-tabs-wrapper]').exists(); | ||
}); | ||
|
||
test('Should render the respective content when a tab is selected', async function (assert) { | ||
await render(hbs`<div class='w-full max-w-md px-2 py-16 mx-auto'> | ||
<TabsGroup class="flex flex-col space-x-1 rounded-xl p-1" as |tabs|> | ||
<div class='flex space-x-1 rounded-xl bg-sky-600/20 p-1'> | ||
<tabs.Title | ||
@index={{1}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4' | ||
> | ||
hello | ||
</tabs.Title> | ||
<tabs.Title | ||
@index={{2}} | ||
@disable={{true}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4'> | ||
hell02 | ||
</tabs.Title> | ||
<tabs.Title | ||
@index={{3}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4'> | ||
hell03 | ||
</tabs.Title> | ||
<tabs.Title | ||
@index={{4}} | ||
class='w-full rounded-lg py-2.5 text-sm font-medium leading-5 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 space-x-4'> | ||
hell04 | ||
</tabs.Title> | ||
</div> | ||
<div class="mx-auto w-full max-w-md shadow-md px-4 py-4 rounded-xl bg-white mt-4"> | ||
<tabs.Content class="content" @content-index={{1}}> | ||
<p>hello world</p> | ||
</tabs.Content> | ||
<tabs.Content class="content" @content-index={{2}}> | ||
<p>hello world 2</p> | ||
</tabs.Content> | ||
<tabs.Content class="content" @content-index={{3}}> | ||
<p>hello world 3</p> | ||
</tabs.Content> | ||
<tabs.Content class="content" @content-index={{4}}> | ||
<p>hello world 4</p> | ||
</tabs.Content> | ||
</div> | ||
</TabsGroup> | ||
</div> | ||
`); | ||
await click('[data-test-headlessui-tabs-title="test-tabs-title-btn-3"]'); | ||
assert | ||
.dom('[data-test-headlessui-tabs-content="test-tabs-content-1"]') | ||
.doesNotExist(); | ||
assert | ||
.dom('[data-test-headlessui-tabs-content="test-tabs-content-2"]') | ||
.doesNotExist(); | ||
assert | ||
.dom('[data-test-headlessui-tabs-content="test-tabs-content-4"]') | ||
.doesNotexist(); | ||
assert | ||
.dom('[data-test-headlessui-tabs-content="test-tabs-content-3"]') | ||
.exists(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have to have this wrapping div?