Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
:inverted="inverted"
:borderless="borderless"
:disabled="disabled"
:spread="spread"
:orientation="orientation"
:tab-list-class="orientation === 'vertical' ? 'd-w264' : undefined"
:activation-mode="activationMode"
Expand Down Expand Up @@ -112,6 +113,11 @@ export default {
default: 'manual',
},

spread: {
type: String,
default: 'none',
},

orientation: {
type: String,
default: 'horizontal',
Expand Down
10 changes: 10 additions & 0 deletions apps/dialtone-documentation/docs/_data/tabs.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
"applies": "N/A",
"description": "Inverts the tablist to work on dark backgrounds."
},
{
"class": "d-tablist--spread-grow",
"applies": ".d-tablist",
"description": "Tabs grow proportionally to fill available space. Longer labels get more room."
},
{
"class": "d-tablist--spread-equal",
"applies": ".d-tablist",
"description": "All tabs are the same width regardless of content length."
},
{
"class": "d-tab",
"applies": "N/A",
Expand Down
60 changes: 60 additions & 0 deletions apps/dialtone-documentation/docs/components/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,66 @@ vueCode='
'
showHtmlWarning />

## Spread

Control how tabs distribute available horizontal space within the tab list. It only applies to horizontal tabs, and has no effect with `orientation="vertical"`.

### Grow

Tabs expand proportionally to fill the container. Longer labels receive more space.

<code-well-header>
<div class="d-w100p">
<dt-tab-group spread="grow">
<template #tabs>
<dt-tab id="sg1" panel-id="sg2" selected>Tab 1</dt-tab>
<dt-tab id="sg3" panel-id="sg4">Tab the second</dt-tab>
<dt-tab id="sg5" panel-id="sg6">Tab the third</dt-tab>
</template>
</dt-tab-group>
</div>
</code-well-header>

<code-example-tabs
vueCode='
<dt-tab-group spread="grow">
<template #tabs>
<dt-tab id="1" panel-id="2" selected>Tab 1</dt-tab>
<dt-tab id="3" panel-id="4">Tab the second</dt-tab>
<dt-tab id="5" panel-id="6">Tab the third</dt-tab>
</template>
</dt-tab-group>
'
showHtmlWarning />

### Equal

All tabs share the same width, regardless of label length.

<code-well-header>
<div class="d-w100p">
<dt-tab-group spread="equal">
<template #tabs>
<dt-tab id="se1" panel-id="se2" selected>Tab 1</dt-tab>
<dt-tab id="se3" panel-id="se4">Tab the second</dt-tab>
<dt-tab id="se5" panel-id="se6">Tab the third</dt-tab>
</template>
</dt-tab-group>
</div>
</code-well-header>
Comment thread
francisrupert marked this conversation as resolved.

<code-example-tabs
vueCode='
<dt-tab-group spread="equal">
<template #tabs>
<dt-tab id="1" panel-id="2" selected>Tab 1</dt-tab>
<dt-tab id="3" panel-id="4">Tab the second</dt-tab>
<dt-tab id="5" panel-id="6">Tab the third</dt-tab>
</template>
</dt-tab-group>
'
showHtmlWarning />

## Sizes

<code-well-header>
Expand Down
40 changes: 40 additions & 0 deletions packages/combinator/src/variants/variants_tab_group.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,46 @@ export default {
},
},

'spread tabs: equal': {
props: {
selected: { initialValue: 'panel-1' },
tabListClass: {
initialValue: 'd-w384',
},
spread: {
initialValue: 'equal',
},
},
slots: {
tabs: {
initialValue: '<dt-tab id="tab-1" panel-id="panel-1" selected>Tab 1</dt-tab><dt-tab id="tab-2" panel-id="panel-2">Tab 2</dt-tab><dt-tab id="tab-3" panel-id="panel-3">Tab the third</dt-tab>',
},
default: {
initialValue: '<div class="d-ba d-baw2 d-bas-dashed d-bc-subtle d-w100p d-py48"><dt-tab-panel id="panel-1" tab-id="tab-1"><dt-text as="p" kind="code" size="xs" tone="muted" align="center" class="d-p16"> <strong>First</strong> tab content panel </dt-text></dt-tab-panel><dt-tab-panel id="panel-2" tab-id="tab-2"><dt-text as="p" kind="code" size="xs" tone="muted" align="center" class="d-p16"> <strong>Second</strong> tab content panel </dt-text></dt-tab-panel><dt-tab-panel id="panel-3" tab-id="tab-3"><dt-text as="p" kind="code" size="xs" tone="muted" align="center" class="d-p16"> <strong>Third</strong> tab content panel </dt-text></dt-tab-panel></div>',
},
},
},

