Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds matchAnchorWidth to FloatingUI components #317

Merged
merged 3 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
);
});
});