From c5a4f77f5d44105a56888f7e4d6fe35f0ac54c13 Mon Sep 17 00:00:00 2001 From: John Kreitlow <863023+radium-v@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:11:03 -0700 Subject: [PATCH] use Floating UI for Select and Combobox (#6452) * 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 --- ...-7f1045f7-4da2-43db-868c-86a3472b38da.json | 7 ++ .../.storybook/preview-head.html | 5 + .../fast-foundation/docs/api-report.md | 25 +--- .../fast-foundation/src/combobox/README.md | 59 +++++---- .../src/combobox/combobox.spec.md | 1 - .../fast-foundation/src/combobox/combobox.ts | 118 +++++++++--------- .../src/combobox/stories/combobox.register.ts | 11 +- .../fast-foundation/src/select/README.md | 63 ++++------ .../fast-foundation/src/select/index.ts | 1 - .../src/select/select.options.ts | 14 --- .../fast-foundation/src/select/select.ts | 114 ++++++++--------- .../src/select/stories/select.register.ts | 67 ++-------- .../src/select/stories/select.stories.ts | 6 - 13 files changed, 193 insertions(+), 298 deletions(-) create mode 100644 change/@microsoft-fast-foundation-7f1045f7-4da2-43db-868c-86a3472b38da.json delete mode 100644 packages/web-components/fast-foundation/src/select/select.options.ts diff --git a/change/@microsoft-fast-foundation-7f1045f7-4da2-43db-868c-86a3472b38da.json b/change/@microsoft-fast-foundation-7f1045f7-4da2-43db-868c-86a3472b38da.json new file mode 100644 index 00000000000..7e47d81e084 --- /dev/null +++ b/change/@microsoft-fast-foundation-7f1045f7-4da2-43db-868c-86a3472b38da.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "use floating-ui for select and combobox", + "packageName": "@microsoft/fast-foundation", + "email": "863023+radium-v@users.noreply.github.com", + "dependentChangeType": "prerelease" +} diff --git a/packages/web-components/fast-foundation/.storybook/preview-head.html b/packages/web-components/fast-foundation/.storybook/preview-head.html index e8f9154b1a6..8aa4d735223 100644 --- a/packages/web-components/fast-foundation/.storybook/preview-head.html +++ b/packages/web-components/fast-foundation/.storybook/preview-head.html @@ -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 { diff --git a/packages/web-components/fast-foundation/docs/api-report.md b/packages/web-components/fast-foundation/docs/api-report.md index 823d967fa9d..76e8aeab9a3 100644 --- a/packages/web-components/fast-foundation/docs/api-report.md +++ b/packages/web-components/fast-foundation/docs/api-report.md @@ -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) @@ -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 @@ -919,8 +922,6 @@ export class FASTCombobox extends FormAssociatedCombobox { listbox: HTMLDivElement; // @internal listboxId: string; - // @internal - maxHeight: number; open: boolean; // @internal protected openChanged(): void; @@ -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 @@ -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 @@ -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 @@ -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(options?: SelectOptions): ElementViewTemplate; diff --git a/packages/web-components/fast-foundation/src/combobox/README.md b/packages/web-components/fast-foundation/src/combobox/README.md index 4252ade7189..67cd6dd6628 100644 --- a/packages/web-components/fast-foundation/src/combobox/README.md +++ b/packages/web-components/fast-foundation/src/combobox/README.md @@ -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 @@ -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 diff --git a/packages/web-components/fast-foundation/src/combobox/combobox.spec.md b/packages/web-components/fast-foundation/src/combobox/combobox.spec.md index 48a3c61acd9..1b1d4d0867b 100644 --- a/packages/web-components/fast-foundation/src/combobox/combobox.spec.md +++ b/packages/web-components/fast-foundation/src/combobox/combobox.spec.md @@ -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. diff --git a/packages/web-components/fast-foundation/src/combobox/combobox.ts b/packages/web-components/fast-foundation/src/combobox/combobox.ts index 6166b743c1e..2a358ac9e19 100644 --- a/packages/web-components/fast-foundation/src/combobox/combobox.ts +++ b/packages/web-components/fast-foundation/src/combobox/combobox.ts @@ -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. * @@ -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)`, + }); + }); + } } /** diff --git a/packages/web-components/fast-foundation/src/combobox/stories/combobox.register.ts b/packages/web-components/fast-foundation/src/combobox/stories/combobox.register.ts index 2e3f78af428..c5b93428bc6 100644 --- a/packages/web-components/fast-foundation/src/combobox/stories/combobox.register.ts +++ b/packages/web-components/fast-foundation/src/combobox/stories/combobox.register.ts @@ -34,17 +34,10 @@ const styles = css` display: inline-flex; flex-direction: column; left: 0; - max-height: calc( - var(--max-height) - - ( - (var(--base-height-multiplier) + var(--density)) * var(--design-unit) * - 1px - ) - ); padding: calc(var(--design-unit) * 1px) 0; overflow-y: auto; - position: absolute; - width: 100%; + position: fixed; + top: 0; z-index: 1; } diff --git a/packages/web-components/fast-foundation/src/select/README.md b/packages/web-components/fast-foundation/src/select/README.md index e59dd07f36a..af3efeb7061 100644 --- a/packages/web-components/fast-foundation/src/select/README.md +++ b/packages/web-components/fast-foundation/src/select/README.md @@ -129,16 +129,6 @@ See [listbox-option](/docs/components/listbox-option) for more information. -### Variables - -| Name | Description | Type | -| ---------------- | ------------------------------------------------------------- | ------------------------------------- | -| `SelectPosition` | Positioning directions for the listbox when a select is open. | `{ above: "above", below: "below", }` | - -
- - - ### class: `FASTSelect` #### Superclass @@ -155,32 +145,30 @@ See [listbox-option](/docs/components/listbox-option) for more information. #### Fields -| Name | Privacy | Type | Default | Description | Inherited From | -| ------------------- | --------- | ----------------------------- | ------- | ------------------------------------------------------------------- | -------------------- | -| `open` | public | `boolean` | `false` | The open attribute. | | -| `value` | public | | | The value property. | | -| `positionAttribute` | public | `SelectPosition or undefined` | | Reflects the placement for the listbox when the select is open. | | -| `position` | public | `SelectPosition or undefined` | | Holds the current state for the calculated position of the listbox. | | -| `displayValue` | public | `string` | | The value displayed on the button. | | -| `proxy` | | | | | FormAssociatedSelect | -| `multiple` | public | `boolean` | | Indicates if the listbox is in multi-selection mode. | FASTListboxElement | -| `size` | public | `number` | | The maximum number of options to display. | FASTListboxElement | -| `length` | public | `number` | | The number of options. | FASTListbox | -| `options` | public | `FASTListboxOption[]` | | The list 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 | +| ------------------ | --------- | --------------------- | ------- | ---------------------------------------------------- | -------------------- | +| `open` | public | `boolean` | `false` | The open attribute. | | +| `value` | public | | | The value property. | | +| `cleanup` | public | `() => void` | | Cleanup function for the listbox positioner. | | +| `displayValue` | public | `string` | | The value displayed on the button. | | +| `proxy` | | | | | FormAssociatedSelect | +| `multiple` | public | `boolean` | | Indicates if the listbox is in multi-selection mode. | FASTListboxElement | +| `size` | public | `number` | | The maximum number of options to display. | FASTListboxElement | +| `length` | public | `number` | | The number of options. | FASTListbox | +| `options` | public | `FASTListboxOption[]` | | The list 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 | -| -------------------- | --------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------ | -------------- | -| `positionChanged` | protected | | `prev: SelectPosition or undefined, next: SelectPosition or undefined` | `void` | | -| `setPositioning` | public | Calculate and apply listbox positioning based on available viewport space. | | `void` | | -| `multipleChanged` | public | Sets the multiple property on the proxy element. | `prev: boolean or undefined, next: boolean` | | | -| `setSelectedOptions` | public | Sets an option as selected and gives it focus. | | | FASTListbox | -| `selectFirstOption` | public | Moves focus to the first selectable option. | | `void` | FASTListbox | +| Name | Privacy | Description | Parameters | Return | Inherited From | +| -------------------- | ------- | -------------------------------------------------------------------------- | ------------------------------------------- | ------ | -------------- | +| `setPositioning` | public | Calculate and apply listbox positioning based on available viewport space. | | `void` | | +| `multipleChanged` | public | Sets the multiple property on the proxy element. | `prev: boolean or undefined, next: boolean` | | | +| `setSelectedOptions` | public | Sets an option as selected and gives it focus. | | | FASTListbox | +| `selectFirstOption` | public | Moves focus to the first selectable option. | | `void` | FASTListbox | #### Events @@ -191,11 +179,10 @@ See [listbox-option](/docs/components/listbox-option) for more information. #### Attributes -| Name | Field | Inherited From | -| ---------- | ----------------- | -------------- | -| `open` | open | | -| `position` | positionAttribute | | -| | multiple | FASTListbox | +| Name | Field | Inherited From | +| ------ | -------- | -------------- | +| `open` | open | | +| | multiple | FASTListbox | #### CSS Parts diff --git a/packages/web-components/fast-foundation/src/select/index.ts b/packages/web-components/fast-foundation/src/select/index.ts index 33f4362e3e8..5e73d060120 100644 --- a/packages/web-components/fast-foundation/src/select/index.ts +++ b/packages/web-components/fast-foundation/src/select/index.ts @@ -1,3 +1,2 @@ export * from "./select.js"; -export * from "./select.options.js"; export * from "./select.template.js"; diff --git a/packages/web-components/fast-foundation/src/select/select.options.ts b/packages/web-components/fast-foundation/src/select/select.options.ts deleted file mode 100644 index e1b12d28094..00000000000 --- a/packages/web-components/fast-foundation/src/select/select.options.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Positioning directions for the listbox when a select is open. - * @public - */ -export const SelectPosition = { - above: "above", - below: "below", -} as const; - -/** - * Types for positioning the select element listbox when open - * @public - */ -export type SelectPosition = typeof SelectPosition[keyof typeof SelectPosition]; diff --git a/packages/web-components/fast-foundation/src/select/select.ts b/packages/web-components/fast-foundation/src/select/select.ts index 4de9cc0c334..123e60771d2 100644 --- a/packages/web-components/fast-foundation/src/select/select.ts +++ b/packages/web-components/fast-foundation/src/select/select.ts @@ -1,5 +1,12 @@ -import { SyntheticViewTemplate, Updates } from "@microsoft/fast-element"; -import { attr, Observable, observable, volatile } from "@microsoft/fast-element"; +import { autoUpdate, computePosition, flip, hide, size } from "@floating-ui/dom"; +import { + attr, + Observable, + observable, + SyntheticViewTemplate, + Updates, + volatile, +} from "@microsoft/fast-element"; import { keyArrowDown, keyArrowUp, @@ -16,7 +23,6 @@ import { DelegatesARIAListbox, FASTListbox } from "../listbox/listbox.js"; import { StartEnd, StartEndOptions } from "../patterns/index.js"; import { applyMixins } from "../utilities/apply-mixins.js"; import { FormAssociatedSelect } from "./select.form-associated.js"; -import { SelectPosition } from "./select.options.js"; /** * Select configuration options @@ -73,7 +79,7 @@ export class FASTSelect extends FormAssociatedSelect { this.ariaControls = this.listboxId; this.ariaExpanded = "true"; - this.setPositioning(); + Updates.enqueue(() => this.setPositioning()); this.focusAndScrollOptionIntoView(); this.indexWhenOpened = this.selectedIndex; @@ -83,6 +89,8 @@ export class FASTSelect extends FormAssociatedSelect { return; } + this.cleanup?.(); + this.ariaControls = ""; this.ariaExpanded = "false"; } @@ -187,36 +195,6 @@ export class FASTSelect extends FormAssociatedSelect { this.updateValue(); } - /** - * Reflects the placement for the listbox when the select is open. - * - * @public - */ - @attr({ attribute: "position" }) - public positionAttribute?: SelectPosition; - - /** - * Indicates the initial state of the position attribute. - * - * @internal - */ - private forcedPosition: boolean = false; - - /** - * Holds the current state for 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(); - } - /** * Reference to the internal listbox element. * @@ -232,36 +210,55 @@ export class FASTSelect extends FormAssociatedSelect { public listboxId: string = uniqueId("listbox-"); /** - * Calculate and apply listbox positioning based on available viewport space. + * Cleanup function for the listbox positioner. * * @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; - } + public cleanup: () => void; /** - * The max height for the listbox when opened. + * Calculate and apply listbox positioning based on available viewport space. * - * @internal + * @public */ - @observable - public maxHeight: number = 0; + public setPositioning(): void { + if (this.$fastController.isConnected) { + this.cleanup = autoUpdate(this, this.listbox, async () => { + const { middlewareData, x, y } = await computePosition( + this.control, + 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; + return; + } + + Object.assign(this.listbox.style, { + position: "fixed", + top: "0", + left: "0", + transform: `translate(${x}px, ${y}px)`, + }); + }); + } + } /** * The value displayed on the button. @@ -558,13 +555,12 @@ export class FASTSelect extends FormAssociatedSelect { public connectedCallback() { super.connectedCallback(); - this.forcedPosition = !!this.positionAttribute; - this.addEventListener("contentchange", this.updateDisplayValue); } public disconnectedCallback() { this.removeEventListener("contentchange", this.updateDisplayValue); + this.cleanup?.(); super.disconnectedCallback(); } diff --git a/packages/web-components/fast-foundation/src/select/stories/select.register.ts b/packages/web-components/fast-foundation/src/select/stories/select.register.ts index c6bb65513d6..4f20985e55c 100644 --- a/packages/web-components/fast-foundation/src/select/stories/select.register.ts +++ b/packages/web-components/fast-foundation/src/select/stories/select.register.ts @@ -1,4 +1,4 @@ -import { css, ElementStyles, observable } from "@microsoft/fast-element"; +import { css, ElementStyles } from "@microsoft/fast-element"; import { FASTSelect } from "../select.js"; import { selectTemplate } from "../select.template.js"; @@ -19,6 +19,7 @@ const styles = css` outline: none; vertical-align: top; } + :host(:not([aria-haspopup])) { --elevation: 0; border: 0; @@ -42,9 +43,9 @@ const styles = css` ) * 1px ); overflow-y: auto; + position: fixed; + top: 0; left: 0; - position: absolute; - width: 100%; z-index: 1; } @@ -173,64 +174,14 @@ const styles = css` export class Select extends FASTSelect { private computedStylesheet?: ElementStyles; - private get listboxMaxHeight(): string { - return Math.floor(this.maxHeight / 40).toString(); - } - - @observable - private listboxScrollWidth: string = ""; - - protected listboxScrollWidthChanged(): void { - this.updateComputedStylesheet(); - } - - private get selectSize(): string { - return `${this.size ?? (this.multiple ? 4 : 0)}`; - } - public multipleChanged(prev: boolean | undefined, next: boolean): void { super.multipleChanged(prev, next); this.updateComputedStylesheet(); } - protected maxHeightChanged(prev: number | undefined, next: number): void { - if (this.$fastController.isConnected) { - if (this.collapsible) { - this.updateComputedStylesheet(); - } - } - } - - public setPositioning(): void { - super.setPositioning(); - this.updateComputedStylesheet(); - } - protected sizeChanged(prev: number | undefined, next: number): void { super.sizeChanged(prev, next); this.updateComputedStylesheet(); - - if (this.collapsible) { - requestAnimationFrame(() => { - this.listbox.style.setProperty("display", "flex"); - this.listbox.style.setProperty("overflow", "visible"); - this.listbox.style.setProperty("visibility", "hidden"); - this.listbox.style.setProperty("width", "auto"); - this.listbox.hidden = false; - - this.listboxScrollWidth = `${this.listbox.scrollWidth}`; - - this.listbox.hidden = true; - this.listbox.style.removeProperty("display"); - this.listbox.style.removeProperty("overflow"); - this.listbox.style.removeProperty("visibility"); - this.listbox.style.removeProperty("width"); - }); - - return; - } - - this.listboxScrollWidth = ""; } /** @@ -239,15 +190,15 @@ export class Select extends FASTSelect { * @internal */ protected updateComputedStylesheet(): void { - if (this.computedStylesheet) { - this.$fastController.removeStyles(this.computedStylesheet); + this.$fastController.removeStyles(this.computedStylesheet); + + if (this.collapsible) { + return; } this.computedStylesheet = css` :host { - --listbox-max-height: ${this.listboxMaxHeight}; - --listbox-scroll-width: ${this.listboxScrollWidth}; - --size: ${this.selectSize}; + --size: ${`${this.size ?? (this.multiple ? 4 : 0)}`}; } `; diff --git a/packages/web-components/fast-foundation/src/select/stories/select.stories.ts b/packages/web-components/fast-foundation/src/select/stories/select.stories.ts index d1c25f75f96..c752805a90b 100644 --- a/packages/web-components/fast-foundation/src/select/stories/select.stories.ts +++ b/packages/web-components/fast-foundation/src/select/stories/select.stories.ts @@ -3,7 +3,6 @@ import { storyTemplate as ListboxOptionTemplate } from "../../listbox-option/sto import type { Meta, Story, StoryArgs } from "../../__test__/helpers.js"; import { renderComponent } from "../../__test__/helpers.js"; import type { FASTSelect } from "../select.js"; -import { SelectPosition } from "../select.options.js"; const storyTemplate = html>` >` ?disabled="${x => x.disabled}" ?multiple="${x => x.multiple}" size="${x => x.size}" - position="${x => x.position}" value="${x => x.value}" > ${x => x.storyContent} @@ -49,10 +47,6 @@ export default { name: { control: "text" }, multiple: { control: "boolean" }, open: { control: "boolean" }, - position: { - control: "select", - options: [undefined, ...Object.values(SelectPosition)], - }, size: { control: "number" }, storyContent: { table: { disable: true } }, storyItems: { control: "object" },