Skip to content

Commit

Permalink
use Floating UI for Select and Combobox (#6452)
Browse files Browse the repository at this point in the history
* remove position attribute and related properties from combobox

* use floating-ui for combobox listbox

* remove position attribute and related properties from select

* use floating-ui for select listbox

* Change files
  • Loading branch information
radium-v authored and janechu committed Jun 10, 2024
1 parent 3d808f2 commit c5a4f77
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 298 deletions.
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
Expand Up @@ -105,6 +105,11 @@
min-height: 100%;
}

/* Disable zoom transform for Firefox, see https://github.com/storybookjs/storybook/issues/16774 */
.docs-story [class^="css-"] {
transform: none !important;
}

html,
body,
#root {
Expand Down
25 changes: 4 additions & 21 deletions packages/web-components/fast-foundation/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ export class FASTCheckbox extends FormAssociatedCheckbox {
// @public
export class FASTCombobox extends FormAssociatedCombobox {
autocomplete: ComboboxAutocomplete | undefined;
cleanup: () => void;
// @internal
clickHandler(e: MouseEvent): boolean | void;
// (undocumented)
Expand All @@ -901,6 +902,8 @@ export class FASTCombobox extends FormAssociatedCombobox {
control: HTMLInputElement;
// @internal
disabledChanged(prev: boolean, next: boolean): void;
// (undocumented)
disconnectedCallback(): void;
filteredOptions: FASTListboxOption[];
filterOptions(): void;
// @internal
Expand All @@ -919,8 +922,6 @@ export class FASTCombobox extends FormAssociatedCombobox {
listbox: HTMLDivElement;
// @internal
listboxId: string;
// @internal
maxHeight: number;
open: boolean;
// @internal
protected openChanged(): void;
Expand All @@ -929,10 +930,6 @@ export class FASTCombobox extends FormAssociatedCombobox {
placeholder: string;
// @internal
protected placeholderChanged(): void;
position?: SelectPosition;
positionAttribute?: SelectPosition;
// (undocumented)
protected positionChanged(prev: SelectPosition | undefined, next: SelectPosition | undefined): void;
// @internal
selectedIndexChanged(prev: number | undefined, next: number): void;
// @internal
Expand Down Expand Up @@ -1704,6 +1701,7 @@ export interface FASTSearch extends StartEnd, DelegatesARIASearch {
//
// @public
export class FASTSelect extends FormAssociatedSelect {
cleanup: () => void;
// @internal
clickHandler(e: MouseEvent): boolean | void;
// @internal
Expand All @@ -1729,18 +1727,12 @@ export class FASTSelect extends FormAssociatedSelect {
listbox: HTMLDivElement;
// @internal
listboxId: string;
// @internal
maxHeight: number;
// @internal @override
mousedownHandler(e: MouseEvent): boolean | void;
multipleChanged(prev: boolean | undefined, next: boolean): void;
open: boolean;
// @internal
protected openChanged(prev: boolean | undefined, next: boolean): void;
position?: SelectPosition;
positionAttribute?: SelectPosition;
// (undocumented)
protected positionChanged(prev: SelectPosition | undefined, next: SelectPosition | undefined): void;
// @internal
selectedIndexChanged(prev: number | undefined, next: number): void;
// @internal @override
Expand Down Expand Up @@ -2568,15 +2560,6 @@ export type SelectOptions = StartEndOptions & {
indicator?: string | SyntheticViewTemplate;
};

// @public
export const SelectPosition: {
readonly above: "above";
readonly below: "below";
};

// @public
export type SelectPosition = typeof SelectPosition[keyof typeof SelectPosition];

// @public
export function selectTemplate<T extends FASTSelect>(options?: SelectOptions): ElementViewTemplate<T>;

Expand Down
59 changes: 28 additions & 31 deletions packages/web-components/fast-foundation/src/combobox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,33 +163,31 @@ See [listbox-option](/docs/components/listbox-option) for more information.

#### Fields

| Name | Privacy | Type | Default | Description | Inherited From |
| ------------------- | --------- | ----------------------------------- | ------- | ---------------------------------------------------------------------------------------- | ---------------------- |
| `autocomplete` | public | `ComboboxAutocomplete or undefined` | | The autocomplete attribute. | |
| `filteredOptions` | public | `FASTListboxOption[]` | `[]` | The collection of currently filtered options. | |
| `open` | public | `boolean` | `false` | The open attribute. | |
| `options` | public | `FASTListboxOption[]` | | The list of options. | FASTListbox |
| `placeholder` | public | `string` | | Sets the placeholder value of the element, generally used to provide a hint to the user. | |
| `positionAttribute` | public | `SelectPosition or undefined` | | The placement for the listbox when the combobox is open. | |
| `position` | public | `SelectPosition or undefined` | | The current state of the calculated position of the listbox. | |
| `value` | public | | | The value property. | |
| `proxy` | | | | | FormAssociatedCombobox |
| `length` | public | `number` | | The number of options. | FASTListbox |
| `typeAheadExpired` | protected | | | | FASTListbox |
| `disabled` | public | `boolean` | | The disabled state of the listbox. | FASTListbox |
| `selectedIndex` | public | `number` | `-1` | The index of the selected option. | FASTListbox |
| `selectedOptions` | public | `FASTListboxOption[]` | `[]` | A collection of the selected options. | FASTListbox |
| Name | Privacy | Type | Default | Description | Inherited From |
| ------------------ | --------- | ----------------------------------- | ------- | ---------------------------------------------------------------------------------------- | ---------------------- |
| `autocomplete` | public | `ComboboxAutocomplete or undefined` | | The autocomplete attribute. | |
| `filteredOptions` | public | `FASTListboxOption[]` | `[]` | The collection of currently filtered options. | |
| `open` | public | `boolean` | `false` | The open attribute. | |
| `options` | public | `FASTListboxOption[]` | | The list of options. | FASTListbox |
| `placeholder` | public | `string` | | Sets the placeholder value of the element, generally used to provide a hint to the user. | |
| `value` | public | | | The value property. | |
| `cleanup` | public | `() => void` | | Cleanup function for the listbox positioner. | |
| `proxy` | | | | | FormAssociatedCombobox |
| `length` | public | `number` | | The number of options. | FASTListbox |
| `typeAheadExpired` | protected | | | | FASTListbox |
| `disabled` | public | `boolean` | | The disabled state of the listbox. | FASTListbox |
| `selectedIndex` | public | `number` | `-1` | The index of the selected option. | FASTListbox |
| `selectedOptions` | public | `FASTListboxOption[]` | `[]` | A collection of the selected options. | FASTListbox |

#### Methods

| Name | Privacy | Description | Parameters | Return | Inherited From |
| -------------------- | --------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------ | -------------- |
| `validate` | public | {@inheritDoc (FormAssociated:interface).validate} | | `void` | |
| `positionChanged` | protected | | `prev: SelectPosition or undefined, next: SelectPosition or undefined` | `void` | |
| `filterOptions` | public | Filter available options by text value. | | `void` | |
| `setPositioning` | public | Calculate and apply listbox positioning based on available viewport space. | `force` | `void` | |
| `selectFirstOption` | public | Moves focus to the first selectable option. | | `void` | FASTListbox |
| `setSelectedOptions` | public | Sets an option as selected and gives it focus. | | | FASTListbox |
| Name | Privacy | Description | Parameters | Return | Inherited From |
| -------------------- | ------- | -------------------------------------------------------------------------- | ---------- | ------ | -------------- |
| `validate` | public | {@inheritDoc (FormAssociated:interface).validate} | | `void` | |
| `filterOptions` | public | Filter available options by text value. | | `void` | |
| `setPositioning` | public | Calculate and apply listbox positioning based on available viewport space. | | `void` | |
| `selectFirstOption` | public | Moves focus to the first selectable option. | | `void` | FASTListbox |
| `setSelectedOptions` | public | Sets an option as selected and gives it focus. | | | FASTListbox |

#### Events

Expand All @@ -199,13 +197,12 @@ See [listbox-option](/docs/components/listbox-option) for more information.

#### Attributes

| Name | Field | Inherited From |
| -------------- | ----------------- | -------------- |
| `autocomplete` | autocomplete | |
| `open` | open | |
| `placeholder` | placeholder | |
| `position` | positionAttribute | |
| | disabled | FASTListbox |
| Name | Field | Inherited From |
| -------------- | ------------ | -------------- |
| `autocomplete` | autocomplete | |
| `open` | open | |
| `placeholder` | placeholder | |
| | disabled | FASTListbox |

#### CSS Parts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ Extends [`listbox`](../listbox/listbox.spec.md) and [form associated custom elem
- `autocomplete` - Handles autocomplete features for the control on pageload. Accepted values are `none`, `inline`, `list`, and `both`.
- `disabled` - Disables the control.
- `name` - Name of the control.
- `position` - The placement for the listbox when the combobox is open. Values may be either `above` or `below`.
- `required` - Boolean value that sets the field as required.
- `value` - The initial value of the combobox.

Expand Down
118 changes: 58 additions & 60 deletions packages/web-components/fast-foundation/src/combobox/combobox.ts
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";
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand All @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -307,7 +281,6 @@ export class FASTCombobox extends FormAssociatedCombobox {

public connectedCallback() {
super.connectedCallback();
this.forcedPosition = !!this.positionAttribute;
if (this.value) {
this.initialValue = this.value;
}
Expand All @@ -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.
*
Expand Down Expand Up @@ -637,26 +615,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(),
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)`,
});
});
}
}

/**
Expand Down
Loading

0 comments on commit c5a4f77

Please sign in to comment.