-
Notifications
You must be signed in to change notification settings - Fork 601
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
use Floating UI for Select and Combobox #6452
Changes from all commits
61ed651
bb4273d
0e7c575
3abc780
188117f
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,7 @@ | ||
{ | ||
"type": "prerelease", | ||
"comment": "use floating-ui for select and combobox", | ||
"packageName": "@microsoft/fast-foundation", | ||
"email": "[email protected]", | ||
"dependentChangeType": "prerelease" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,15 @@ | ||
import { SyntheticViewTemplate, Updates } from "@microsoft/fast-element"; | ||
import { attr, Observable, observable } from "@microsoft/fast-element"; | ||
import { autoUpdate, computePosition, flip, hide, size } from "@floating-ui/dom"; | ||
import { | ||
attr, | ||
Observable, | ||
observable, | ||
SyntheticViewTemplate, | ||
Updates, | ||
} from "@microsoft/fast-element"; | ||
import { limit, uniqueId } from "@microsoft/fast-web-utilities"; | ||
import type { FASTListboxOption } from "../listbox-option/listbox-option.js"; | ||
import { DelegatesARIAListbox } from "../listbox/listbox.js"; | ||
import { StartEnd, StartEndOptions } from "../patterns/index.js"; | ||
import { SelectPosition } from "../select/select.options.js"; | ||
import { applyMixins } from "../utilities/apply-mixins.js"; | ||
import { FormAssociatedCombobox } from "./combobox.form-associated.js"; | ||
import { ComboboxAutocomplete } from "./combobox.options.js"; | ||
|
@@ -80,13 +85,6 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
*/ | ||
private filter: string = ""; | ||
|
||
/** | ||
* The initial state of the position attribute. | ||
* | ||
* @internal | ||
*/ | ||
private forcedPosition: boolean = false; | ||
|
||
/** | ||
* Reset the element to its first selectable option when its parent form is reset. | ||
* | ||
|
@@ -130,14 +128,6 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
*/ | ||
public listboxId: string = uniqueId("listbox-"); | ||
|
||
/** | ||
* The max height for the listbox when opened. | ||
* | ||
* @internal | ||
*/ | ||
@observable | ||
public maxHeight: number = 0; | ||
|
||
/** | ||
* The open attribute. | ||
* | ||
|
@@ -161,7 +151,7 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
this.ariaControls = this.listboxId; | ||
this.ariaExpanded = "true"; | ||
|
||
this.setPositioning(); | ||
Updates.enqueue(() => this.setPositioning()); | ||
this.focusAndScrollOptionIntoView(); | ||
|
||
// focus is directed to the element when `open` is changed programmatically | ||
|
@@ -211,29 +201,6 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
} | ||
} | ||
|
||
/** | ||
* The placement for the listbox when the combobox is open. | ||
* | ||
* @public | ||
*/ | ||
@attr({ attribute: "position" }) | ||
public positionAttribute?: SelectPosition; | ||
|
||
/** | ||
* The current state of the calculated position of the listbox. | ||
* | ||
* @public | ||
*/ | ||
@observable | ||
public position?: SelectPosition; | ||
protected positionChanged( | ||
prev: SelectPosition | undefined, | ||
next: SelectPosition | undefined | ||
): void { | ||
this.positionAttribute = next; | ||
this.setPositioning(); | ||
} | ||
|
||
/** | ||
* The value property. | ||
* | ||
|
@@ -270,6 +237,13 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
} | ||
} | ||
|
||
/** | ||
* Cleanup function for the listbox positioner. | ||
* | ||
* @public | ||
*/ | ||
public cleanup: () => void; | ||
|
||
/** | ||
* Handle opening and closing the listbox when the combobox is clicked. | ||
* | ||
|
@@ -307,7 +281,6 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
|
||
public connectedCallback() { | ||
super.connectedCallback(); | ||
this.forcedPosition = !!this.positionAttribute; | ||
if (this.value) { | ||
this.initialValue = this.value; | ||
} | ||
|
@@ -328,6 +301,11 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
this.ariaDisabled = this.disabled ? "true" : "false"; | ||
} | ||
|
||
public disconnectedCallback(): void { | ||
this.cleanup?.(); | ||
super.disconnectedCallback(); | ||
} | ||
|
||
/** | ||
* Filter available options by text value. | ||
* | ||
|
@@ -608,26 +586,46 @@ export class FASTCombobox extends FormAssociatedCombobox { | |
/** | ||
* Calculate and apply listbox positioning based on available viewport space. | ||
* | ||
* @param force - direction to force the listbox to display | ||
* @public | ||
*/ | ||
public setPositioning(): void { | ||
const currentBox = this.getBoundingClientRect(); | ||
const viewportHeight = window.innerHeight; | ||
const availableBottom = viewportHeight - currentBox.bottom; | ||
|
||
this.position = this.forcedPosition | ||
? this.positionAttribute | ||
: currentBox.top > availableBottom | ||
? SelectPosition.above | ||
: SelectPosition.below; | ||
|
||
this.positionAttribute = this.forcedPosition | ||
? this.positionAttribute | ||
: this.position; | ||
|
||
this.maxHeight = | ||
this.position === SelectPosition.above ? ~~currentBox.top : ~~availableBottom; | ||
if (this.$fastController.isConnected) { | ||
this.cleanup = autoUpdate(this, this.listbox, async () => { | ||
const { middlewareData, x, y } = await computePosition( | ||
this, | ||
this.listbox, | ||
{ | ||
placement: "bottom", | ||
strategy: "fixed", | ||
middleware: [ | ||
flip(), | ||
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. Am I correct in assuming that though is is positioned to the bottom, if there's not enough space, this will "flip" it to the top? 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. Yep, that's what this middleware does: https://floating-ui.com/docs/flip |
||
size({ | ||
apply: ({ availableHeight, rects }) => { | ||
Object.assign(this.listbox.style, { | ||
maxHeight: `${availableHeight}px`, | ||
width: `${rects.reference.width}px`, | ||
}); | ||
}, | ||
}), | ||
hide(), | ||
], | ||
} | ||
); | ||
|
||
if (middlewareData.hide?.referenceHidden) { | ||
this.open = false; | ||
this.cleanup(); | ||
return; | ||
} | ||
|
||
Object.assign(this.listbox.style, { | ||
position: "fixed", | ||
top: "0", | ||
left: "0", | ||
transform: `translate(${x}px, ${y}px)`, | ||
}); | ||
}); | ||
Comment on lines
+593
to
+627
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'd love to see us look into abstracting this into a utility which could be shared rather than repeated once we have a good baseline for implementation. |
||
} | ||
} | ||
|
||
/** | ||
|
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.
Nice use of the operator here!