Ember Floating UI


For components like Tooltips, we leverage Floating UI's positioning utilities, which are framework-agnostic. The hover, focus etc interactions Floating UI provides are, unfortunately, written specifically for React. To use Floating UI in Ember, Pluma ships with Ember versions of Floating UI's React hooks.

Configuration

To use Pluma's Ember Floating UI, first we need to import useFloatingUi and initialize it like this:

import { useFloatingUi } from '@customerio/pluma-components/ember/floating-ui';

// ...
export default class MyComponent extends Component {
	floatingUi = useFloatingUi(this, () => ({
		//... options here
	}));
}

this needs to be provided so that the plugin can run cleanup when the parent component gets destroyed.

The second argument is a function that returns a configuration object, which accepts almost the same options as the useFloating React hook, for example:

floatingUi = useFloatingUi(this, () => ({
	open: this.isOpen,
	onOpenChange: this.handleOpenChange,
	placement: this.placement,
	strategy: this.strategy,
	middleware: [offset(10), flip({}), shift()],
	whileElementsMounted: autoUpdate,
	plugins: [ ],
}));

The second argument is a function, instead of a plain object, to enable the Floating UI class to recompute when tracked properties (like this.isOpen) change. If a plain object was used, the class would initialize with the current values of the properties, and would never recompute/update.

Plugins

One option that differs is plugins, which accepts interaction plugins that resemble the useHover etc hooks.

Currently, the following plugins exist:

import {
	Hover,
	Focus,
	Role,
	Dismiss
} from '@customerio/pluma-components/ember/floating-ui';

Each plugin accepts options, which can be provided by calling the static .with method on the import:

floatingUi = useFloatingUi(this, () => ({
	plugins: [
		Hover.with(() => ({ move: false })),
		Dismiss.with(() => ({ enabled: this.dismissEnabled }))
	],
}));

Transition Status

There is one additional plugin, TransitionStatus. The other plugins are all interactions, which Floating UI uses to attach event listeners to the reference and floating elements. TransitionStatus doesn't attach any listeners, and instead returns state that can be used for animating floating elements. For example:

import Component from '@glimmer/component';
import { useTransitionStatus } from '@customerio/pluma-components/ember/floating-ui';

export default class Floating extends Component {
	transitionStatus = useTransitionStatus(this.args.floatingUi);

	get status() {
		return this.transitionStatus.status;
	}

	get isMounted() {
		return this.transitionStatus.isMounted;
	}

	<template>
		{{#if this.isMounted}}
			<div
				...attributes
				{{@floatingUi.setFloating}}
			>
				<div id="#floating" data-status={{this.status}} data-placement={{this.placement}}>
					Floating content
				</idv>
			</div>
		{{/if}}
	</template>
}
#floating {
	transition-property: opacity, transform;
}
#floating[data-status='open'],
#floating[data-status='close'] {
	transition-duration: 250ms;
}
#floating[data-status='initial'],
#floating[data-status='close'] {
	opacity: 0;
}
#floating[data-status='initial'][data-placement^='top'],
#floating[data-status='close'][data-placement^='top'] {
	transform: translateY(5px);
}
#floating[data-status='initial'][data-placement^='bottom'],
#floating[data-status='close'][data-placement^='bottom'] {
	transform: translateY(-5px);
}
#floating[data-status='initial'][data-placement^='left'],
#floating[data-status='close'][data-placement^='left'] {
	transform: translateX(5px);
}
#floating[data-status='initial'][data-placement^='right'],
#floating[data-status='close'][data-placement^='right'] {
	transform: translateX(-5px);
}

On this page