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

Make combobox and select dropdowns float above other UI #1244

Merged
merged 11 commits into from
May 23, 2023
Merged
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
"type": "patch",
"comment": "Use anchored region in Select and Combobox",
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
"packageName": "@ni/nimble-components",
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
"email": "[email protected]",
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
"dependentChangeType": "patch"
}
48 changes: 46 additions & 2 deletions packages/nimble-components/src/combobox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { attr, html, observable, ref } from '@microsoft/fast-element';
import {
DesignSystem,
Combobox as FoundationCombobox,
ComboboxOptions,
comboboxTemplate as template
ComboboxOptions
} from '@microsoft/fast-foundation';
import {
keyArrowDown,
Expand All @@ -20,6 +19,8 @@ import { styles } from './styles';
import type { ErrorPattern } from '../patterns/error/types';
import type { DropdownPattern } from '../patterns/dropdown/types';
import { DropdownAppearance } from '../patterns/dropdown/types';
import type { AnchoredRegion } from '../anchored-region';
import { template } from './template';

declare global {
interface HTMLElementTagNameMap {
Expand Down Expand Up @@ -57,6 +58,18 @@ export class Combobox
@attr({ attribute: 'error-visible', mode: 'boolean' })
public errorVisible = false;

/**
* @internal
*/
@observable
public region?: AnchoredRegion;

/**
* @internal
*/
@observable
public controlWrapper?: HTMLElement;
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
m-akinc marked this conversation as resolved.
Show resolved Hide resolved

private valueUpdatedByInput = false;
private valueBeforeTextUpdate?: string;

Expand Down Expand Up @@ -173,11 +186,42 @@ export class Combobox
}
}

private regionChanged(
_prev: AnchoredRegion | undefined,
_next: AnchoredRegion | undefined
): void {
if (this.region && this.controlWrapper) {
this.region.anchorElement = this.controlWrapper;
}
}

private controlWrapperChanged(
_prev: HTMLElement | undefined,
_next: HTMLElement | undefined
): void {
if (this.region && this.controlWrapper) {
this.region.anchorElement = this.controlWrapper;
}
}

// Workaround for https://github.com/microsoft/fast/issues/6041.
private ariaLabelChanged(_oldValue: string, _newValue: string): void {
this.updateInputAriaLabel();
}

private maxHeightChanged(): void {
this.updateListboxMaxHeightCssVariable();
}

private updateListboxMaxHeightCssVariable(): void {
if (this.listbox) {
this.listbox.style.setProperty(
'--ni-private-select-max-height',
`${this.maxHeight}px`
);
}
}

private updateInputAriaLabel(): void {
const inputElement = this.shadowRoot?.querySelector('.selected-value');
if (this.ariaLabel) {
Expand Down
86 changes: 86 additions & 0 deletions packages/nimble-components/src/combobox/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { type ViewTemplate, html, ref, slotted } from '@microsoft/fast-element';
import {
type FoundationElementTemplate,
type ComboboxOptions,
startSlotTemplate,
endSlotTemplate,
Listbox
} from '@microsoft/fast-foundation';
import type { Combobox } from '.';
import { anchoredRegionTag } from '../anchored-region';
import { DropdownPosition } from '../patterns/dropdown/types';

// prettier-ignore
export const template: FoundationElementTemplate<
ViewTemplate<Combobox>,
ComboboxOptions
> = (context, definition) => html`
<template
aria-disabled="${x => x.ariaDisabled}"
autocomplete="${x => x.autocomplete}"
class="${x => (x.open ? 'open' : '')} ${x => (x.disabled ? 'disabled' : '')} ${x => x.position}"
?open="${x => x.open}"
tabindex="${x => (!x.disabled ? '0' : null)}"
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
@focusout="${(x, c) => x.focusoutHandler(c.event as FocusEvent)}"
@keydown="${(x, c) => x.keydownHandler(c.event as KeyboardEvent)}"
>
<div class="control" part="control" ${ref('controlWrapper')}>
${startSlotTemplate(context, definition)}
<slot name="control">
<input
aria-activedescendant="${x => (x.open ? x.ariaActiveDescendant : null)}"
aria-autocomplete="${x => x.ariaAutoComplete}"
aria-controls="${x => x.ariaControls}"
aria-disabled="${x => x.ariaDisabled}"
aria-expanded="${x => x.ariaExpanded}"
aria-haspopup="listbox"
class="selected-value"
part="selected-value"
placeholder="${x => x.placeholder}"
role="combobox"
type="text"
?disabled="${x => x.disabled}"
:value="${x => x.value}"
@input="${(x, c) => x.inputHandler(c.event as InputEvent)}"
@keyup="${(x, c) => x.keyupHandler(c.event as KeyboardEvent)}"
${ref('control')}
/>
<div class="indicator" part="indicator" aria-hidden="true">
<slot name="indicator">
${definition.indicator || ''}
</slot>
</div>
</slot>
${endSlotTemplate(context, definition)}
</div>
<${anchoredRegionTag}
${ref('region')}
class="anchoredRegion"
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
fixed-placement
auto-update-mode="auto"
vertical-default-position="${x => (x.positionAttribute === DropdownPosition.above ? 'top' : 'bottom')}"
vertical-positioning-mode="${x => (!x.positionAttribute ? 'dynamic' : 'locktodefault')}"
horizontal-default-position="center"
horizontal-positioning-mode="locktodefault"
horizontal-scaling="anchor"
?hidden="${x => !x.open}">
<div
class="listbox"
id="${x => x.listboxId}"
part="listbox"
role="listbox"
?disabled="${x => x.disabled}"
${ref('listbox')}
>
<slot
${slotted({
filter: (n: Node) => n instanceof HTMLElement && Listbox.slottedOptionFilter(n),
flatten: true,
property: 'slottedOptions',
})}
></slot>
</div>
</${anchoredRegionTag}>
</template>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
import { createStory } from '../../utilities/tests/storybook';
import { sharedMatrixParameters } from '../../utilities/tests/matrix';
import { listOptionTag } from '../../list-option';
import { comboboxTag } from '..';

const metadata: Meta = {
title: 'Tests/Combobox',
parameters: {
...sharedMatrixParameters()
}
};

export default metadata;

const positionStates = [
['below', ''],
['above', 'margin-top: 120px;']
] as const;
type PositionState = (typeof positionStates)[number];

// prettier-ignore
const component = ([
position,
positionStyle
]: PositionState): ViewTemplate => html`
<div style=${() => (position === 'below' ? 'height: 150px' : null)}>
<div style="overflow: auto; border: 2px solid red; ${() => positionStyle}">
<${comboboxTag} open position="${() => position}">
<${listOptionTag} value="1">Option 1</${listOptionTag}>
<${listOptionTag} value="2">Option 2</${listOptionTag}>
<${listOptionTag} value="3">Option 3</${listOptionTag}>
</${comboboxTag}>
</div>
</div>
`;

export const comboboxBelowNotConfinedByDiv: StoryFn = createStory(
component(positionStates[0])
);
export const comboboxAboveNotConfinedByDiv: StoryFn = createStory(
component(positionStates[1])
);
Loading