Skip to content

Commit

Permalink
Merge pull request #380 from universal-ember/document-floating-ui
Browse files Browse the repository at this point in the history
[Breaking] Document and solidify the public API of FloatingUI
  • Loading branch information
NullVoxPopuli authored Oct 15, 2024
2 parents fcdccfb + a7bed14 commit 315d219
Show file tree
Hide file tree
Showing 16 changed files with 514 additions and 153 deletions.
22 changes: 19 additions & 3 deletions docs-app/app/components/nav.gts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ import { getAnchor } from 'should-handle-link';
import type { TOC } from '@ember/component/template-only';
import type { DocsService, Page } from 'kolay';

type CustomPage = Page & {
title?: string;
};

function fixWords(text: string) {
switch (text.toLowerCase()) {
case 'ui':
return 'UI';
case 'iframe':
return 'IFrame';
default:
return text;
}
}

/**
* Converts 1-2-hyphenated-thing
* to
Expand All @@ -22,19 +37,20 @@ const titleize = (str: string) => {
.filter(Boolean)
.filter((text) => !text.match(/^[\d]+$/))
.map((text) => `${text[0]?.toLocaleUpperCase()}${text.slice(1, text.length)}`)
.map((text) => fixWords(text))
.join(' ')
.split('.')[0] || ''
);
};

function nameFor(x: Page) {
// We defined componentName via json file
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ('componentName' in x) {
return `${x.componentName}`;
}

return sentenceCase(x.name);
let page = x as CustomPage;

return page.title ? page.title : sentenceCase(page.name);
}

const asComponent = (str: string) => {
Expand Down
16 changes: 15 additions & 1 deletion docs-app/app/routes/api-docs.gts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { APIDocs as KolayAPIDocs, ComponentSignature as KolayComponentSignature } from 'kolay';
import {
APIDocs as KolayAPIDocs,
ComponentSignature as KolayComponentSignature,
ModifierSignature as KolayModifierSignature,
} from 'kolay';

import type { TOC } from '@ember/component/template-only';

Expand All @@ -21,3 +25,13 @@ export const ComponentSignature: TOC<{
@name={{@name}}
/>
</template>;

export const ModifierSignature: TOC<{
Args: { declaration: string; name: string };
}> = <template>
<KolayModifierSignature
@package="ember-primitives"
@module="declarations/{{@declaration}}"
@name={{@name}}
/>
</template>;
4 changes: 3 additions & 1 deletion docs-app/app/routes/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Callout } from 'docs-app/components/callout';
import { getHighlighterCore } from 'shiki/core';
import getWasm from 'shiki/wasm';

import { APIDocs, ComponentSignature } from './api-docs';
import { APIDocs, ComponentSignature, ModifierSignature } from './api-docs';

import type { SetupService } from 'ember-primitives';
import type { DocsService } from 'kolay';
Expand Down Expand Up @@ -45,6 +45,7 @@ export default class Application extends Route {
Callout,
APIDocs,
ComponentSignature,
ModifierSignature,
},
resolve: {
// ember-primitives
Expand All @@ -65,6 +66,7 @@ export default class Application extends Route {
// utility
'lorem-ipsum': import('lorem-ipsum'),
'form-data-utils': import('form-data-utils'),
kolay: import('kolay'),
},
rehypePlugins: [
[
Expand Down
166 changes: 166 additions & 0 deletions docs-app/public/docs/5-floaty-bits/floating-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Floating UI

The `FloatingUI` component (and modifier) provides a wrapper for using [Floating UI](https://floating-ui.com/), for associating a floating element to an anchor element (such as for menus, popovers, etc.

<Callout>

The usage of a 3rd-party library will be removed when [CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) lands and is widely supported (This component and modifier will still exist for the purpose of wiring up the ids between anchor and target).

</Callout>

Several of Floating UI's functions and [middleware](https://floating-ui.com/docs/middleware) are used to create an experience out of the box that is useful and expected.
See Floating UI's [documentation](https://floating-ui.com/docs/getting-started) for more information on any of the following included functionality.

## Setup

```bash
pnpm add ember-primitives
```


## `{{anchorTo}}`

The main modifier for creating floating UIs with any elements.

Requires you to maintain a unique ID for every invocation.


<div class="featured-demo">

```gjs live preview no-shadow
import { anchorTo } from 'ember-primitives/floating-ui';
<template>
<button id="reference" popovertarget="floating">Click the reference element</button>
<menu popover id="floating" {{anchorTo "#reference"}}>Here is <br> floating element</menu>
<style>
menu#floating {
width: max-content;
position: absolute;
top: 0;
left: 0;
background: #222;
color: white;
font-weight: bold;
padding: 2rem;
border-radius: 4px;
font-size: 90%;
filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.4));
z-index: 10;
}
button#reference {
padding: 0.5rem;
border: 1px solid;
display: inline-block;
background: white;
color: black;
border-radius: 0.25rem;
&:hover {
background: #ddd;
}
}
</style>
</template>
```

</div>

Note that in this demo thare are _two_ sets of ids. One pair for the floating behavior, and another pair for the [popover](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover) wiring. The component below handles the floating id, but to avoid needing to maintain _unique_ pairs of ids for each floating-ui you may be interested in the [Popover](/5-floaty-bits/popover.md) component (which also includes arrow support).

### API Reference for `{{anchorTo}}`

```gjs live no-shadow
import { ModifierSignature } from 'kolay';
<template>
<ModifierSignature
@package="ember-primitives"
@module="declarations/floating-ui/modifier"
@name="Signature"
/>
</template>
```


## `<FloatingUI>`

This component takes the above modifier and abstracts away the need to manage the `id`-relationship between reference and floating elements -- since every ID on the page needs to be unique, it is useful to have this automatically managed for you.

This component has no DOM of its own, but provides two modifiers to attach to both reference and floating elements.

<div class="featured-demo">

```gjs live preview no-shadow
import { FloatingUI } from 'ember-primitives/floating-ui';
<template>
<FloatingUI as |reference floating|>
<button {{reference}} popovertarget="floating2">Click the reference element</button>
<menu {{floating}} popover id="floating2">Here is <br> floating element</menu>
</FloatingUI>
<style>
menu#floating2 {
width: max-content;
position: absolute;
top: 0;
left: 0;
background: #222;
color: white;
font-weight: bold;
padding: 2rem;
border-radius: 4px;
font-size: 90%;
filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.4));
z-index: 10;
}
button[popovertarget="floating2"] {
padding: 0.5rem;
border: 1px solid;
display: inline-block;
background: white;
color: black;
border-radius: 0.25rem;
&:hover {
background: #ddd;
}
}
</style>
</template>
```

</div>

Note that this demo has to main a unique id/target for the popover behavior. If you'd like to not have to manage ids at all, you may be interested in the [Popover](/5-floaty-bits/popover.md) component (which also includes arrow support).

### API Reference for `<FloatingUI>`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';
<template>
<ComponentSignature
@package="ember-primitives"
@module="declarations/floating-ui/component"
@name="Signature" />
</template>
```

## Comparison to similar projects

Similar projects include:

* [ember-popperjs](https://github.com/NullVoxPopuli/ember-popperjs)
* [ember-popper-modifier](https://github.com/adopted-ember-addons/ember-popper-modifier)

The above projects both use [Popper](https://popper.js.org/). In contrast, Ember Velcro uses Floating UI. Floating UI is the successor to Popper - see their [migration guide](https://floating-ui.com/docs/migration) for a complete comparison.

There is also:

* [ember-velcro](https://github.com/CrowdStrike/ember-velcro)

which this project is a fork up, and ditches the velcro (hook / loop) verbiage and fixes bugs and improves ergonomics.

16 changes: 5 additions & 11 deletions docs-app/public/docs/5-floaty-bits/popover.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
# Popover

Popovers are built with [ember-velcro][gh-e-velcro], which is an ergonomic wrapper around [Floating UI][docs-floating], the successor to older (and more clunky) [Popper.JS][docs-popper].
Popovers are built with [Floating UI][docs-floating-ui], a set of utilities for making floating elements relate to each other with minimal configuration.


<!--
The goal of a popover is to provide additional behavioral functionality to make interacting with floaty bits easier:
- focus trapping (TODO)
- focus returning (TODO)
-->

The `<Popover>` component uses portals in a way that totally solves layering issues. No more worrying about tooltips on varying layers of your UI sometimes appearing behind other floaty bits. See the `<Portal>` and `<PortalTargets>` pages for more information.

One thing to note is that the position of the popover can _escape_ the boundary of a [ShadowDom][docs-shadow-dom] -- all demos on this docs site for `ember-primitives` use a `ShadowDom` to allow for isolated CSS usage within the demos.

[gh-e-velcro]: https://github.com/CrowdStrike/ember-velcro
[docs-floating-ui]: /5-floaty-bits/floating-ui.md
[docs-floating]: https://floating-ui.com/
[docs-popper]: https://popper.js.org/
[docs-shadow-dom]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM
Expand All @@ -32,7 +26,7 @@ import { loremIpsum } from 'lorem-ipsum';
{{loremIpsum (hash count=1 units="paragraphs")}}
<Popover @placement="top" @offsetOptions={{8}} as |p|>
<div class="hook" {{p.hook}}>
<div class="hook" {{p.reference}}>
the hook / anchor of the popover.
<br> it sticks the boundary of this element.
</div>
Expand Down Expand Up @@ -110,7 +104,7 @@ const settings = cell(true);
<span>My App</span>
<Popover @offsetOptions={{8}} as |p|>
<button class="hook" {{p.hook}} {{on 'click' settings.toggle}}>
<button class="hook" {{p.reference}} {{on 'click' settings.toggle}}>
Settings
</button>
{{#if settings.current}}
Expand All @@ -124,7 +118,7 @@ const settings = cell(true);
things<br>
<Popover @placement="left" @offsetOptions={{16}} as |pp|>
<button {{pp.hook}}>view profile</button>
<button {{pp.reference}}>view profile</button>
<pp.Content class="floatybit">
View or edit your profile settings
Expand Down
3 changes: 3 additions & 0 deletions docs-app/public/docs/6-utils/data-from-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Data from Form Events 📦"
}
3 changes: 3 additions & 0 deletions docs-app/public/docs/6-utils/should-handle-link.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Should Handle Link 📦"
}
12 changes: 6 additions & 6 deletions ember-primitives/src/components/menu.gts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface Signature {
arrow: PopoverBlockParams['arrow'];
trigger: WithBoundArgs<
typeof trigger,
'triggerElement' | 'contentId' | 'isOpen' | 'setHook'
'triggerElement' | 'contentId' | 'isOpen' | 'setReference'
>;
Trigger: WithBoundArgs<typeof Trigger, 'triggerModifier'>;
Content: WithBoundArgs<
Expand Down Expand Up @@ -187,7 +187,7 @@ interface PrivateTriggerModifierSignature {
triggerElement: Cell<HTMLElement>;
isOpen: Cell<boolean>;
contentId: string;
setHook: PopoverBlockParams['setHook'];
setReference: PopoverBlockParams['setReference'];
};
};
}
Expand All @@ -197,7 +197,7 @@ export interface TriggerModifierSignature {
}

const trigger = eModifier<PrivateTriggerModifierSignature>(
(element, _: [], { triggerElement, isOpen, contentId, setHook }) => {
(element, _: [], { triggerElement, isOpen, contentId, setReference }) => {
element.setAttribute('aria-haspopup', 'menu');

if (isOpen.current) {
Expand All @@ -215,7 +215,7 @@ const trigger = eModifier<PrivateTriggerModifierSignature>(
element.addEventListener('click', onTriggerClick);

triggerElement.current = element;
setHook(element);
setReference(element);

return () => {
element.removeEventListener('click', onTriggerClick);
Expand All @@ -228,7 +228,7 @@ interface PrivateTriggerSignature {
Args: {
triggerModifier: WithBoundArgs<
typeof trigger,
'triggerElement' | 'contentId' | 'isOpen' | 'setHook'
'triggerElement' | 'contentId' | 'isOpen' | 'setReference'
>;
};
Blocks: { default: [] };
Expand Down Expand Up @@ -270,7 +270,7 @@ export class Menu extends Component<Signature> {
triggerElement=triggerEl
isOpen=isOpen
contentId=this.contentId
setHook=p.setHook
setReference=p.setReference
)
as |triggerModifier|
}}
Expand Down
Loading

0 comments on commit 315d219

Please sign in to comment.