Skip to content
Open
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
152 changes: 105 additions & 47 deletions packages/host/app/components/host-mode/stack.gts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { TemplateOnlyComponent } from '@ember/component/template-only';
import { on } from '@ember/modifier';
import { action } from '@ember/object';
import Component from '@glimmer/component';

import HostModeStackItem from './stack-item';

Expand All @@ -10,57 +12,113 @@ interface Signature {
};
}

const HostModeStack: TemplateOnlyComponent<Signature> = <template>
<div class='host-mode-stack' ...attributes>
<div class='inner'>
{{#each @stackItemCardIds key='cardId' as |cardId index|}}
<HostModeStackItem
@cardId={{cardId}}
@index={{index}}
@stackItemCardIds={{@stackItemCardIds}}
@close={{@close}}
/>
{{/each}}
</div>
</div>

<style scoped>
.host-mode-stack {
z-index: 1;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.35);
background-position: center;
background-size: cover;
padding: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
export default class HostModeStack extends Component<Signature> {
@action
handleBackdropClick(event: MouseEvent) {
// Only handle clicks directly on the inner div or backdrop, not on children
const target = event.target as HTMLElement;
if (
!target.classList.contains('inner') &&

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop using generic .inner class to detect backdrop clicks

The click guard in handleBackdropClick treats any event target with class inner as a backdrop click, but this handler is attached to the stack container and receives bubbled events from card content. Because card templates can legitimately include an .inner element (for example packages/experiments-realm/transition-tray.gts), clicking inside a card can incorrectly close the top stacked card, which directly violates the new "clicking on a stack card does not close it" behavior for those cards.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a way to address this would be to add a modifier to the .inner element so you have a handle for it, then you can compare the element vs the class name.

!target.classList.contains('backdrop-overlay') &&
!target.closest('.backdrop-overlay')
) {
return;
}

.inner {
height: 100%;
position: relative;
display: flex;
justify-content: center;
margin: 0 auto;
border-bottom-left-radius: var(--boxel-border-radius);
border-bottom-right-radius: var(--boxel-border-radius);
// Close the top card (last in array)
if (this.args.close && this.args.stackItemCardIds.length > 0) {
const topCardId =
this.args.stackItemCardIds[this.args.stackItemCardIds.length - 1];
this.args.close(topCardId);
}
}

<template>
<div class='host-mode-stack' ...attributes>
{{! Backdrop button for closing top card }}
<button
type='button'
class='backdrop-overlay'
{{on 'click' this.handleBackdropClick}}
aria-label='Close top card'
data-test-host-mode-stack-backdrop
>
<span class='boxel-sr-only'>Close top card</span>
</button>

<div class='inner' tabindex='-1' {{on 'click' this.handleBackdropClick}}>
{{#each @stackItemCardIds key='cardId' as |cardId index|}}
<HostModeStackItem
@cardId={{cardId}}
@index={{index}}
@stackItemCardIds={{@stackItemCardIds}}
@close={{@close}}
/>
{{/each}}
</div>
</div>

<style scoped>
.host-mode-stack {
z-index: 1;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.35);
background-position: center;
background-size: cover;
padding: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

.backdrop-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: none;
background: transparent;
cursor: default;
z-index: 0;
}

.backdrop-overlay:focus {
outline: none;
}

@media screen {
.inner {
overflow: auto;
height: 100%;
position: relative;
display: flex;
justify-content: center;
margin: 0 auto;
border-bottom-left-radius: var(--boxel-border-radius);
border-bottom-right-radius: var(--boxel-border-radius);
z-index: 1;
}
/* .inner will handle overflow in host mode stack */
.host-mode-stack :deep(.host-mode-card, .card) {
overflow: hidden;
min-height: 80cqh;

@media screen {
.inner {
overflow: auto;
}
/* .inner will handle overflow in host mode stack */
.host-mode-stack :deep(.host-mode-card, .card) {
overflow: hidden;
min-height: 80cqh;
}
}
}
</style>
</template>;

export default HostModeStack;
@media print {
.backdrop-overlay {
display: none;
}
}
</style>
</template>
}
54 changes: 54 additions & 0 deletions packages/host/tests/acceptance/host-mode-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,60 @@ module('Acceptance | host mode tests', function (hooks) {
);
});

test('clicking the stack backdrop closes the top card', async function (assert) {
let hostModeStackValue = encodeURIComponent(
JSON.stringify([`${testHostModeRealmURL}index`]),
);
await visit(`/test/Pet/mango.json?hostModeStack=${hostModeStackValue}`);

// Wait for stack item to appear
await waitFor(
`[data-test-host-mode-stack-item="${testHostModeRealmURL}index"]`,
);

// Verify stack item exists
assert
.dom(`[data-test-host-mode-stack-item="${testHostModeRealmURL}index"]`)
.exists();

// Click backdrop
await click('[data-test-host-mode-stack-backdrop]');

// Stack item should be removed
await waitUntil(() => {
return !document.querySelector(
`[data-test-host-mode-stack-item="${testHostModeRealmURL}index"]`,
);
});
assert
.dom(`[data-test-host-mode-stack-item="${testHostModeRealmURL}index"]`)
.doesNotExist();
});

test('clicking on a stack card does not close it', async function (assert) {
let hostModeStackValue = encodeURIComponent(
JSON.stringify([`${testHostModeRealmURL}index`]),
);
await visit(`/test/Pet/mango.json?hostModeStack=${hostModeStackValue}`);

let stackSelector = `[data-test-host-mode-stack-item="${testHostModeRealmURL}index"]`;
assert.dom(stackSelector).exists();

// Click on the card content itself
await click(stackSelector);

// Card should still exist
assert.dom(stackSelector).exists();
});

test('backdrop click with empty stack does nothing', async function (assert) {
// Visit card with no stack
await visit('/test/Pet/mango.json');

// Stack backdrop shouldn't exist when there's no stack
assert.dom('[data-test-host-mode-stack-backdrop]').doesNotExist();
});

module('with a custom subdomain', function (hooks) {
hooks.beforeEach(function (this) {
let owner = getOwner(this)!;
Expand Down