Skip to content

Commit 161412a

Browse files
authored
[chore]: refactor out base classes for tree and tree-item (#33990)
1 parent 40fc696 commit 161412a

14 files changed

+519
-475
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": "refactor out base classes for tree and tree item",
4+
"packageName": "@fluentui/web-components",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2801,8 +2801,10 @@ export function isDropdownOption(value: Node | null, tagName?: string): value is
28012801
// @public
28022802
export function isListbox(element?: Node | null, tagName?: string): element is Listbox;
28032803

2804+
// Warning: (ae-forgotten-export) The symbol "BaseTreeItem" needs to be exported by the entry point index.d.ts
2805+
//
28042806
// @public
2805-
export function isTreeItem(element?: Node | null, tagName?: string): element is TreeItem;
2807+
export function isTreeItem(element?: Node | null, tagName?: string): element is BaseTreeItem;
28062808

28072809
// @public
28082810
export class Label extends FASTElement {
@@ -4371,29 +4373,12 @@ export const TooltipTemplate: ViewTemplate<Tooltip, any>;
43714373
// Warning: (ae-missing-release-tag) "TreeItem" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
43724374
//
43734375
// @public (undocumented)
4374-
export class TreeItem extends FASTElement {
4375-
constructor();
4376+
export class TreeItem extends BaseTreeItem {
43764377
appearance: TreeItemAppearance;
4377-
blurHandler(e: FocusEvent): void;
4378-
// (undocumented)
4379-
childTreeItems: TreeItem[] | undefined;
4380-
// @internal
4381-
protected childTreeItemsChanged(): void;
4382-
dataIndent: number | undefined;
4383-
// @internal
4384-
elementInternals: ElementInternals;
4385-
empty: boolean;
4386-
expanded: boolean;
4387-
expandedChanged(prev: boolean, next: boolean): void;
4388-
focusHandler(e: FocusEvent): void;
4389-
// @internal
4390-
get isNestedItem(): boolean;
4391-
selected: boolean;
43924378
// @internal
4393-
protected selectedChanged(prev: boolean, next: boolean): void;
4379+
childTreeItemsChanged(): void;
43944380
size: TreeItemSize;
4395-
toggleExpansion(): void;
4396-
toggleSelection(): void;
4381+
updateSizeAndAppearance(): void;
43974382
}
43984383

43994384
// Warning: (ae-missing-release-tag) "TreeItemAppearance" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)

packages/web-components/src/tree-item/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { BaseTreeItem } from './tree-item.base.js';
12
export { TreeItem } from './tree-item.js';
23
export { template as TreeItemTemplate } from './tree-item.template.js';
34
export { styles as TreeItemStyles } from './tree-item.styles.js';
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { attr, css, ElementStyles, FASTElement, observable } from '@microsoft/fast-element';
2+
import { toggleState } from '../utils/element-internals.js';
3+
import { isTreeItem } from './tree-item.options.js';
4+
5+
export class BaseTreeItem extends FASTElement {
6+
/**
7+
* The internal {@link https://developer.mozilla.org/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component.
8+
*
9+
* @internal
10+
*/
11+
public elementInternals: ElementInternals = this.attachInternals();
12+
13+
constructor() {
14+
super();
15+
this.elementInternals.role = 'treeitem';
16+
}
17+
18+
/**
19+
* When true, the control will be appear expanded by user interaction.
20+
* @public
21+
* HTML Attribute: `expanded`
22+
*/
23+
@attr({ mode: 'boolean' })
24+
expanded: boolean = false;
25+
26+
/**
27+
* Handles changes to the expanded attribute
28+
* @param prev - the previous state
29+
* @param next - the next state
30+
*
31+
* @public
32+
*/
33+
public expandedChanged(prev: boolean, next: boolean): void {
34+
toggleState(this.elementInternals, 'expanded', next);
35+
if (this.childTreeItems && this.childTreeItems.length > 0) {
36+
this.elementInternals.ariaExpanded = next ? 'true' : 'false';
37+
}
38+
}
39+
40+
/**
41+
* When true, the control will appear selected by user interaction.
42+
* @public
43+
* @remarks
44+
* HTML Attribute: selected
45+
*/
46+
@attr({ mode: 'boolean' })
47+
selected: boolean = false;
48+
49+
/**
50+
* Handles changes to the selected attribute
51+
* @param prev - the previous state
52+
* @param next - the next state
53+
*
54+
* @internal
55+
*/
56+
protected selectedChanged(prev: boolean, next: boolean): void {
57+
this.$emit('change');
58+
toggleState(this.elementInternals, 'selected', next);
59+
this.elementInternals.ariaSelected = next ? 'true' : 'false';
60+
}
61+
62+
/**
63+
* When true, the control has no child tree items
64+
* @public
65+
* HTML Attribute: empty
66+
*/
67+
@attr({ mode: 'boolean' })
68+
public empty: boolean = false;
69+
70+
private styles: ElementStyles | undefined;
71+
72+
/**
73+
* The indent of the tree item element.
74+
* This is not needed once css attr() is supported (--indent: attr(data-indent type(<number>)));
75+
* @public
76+
*/
77+
@attr({ attribute: 'data-indent' })
78+
public dataIndent!: number | undefined;
79+
80+
private dataIndentChanged(prev: number, next: number) {
81+
if (this.styles !== undefined) {
82+
this.$fastController.removeStyles(this.styles);
83+
}
84+
85+
this.styles = css`
86+
:host {
87+
--indent: ${next as any};
88+
}
89+
`;
90+
91+
this.$fastController.addStyles(this.styles);
92+
}
93+
94+
@observable
95+
public childTreeItems: BaseTreeItem[] | undefined = [];
96+
97+
/**
98+
* Handles changes to the child tree items
99+
*
100+
* @public
101+
*/
102+
public childTreeItemsChanged() {
103+
this.empty = this.childTreeItems?.length === 0;
104+
this.updateChildTreeItems();
105+
}
106+
107+
/**
108+
* Updates the childrens indent
109+
*
110+
* @public
111+
*/
112+
public updateChildTreeItems() {
113+
if (!this.childTreeItems?.length) {
114+
return;
115+
}
116+
117+
this.childTreeItems.forEach(item => {
118+
this.setIndent(item);
119+
});
120+
}
121+
122+
/**
123+
* Sets the indent for each item
124+
*/
125+
private setIndent(item: BaseTreeItem): void {
126+
const indent = this.dataIndent ?? 0;
127+
item.dataIndent = indent + 1;
128+
}
129+
130+
/**
131+
* Handle focus events
132+
*
133+
* @public
134+
*/
135+
public focusHandler(e: FocusEvent): void {
136+
if (
137+
e.target === this ||
138+
// In case where the tree-item contains a focusable element, we should not set the tabindex to 0 when the focus is on its child focusable element,
139+
// so users can shift+tab to navigate to the tree-item from its child focusable element.
140+
this.contains(e.target as Node)
141+
) {
142+
this.setAttribute('tabindex', '0');
143+
}
144+
}
145+
146+
/**
147+
* Handle blur events
148+
*
149+
* @public
150+
*/
151+
public blurHandler(e: FocusEvent): void {
152+
if (e.target === this) {
153+
this.setAttribute('tabindex', '-1');
154+
}
155+
}
156+
157+
/**
158+
* Toggle the expansion state of the tree item
159+
*
160+
* @public
161+
*/
162+
public toggleExpansion() {
163+
if (this.childTreeItems?.length) {
164+
this.expanded = !this.expanded;
165+
}
166+
}
167+
168+
/**
169+
* Toggle the single selection state of the tree item
170+
*
171+
* @public
172+
*/
173+
public toggleSelection() {
174+
this.selected = !this.selected;
175+
}
176+
177+
/**
178+
* Whether the tree is nested
179+
* @internal
180+
*/
181+
get isNestedItem() {
182+
return isTreeItem(this.parentElement);
183+
}
184+
}

packages/web-components/src/tree-item/tree-item.options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ValuesOf } from '../utils/typings.js';
2-
import { TreeItem } from './tree-item.js';
2+
import { BaseTreeItem } from './tree-item.base.js';
33

44
export const TreeItemAppearance = {
55
subtle: 'subtle',
@@ -24,7 +24,7 @@ export type TreeItemSize = ValuesOf<typeof TreeItemSize>;
2424
* @returns true if the element is a dropdown.
2525
* @public
2626
*/
27-
export function isTreeItem(element?: Node | null, tagName: string = '-tree-item'): element is TreeItem {
27+
export function isTreeItem(element?: Node | null, tagName: string = '-tree-item'): element is BaseTreeItem {
2828
if (element?.nodeType !== Node.ELEMENT_NODE) {
2929
return false;
3030
}

packages/web-components/src/tree-item/tree-item.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '../../test/playwright/index.js';
2-
import type { TreeItem } from './tree-item.js';
2+
import type { BaseTreeItem } from './tree-item.base.js';
33

44
test.describe('Tree Item', () => {
55
test.use({
@@ -41,7 +41,7 @@ test.describe('Tree Item', () => {
4141
const nestedItems = element.locator('fluent-tree-item');
4242
await expect(nestedItems).toBeHidden();
4343

44-
await element.nth(0).evaluate((node: TreeItem) => {
44+
await element.nth(0).evaluate((node: BaseTreeItem) => {
4545
node.expanded = true;
4646
});
4747
await expect(element.nth(0)).toHaveAttribute('expanded');

packages/web-components/src/tree-item/tree-item.styles.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,13 @@ export const styles = css`
7979
.content {
8080
display: flex;
8181
align-items: center;
82-
min-width: 0;
8382
gap: ${spacingHorizontalXS};
8483
}
8584
8685
.chevron {
8786
display: flex;
8887
align-items: center;
8988
flex-shrink: 0;
90-
min-width: 0;
9189
justify-content: center;
9290
width: ${spacingHorizontalXXL};
9391
height: ${spacingVerticalXXL};
@@ -99,12 +97,9 @@ export const styles = css`
9997
transform: rotate(180deg);
10098
}
10199
102-
.badging,
103-
.toolbar {
100+
.aside {
104101
display: flex;
105102
align-items: center;
106-
min-width: 0;
107-
font-size: ${fontSizeBase300};
108103
}
109104
110105
.positioning-region:hover {

packages/web-components/src/tree-item/tree-item.template.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,8 @@ export const template = html<TreeItem>`
3030
<slot></slot>
3131
<slot name="end"></slot>
3232
</div>
33-
<div class="badging" part="badging">
34-
<slot name="badging"></slot>
35-
</div>
36-
<div class="toolbar" part="toolbar">
37-
<slot name="toolbar"></slot>
33+
<div class="aside" part="aside">
34+
<slot name="aside"></slot>
3835
</div>
3936
</div>
4037
<div role="group" class="items" part="items">

0 commit comments

Comments
 (0)