'spread tabs: grow': {
props: {
selected: { initialValue: 'panel-1' },
tabListClass: {
initialValue: 'd-w384',
},
spread: {
initialValue: 'grow',
},
},
slots: {
tabs: {
initialValue: '<dt-tab id="tab-1" panel-id="panel-1" selected>Tab 1</dt-tab><dt-tab id="tab-2" panel-id="panel-2">Tab the second</dt-tab><dt-tab id="tab-3" panel-id="panel-3">Tab the third</dt-tab>',
},
default: {
initialValue: '<div class="d-ba d-baw2 d-bas-dashed d-bc-subtle d-w100p d-py48"><dt-tab-panel id="panel-1" tab-id="tab-1"><dt-text as="p" kind="code" size="xs" tone="muted" align="center" class="d-p16"> <strong>First</strong> tab content panel </dt-text></dt-tab-panel><dt-tab-panel id="panel-2" tab-id="tab-2"><dt-text as="p" kind="code" size="xs" tone="muted" align="center" class="d-p16"> <strong>Second</strong> tab content panel </dt-text></dt-tab-panel><dt-tab-panel id="panel-3" tab-id="tab-3"><dt-text as="p" kind="code" size="xs" tone="muted" align="center" class="d-p16"> <strong>Third</strong> tab content panel </dt-text></dt-tab-panel></div>',
},
},
},

