Skip to content

Commit

Permalink
feat(select): positioning
Browse files Browse the repository at this point in the history
  • Loading branch information
fbasso committed Dec 13, 2023
1 parent 4e25c1a commit 555e168
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 38 deletions.
1 change: 0 additions & 1 deletion angular/demo/src/app/samples/select/default.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {Component} from '@angular/core';
template: `
<div style="height: 400px;">
<div class="mb-3">
<label class="form-label">Start typing with a, b or c</label>
<div
auSelect
[auItems]="items"
Expand Down
29 changes: 25 additions & 4 deletions angular/lib/src/select/select.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import type {AdaptSlotContentProps, ItemContext, SelectItemContext, SelectWidget, SlotContent, WidgetState} from '@agnos-ui/angular-headless';
import type {
AdaptSlotContentProps,
ItemContext,
SelectItemContext,
SelectWidget,
SlotContent,
WidgetState,
floatingUI,
} from '@agnos-ui/angular-headless';
import {
SlotDirective,
UseDirective,
auBooleanAttribute,
callWidgetFactory,
createSelect,
mergeDirectives,
patchSimpleChanges,
toAngularSignal,
toSlotContextWidget,
useDirectiveForHost,
} from '@agnos-ui/angular-headless';
import type {AfterContentChecked, OnChanges, Signal, SimpleChanges} from '@angular/core';
import {ChangeDetectionStrategy, Component, ContentChild, Directive, EventEmitter, Input, Output, TemplateRef, inject} from '@angular/core';
Expand Down Expand Up @@ -69,10 +79,9 @@ export class SelectItemDirective<Item> {
</div>
@if (state$().open && state$().visibleItems.length) {
<ul
[auUse]="_widget.directives.hasFocusDirective"
[auUse]="menuDirective"
[class]="'dropdown-menu show ' + (menuClassName || '')"
data-popper-placement="bottom-start"
data-bs-popper="static"
[attr.data-popper-placement]="state$().placement"
(mousedown)="$event.preventDefault()"
>
@for (itemContext of state$().visibleItems; track itemCtxTrackBy($index, itemContext)) {
Expand Down Expand Up @@ -106,6 +115,12 @@ export class SelectComponent<Item> implements OnChanges, AfterContentChecked {
*/
@Input('auItems') items: Item[] | undefined;

/**
* List of allowed placements for the dropdown.
* This refers to the [allowedPlacements from floating UI](https://floating-ui.com/docs/autoPlacement#allowedplacements), given the different [Placement possibilities](https://floating-ui.com/docs/computePosition#placement).
*/
@Input('auAllowedPlacements') allowedPlacements: floatingUI.Placement[] | undefined;

/**
* true if the select is open
*/
Expand Down Expand Up @@ -190,8 +205,14 @@ export class SelectComponent<Item> implements OnChanges, AfterContentChecked {
readonly widget = toSlotContextWidget(this._widget);
readonly api = this._widget.api;

readonly menuDirective = mergeDirectives(this._widget.directives.hasFocusDirective, this._widget.directives.floatingDirective);

state$: Signal<WidgetState<SelectWidget<Item>>> = toAngularSignal(this._widget.state$);

constructor() {
useDirectiveForHost(this._widget.directives.referenceDirective);
}

ngOnChanges(changes: SimpleChanges) {
patchSimpleChanges(this._widget.patch, changes);
}
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ describe(`Select model`, () => {
menuClassName: '',
menuItemClassName: '',
open: false,
placement: undefined,
selected: [],
selectedContexts: [],
slotBadgeLabel: '(function)',
Expand All @@ -133,6 +134,7 @@ describe(`Select model`, () => {
menuClassName: '',
menuItemClassName: '',
open: true,
placement: undefined,
selected: [],
selectedContexts: [],
slotBadgeLabel: '(function)',
Expand Down Expand Up @@ -619,6 +621,7 @@ describe(`Select model`, () => {
menuClassName: '',
menuItemClassName: '',
badgeClassName: '',
placement: undefined,
slotBadgeLabel: '(function)',
slotItem: '(function)',
};
Expand Down
62 changes: 58 additions & 4 deletions core/src/components/select/select.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {asWritable, computed, writable} from '@amadeus-it-group/tansu';
import type {WidgetsCommonPropsAndState} from '../commonProps';
import type {FloatingUI} from '../../services/floatingUI';
import {createFloatingUI, floatingUI} from '../../services/floatingUI';
import type {HasFocus} from '../../services/focustrack';
import {createHasFocus} from '../../services/focustrack';
import {bindableDerived, stateStores, writablesForProps} from '../../utils/stores';
import type {PropsConfig, SlotContent, Widget, WidgetSlotContext} from '../../types';
import {noop} from '../../utils/internal/func';
import {bindableDerived, stateStores, writablesForProps} from '../../utils/stores';
import type {WidgetsCommonPropsAndState} from '../commonProps';

/**
* A type for the slot context of the pagination widget
Expand Down Expand Up @@ -88,6 +90,12 @@ export interface SelectProps<T> extends SelectCommonPropsAndState<T> {
*/
items: T[];

/**
* List of allowed placements for the dropdown.
* This refers to the [allowedPlacements from floating UI](https://floating-ui.com/docs/autoPlacement#allowedplacements), given the different [Placement possibilities](https://floating-ui.com/docs/computePosition#placement).
*/
allowedPlacements: floatingUI.Placement[];

/**
* Custom function to get the id of an item
* By default, the item is returned
Expand Down Expand Up @@ -151,6 +159,11 @@ export interface SelectState<Item> extends SelectCommonPropsAndState<Item> {
* It is designed to define the highlighted item in the dropdown menu
*/
highlighted: ItemContext<Item> | undefined;

/**
* Current placement of the dropdown
*/
placement: floatingUI.Placement | undefined;
}

export interface SelectApi<Item> {
Expand Down Expand Up @@ -251,6 +264,16 @@ export interface SelectDirectives {
* Directive to be used in the input group and the menu containers
*/
hasFocusDirective: HasFocus['directive'];

/**
* Directive that enables dynamic positioning of menu element
*/
floatingDirective: FloatingUI['directives']['floatingDirective'];

/**
* A directive to be applied to the input group element serves as the base for menu positioning
*/
referenceDirective: FloatingUI['directives']['referenceDirective'];
}

export interface SelectActions {
Expand All @@ -272,7 +295,7 @@ export type SelectWidget<Item> = Widget<SelectProps<Item>, SelectState<Item>, Se

const defaultItemId = (item: any) => '' + item;

const defaultConfig: SelectProps<any> = {
export const defaultConfig: SelectProps<any> = {
id: undefined,
ariaLabel: 'Select',
open: false,
Expand All @@ -285,6 +308,7 @@ const defaultConfig: SelectProps<any> = {
onOpenChange: noop,
onFilterTextChange: noop,
onSelectedChange: noop,
allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
className: '',
menuClassName: '',
menuItemClassName: '',
Expand All @@ -309,7 +333,17 @@ export function getSelectDefaultConfig() {
export function createSelect<Item>(config?: PropsConfig<SelectProps<Item>>): SelectWidget<Item> {
// Props
const [
{open$: _dirtyOpen$, filterText$: _dirtyFilterText$, items$, itemIdFn$, onOpenChange$, onFilterTextChange$, onSelectedChange$, ...stateProps},
{
open$: _dirtyOpen$,
filterText$: _dirtyFilterText$,
items$,
itemIdFn$,
onOpenChange$,
onFilterTextChange$,
onSelectedChange$,
allowedPlacements$,
...stateProps
},
patch,
] = writablesForProps<SelectProps<Item>>(defaultConfig, config);
const {selected$} = stateProps;
Expand Down Expand Up @@ -378,13 +412,31 @@ export function createSelect<Item>(config?: PropsConfig<SelectProps<Item>>): Sel
return visibleItems.length && highlightedIndex != undefined ? visibleItems[highlightedIndex] : undefined;
});

const {
directives: {floatingDirective, referenceDirective},
stores: {placement$},
} = createFloatingUI({
props: {
computePositionOptions: computed(() => ({
middleware: [
floatingUI.offset(5),
floatingUI.autoPlacement({
allowedPlacements: allowedPlacements$(),
}),
floatingUI.size(),
],
})) as any, // TODO remove as any by allowing Readable instead of Writable only
},
});

const widget: SelectWidget<Item> = {
...stateStores({
visibleItems$,
highlighted$,
open$,
selectedContexts$,
filterText$,
placement$,

...stateProps,
}),
Expand Down Expand Up @@ -480,6 +532,8 @@ export function createSelect<Item>(config?: PropsConfig<SelectProps<Item>>): Sel
},
directives: {
hasFocusDirective,
floatingDirective,
referenceDirective,
},
actions: {
onInput({target}: {target: HTMLInputElement}) {
Expand Down
12 changes: 12 additions & 0 deletions core/src/services/floatingUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const defaultConfig: FloatingUIProps = {
arrowOptions: {},
};

export type FloatingUI = ReturnType<typeof createFloatingUI>;

export const createFloatingUI = (propsConfig?: PropsConfig<FloatingUIProps>) => {
const [{autoUpdateOptions$, computePositionOptions$: computePositionInputOptions$, arrowOptions$: arrowInputOptions$}, patch] = writablesForProps(
defaultConfig,
Expand Down Expand Up @@ -124,8 +126,18 @@ export const createFloatingUI = (propsConfig?: PropsConfig<FloatingUIProps>) =>
middlewareData$,
}),
directives: {
/**
* Directive to be used on the reference element from where the floating element will be positioned
*/
referenceDirective,
/**
* Directive to be used on the floating element
*/
floatingDirective: mergeDirectives(floatingDirective, directiveSubscribe(floatingStyleApplyAction$)),

/**
* Directive to be used on the arrow element, if any
*/
arrowDirective: mergeDirectives(arrowDirective, directiveSubscribe(arrowStyleApplyAction$)),
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
</script>

<Section label="Basic examples" id="default" level={2}>
<span class="ps-3">Start typing with a, b or c</span>
<Sample title="Simple select" sample={defaultSample} height={400} />
</Section>
<Section label="Custom example" id="wikipedia" level={2}>
An example that allows to select pages from the WikipediaService
<span class="ps-3">An example that allows to select pages from the WikipediaService</span>
<Sample title="Wikipedia example" sample={customSample} height={400} />
</Section>
2 changes: 1 addition & 1 deletion e2e/samplesMarkup.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test.describe.parallel(`Samples markup consistency check`, () => {
const routesExtraAction: Record<string, (page: Page) => Promise<void>> = {
'modal/default': openDemoModal,
'modal/stack': openDemoModal,
'select/basic': (page: Page) => typeAndSelect(page, 'a'),
'select/default': (page: Page) => typeAndSelect(page, 'a'),
};

for (const route of allRoutes) {
Expand Down
55 changes: 48 additions & 7 deletions e2e/samplesMarkup.e2e-spec.ts-snapshots/select-default.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@
<div
class="mb-3"
>
<label
class="form-label"
>
"Start typing with a, b or c"
</label>
<div
class="au-select border border-1 d-block dropdown mb-3 p-1"
>
<div
aria-expanded="false"
aria-expanded="true"
aria-haspopup="listbox"
class="align-items-center d-flex flex-wrap"
role="combobox"
>
<div
class="au-select-badge badge me-1 text-bg-light"
>
"apple"
</div>
<input
aria-autocomplete="list"
aria-label="Select"
Expand All @@ -33,9 +33,50 @@
autocorrect="off"
class="au-select-input border-0 flex-grow-1"
type="text"
value=""
value="a"
/>
</div>
<ul
class="dropdown-menu show"
data-popper-placement="bottom-start"
style="left: -1px; top: 40px;"
>
<li
class="au-select-item bg-light dropdown-item position-relative selected"
>
"apple"
</li>
<li
class="au-select-item dropdown-item position-relative"
>
"apricot"
</li>
<li
class="au-select-item dropdown-item position-relative"
>
"asparagus"
</li>
<li
class="au-select-item dropdown-item position-relative"
>
"astronaut"
</li>
<li
class="au-select-item dropdown-item position-relative"
>
"athletic"
</li>
<li
class="au-select-item dropdown-item position-relative"
>
"autumn"
</li>
<li
class="au-select-item dropdown-item position-relative"
>
"avocado"
</li>
</ul>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion react/demo/src/app/samples/select/Default.route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const Default = () => {
return (
<div style={{height: '400px'}}>
<div className="mb-3">
<label className="form-label">Start typing with a, b or c</label>
<Select items={items} filterText={filterTextProp} onFilterTextChange={onFilterTextChange} badgeClassName="badge text-bg-light" />
</div>
</div>
Expand Down
Loading

0 comments on commit 555e168

Please sign in to comment.