Skip to content

Commit b187b07

Browse files
authored
[fix]: vertical tablist and start and end slots in tabs were broken (#34704)
1 parent b3e8218 commit b187b07

File tree

11 files changed

+138
-22
lines changed

11 files changed

+138
-22
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "fix start and end slot for tabs",
4+
"packageName": "@fluentui/web-components",
5+
"email": "jes@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/docs/web-components.api.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ export class BaseTablist extends FASTElement {
759759
activeid: string;
760760
// @internal (undocumented)
761761
protected activeidChanged(oldValue: string, newValue: string): void;
762-
activetab: HTMLElement;
762+
activetab: Tab;
763763
adjust(adjustment: number): void;
764764
// @internal (undocumented)
765765
connectedCallback(): void;
@@ -772,8 +772,12 @@ export class BaseTablist extends FASTElement {
772772
// @internal (undocumented)
773773
protected orientationChanged(prev: TablistOrientation, next: TablistOrientation): void;
774774
protected setTabs(): void;
775+
// @internal
776+
slottedTabs: Node[];
777+
// @internal
778+
slottedTabsChanged(prev: Node[] | undefined, next: Node[] | undefined): void;
775779
// @internal (undocumented)
776-
tabs: HTMLElement[];
780+
tabs: Tab[];
777781
// @internal (undocumented)
778782
protected tabsChanged(): void;
779783
}
@@ -2803,6 +2807,9 @@ export function isDropdownOption(value: Node | null, tagName?: string): value is
28032807
// @public
28042808
export function isListbox(element?: Node | null, tagName?: string): element is Listbox;
28052809

2810+
// @public
2811+
export function isTab(element?: Node | null, tagName?: string): element is Tab;
2812+
28062813
// @public
28072814
export function isTreeItem(element?: Node | null, tagName?: string): element is BaseTreeItem;
28082815

packages/web-components/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ export {
245245
SwitchStyles,
246246
SwitchTemplate,
247247
} from './switch/index.js';
248-
export { Tab, TabOptions, TabTemplate, TabStyles, TabDefinition } from './tab/index.js';
248+
export { isTab, Tab, TabOptions, TabTemplate, TabStyles, TabDefinition } from './tab/index.js';
249249
export { TabPanel, TabPanelTemplate, TabPanelStyles, TabPanelDefinition } from './tab-panel/index.js';
250250
export {
251251
Tabs,

packages/web-components/src/tab/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { definition as TabDefinition } from './tab.definition.js';
2+
export { isTab } from './tab.options.js';
23
export { Tab } from './tab.js';
34
export type { TabOptions } from './tab.js';
45
export { styles as TabStyles } from './tab.styles.js';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Tab } from './tab.js';
2+
3+
/**
4+
* Predicate function that determines if the element should be considered a tab.
5+
*
6+
* @param element - The element to check.
7+
* @param tagName - The tag name to check.
8+
* @returns true if the element is a tab.
9+
* @public
10+
*/
11+
export function isTab(element?: Node | null, tagName: string = '-tab'): element is Tab {
12+
if (element?.nodeType !== Node.ELEMENT_NODE) {
13+
return false;
14+
}
15+
16+
return (element as Element).tagName.toLowerCase().endsWith(tagName);
17+
}

packages/web-components/src/tab/tab.styles.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export const styles = css`
2424
2525
:host {
2626
position: relative;
27-
flex-direction: column;
27+
flex-direction: row;
28+
align-items: center;
2829
cursor: pointer;
2930
box-sizing: border-box;
3031
justify-content: center;
@@ -36,6 +37,7 @@ export const styles = css`
3637
grid-row: 1;
3738
padding: ${spacingHorizontalM} ${spacingHorizontalMNudge};
3839
border-radius: ${borderRadiusMedium};
40+
gap: 4px;
3941
}
4042
:host .tab-content {
4143
display: inline-flex;
@@ -83,12 +85,6 @@ export const styles = css`
8385
::slotted([slot='end']) {
8486
display: flex;
8587
}
86-
::slotted([slot='start']) {
87-
margin-inline-end: 11px;
88-
}
89-
::slotted([slot='end']) {
90-
margin-inline-start: 11px;
91-
}
9288
:host([disabled]) {
9389
cursor: not-allowed;
9490
fill: ${colorNeutralForegroundDisabled};
@@ -108,6 +104,15 @@ export const styles = css`
108104
box-shadow: 0 0 0 3px ${colorStrokeFocus2};
109105
outline: 1px solid ${colorStrokeFocus1};
110106
}
107+
108+
:host([data-hasIndent]) {
109+
display: grid;
110+
grid-template-columns: 20px 1fr auto;
111+
}
112+
113+
:host([data-hasIndent]) .tab-content {
114+
grid-column: 2;
115+
}
111116
`.withBehaviors(
112117
forcedColorsStylesheetBehavior(css`
113118
:host([aria-selected='true'])::after {

packages/web-components/src/tablist/tablist.base.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
import { getDirection } from '../utils/index.js';
1313
import { swapStates, toggleState } from '../utils/element-internals.js';
1414
import { isFocusableElement } from '../utils/focusable-element.js';
15+
import type { Tab } from '../tab/tab.js';
16+
import { isTab } from '../tab/tab.options.js';
1517
import { TablistOrientation } from './tablist.options.js';
1618

1719
/**
@@ -105,10 +107,25 @@ export class BaseTablist extends FASTElement {
105107
}
106108

107109
/**
110+
* Content slotted in the tab slot.
108111
* @internal
109112
*/
110113
@observable
111-
public tabs!: HTMLElement[];
114+
public slottedTabs!: Node[];
115+
116+
/**
117+
* Updates the tabs property when content in the tabs slot changes.
118+
* @internal
119+
*/
120+
public slottedTabsChanged(prev: Node[] | undefined, next: Node[] | undefined): void {
121+
this.tabs = (next?.filter(tab => isTab(tab)) as Tab[]) ?? [];
122+
}
123+
124+
/**
125+
* @internal
126+
*/
127+
@observable
128+
public tabs!: Tab[];
112129
/**
113130
* @internal
114131
*/
@@ -134,7 +151,7 @@ export class BaseTablist extends FASTElement {
134151
* A reference to the active tab
135152
* @public
136153
*/
137-
public activetab!: HTMLElement;
154+
public activetab!: Tab;
138155

139156
private prevActiveTabIndex: number = 0;
140157
private activeTabIndex: number = 0;
@@ -163,10 +180,11 @@ export class BaseTablist extends FASTElement {
163180
protected setTabs(): void {
164181
this.activeTabIndex = this.getActiveIndex();
165182

166-
this.tabs.forEach((tab: HTMLElement, index: number) => {
183+
const hasStartSlot = this.tabs.some(tab => tab.start.assignedNodes().length > 0);
184+
185+
this.tabs.forEach((tab: Tab, index: number) => {
167186
if (tab.slot === 'tab') {
168187
const isActiveTab = this.activeTabIndex === index && isFocusableElement(tab);
169-
170188
const tabId: string = this.tabIds[index];
171189
tab.setAttribute('id', tabId);
172190
tab.setAttribute('aria-selected', isActiveTab ? 'true' : 'false');
@@ -177,6 +195,10 @@ export class BaseTablist extends FASTElement {
177195
this.activetab = tab;
178196
this.activeid = tabId;
179197
}
198+
// Only set the data-hasIndent attribute if the tab has a start slot and the orientation is vertical
199+
if (hasStartSlot && this.orientation === TablistOrientation.vertical) {
200+
tab.setAttribute('data-hasIndent', '');
201+
}
180202
}
181203
});
182204
}
@@ -195,7 +217,7 @@ export class BaseTablist extends FASTElement {
195217
}
196218

197219
private handleTabClick = (event: MouseEvent): void => {
198-
const selectedTab = event.currentTarget as HTMLElement;
220+
const selectedTab = event.currentTarget as Tab;
199221
if (selectedTab.nodeType === Node.ELEMENT_NODE && isFocusableElement(selectedTab)) {
200222
this.prevActiveTabIndex = this.activeTabIndex;
201223
this.activeTabIndex = this.tabs.indexOf(selectedTab);
@@ -269,8 +291,8 @@ export class BaseTablist extends FASTElement {
269291
}
270292
}
271293

272-
private activateTabByIndex(group: HTMLElement[], index: number) {
273-
const tab: HTMLElement = group[index] as HTMLElement;
294+
private activateTabByIndex(group: Tab[], index: number) {
295+
const tab = group[index];
274296
this.activetab = tab;
275297
this.prevActiveTabIndex = this.activeTabIndex;
276298
this.activeTabIndex = index;

packages/web-components/src/tablist/tablist.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,21 @@ test.describe('Tablist', () => {
345345
await expect(secondPanel).toBeVisible();
346346
await expect(thirdPanel).toBeHidden();
347347
});
348+
349+
test('should set data-hasIndent on all tabs when any tab has a start slot', async ({ fastPage }) => {
350+
const { element } = fastPage;
351+
await fastPage.setTemplate({
352+
attributes: { orientation: 'vertical' },
353+
innerHTML: /* html */ `
354+
<fluent-tab>Tab one</fluent-tab>
355+
<fluent-tab><span slot="start">T</span>Tab two</fluent-tab>
356+
<fluent-tab>Tab three</fluent-tab>
357+
`,
358+
});
359+
const tabs = element.locator('fluent-tab');
360+
361+
await expect(tabs.nth(0)).toHaveAttribute('data-hasIndent');
362+
await expect(tabs.nth(1)).toHaveAttribute('data-hasIndent');
363+
await expect(tabs.nth(2)).toHaveAttribute('data-hasIndent');
364+
});
348365
});

packages/web-components/src/tablist/tablist.stories.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { html, ref } from '@microsoft/fast-element';
1+
import { html, ref, when } from '@microsoft/fast-element';
22
import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js';
33
import type { Tablist as FluentTablist } from './tablist.js';
44
import { TablistAppearance as TablistAppearanceValues, TablistOrientation, TablistSize } from './tablist.options.js';
@@ -19,7 +19,27 @@ const storyTemplate = html<StoryArgs<FluentTablist>>`
1919
${ref('tablist')}
2020
>
2121
<fluent-tab id="first-tab">First Tab</fluent-tab>
22-
<fluent-tab id="second-tab">Second Tab</fluent-tab>
22+
<fluent-tab id="second-tab">
23+
${when(
24+
story => story.hasStartSlot,
25+
html`<span slot="start">
26+
<svg
27+
fill="currentColor"
28+
aria-hidden="true"
29+
width="20px"
30+
height="20px"
31+
viewBox="0 0 20 20"
32+
xmlns="http://www.w3.org/2000/svg"
33+
>
34+
<path
35+
d="M14.5 3A2.5 2.5 0 0117 5.5v9a2.5 2.5 0 01-2.5 2.5h-9A2.5 2.5 0 013 14.5v-9A2.5 2.5 0 015.5 3h9zm0 1h-9C4.67 4 4 4.67 4 5.5v9c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5zM7 11a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zM7 7a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2z"
36+
fill="currentColor"
37+
></path>
38+
</svg>
39+
</span> `,
40+
)}
41+
Second Tab
42+
</fluent-tab>
2343
<fluent-tab id="third-tab">Third Tab</fluent-tab>
2444
<fluent-tab id="fourth-tab">Fourth Tab</fluent-tab>
2545
</fluent-tablist>
@@ -78,6 +98,12 @@ export default {
7898

7999
export const Default: Story = {};
80100

101+
export const HorizontalWithStartSlot: Story = {
102+
args: {
103+
hasStartSlot: true,
104+
},
105+
};
106+
81107
export const VerticalOrientation: Story = {
82108
args: {
83109
orientation: TablistOrientation.vertical,
@@ -91,6 +117,20 @@ export const VerticalOrientation: Story = {
91117
],
92118
};
93119

120+
export const VerticalOrientationWithStartSlot: Story = {
121+
args: {
122+
orientation: TablistOrientation.vertical,
123+
hasStartSlot: true,
124+
},
125+
decorators: [
126+
Story => {
127+
const story = Story() as HTMLDivElement;
128+
story.style.flexDirection = 'row';
129+
return story;
130+
},
131+
],
132+
};
133+
94134
export const Disabled: Story = {
95135
args: {
96136
disabled: true,

packages/web-components/src/tablist/tablist.styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ export const styles = css`
4545
flex-direction: column;
4646
}
4747
48-
:host ::slotted([role='tab']) {
49-
align-items: flex-start;
48+
:host([orientation='vertical']) ::slotted([role='tab']) {
49+
justify-content: flex-start;
5050
}
5151
5252
/* indicator animation */

0 commit comments

Comments
 (0)