Skip to content
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

Merged
merged 5 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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?.();
Copy link
Contributor

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!

super.disconnectedCallback();
}

/**
* Filter available options by text value.
*
Expand Down Expand Up @@ -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(),
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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.

}
}

/**
Expand Down
Loading