Skip to content

Commit

Permalink
feat: floating UI
Browse files Browse the repository at this point in the history
  • Loading branch information
divdavem committed Nov 24, 2023
1 parent a081136 commit 259d924
Show file tree
Hide file tree
Showing 14 changed files with 718 additions and 3 deletions.
54 changes: 54 additions & 0 deletions angular/demo/src/app/samples/floatingUI/floatingUI.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {AgnosUIAngularModule, createFloatingUI, floatingUI, toAngularSignal} from '@agnos-ui/angular';
import {ChangeDetectionStrategy, Component} from '@angular/core';

const scrollToMiddle = (element: HTMLElement) => {
element.scrollTo({left: 326, top: 420});
};

@Component({
standalone: true,
imports: [AgnosUIAngularModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div class="position-relative overflow-auto border border-primary-subtle demo-floatingui" [auUse]="scrollToMiddle">
<button [auUse]="floatingUI.directives.referenceDirective" type="button" class="btn btn-primary" (click)="displayPopover = !displayPopover">
Toggle popover
</button>
@if (displayPopover) {
<div
[auUse]="floatingUI.directives.floatingDirective"
[attr.data-popper-placement]="floatingUIState().placement"
class="popover bs-popover-auto position-absolute"
[class.invisible]="floatingUIState().middlewareData?.hide?.referenceHidden"
role="tooltip"
>
<div class="popover-arrow position-absolute" [auUse]="floatingUI.directives.arrowDirective"></div>
<div class="popover-body text-center">This is a sample popover</div>
</div>
}
</div>`,

styles: "@import '@agnos-ui/common/samples/floatingui/floatingui.scss';",
})
export default class FloatingUIComponent {
displayPopover = true;

floatingUI = createFloatingUI({
props: {
arrowOptions: {
padding: 6,
},
computePositionOptions: {
middleware: [
floatingUI.offset(10),
floatingUI.autoPlacement(),
floatingUI.shift({
padding: 5,
}),
floatingUI.hide(),
],
},
},
});
floatingUIState = toAngularSignal(this.floatingUI.state$);
scrollToMiddle = scrollToMiddle;
}
12 changes: 12 additions & 0 deletions common/samples/floatingui/floatingui.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
div.demo-floatingui {
width: 500px;
height: 200px;

button {
margin: 500px;
width: 150px;
}
.popover {
width: 250px;
}
}
3 changes: 2 additions & 1 deletion core/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
}
},
"dependencies": {
"@amadeus-it-group/tansu": "0.0.23"
"@amadeus-it-group/tansu": "0.0.23",
"@floating-ui/dom": "^1.5.3"
},
"sideEffects": false
}
131 changes: 131 additions & 0 deletions core/lib/services/floatingUI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {computed, derived} from '@amadeus-it-group/tansu';
import type {ArrowOptions, AutoUpdateOptions, ComputePositionConfig, ComputePositionReturn, Derivable} from '@floating-ui/dom';
import {arrow, autoUpdate, computePosition} from '@floating-ui/dom';
import {createStoreDirective, directiveSubscribe, mergeDirectives} from './directiveUtils';
import {stateStores, writablesForProps, type PropsConfig} from './stores';

import * as floatingUI from '@floating-ui/dom';
import {promiseStoreToValueStore} from './promiseStoreUtils';
export {floatingUI};

export interface FloatingUIProps {
/**
* Options to use when calling computePosition from Floating UI
*/
computePositionOptions: ComputePositionConfig;

/**
* Options to use when calling autoUpdate from Floating UI
*/
autoUpdateOptions: AutoUpdateOptions;

/**
* Options to use when calling the arrow middleware from Floating UI
*/
arrowOptions: Omit<ArrowOptions, 'element'> | Derivable<Omit<ArrowOptions, 'element'>>;
}

const defaultConfig: FloatingUIProps = {
computePositionOptions: {},
autoUpdateOptions: {},
arrowOptions: {},
};

export const createFloatingUI = (propsConfig?: PropsConfig<FloatingUIProps>) => {
const [{autoUpdateOptions$, computePositionOptions$: computePositionInputOptions$, arrowOptions$: arrowInputOptions$}, patch] = writablesForProps(
defaultConfig,
propsConfig,
);

const {directive: floatingDirective, element$: floatingElement$} = createStoreDirective();
const {directive: referenceDirective, element$: referenceElement$} = createStoreDirective();
const {directive: arrowDirective, element$: arrowElement$} = createStoreDirective();

const arrowOptions$ = computed((): null | ArrowOptions | Derivable<ArrowOptions> => {
const arrowElement = arrowElement$();
if (!arrowElement) {
return null;
}
const arrowInputOptions = arrowInputOptions$();
return typeof arrowInputOptions === 'function'
? (state) => ({...arrowInputOptions(state), element: arrowElement})

Check warning on line 51 in core/lib/services/floatingUI.ts

View check run for this annotation

Codecov / codecov/patch

core/lib/services/floatingUI.ts#L51

Added line #L51 was not covered by tests
: {...arrowInputOptions, element: arrowElement};
});

const computePositionOptions$ = computed(() => {
let options = computePositionInputOptions$();
const arrowOptions = arrowOptions$();
if (arrowOptions) {
options = {
...options,
middleware: [...(options.middleware ?? []), arrow(arrowOptions)],
};
}
return options;
});

const promisePosition$ = derived(
[floatingElement$, referenceElement$, computePositionOptions$, autoUpdateOptions$],
([floatingElement, referenceElement, computePositionOptions, autoUpdateOptions], set) => {
if (floatingElement && referenceElement) {
const clean = autoUpdate(
referenceElement,
floatingElement,
() => {
set(computePosition(referenceElement, floatingElement, computePositionOptions));
},
autoUpdateOptions,
);
return () => {
set(null);
clean();
};
}
return undefined;
},
null as null | Promise<ComputePositionReturn>,
);
const position$ = promiseStoreToValueStore(promisePosition$, null);

const placement$ = computed(() => position$()?.placement);
const middlewareData$ = computed(() => position$()?.middlewareData);
const x$ = computed(() => position$()?.x);
const y$ = computed(() => position$()?.y);
const strategy$ = computed(() => position$()?.strategy);
const arrowX$ = computed(() => middlewareData$()?.arrow?.x);
const arrowY$ = computed(() => middlewareData$()?.arrow?.y);

const floatingStyleApplyAction$ = computed(() => {
const floatingElement = floatingElement$();
if (floatingElement) {
floatingElement.style.left = `${x$() ?? 0}px`;
floatingElement.style.top = `${y$() ?? 0}px`;
}
});

const arrowStyleApplyAction$ = computed(() => {
const arrowElement = arrowElement$();
if (arrowElement) {
const arrowX = arrowX$();
const arrowY = arrowY$();
arrowElement.style.left = arrowX != null ? `${arrowX}px` : '';
arrowElement.style.top = arrowY != null ? `${arrowY}px` : '';
}
});

return {
patch,
...stateStores({
x$,
y$,
strategy$,
placement$,
middlewareData$,
}),
directives: {
referenceDirective,
floatingDirective: mergeDirectives(floatingDirective, directiveSubscribe(floatingStyleApplyAction$)),
arrowDirective: mergeDirectives(arrowDirective, directiveSubscribe(arrowStyleApplyAction$)),
},
};
};
1 change: 1 addition & 0 deletions core/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './writables';
export * from './navManager';
export * from './isFocusable';
export * from './domUtils';
export * from './floatingUI';
Loading

0 comments on commit 259d924

Please sign in to comment.