Skip to content

Commit 683cd05

Browse files
authored
[chore] move core functionality of dropdown to base class (#34033)
1 parent 5c27dee commit 683cd05

File tree

5 files changed

+83
-150
lines changed

5 files changed

+83
-150
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "[chore]: move core functionality to base dropdown",
4+
"packageName": "@fluentui/web-components",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

Diff for: packages/web-components/docs/web-components.api.md

+4-7
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,8 @@ export class BaseDropdown extends FASTElement {
600600
ariaLabelledBy: string;
601601
changeHandler(e: Event): boolean | void;
602602
clickHandler(e: PointerEvent): boolean | void;
603+
// (undocumented)
604+
connectedCallback(): void;
603605
// @internal
604606
control: HTMLInputElement;
605607
// @internal
@@ -608,6 +610,8 @@ export class BaseDropdown extends FASTElement {
608610
controlSlot: HTMLSlotElement;
609611
disabled?: boolean;
610612
disabledChanged(prev: boolean | undefined, next: boolean | undefined): void;
613+
// (undocumented)
614+
disconnectedCallback(): void;
611615
get displayValue(): string;
612616
// @internal
613617
elementInternals: ElementInternals;
@@ -2484,16 +2488,9 @@ export type DrawerType = ValuesOf<typeof DrawerType>;
24842488

24852489
// @public
24862490
export class Dropdown extends BaseDropdown {
2487-
constructor();
24882491
appearance: DropdownAppearance;
24892492
// @internal
24902493
appearanceChanged(prev: DropdownAppearance | undefined, next: DropdownAppearance | undefined): void;
2491-
// (undocumented)
2492-
connectedCallback(): void;
2493-
// (undocumented)
2494-
disconnectedCallback(): void;
2495-
// @internal
2496-
openChanged(prev: boolean | undefined, next: boolean | undefined): void;
24972494
size?: DropdownSize;
24982495
// @internal
24992496
sizeChanged(prev: DropdownSize | undefined, next: DropdownSize | undefined): void;

Diff for: packages/web-components/src/dropdown/dropdown.base.ts

+71
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { attr, FASTElement, Observable, observable, Updates, volatile } from '@microsoft/fast-element';
22
import type { Listbox } from '../listbox/listbox.js';
3+
import { isListbox } from '../listbox/listbox.options.js';
34
import type { DropdownOption } from '../option/option.js';
45
import { isDropdownOption } from '../option/option.options.js';
56
import { toggleState } from '../utils/element-internals.js';
@@ -24,6 +25,14 @@ import { dropdownButtonTemplate, dropdownInputTemplate } from './dropdown.templa
2425
* @public
2526
*/
2627
export class BaseDropdown extends FASTElement {
28+
/**
29+
* Static property for the anchor positioning fallback observer. The observer is used to flip the listbox when it is
30+
* out of view.
31+
* @remarks This is only used when the browser does not support CSS anchor positioning.
32+
* @internal
33+
*/
34+
private static AnchorPositionFallbackObserver: IntersectionObserver;
35+
2736
/**
2837
* The ID of the current active descendant.
2938
*
@@ -307,6 +316,13 @@ export class BaseDropdown extends FASTElement {
307316
toggleState(this.elementInternals, 'open', next);
308317
this.elementInternals.ariaExpanded = next ? 'true' : 'false';
309318
this.activeIndex = this.selectedIndex ?? -1;
319+
320+
if (next) {
321+
BaseDropdown.AnchorPositionFallbackObserver?.observe(this.listbox);
322+
return;
323+
}
324+
325+
BaseDropdown.AnchorPositionFallbackObserver?.unobserve(this.listbox);
310326
}
311327

312328
/**
@@ -594,6 +610,8 @@ export class BaseDropdown extends FASTElement {
594610

595611
this.elementInternals.role = 'presentation';
596612

613+
this.addEventListener('connected', this.listboxConnectedHandler);
614+
597615
Updates.enqueue(() => {
598616
this.insertControl();
599617
});
@@ -866,4 +884,57 @@ export class BaseDropdown extends FASTElement {
866884
this.freeformOption.value = value;
867885
this.freeformOption.hidden = false;
868886
}
887+
888+
connectedCallback(): void {
889+
super.connectedCallback();
890+
this.anchorPositionFallback();
891+
}
892+
893+
disconnectedCallback(): void {
894+
BaseDropdown.AnchorPositionFallbackObserver?.unobserve(this.listbox);
895+
896+
super.disconnectedCallback();
897+
}
898+
899+
/**
900+
* Handles the connected event for the listbox.
901+
*
902+
* @param e - the event object
903+
* @internal
904+
*/
905+
private listboxConnectedHandler(e: Event): void {
906+
const target = e.target as HTMLElement;
907+
908+
if (isListbox(target)) {
909+
this.listbox = target;
910+
}
911+
}
912+
913+
/**
914+
* When anchor positioning isn't supported, an intersection observer is used to flip the listbox when it hits the
915+
* viewport bounds. One static observer is used for all dropdowns.
916+
*
917+
* @internal
918+
*/
919+
private anchorPositionFallback(): void {
920+
BaseDropdown.AnchorPositionFallbackObserver =
921+
BaseDropdown.AnchorPositionFallbackObserver ??
922+
new IntersectionObserver(
923+
(entries: IntersectionObserverEntry[]): void => {
924+
entries.forEach(({ boundingClientRect, isIntersecting, target }) => {
925+
if (isListbox(target) && !isIntersecting) {
926+
if (boundingClientRect.bottom > window.innerHeight) {
927+
toggleState(target.dropdown!.elementInternals, 'flip-block', true);
928+
return;
929+
}
930+
931+
if (boundingClientRect.top < 0) {
932+
toggleState(target.dropdown!.elementInternals, 'flip-block', false);
933+
}
934+
}
935+
});
936+
},
937+
{ threshold: 1 },
938+
);
939+
}
869940
}

Diff for: packages/web-components/src/dropdown/dropdown.ts

+1-87
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { attr } from '@microsoft/fast-element';
2-
import { isListbox } from '../listbox/listbox.options.js';
3-
import { swapStates, toggleState } from '../utils/element-internals.js';
2+
import { swapStates } from '../utils/element-internals.js';
43
import { BaseDropdown } from './dropdown.base.js';
54
import { DropdownAppearance, DropdownSize } from './dropdown.options.js';
65

@@ -14,14 +13,6 @@ import { DropdownAppearance, DropdownSize } from './dropdown.options.js';
1413
* @public
1514
*/
1615
export class Dropdown extends BaseDropdown {
17-
/**
18-
* Static property for the anchor positioning fallback observer. The observer is used to flip the listbox when it is
19-
* out of view.
20-
* @remarks This is only used when the browser does not support CSS anchor positioning.
21-
* @internal
22-
*/
23-
private static AnchorPositionFallbackObserver: IntersectionObserver;
24-
2516
/**
2617
* The appearance of the dropdown.
2718
*
@@ -62,81 +53,4 @@ export class Dropdown extends BaseDropdown {
6253
public sizeChanged(prev: DropdownSize | undefined, next: DropdownSize | undefined): void {
6354
swapStates(this.elementInternals, prev, next, DropdownSize);
6455
}
65-
66-
connectedCallback(): void {
67-
super.connectedCallback();
68-
this.anchorPositionFallback();
69-
}
70-
71-
constructor() {
72-
super();
73-
74-
this.addEventListener('connected', this.listboxConnectedHandler);
75-
}
76-
77-
disconnectedCallback(): void {
78-
Dropdown.AnchorPositionFallbackObserver?.unobserve(this.listbox);
79-
80-
super.disconnectedCallback();
81-
}
82-
83-
/**
84-
* Handles the connected event for the listbox.
85-
*
86-
* @param e - the event object
87-
* @internal
88-
*/
89-
private listboxConnectedHandler(e: Event): void {
90-
const target = e.target as HTMLElement;
91-
92-
if (isListbox(target)) {
93-
this.listbox = target;
94-
}
95-
}
96-
97-
/**
98-
* Adds or removes the window event listener based on the open state.
99-
*
100-
* @param prev - the previous open state
101-
* @param next - the current open state
102-
* @internal
103-
*/
104-
public openChanged(prev: boolean | undefined, next: boolean | undefined): void {
105-
super.openChanged(prev, next);
106-
107-
if (next) {
108-
Dropdown.AnchorPositionFallbackObserver?.observe(this.listbox);
109-
return;
110-
}
111-
112-
Dropdown.AnchorPositionFallbackObserver?.unobserve(this.listbox);
113-
}
114-
115-
/**
116-
* When anchor positioning isn't supported, an intersection observer is used to flip the listbox when it hits the
117-
* viewport bounds. One static observer is used for all dropdowns.
118-
*
119-
* @internal
120-
*/
121-
private anchorPositionFallback(): void {
122-
Dropdown.AnchorPositionFallbackObserver =
123-
Dropdown.AnchorPositionFallbackObserver ??
124-
new IntersectionObserver(
125-
(entries: IntersectionObserverEntry[]): void => {
126-
entries.forEach(({ boundingClientRect, isIntersecting, target }) => {
127-
if (isListbox(target) && !isIntersecting) {
128-
if (boundingClientRect.bottom > window.innerHeight) {
129-
toggleState(target.dropdown!.elementInternals, 'flip-block', true);
130-
return;
131-
}
132-
133-
if (boundingClientRect.top < 0) {
134-
toggleState(target.dropdown!.elementInternals, 'flip-block', false);
135-
}
136-
}
137-
});
138-
},
139-
{ threshold: 1 },
140-
);
141-
}
14256
}

Diff for: packages/web-components/src/listbox/listbox.stories.ts

-56
This file was deleted.

0 commit comments

Comments
 (0)