Skip to content

Commit

Permalink
Adds matchAnchorWidth to FloatingUI components (#317)
Browse files Browse the repository at this point in the history
* Add `matchAnchorWidth` property to FloatingUI

* Add `additionalWidth`; write tests

* Improve type
  • Loading branch information
jeffdaley authored Sep 1, 2023
1 parent 3338f3f commit f9c16f5
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 19 deletions.
51 changes: 42 additions & 9 deletions web/app/components/floating-u-i/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import {
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";

export type MatchAnchorWidthOptions =
| boolean
| {
enabled: boolean;
additionalWidth: number;
};

interface FloatingUIContentSignature {
Element: HTMLDivElement;
Args: {
Expand All @@ -22,6 +29,7 @@ interface FloatingUIContentSignature {
placement?: Placement | null;
renderOut?: boolean;
offset?: OffsetOptions;
matchAnchorWidth?: MatchAnchorWidthOptions;
};
Blocks: {
default: [];
Expand All @@ -42,31 +50,56 @@ export default class FloatingUIContent extends Component<FloatingUIContentSignat
@action didInsert(e: HTMLElement) {
this._content = e;

if (this.args.placement === null) {
this.content.removeAttribute("data-floating-ui-placement");
this.content.classList.add("non-floating-content");
const { matchAnchorWidth, anchor, placement } = this.args;
const { content } = this;

this.maybeMatchAnchorWidth();

if (placement === null) {
content.removeAttribute("data-floating-ui-placement");
content.classList.add("non-floating-content");
this.cleanup = () => {};
return;
}

let updatePosition = async () => {
let placement = this.args.placement || "bottom-start";
let _placement = placement || "bottom-start";

computePosition(this.args.anchor, this.content, {
computePosition(anchor, content, {
platform,
placement: placement as Placement,
placement: _placement as Placement,
middleware: [offset(this.offset), flip(), shift()],
}).then(({ x, y, placement }) => {
this.content.setAttribute("data-floating-ui-placement", placement);
this.maybeMatchAnchorWidth();
content.setAttribute("data-floating-ui-placement", placement);

Object.assign(this.content.style, {
Object.assign(content.style, {
left: `${x}px`,
top: `${y}px`,
});
});
};

this.cleanup = autoUpdate(this.args.anchor, this.content, updatePosition);
this.cleanup = autoUpdate(anchor, content, updatePosition);
}

private maybeMatchAnchorWidth() {
const { matchAnchorWidth, anchor } = this.args;
const { content } = this;

if (!matchAnchorWidth) {
return;
}

if (typeof matchAnchorWidth === "boolean") {
content.style.width = `${anchor.offsetWidth}px`;
} else {
content.style.width = `${
anchor.offsetWidth + matchAnchorWidth.additionalWidth
}px`;
}

content.style.maxWidth = "none";
}
}

Expand Down
1 change: 1 addition & 0 deletions web/app/components/floating-u-i/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
@renderOut={{@renderOut}}
@anchor={{this.anchor}}
@id={{this.contentID}}
@matchAnchorWidth={{@matchAnchorWidth}}
...attributes
>
{{yield
Expand Down
2 changes: 2 additions & 0 deletions web/app/components/floating-u-i/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { guidFor } from "@ember/object/internals";
import { OffsetOptions, Placement } from "@floating-ui/dom";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { MatchAnchorWidthOptions } from "./content";

interface FloatingUIAnchorAPI {
contentIsShown: boolean;
Expand All @@ -28,6 +29,7 @@ interface FloatingUIComponentSignature {
placement?: Placement | null;
disableClose?: boolean;
offset?: OffsetOptions;
matchAnchorWidth?: MatchAnchorWidthOptions;
};
Blocks: {
anchor: [dd: FloatingUIAnchorAPI];
Expand Down
5 changes: 3 additions & 2 deletions web/app/components/header/search.hbs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{{on-document "keydown" this.maybeFocusInput}}

<div ...attributes>
<form class="w-full relative" {{on "submit" this.viewAllResults}}>
<form class="relative w-full" {{on "submit" this.viewAllResults}}>
<X::DropdownList
@items={{this.itemsToShow}}
@offset={{hash mainAxis=0 crossAxis=3}}
@offset={{hash mainAxis=0 crossAxis=2}}
@placement="bottom-end"
@matchAnchorWidth={{hash enabled=true additionalWidth=4}}
class="search-popover
{{unless this.bestMatchesHeaderIsShown 'no-best-matches'}}"
>
Expand Down
3 changes: 2 additions & 1 deletion web/app/components/x/dropdown-list/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@placement={{@placement}}
@offset={{@offset}}
@disableClose={{@disableClose}}
@matchAnchorWidth={{@matchAnchorWidth}}
class="{{unless (eq @placement null) 'hermes-popover'}} x-dropdown-list"
data-test-x-dropdown-list-content
{{will-destroy this.onDestroy}}
Expand Down Expand Up @@ -95,7 +96,7 @@
{{else}}
<div
data-test-x-dropdown-list-loaded-content
class="overflow-hidden flex flex-col"
class="flex flex-col overflow-hidden"
{{did-insert this.didInsertContent}}
>
{{#if this.inputIsShown}}
Expand Down
2 changes: 2 additions & 0 deletions web/app/components/x/dropdown-list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import XDropdownListToggleButtonComponent from "./toggle-button";
import { XDropdownListItemAPI } from "./item";
import { restartableTask, timeout } from "ember-concurrency";
import maybeScrollIntoView from "hermes/utils/maybe-scroll-into-view";
import { MatchAnchorWidthOptions } from "hermes/components/floating-u-i/content";

export type XDropdownListToggleComponentBoundArgs =
| "contentIsShown"
Expand Down Expand Up @@ -53,6 +54,7 @@ interface XDropdownListComponentSignature {
disabled?: boolean;
offset?: OffsetOptions;
label?: string;
matchAnchorWidth?: MatchAnchorWidthOptions;

/**
* Whether an asynchronous list is loading.
Expand Down
12 changes: 6 additions & 6 deletions web/app/styles/components/header/search.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.x-dropdown-list.search-popover {
@apply w-[calc(100%+4px)] max-h-[none] min-w-[420px] max-w-[none];
@apply max-h-[none] min-w-[420px];

&.no-best-matches {
.x-dropdown-list-items {
Expand All @@ -10,7 +10,7 @@
@apply items-start;

&.global-search-popover-header-link {
@apply pt-2.5 pb-[10px] items-center;
@apply items-center pt-2.5 pb-[10px];
}

&.is-aria-selected {
Expand All @@ -28,21 +28,21 @@
}

.global-search-best-matches-header {
@apply border-t border-t-color-border-primary pt-2 px-3;
@apply border-t border-t-color-border-primary px-3 pt-2;

h5 {
@apply text-display-100 text-color-foreground-faint font-medium;
@apply text-display-100 font-medium text-color-foreground-faint;
}
}

.global-search-result {
@apply flex items-center space-x-3 py-2 px-3 h-20;
@apply flex h-20 items-center space-x-3 py-2 px-3;
}

.global-search-result-text-content {
@apply flex flex-col space-y-1 overflow-hidden;
}

.global-search-result-title {
@apply text-body-200 font-semibold text-color-foreground-strong truncate;
@apply truncate text-body-200 font-semibold text-color-foreground-strong;
}
90 changes: 89 additions & 1 deletion web/tests/integration/components/floating-u-i/index-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { module, test, todo } from "qunit";
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { click, render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
Expand Down Expand Up @@ -90,4 +90,92 @@ module("Integration | Component | floating-u-i/index", function (hooks) {

assert.dom(".close-button").exists('the "close" action was disabled');
});

test("the popover can match the anchor width", async function (assert) {
await render(hbs`
<FloatingUI @matchAnchorWidth={{true}}>
<:anchor as |f|>
<Action
id="open-button-1"
style="width:500px;"
{{on "click" f.showContent}}
{{did-insert f.registerAnchor}}
>
Open
</Action>
</:anchor>
<:content>
<div id="content-1">
Content
</div>
</:content>
</FloatingUI>
<FloatingUI @matchAnchorWidth={{hash enabled=true additionalWidth=100}}>
<:anchor as |f|>
<Action
id="open-button-2"
style="width:500px;"
{{on "click" f.showContent}}
{{did-insert f.registerAnchor}}
>
Open
</Action>
</:anchor>
<:content>
<div id="content-2">
Content
</div>
</:content>
</FloatingUI>
<FloatingUI @matchAnchorWidth={{hash enabled=true additionalWidth=-100}}>
<:anchor as |f|>
<Action
id="open-button-3"
style="width:500px;"
{{on "click" f.showContent}}
{{did-insert f.registerAnchor}}
>
Open
</Action>
</:anchor>
<:content>
<div id="content-3">
Content
</div>
</:content>
</FloatingUI>
`);

await click("#open-button-1");

let contentWidth = htmlElement("#content-1").offsetWidth;

assert.equal(
contentWidth,
500,
"the content width matches the anchor width"
);

await click("#open-button-2");

contentWidth = htmlElement("#content-2").offsetWidth;

assert.equal(
contentWidth,
600,
"the content width matches the anchor width plus the additional width"
);

await click("#open-button-3");

contentWidth = htmlElement("#content-3").offsetWidth;

assert.equal(
contentWidth,
400,
"the content width matches the anchor width minus the additional width"
);
});
});

0 comments on commit f9c16f5

Please sign in to comment.