From f5846d52a7b04a5f9185d7a5c6153d78b8610108 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 5 May 2025 13:20:21 -0500 Subject: [PATCH 01/18] Allow rich-text mention listbox to open above the cursor --- .../src/rich-text/mention-listbox/index.ts | 6 ++++-- .../src/rich-text/mention-listbox/template.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/nimble-components/src/rich-text/mention-listbox/index.ts b/packages/nimble-components/src/rich-text/mention-listbox/index.ts index e288d81d45..db35164406 100644 --- a/packages/nimble-components/src/rich-text/mention-listbox/index.ts +++ b/packages/nimble-components/src/rich-text/mention-listbox/index.ts @@ -108,9 +108,11 @@ export class RichTextMentionListbox extends FoundationListbox { * @public */ public show(options: MentionListboxShowOptions): void { - const listboxTop = options.anchorNode.getBoundingClientRect().bottom; + const anchorRect = options.anchorNode.getBoundingClientRect(); + const availableSpaceBelow = window.innerHeight - anchorRect.bottom; + const availableSpaceAbove = anchorRect.top; this.availableViewportHeight = Math.trunc( - window.innerHeight - listboxTop + Math.max(availableSpaceAbove, availableSpaceBelow) ); this.filter = options.filter; this.anchorElement = options.anchorNode; diff --git a/packages/nimble-components/src/rich-text/mention-listbox/template.ts b/packages/nimble-components/src/rich-text/mention-listbox/template.ts index a7fd972e05..2d16ddd980 100644 --- a/packages/nimble-components/src/rich-text/mention-listbox/template.ts +++ b/packages/nimble-components/src/rich-text/mention-listbox/template.ts @@ -14,7 +14,7 @@ export const template = html` fixed-placement auto-update-mode="auto" vertical-default-position="bottom" - vertical-positioning-mode="locktodefault" + vertical-positioning-mode="dynamic" horizontal-default-position="center" horizontal-positioning-mode="locktodefault" horizontal-scaling="anchor" From cb401746e35388633694d69cbb1f02c60d742bf6 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 5 May 2025 13:20:43 -0500 Subject: [PATCH 02/18] Change files --- ...le-components-f4992b1c-b7f0-46e1-a684-b5afff8683f6.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@ni-nimble-components-f4992b1c-b7f0-46e1-a684-b5afff8683f6.json diff --git a/change/@ni-nimble-components-f4992b1c-b7f0-46e1-a684-b5afff8683f6.json b/change/@ni-nimble-components-f4992b1c-b7f0-46e1-a684-b5afff8683f6.json new file mode 100644 index 0000000000..45fc02f1ac --- /dev/null +++ b/change/@ni-nimble-components-f4992b1c-b7f0-46e1-a684-b5afff8683f6.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Allow rich-text mention listbox to open above the cursor", + "packageName": "@ni/nimble-components", + "email": "7282195+m-akinc@users.noreply.github.com", + "dependentChangeType": "patch" +} From 82f818fafc0075dad80ef3bbc6758df17b549d25 Mon Sep 17 00:00:00 2001 From: rajsite Date: Tue, 6 May 2025 12:37:02 -0500 Subject: [PATCH 03/18] Align to listbox implementation --- .../nimble-components/src/combobox/index.ts | 68 +++++++++---------- .../src/rich-text/mention-listbox/index.ts | 27 ++++++-- .../nimble-components/src/select/index.ts | 16 ++--- 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/packages/nimble-components/src/combobox/index.ts b/packages/nimble-components/src/combobox/index.ts index e8f036e52b..e25698e77a 100644 --- a/packages/nimble-components/src/combobox/index.ts +++ b/packages/nimble-components/src/combobox/index.ts @@ -3,7 +3,6 @@ import { DesignSystem, type ComboboxOptions, ComboboxAutocomplete, - SelectPosition, ListboxOption, DelegatesARIACombobox, applyMixins, @@ -28,6 +27,7 @@ import { styles } from './styles'; import { mixinErrorPattern } from '../patterns/error/types'; import { DropdownAppearance, + DropdownPosition, type DropdownPattern } from '../patterns/dropdown/types'; import type { AnchoredRegion } from '../anchored-region'; @@ -69,7 +69,7 @@ export class Combobox * The placement for the listbox when the combobox is open. */ @attr({ attribute: 'position' }) - public positionAttribute?: SelectPosition; + public positionAttribute?: DropdownPosition; /** * The open attribute. @@ -92,7 +92,7 @@ export class Combobox * @public */ @observable - public position?: SelectPosition; + public position?: DropdownPosition; /** * @internal @@ -620,37 +620,6 @@ export class Combobox } } - /** - * @internal - */ - public setPositioning(): void { - // Workaround for https://github.com/microsoft/fast/issues/5123 - if (!this.$fastController.isConnected) { - // Don't call setPositioning() until we're connected, - // since this.forcedPosition isn't initialized yet. - return; - } - const currentBox = this.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - const availableBottom = viewportHeight - currentBox.bottom; - - if (this.forcedPosition) { - this.position = this.positionAttribute; - } else if (currentBox.top > availableBottom) { - this.position = SelectPosition.above; - } else { - this.position = SelectPosition.below; - } - - this.positionAttribute = this.forcedPosition - ? this.positionAttribute - : this.position; - - this.availableViewportHeight = this.position === SelectPosition.above - ? Math.trunc(currentBox.top) - : Math.trunc(availableBottom); - } - /** * Focus the control and scroll the first selected option into view. * @@ -729,8 +698,8 @@ export class Combobox } protected positionChanged( - _: SelectPosition | undefined, - next: SelectPosition | undefined + _: DropdownPosition | undefined, + next: DropdownPosition | undefined ): void { this.positionAttribute = next; this.setPositioning(); @@ -759,6 +728,33 @@ export class Combobox this.updateInputAriaLabel(); } + private setPositioning(): void { + if (!this.$fastController.isConnected) { + // Don't call setPositioning() until we're connected, + // since this.forcedPosition isn't initialized yet. + return; + } + const currentBox = this.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const availableBottom = viewportHeight - currentBox.bottom; + + if (this.forcedPosition) { + this.position = this.positionAttribute; + } else if (currentBox.top > availableBottom) { + this.position = DropdownPosition.above; + } else { + this.position = DropdownPosition.below; + } + + this.positionAttribute = this.forcedPosition + ? this.positionAttribute + : this.position; + + this.availableViewportHeight = this.position === DropdownPosition.above + ? Math.trunc(currentBox.top) + : Math.trunc(availableBottom); + } + /** * Sets the value and to match the first selected option. */ diff --git a/packages/nimble-components/src/rich-text/mention-listbox/index.ts b/packages/nimble-components/src/rich-text/mention-listbox/index.ts index db35164406..bd07f64ec8 100644 --- a/packages/nimble-components/src/rich-text/mention-listbox/index.ts +++ b/packages/nimble-components/src/rich-text/mention-listbox/index.ts @@ -12,6 +12,7 @@ import type { AnchoredRegion } from '../../anchored-region'; import { diacriticInsensitiveStringNormalizer } from '../../utilities/models/string-normalizers'; import type { ListOption } from '../../list-option'; import type { MentionListboxShowOptions } from './types'; +import { DropdownPosition } from '../../patterns/dropdown/types'; declare global { interface HTMLElementTagNameMap { @@ -108,12 +109,7 @@ export class RichTextMentionListbox extends FoundationListbox { * @public */ public show(options: MentionListboxShowOptions): void { - const anchorRect = options.anchorNode.getBoundingClientRect(); - const availableSpaceBelow = window.innerHeight - anchorRect.bottom; - const availableSpaceAbove = anchorRect.top; - this.availableViewportHeight = Math.trunc( - Math.max(availableSpaceAbove, availableSpaceBelow) - ); + this.setPositioning(options); this.filter = options.filter; this.anchorElement = options.anchorNode; this.setOpen(true); @@ -289,6 +285,25 @@ export class RichTextMentionListbox extends FoundationListbox { private setOpen(value: boolean): void { this.open = value; } + + // Aligns with select / combobox + // Modified to remove forced position concept + private setPositioning(options: MentionListboxShowOptions): void { + const currentBox = options.anchorNode.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const availableBottom = viewportHeight - currentBox.bottom; + + let position; + if (currentBox.top > availableBottom) { + position = DropdownPosition.above; + } else { + position = DropdownPosition.below; + } + + this.availableViewportHeight = position === DropdownPosition.above + ? Math.trunc(currentBox.top) + : Math.trunc(availableBottom); + } } const nimbleRichTextMentionListbox = RichTextMentionListbox.compose({ diff --git a/packages/nimble-components/src/select/index.ts b/packages/nimble-components/src/select/index.ts index e6e5d2fe48..67bc64568e 100644 --- a/packages/nimble-components/src/select/index.ts +++ b/packages/nimble-components/src/select/index.ts @@ -6,7 +6,6 @@ import { Select as FoundationSelect, ListboxOption, type SelectOptions, - SelectPosition, applyMixins, StartEnd, DelegatesARIASelect @@ -26,6 +25,7 @@ import { arrowExpanderDown16X16 } from '@ni/nimble-tokens/dist/icons/js'; import { styles } from './styles'; import { DropdownAppearance, + DropdownPosition, type ListOptionOwner } from '../patterns/dropdown/types'; import { errorTextTemplate } from '../patterns/error/template'; @@ -83,7 +83,7 @@ export class Select * @public */ @attr({ attribute: 'position' }) - public positionAttribute?: SelectPosition; + public positionAttribute?: DropdownPosition; @attr({ attribute: 'filter-mode' }) public filterMode: FilterMode = FilterMode.none; @@ -112,7 +112,7 @@ export class Select * @public */ @observable - public position?: SelectPosition; + public position?: DropdownPosition; /** * The ref to the internal `.control` element. @@ -884,8 +884,8 @@ export class Select } protected positionChanged( - _: SelectPosition | undefined, - next: SelectPosition | undefined + _: DropdownPosition | undefined, + next: DropdownPosition | undefined ): void { this.positionAttribute = next; this.setPositioning(); @@ -1080,16 +1080,16 @@ export class Select if (this.forcedPosition) { this.position = this.positionAttribute; } else if (currentBox.top > availableBottom) { - this.position = SelectPosition.above; + this.position = DropdownPosition.above; } else { - this.position = SelectPosition.below; + this.position = DropdownPosition.below; } this.positionAttribute = this.forcedPosition ? this.positionAttribute : this.position; - this.availableViewportHeight = this.position === SelectPosition.above + this.availableViewportHeight = this.position === DropdownPosition.above ? Math.trunc(currentBox.top) : Math.trunc(availableBottom); } From 3614999f0701916f4282b1fd966d4a68078eedd8 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Fri, 9 May 2025 18:56:57 -0500 Subject: [PATCH 04/18] Fix inconsistency with select/combobox --- .../src/rich-text/mention-listbox/index.ts | 13 +++++++++---- .../src/rich-text/mention-listbox/template.ts | 5 +++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/nimble-components/src/rich-text/mention-listbox/index.ts b/packages/nimble-components/src/rich-text/mention-listbox/index.ts index bd07f64ec8..af5fffda8a 100644 --- a/packages/nimble-components/src/rich-text/mention-listbox/index.ts +++ b/packages/nimble-components/src/rich-text/mention-listbox/index.ts @@ -36,6 +36,12 @@ export class RichTextMentionListbox extends FoundationListbox { @observable public region?: AnchoredRegion; + /** + * @internal + */ + @observable + public position?: DropdownPosition; + /** * The space available in the viewport for the listbox when opened. * @@ -293,14 +299,13 @@ export class RichTextMentionListbox extends FoundationListbox { const viewportHeight = window.innerHeight; const availableBottom = viewportHeight - currentBox.bottom; - let position; if (currentBox.top > availableBottom) { - position = DropdownPosition.above; + this.position = DropdownPosition.above; } else { - position = DropdownPosition.below; + this.position = DropdownPosition.below; } - this.availableViewportHeight = position === DropdownPosition.above + this.availableViewportHeight = this.position === DropdownPosition.above ? Math.trunc(currentBox.top) : Math.trunc(availableBottom); } diff --git a/packages/nimble-components/src/rich-text/mention-listbox/template.ts b/packages/nimble-components/src/rich-text/mention-listbox/template.ts index 2d16ddd980..92ada94529 100644 --- a/packages/nimble-components/src/rich-text/mention-listbox/template.ts +++ b/packages/nimble-components/src/rich-text/mention-listbox/template.ts @@ -3,6 +3,7 @@ import { Listbox } from '@ni/fast-foundation'; import type { RichTextMentionListbox } from '.'; import { anchoredRegionTag } from '../../anchored-region'; import { filterNoResultsLabel } from '../../label-provider/core/label-tokens'; +import { DropdownPosition } from '../../patterns/dropdown/types'; /* eslint-disable @typescript-eslint/indent */ // prettier-ignore @@ -13,8 +14,8 @@ export const template = html` class="anchored-region" fixed-placement auto-update-mode="auto" - vertical-default-position="bottom" - vertical-positioning-mode="dynamic" + vertical-default-position="${x => (x.position === DropdownPosition.above ? 'top' : 'bottom')}" + vertical-positioning-mode="${x => (!x.position ? 'dynamic' : 'locktodefault')}" horizontal-default-position="center" horizontal-positioning-mode="locktodefault" horizontal-scaling="anchor" From fe2e3e07131c62cbd0da5c536dca69b00e0f3b19 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Wed, 21 May 2025 16:25:10 -0500 Subject: [PATCH 05/18] Make combobox and select use AR's dynamic vertical positioning --- .../src/anchored-region/index.ts | 10 ++- .../nimble-components/src/combobox/index.ts | 85 +++++++++++-------- .../src/combobox/template.ts | 2 +- .../src/patterns/dropdown/styles.ts | 38 +++++---- .../src/patterns/dropdown/utility.ts | 10 +++ .../src/rich-text/mention-listbox/index.ts | 27 ------ .../src/rich-text/mention-listbox/template.ts | 1 - .../nimble-components/src/select/index.ts | 77 ++++++++++------- .../nimble-components/src/select/template.ts | 8 +- 9 files changed, 140 insertions(+), 118 deletions(-) create mode 100644 packages/nimble-components/src/patterns/dropdown/utility.ts diff --git a/packages/nimble-components/src/anchored-region/index.ts b/packages/nimble-components/src/anchored-region/index.ts index 1df8c8d2c8..ee3488a261 100644 --- a/packages/nimble-components/src/anchored-region/index.ts +++ b/packages/nimble-components/src/anchored-region/index.ts @@ -1,7 +1,9 @@ +import { observable } from '@ni/fast-element'; import { DesignSystem, AnchoredRegion as FoundationAnchoredRegion, - anchoredRegionTemplate as template + anchoredRegionTemplate as template, + type AnchoredRegionPositionLabel } from '@ni/fast-foundation'; import { styles } from './styles'; @@ -22,7 +24,11 @@ declare global { /** * A nimble-styled anchored region control. */ -export class AnchoredRegion extends FoundationAnchoredRegion {} +export class AnchoredRegion extends FoundationAnchoredRegion { + /* @internal */ + @observable + public override verticalPosition: AnchoredRegionPositionLabel | undefined; +} const nimbleAnchoredRegion = AnchoredRegion.compose({ baseName: 'anchored-region', diff --git a/packages/nimble-components/src/combobox/index.ts b/packages/nimble-components/src/combobox/index.ts index e25698e77a..ae83c660eb 100644 --- a/packages/nimble-components/src/combobox/index.ts +++ b/packages/nimble-components/src/combobox/index.ts @@ -1,4 +1,12 @@ -import { DOM, Observable, attr, html, observable, ref } from '@ni/fast-element'; +import { + attr, + DOM, + html, + observable, + Observable, + ref, + type Notifier +} from '@ni/fast-element'; import { DesignSystem, type ComboboxOptions, @@ -30,6 +38,7 @@ import { DropdownPosition, type DropdownPattern } from '../patterns/dropdown/types'; +import { anchoredRegionPositionToDropdownPosition } from '../patterns/dropdown/utility'; import type { AnchoredRegion } from '../anchored-region'; import { template } from './template'; import { FormAssociatedCombobox } from './models/combobox-form-associated'; @@ -206,11 +215,7 @@ export class Combobox private valueBeforeTextUpdate?: string; private _value = ''; private filter = ''; - - /** - * The initial state of the position attribute. - */ - private forcedPosition = false; + private anchoredRegionNotifier?: Notifier; private get isAutocompleteInline(): boolean { return ( @@ -245,11 +250,10 @@ export class Combobox public override connectedCallback(): void { super.connectedCallback(); - this.forcedPosition = !!this.positionAttribute; if (this.value) { this.initialValue = this.value; } - this.setPositioning(); + this.updateAvailableViewportHeight(); this.updateInputAriaLabel(); } @@ -620,6 +624,16 @@ export class Combobox } } + /* @internal */ + public override handleChange(source: unknown, propertyName: string): void { + super.handleChange(source, propertyName); + if (propertyName === 'verticalPosition') { + this.position = anchoredRegionPositionToDropdownPosition( + this.region?.verticalPosition + ); + } + } + /** * Focus the control and scroll the first selected option into view. * @@ -647,7 +661,7 @@ export class Combobox this.ariaControls = this.listboxId; this.ariaExpanded = 'true'; - this.setPositioning(); + this.updateAvailableViewportHeight(); this.focusAndScrollOptionIntoView(); // focus is directed to the element when `open` is changed programmatically @@ -698,19 +712,24 @@ export class Combobox } protected positionChanged( - _: DropdownPosition | undefined, - next: DropdownPosition | undefined + _prev: DropdownPosition | undefined, + _next: DropdownPosition | undefined ): void { - this.positionAttribute = next; - this.setPositioning(); + this.updateAvailableViewportHeight(); } private regionChanged( _prev: AnchoredRegion | undefined, _next: AnchoredRegion | undefined ): void { + if (this.anchoredRegionNotifier) { + this.anchoredRegionNotifier?.unsubscribe(this, 'verticalPosition'); + this.anchoredRegionNotifier = undefined; + } if (this.region && this.controlWrapper) { this.region.anchorElement = this.controlWrapper; + this.anchoredRegionNotifier = Observable.getNotifier(this.region); + this.anchoredRegionNotifier.subscribe(this, 'verticalPosition'); } } @@ -728,31 +747,27 @@ export class Combobox this.updateInputAriaLabel(); } - private setPositioning(): void { - if (!this.$fastController.isConnected) { - // Don't call setPositioning() until we're connected, - // since this.forcedPosition isn't initialized yet. - return; - } + private updateAvailableViewportHeight(): void { const currentBox = this.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - const availableBottom = viewportHeight - currentBox.bottom; + const viewportHeight = document.documentElement.getBoundingClientRect().height; + const availableSpaceAbove = Math.trunc(currentBox.top); + const availableSpaceBelow = Math.trunc( + viewportHeight - currentBox.bottom + ); - if (this.forcedPosition) { - this.position = this.positionAttribute; - } else if (currentBox.top > availableBottom) { - this.position = DropdownPosition.above; - } else { - this.position = DropdownPosition.below; + switch (this.positionAttribute) { + case DropdownPosition.above: + this.availableViewportHeight = availableSpaceAbove; + break; + case DropdownPosition.below: + this.availableViewportHeight = availableSpaceBelow; + break; + default: + this.availableViewportHeight = Math.max( + availableSpaceAbove, + availableSpaceBelow + ); } - - this.positionAttribute = this.forcedPosition - ? this.positionAttribute - : this.position; - - this.availableViewportHeight = this.position === DropdownPosition.above - ? Math.trunc(currentBox.top) - : Math.trunc(availableBottom); } /** diff --git a/packages/nimble-components/src/combobox/template.ts b/packages/nimble-components/src/combobox/template.ts index c4eacb369c..b108cc3238 100644 --- a/packages/nimble-components/src/combobox/template.ts +++ b/packages/nimble-components/src/combobox/template.ts @@ -70,7 +70,7 @@ ComboboxOptions <${anchoredRegionTag} ${ref('region')} - class="anchored-region" + class="anchored-region confined-to-view" fixed-placement auto-update-mode="auto" vertical-default-position="${x => (x.positionAttribute === DropdownPosition.above ? 'top' : 'bottom')}" diff --git a/packages/nimble-components/src/patterns/dropdown/styles.ts b/packages/nimble-components/src/patterns/dropdown/styles.ts index 65fe892686..ed0475cfdd 100644 --- a/packages/nimble-components/src/patterns/dropdown/styles.ts +++ b/packages/nimble-components/src/patterns/dropdown/styles.ts @@ -202,12 +202,9 @@ export const styles = css` margin-inline-start: auto; } - :host([open][position='above']) .anchored-region { - padding-bottom: ${smallPadding}; - } - - :host([open][position='below']) .anchored-region { + :host([open]) .anchored-region { padding-top: ${smallPadding}; + padding-bottom: ${smallPadding}; } .listbox { @@ -219,23 +216,28 @@ export const styles = css` --ni-private-listbox-padding: ${smallPadding}; --ni-private-listbox-filter-height: 0px; --ni-private-listbox-loading-indicator-height: 0px; + --ni-private-listbox-ideal-height: calc( + var(--ni-private-listbox-anchor-element-gap) + + 2 * ${borderWidth} + + var(--ni-private-listbox-padding) + + ${controlHeight} * var(--ni-private-listbox-visible-option-count) + + var(--ni-private-listbox-filter-height) + + var(--ni-private-listbox-loading-indicator-height) + ); + max-height: var(--ni-private-listbox-ideal-height); + box-shadow: ${elevation2BoxShadow}; + border: ${borderWidth} solid ${popupBorderColor}; + background-color: ${applicationBackgroundColor}; + } + + .anchored-region.confined-to-view .listbox { max-height: min( - calc( - var(--ni-private-listbox-anchor-element-gap) + - 2 * ${borderWidth} + - var(--ni-private-listbox-padding) + - ${controlHeight} * var(--ni-private-listbox-visible-option-count) + - var(--ni-private-listbox-filter-height) + - var(--ni-private-listbox-loading-indicator-height) - ), + var(--ni-private-listbox-ideal-height), calc( var(--ni-private-listbox-available-viewport-height) - var(--ni-private-listbox-anchor-element-gap) ) ); - box-shadow: ${elevation2BoxShadow}; - border: ${borderWidth} solid ${popupBorderColor}; - background-color: ${applicationBackgroundColor}; } .listbox:has(.filter-field) { @@ -246,12 +248,12 @@ export const styles = css` --ni-private-listbox-loading-indicator-height: ${controlHeight}; } - :host([open][position='above']) .listbox { + :host([open]) .listbox.top{ border-bottom-left-radius: 0; border-bottom-right-radius: 0; } - :host([open][position='below']) .listbox { + :host([open]) .listbox.bottom{ border-top-left-radius: 0; border-top-right-radius: 0; } diff --git a/packages/nimble-components/src/patterns/dropdown/utility.ts b/packages/nimble-components/src/patterns/dropdown/utility.ts new file mode 100644 index 0000000000..ff1c892a61 --- /dev/null +++ b/packages/nimble-components/src/patterns/dropdown/utility.ts @@ -0,0 +1,10 @@ +import type { AnchoredRegionPositionLabel } from '@ni/fast-foundation'; +import { DropdownPosition } from './types'; + +export function anchoredRegionPositionToDropdownPosition( + anchoredRegionPosition?: AnchoredRegionPositionLabel +): DropdownPosition { + return anchoredRegionPosition === 'start' + ? DropdownPosition.above + : DropdownPosition.below; +} diff --git a/packages/nimble-components/src/rich-text/mention-listbox/index.ts b/packages/nimble-components/src/rich-text/mention-listbox/index.ts index af5fffda8a..aac435deeb 100644 --- a/packages/nimble-components/src/rich-text/mention-listbox/index.ts +++ b/packages/nimble-components/src/rich-text/mention-listbox/index.ts @@ -42,14 +42,6 @@ export class RichTextMentionListbox extends FoundationListbox { @observable public position?: DropdownPosition; - /** - * The space available in the viewport for the listbox when opened. - * - * @internal - */ - @observable - public availableViewportHeight = 0; - /** * @internal */ @@ -115,7 +107,6 @@ export class RichTextMentionListbox extends FoundationListbox { * @public */ public show(options: MentionListboxShowOptions): void { - this.setPositioning(options); this.filter = options.filter; this.anchorElement = options.anchorNode; this.setOpen(true); @@ -291,24 +282,6 @@ export class RichTextMentionListbox extends FoundationListbox { private setOpen(value: boolean): void { this.open = value; } - - // Aligns with select / combobox - // Modified to remove forced position concept - private setPositioning(options: MentionListboxShowOptions): void { - const currentBox = options.anchorNode.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - const availableBottom = viewportHeight - currentBox.bottom; - - if (currentBox.top > availableBottom) { - this.position = DropdownPosition.above; - } else { - this.position = DropdownPosition.below; - } - - this.availableViewportHeight = this.position === DropdownPosition.above - ? Math.trunc(currentBox.top) - : Math.trunc(availableBottom); - } } const nimbleRichTextMentionListbox = RichTextMentionListbox.compose({ diff --git a/packages/nimble-components/src/rich-text/mention-listbox/template.ts b/packages/nimble-components/src/rich-text/mention-listbox/template.ts index 92ada94529..c4237615b1 100644 --- a/packages/nimble-components/src/rich-text/mention-listbox/template.ts +++ b/packages/nimble-components/src/rich-text/mention-listbox/template.ts @@ -30,7 +30,6 @@ export const template = html` role="listbox" @click="${(x, c) => x.clickHandler(c.event as MouseEvent)}" ?disabled="${x => x.disabled}" - style="--ni-private-listbox-available-viewport-height: ${x => x.availableViewportHeight}px;" ${ref('listbox')} > { this.updateDisplayValue(); }); + private anchoredRegionNotifier?: Notifier; + /** * @internal */ public override connectedCallback(): void { super.connectedCallback(); - this.forcedPosition = !!this.positionAttribute; if (this.open) { this.initializeOpenState(); } @@ -277,8 +285,16 @@ export class Select _prev: AnchoredRegion | undefined, _next: AnchoredRegion | undefined ): void { + if (this.anchoredRegionNotifier) { + this.anchoredRegionNotifier?.unsubscribe(this, 'verticalPosition'); + this.anchoredRegionNotifier = undefined; + } if (this.anchoredRegion && this.control) { this.anchoredRegion.anchorElement = this.control; + this.anchoredRegionNotifier = Observable.getNotifier( + this.anchoredRegion + ); + this.anchoredRegionNotifier.subscribe(this, 'verticalPosition'); } } @@ -435,6 +451,12 @@ export class Select ); break; } + case 'verticalPosition': { + this.position = anchoredRegionPositionToDropdownPosition( + this.anchoredRegion?.verticalPosition + ); + break; + } default: break; } @@ -884,11 +906,10 @@ export class Select } protected positionChanged( - _: DropdownPosition | undefined, - next: DropdownPosition | undefined + _prev: DropdownPosition | undefined, + _next: DropdownPosition | undefined ): void { - this.positionAttribute = next; - this.setPositioning(); + this.updateAvailableViewportHeight(); } /** @@ -1067,31 +1088,27 @@ export class Select return this.options.find(o => o.hidden && o.disabled) as ListOption; } - private setPositioning(): void { - if (!this.$fastController.isConnected) { - // Don't call setPositioning() until we're connected, - // since this.forcedPosition isn't initialized yet. - return; - } + private updateAvailableViewportHeight(): void { const currentBox = this.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - const availableBottom = viewportHeight - currentBox.bottom; + const viewportHeight = document.documentElement.getBoundingClientRect().height; + const availableSpaceAbove = Math.trunc(currentBox.top); + const availableSpaceBelow = Math.trunc( + viewportHeight - currentBox.bottom + ); - if (this.forcedPosition) { - this.position = this.positionAttribute; - } else if (currentBox.top > availableBottom) { - this.position = DropdownPosition.above; - } else { - this.position = DropdownPosition.below; + switch (this.positionAttribute) { + case DropdownPosition.above: + this.availableViewportHeight = availableSpaceAbove; + break; + case DropdownPosition.below: + this.availableViewportHeight = availableSpaceBelow; + break; + default: + this.availableViewportHeight = Math.max( + availableSpaceAbove, + availableSpaceBelow + ); } - - this.positionAttribute = this.forcedPosition - ? this.positionAttribute - : this.position; - - this.availableViewportHeight = this.position === DropdownPosition.above - ? Math.trunc(currentBox.top) - : Math.trunc(availableBottom); } private updateAdjacentSeparatorState( @@ -1303,7 +1320,7 @@ export class Select this.ariaControls = this.listboxId; this.ariaExpanded = 'true'; - this.setPositioning(); + this.updateAvailableViewportHeight(); this.focusAndScrollOptionIntoView(); } diff --git a/packages/nimble-components/src/select/template.ts b/packages/nimble-components/src/select/template.ts index 853eaad88c..657b1191c5 100644 --- a/packages/nimble-components/src/select/template.ts +++ b/packages/nimble-components/src/select/template.ts @@ -108,7 +108,7 @@ SelectOptions } <${anchoredRegionTag} ${ref('anchoredRegion')} - class="anchored-region" + class="anchored-region confined-to-view" fixed-placement auto-update-mode="auto" vertical-default-position="${x => (x.positionAttribute === DropdownPosition.above ? 'top' : 'bottom')}" @@ -123,7 +123,7 @@ SelectOptions class=" listbox ${x => (x.filteredOptions.length === 0 ? 'empty' : '')} - ${x => x.positionAttribute} + ${x => x.position || ''} " id="${x => x.listboxId}" part="listbox" @@ -133,7 +133,7 @@ SelectOptions ${ref('listbox')} > ${when(x => x.filterMode !== FilterMode.none, html ${when(x => x.loadingVisible, html` -
+
<${iconMagnifyingGlassTag} class="filter-icon"> ${when(x => x.loadingVisible, html