mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Keep the footer while a menu opened from it is open
- In a short window the footer fades out when the pointer leaves the window. The chevron menus are portalled to the document, so they stayed behind, their trigger faded out beneath them. - The footer already stays for keyboard focus. A menu's focus is outside it, so the trigger's aria-expanded tracks it instead. - Pre-existing: the camera chevron does the same on main today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,10 @@ Please see LICENSE in the repository root for full details.
|
||||
inset-inline: 0;
|
||||
}
|
||||
|
||||
.footer.overlay:has(:focus-visible) {
|
||||
/* Keep the footer while it is in use: focus inside it, or a menu opened from
|
||||
it. The menu is portalled out, so the trigger's aria-expanded tracks it. */
|
||||
.footer.overlay:has(:focus-visible),
|
||||
.footer.overlay:has([aria-expanded="true"]) {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Link } from "@vector-im/compound-web";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { CallFooter, type FooterSnapshot } from "./CallFooter";
|
||||
import inCallViewStyles from "../room/InCallView.module.css";
|
||||
import styles from "./CallFooter.module.css";
|
||||
import { useStaticViewModel } from "../state/ViewModel";
|
||||
import { ReactionsSenderContext } from "../reactions/useReactionsSender";
|
||||
import { type ReactionOption } from "../reactions";
|
||||
@@ -175,6 +176,41 @@ export const WithAudioAndVideoOptions: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const StaysWhileAMenuIsOpen: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...WithAudioAndVideoOptions.args,
|
||||
// As it is in a short window, where the footer overlays the call and hides
|
||||
// itself once nothing is happening.
|
||||
asOverlay: true,
|
||||
showFooter: true,
|
||||
},
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const footer = canvasElement.querySelector<HTMLElement>(
|
||||
'[data-testid="footer-container"]',
|
||||
)!;
|
||||
await userEvent.click(
|
||||
within(canvasElement).getByRole("button", { name: "Microphone" }),
|
||||
);
|
||||
await expect(document.body.querySelector('[role="menu"]')).not.toBeNull();
|
||||
|
||||
// The call now decides to hide the footer, which is the class it does it
|
||||
// with. The menu is portalled out of the footer, so the focus inside it is
|
||||
// not something the footer can see: without the trigger's aria-expanded to
|
||||
// go on, this would fade the footer out and take the menu's anchor with it.
|
||||
// The call now decides to hide the footer, which is the class it does that
|
||||
// with. The menu is portalled out of the footer, so the focus inside it is
|
||||
// not something the footer can see: without the trigger's aria-expanded to
|
||||
// go on, this fades the footer out and takes the menu's anchor with it.
|
||||
footer.classList.add(styles.hidden);
|
||||
|
||||
// The footer fades over 0.15s, so a reading taken now is the value it
|
||||
// started from whatever happens next. Let the transition finish first.
|
||||
await new Promise((settled) => setTimeout(settled, 400));
|
||||
await expect(getComputedStyle(footer).opacity).toBe("1");
|
||||
},
|
||||
};
|
||||
|
||||
export const AudioBusy: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
|
||||
Reference in New Issue
Block a user