Skip to content
Draft
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
13 changes: 12 additions & 1 deletion packages/host/app/components/search/hydratable-card.gts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ interface Signature {
// and no overlay (chip / options menu / selection toggle) ever anchors to
// it — for a consumer that lays results out in its own UI.
overlays?: boolean;
// Whether the row renders its card container chrome — the boundary ring
// (defaults to `true`). `false` hides it on both the inert HTML (via the
// container's `hide-boundaries` class) and the hydrated live card (via
// `@displayContainer={{false}}`), matching the `@fields` switch, so a row
// looks the same before and after hydration either way.
displayContainer?: boolean;
// The format the live/hydrated card renders as, so it matches the
// prerendered HTML the query selected (defaults to `fitted`).
format?: Format;
Expand Down Expand Up @@ -266,6 +272,10 @@ export default class HydratableCard extends Component<Signature> {
return cardContext;
}

private get displayContainer(): boolean {
return this.args.displayContainer ?? true;
}

@action private hydrate() {
if (this.args.isError) {
return;
Expand All @@ -284,13 +294,14 @@ export default class HydratableCard extends Component<Signature> {
@card={{this.liveCard}}
@format={{this.format}}
@codeRef={{@renderType}}
@displayContainer={{false}}
@displayContainer={{this.displayContainer}}
data-hydration={{this.hydrationState}}
data-test-hydratable-card={{@cardId}}
...attributes
/>
{{else if @component}}
<@component
class={{unless this.displayContainer 'hide-boundaries'}}
{{hydrationTrigger this.mode this.hydrate}}
{{this.trackElement
cardId=@cardId
Expand Down
6 changes: 6 additions & 0 deletions packages/host/app/components/search/search-results.gts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export default class SearchResults extends Component<HostSearchResultsSignature>
return this.args.overlays ?? true;
}

private get displayContainer(): boolean {
return this.args.displayContainer ?? true;
}

// Created once per component: the view-model layer memoizes render-stable
// entries on top of a search resource. With `@resource` it wraps the
// caller-owned resource (whose subscriptions and re-runs outlive this
Expand All @@ -77,12 +81,14 @@ export default class SearchResults extends Component<HostSearchResultsSignature>
this.args.resource,
() => this.mode,
() => this.overlays,
() => this.displayContainer,
)
: getRenderableSearchEntries(
this,
() => this.args.query,
() => this.mode,
() => this.overlays,
() => this.displayContainer,
{
cardInitiated: this.args.cardInitiated,
getDefaultRealm: this.args.getDefaultRealm,
Expand Down
6 changes: 6 additions & 0 deletions packages/host/app/lib/hydratable-entry-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export interface HydratableEntryArgs {
// Whether the row registers with the operator-mode overlay; `false` renders
// it plainly (no chip / options menu / selection toggle).
overlays: boolean;
// Whether the row renders its card container chrome (the boundary ring);
// `false` hides it, as `@displayContainer={{false}}` does on `@fields`.
displayContainer: boolean;
}

class _HydratableEntryComponent {
Expand All @@ -71,6 +74,7 @@ class _HydratableEntryComponent {
readonly errorDoc: ErrorEntry | undefined,
readonly mode: HydrationMode,
readonly overlays: boolean,
readonly displayContainer: boolean,
) {}
}

Expand All @@ -87,6 +91,7 @@ setComponentTemplate(
@errorDoc={{this.errorDoc}}
@mode={{this.mode}}
@overlays={{this.overlays}}
@displayContainer={{this.displayContainer}}
...attributes
/>`,
{
Expand Down Expand Up @@ -133,5 +138,6 @@ export function hydratableEntryComponent(
args.errorDoc,
args.mode,
args.overlays,
args.displayContainer,
) as unknown as EntryComponent;
}
8 changes: 8 additions & 0 deletions packages/host/app/resources/renderable-search-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export class RenderableSearchEntry {
private fallbackFormat: PrerenderedHtmlFormat,
private mode: HydrationMode,
private overlays: boolean,
private displayContainer: boolean,
) {}

get id(): string {
Expand Down Expand Up @@ -214,6 +215,7 @@ export class RenderableSearchEntry {
errorDoc: this.errorDoc,
mode: this.mode,
overlays: this.overlays,
displayContainer: this.displayContainer,
});
}
return this.#component;
Expand Down Expand Up @@ -242,6 +244,7 @@ export class RenderableSearchEntries {
private resource: ReturnType<typeof getSearchEntriesResource>,
private getMode: () => HydrationMode,
private getOverlays: () => boolean,
private getDisplayContainer: () => boolean,
) {}

private get fallbackRenderType(): ResolvedCodeRef | undefined {
Expand All @@ -257,9 +260,11 @@ export class RenderableSearchEntries {
let fallbackFormat = this.fallbackFormat;
let mode = this.getMode();
let overlays = this.getOverlays();
let displayContainer = this.getDisplayContainer();
let inputsKey = JSON.stringify([
mode,
overlays,
displayContainer,
fallbackRenderType,
fallbackFormat,
]);
Expand All @@ -282,6 +287,7 @@ export class RenderableSearchEntries {
fallbackFormat,
mode,
overlays,
displayContainer,
);
// Pure memoization keyed on the resource's stable entry identity — it
// dirties no tracked state, and keeping unchanged rows' view-models (and
Expand Down Expand Up @@ -317,6 +323,7 @@ export function getRenderableSearchEntries(
getQuery: () => SearchEntryWireQuery | undefined,
getMode: () => HydrationMode,
getOverlays: () => boolean = () => true,
getDisplayContainer: () => boolean = () => true,
opts?: {
// Forwarded to the underlying resource: scope a no-realm card search to the
// current realm instead of fanning out. Set only by the card-facing
Expand All @@ -329,5 +336,6 @@ export function getRenderableSearchEntries(
getSearchEntriesResource(owner, getQuery, opts),
getMode,
getOverlays,
getDisplayContainer,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ class TestContext extends GlimmerComponent<TestContextSignature> {

const HASSAN = `${testRealmURL}Person/hassan`;
const INERT_HTML = `<div class='inert' data-test-inert-card>Inert</div>`;
// Inert HTML as the prerender captures it: the card-api container with its
// boundary class already on it.
const BOUNDED_INERT_HTML = `<div class='boxel-card-container boxel-card-container--boundaries inert' data-test-inert-card>Inert</div>`;

// Drives a mount/unmount toggle so a teardown test can destroy the rendered
// HydratableCard and assert it releases its Store reference.
Expand Down Expand Up @@ -294,6 +297,72 @@ module('Integration | Component | hydratable-card', function (hooks) {
);
});

// The boundary ring is on by default and survives hydration: the inert HTML
// keeps whatever boundary class it was prerendered with, and the live card
// renders inside its own bounded container.
test('displayContainer defaults to true — the hydrated card keeps its container boundaries', async function (assert) {
let inert = htmlComponent(BOUNDED_INERT_HTML);
await render(
<template>
<TestContext>
<HydratableCard
@cardId={{HASSAN}}
@component={{inert}}
@mode='hover'
/>
</TestContext>
</template>,
);

assert
.dom('[data-test-inert-card]')
.hasClass('boxel-card-container--boundaries', 'inert ring is kept')
.doesNotHaveClass('hide-boundaries', 'nothing hides the inert ring');

await triggerEvent('[data-test-hydratable-card]', 'mouseenter');

assert
.dom('[data-test-hydratable-card]')
.hasClass(
'boxel-card-container--boundaries',
'the live container draws its boundaries too',
);
});

// `@displayContainer={{false}}` hides the ring on both sides of the swap, the
// way `<@fields.x @displayContainer={{false}} />` does for a field render.
test('displayContainer=false — hides the boundaries on the inert HTML and the live card alike', async function (assert) {
let inert = htmlComponent(BOUNDED_INERT_HTML);
await render(
<template>
<TestContext>
<HydratableCard
@cardId={{HASSAN}}
@component={{inert}}
@mode='hover'
@displayContainer={{false}}
/>
</TestContext>
</template>,
);

assert
.dom('[data-test-inert-card]')
.hasClass('hide-boundaries', 'the inert ring is suppressed');

await triggerEvent('[data-test-hydratable-card]', 'mouseenter');

assert
.dom('[data-test-live-card]')
.hasText('Live: Hassan', 'still hydrates');
assert
.dom('[data-test-hydratable-card]')
.doesNotHaveClass(
'boxel-card-container--boundaries',
'the live container renders without boundaries',
);
});

// `none` stays inert with the diagnostic attribute and never fetches.
test('none — stays inert, marks data-hydration=none, and never fetches', async function (assert) {
let inert = htmlComponent(INERT_HTML);
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime-common/search-results-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ export interface SearchResultsComponentSignature {
// pass `false` for a card that lays results out in its own UI and wants
// them rendered plainly, with no overlay even inside operator mode.
overlays?: boolean;
// Whether each result renders inside its card container chrome (the
// boundary ring), the same switch `<@fields.x @displayContainer={{false}} />`
// offers. Defaults to `true`; pass `false` for a consumer that frames
// results itself.
displayContainer?: boolean;
};
Blocks: {
default: [SearchResultsYield];
Expand Down
Loading