'muted, small': {
props: {
kind: { initialValue: 'muted' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,14 @@

&__tabset-list {
gap: var(--dt-size-0);
justify-content: space-between;

&::after {
background-color: var(--dt-color-surface-moderate) !important;
}

.d-tab {
--tab-padding-y: var(--dt-size-400);
--tab-padding-x: var(--dt-size-300);

flex-grow: 1;
.d-tablist__item {
--button-padding-y: var(--dt-size-350);
--button-padding-x: var(--dt-size-100);

&.d-tab--selected {
&::after {
Expand Down
21 changes: 21 additions & 0 deletions packages/dialtone-css/lib/build/less/components/tabs.less
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,27 @@
}
}

// Spread β€” shared
&--spread-grow,
&--spread-equal {
flex-wrap: nowrap;
}

// Spread grow β€” tabs grow proportionally to fill space
&--spread-grow {
> :where(.d-tablist__item) {
flex: 1 1 auto;
}
}

// Spread equal β€” all tabs are the same width
&--spread-equal {
> :where(.d-tablist__item) {
flex: 1 1 0;
min-inline-size: 0;
}
}

// Inverted
&--inverted {
--tab-color-text: var(--dt-action-color-foreground-inverted-default);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<dt-tab-group
:selected="selectedTab"
size="sm"
spread="equal"
tab-list-class="d-emoji-picker__tabset-list"
>
<template #tabs>
Expand Down
1 change: 1 addition & 0 deletions packages/dialtone-vue/components/tab/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export {
TAB_LIST_SIZE_MODIFIERS,
TAB_ACTIVATION_MODES,
TAB_GROUP_KINDS,
TAB_SPREADS,
} from './tabs_constants';
6 changes: 0 additions & 6 deletions packages/dialtone-vue/components/tab/tab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@ describe('DtTab Tests', () => {
expect(tab.text()).toBe(MOCK_DEFAULT_SLOT);
});

it('should set data-content to match the slot text', () => {
const label = tab.find('[data-qa="dt-tab-label"]');

expect(label.attributes('data-content')).toBe(MOCK_DEFAULT_SLOT);
});

describe('Selected Tab by default', () => {
it('Group context should have set selected tab', () => {
mockProps = { selected: true };
Expand Down
24 changes: 1 addition & 23 deletions packages/dialtone-vue/components/tab/tab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,7 @@
<slot name="trailing" />
</template>
<!-- @slot default slot, defaults contains dt-button -->
<span
ref="tabLabel"
class="d-tablist__item-label"
data-qa="dt-tab-label"
>
<slot />
</span>
<slot />
</dt-button>
</template>

Expand Down Expand Up @@ -246,28 +240,12 @@ export default {
},

mounted () {
this.syncDataContent();
if (this.selected) {
this.groupContext.selected = this.panelId;
}
},

updated () {
this.syncDataContent();
},

methods: {
// Sets data-content to match the rendered label text so CSS can use
// `content: attr(data-content)` on a hidden ::after pseudo-element to
// hold the bold-width and prevent layout shift on selection.
syncDataContent () {
const el = this.$refs.tabLabel;
if (!el) return;
const text = el.textContent?.trim() || '';
if (el.getAttribute('data-content') !== text) {
el.setAttribute('data-content', text);
}
},
},
};
</script>
40 changes: 39 additions & 1 deletion packages/dialtone-vue/components/tab/tab_group.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import DtTabGroup from './tab_group.vue';
import DtTabPanel from './tab_panel.vue';
import DtTab from './tab.vue';
import { returnFirstEl } from '@/common/utils';
import { TAB_LIST_KIND_MODIFIERS, TAB_LIST_SIZE_MODIFIERS, TAB_LIST_IMPORTANCE_MODIFIERS, TAB_ORIENTATION_MODIFIERS } from './tabs_constants';
import { TAB_LIST_KIND_MODIFIERS, TAB_LIST_SIZE_MODIFIERS, TAB_LIST_IMPORTANCE_MODIFIERS, TAB_ORIENTATION_MODIFIERS, TAB_SPREAD_MODIFIERS } from './tabs_constants';
import { h } from 'vue';

const optionTabPanel = [
Expand Down Expand Up @@ -147,6 +147,35 @@ describe('DtTabGroup Tests', () => {
expect(tabList.classes(TAB_LIST_IMPORTANCE_MODIFIERS.borderless)).toBe(true);
});
});

describe('Correct spread modifiers', () => {
afterEach(() => {
delete props.spread;
delete props.orientation;
});

it('should apply spread modifier when spread is grow', () => {
props.spread = 'grow';
_mountWrapper();

expect(tabList.classes(TAB_SPREAD_MODIFIERS.grow)).toBe(true);
});

it('should apply spread-equal modifier when spread is equal', () => {
props.spread = 'equal';
_mountWrapper();

expect(tabList.classes(TAB_SPREAD_MODIFIERS.equal)).toBe(true);
});

it('should not apply spread modifier when orientation is vertical', () => {
props.spread = 'grow';
props.orientation = 'vertical';
_mountWrapper();

expect(tabList.classes(TAB_SPREAD_MODIFIERS.grow)).toBe(false);
});
});
});

describe('Interactivity Tests', () => {
Expand Down Expand Up @@ -754,6 +783,15 @@ describe('DtTabGroup Tests', () => {
});
});

describe('Spread prop', () => {
it('should pass spread through groupContext', () => {
props.spread = 'grow';
_mountWrapper();

expect(wrapper.vm.provideObj.spread).toBe('grow');
});
});

describe('Extendability Tests', () => {
describe('When tab list class is provided', () => {
beforeEach(() => {
Expand Down
24 changes: 24 additions & 0 deletions packages/dialtone-vue/components/tab/tab_group.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
'd-tablist',
TAB_LIST_SIZE_MODIFIERS[size],
TAB_ORIENTATION_MODIFIERS[orientation],
orientation !== 'vertical' && TAB_SPREAD_MODIFIERS[spread],
{
[TAB_LIST_KIND_MODIFIERS.inverted]: inverted,
[TAB_LIST_IMPORTANCE_MODIFIERS.borderless]: borderless,
Expand Down Expand Up @@ -48,6 +49,8 @@ import {
TAB_ORIENTATION_MODIFIERS,
TAB_ACTIVATION_MODES,
TAB_GROUP_KINDS,
TAB_SPREADS,
TAB_SPREAD_MODIFIERS,
} from './tabs_constants';

/**
Expand Down Expand Up @@ -110,6 +113,18 @@ export default {
default: false,
},

/**
* Controls how tabs distribute available space within the tab list.
* @values none, grow, equal
*/
spread: {
type: String,
default: 'none',
validator (value) {
return TAB_SPREADS.includes(value);
},
},

/**
* The orientation of the tab list
* @values horizontal, vertical
Expand Down Expand Up @@ -212,6 +227,7 @@ export default {
kind: 'default',
outlined: false,
orientation: 'horizontal',
spread: 'none',
},

focusId: null,
Expand All @@ -220,6 +236,7 @@ export default {
TAB_LIST_KIND_MODIFIERS,
TAB_LIST_IMPORTANCE_MODIFIERS,
TAB_ORIENTATION_MODIFIERS,
TAB_SPREAD_MODIFIERS,
};
},

Expand Down Expand Up @@ -265,6 +282,13 @@ export default {
this.provideObj.orientation = this.orientation;
},
},

spread: {
immediate: true,
handler () {
this.provideObj.spread = this.spread;
},
},
},

mounted () {
Expand Down
Loading
Loading