mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-25 22:35:49 +00:00
Hold the camera menu at 296px, as design sets it
- A long device name now wraps instead of widening the menu over more of the picture the user is judging. Narrower only where the call area is, so it still fits inside it: 268px in a 300px call. - The self-preview is always the sixteen by nine it is drawn at, 294 by 166 inside the frame, and Safari's narrowed preview has nothing left to happen to. - Tiles are thirds of the set width, about the 80px design draws, where their floor was there only to push the menu wider. - Nothing else in the menu gets to set its width either: the picture sits over its box, and the notice and the refusal fit the menu rather than asking for their sentence on one line, which took it to 340px. - The microphone menu is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,8 @@ Please see LICENSE in the repository root for full details.
|
||||
component: the menu is portalled outside the root, so neither a container
|
||||
query nor a viewport unit describes the space it is allowed to fill. */
|
||||
.deviceList {
|
||||
/* The camera menu's width, set by the component; auto everywhere else. */
|
||||
inline-size: var(--device-list-inline-size, auto);
|
||||
overflow-y: auto;
|
||||
min-block-size: 0;
|
||||
max-block-size: var(--device-list-max-height);
|
||||
@@ -150,11 +152,11 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
.effectGrid {
|
||||
display: grid;
|
||||
/* A floor, not just a share. The menu sizes itself to its content, so
|
||||
dividing whatever width the device names happen to need makes the tiles
|
||||
narrower the shorter those names are; this makes the menu wide enough for
|
||||
three proper tiles instead. */
|
||||
grid-template-columns: repeat(3, minmax(84px, 1fr));
|
||||
/* Thirds of a menu whose width is set, so the tiles are the size design
|
||||
draws them — about eighty pixels — whatever the device names are. Zero
|
||||
rather than the content as the floor, so a long label wraps inside its
|
||||
tile instead of pushing the grid wider than the menu. */
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
/* Every tile the same height, set by the tallest — the ones carrying a
|
||||
label. Forcing a ratio instead clips the label as soon as the menu is
|
||||
narrow enough that the ratio cannot hold it. */
|
||||
@@ -383,6 +385,9 @@ Please see LICENSE in the repository root for full details.
|
||||
supplies the lower half, so only the upper half is set here. Without it the
|
||||
message sits flush against the tiles with a gap twice its size underneath. */
|
||||
.effectError {
|
||||
/* For the same reason as the notice below: a refusal's title is a sentence,
|
||||
and would otherwise set the menu's width rather than fit inside it. */
|
||||
contain: inline-size;
|
||||
/* A step short of the margin it keeps at the sides: the menu is a column
|
||||
with a gap of its own, which makes up the difference above. */
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-4x) 0;
|
||||
@@ -408,6 +413,11 @@ Please see LICENSE in the repository root for full details.
|
||||
* before the menu opened and will be true after it closes.
|
||||
*/
|
||||
.effectNotice {
|
||||
/* Takes the width the menu has rather than lending it one. The menu is as
|
||||
wide as its widest content, and a sentence asks for its whole length on
|
||||
one line: without this the notice widened the camera menu from 296 to 340
|
||||
pixels, past the width the grid and the preview are drawn for. */
|
||||
contain: inline-size;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--cpd-space-3x);
|
||||
@@ -436,12 +446,12 @@ Please see LICENSE in the repository root for full details.
|
||||
* frame's corner, because a video paints on a layer of its own and would
|
||||
* otherwise draw over the outline the menu is framed with.
|
||||
*
|
||||
* A fixed height rather than a ratio: sixteen by nine at the menu's narrowest,
|
||||
* and wider as a long device name widens the menu, cropped rather than
|
||||
* letterboxed — a face cut at the edge reads as a self-view, a face in bars
|
||||
* reads as a broken one. Not aspect-ratio with a height cap: once the cap
|
||||
* holds, Safari keeps the ratio by narrowing the box instead of the picture,
|
||||
* and the preview stops short of the menu's right edge.
|
||||
* Sixteen by nine across the camera menu's fixed width, less its frame, and
|
||||
* written as a height rather than a ratio: with a ratio and a cap Safari kept
|
||||
* the ratio by narrowing the box, and the preview stopped short of the menu's
|
||||
* right edge. Cropped rather than letterboxed where the call is narrow enough
|
||||
* to narrow the menu — a face cut at the edge reads as a self-view, a face in
|
||||
* bars reads as a broken one.
|
||||
*/
|
||||
.selfPreview {
|
||||
position: relative;
|
||||
@@ -453,7 +463,7 @@ Please see LICENSE in the repository root for full details.
|
||||
);
|
||||
margin-inline: var(--cpd-border-width-1);
|
||||
margin-block-end: var(--cpd-space-2x);
|
||||
block-size: 176px;
|
||||
block-size: 166px;
|
||||
overflow: hidden;
|
||||
border-start-start-radius: calc(
|
||||
var(--cpd-space-3x) - var(--cpd-border-width-1)
|
||||
@@ -465,8 +475,14 @@ Please see LICENSE in the repository root for full details.
|
||||
color: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
|
||||
/* Laid over the box rather than in it. In flow, a sixteen-by-nine picture
|
||||
166 pixels tall asks for 295 of width, and the menu, sized to its widest
|
||||
content, grew by the pixel that asked for. Out of flow it only fills what
|
||||
the menu's width already gives it. */
|
||||
.selfPreview > video,
|
||||
.selfPreview > img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
@@ -96,6 +96,10 @@ const WithACallArea: FC<{ children: ReactNode; height?: number }> = ({
|
||||
// which no real call does. Tall enough to leave the menu room to open
|
||||
// upward and still be wholly on screen in the story's frame.
|
||||
blockSize: height,
|
||||
// And a call's width, for the same reason: the camera menu narrows to
|
||||
// fit a call narrower than itself, and the centred layout Storybook
|
||||
// gives every story would otherwise shrink this to the button's width.
|
||||
inlineSize: "100vw",
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "center",
|
||||
@@ -927,6 +931,16 @@ export const BackgroundEffectsSlowInThisBrowser: Story = {
|
||||
await expect(Math.round(frame.bottom - box.bottom)).toBe(
|
||||
Math.round(box.left - frame.left),
|
||||
);
|
||||
|
||||
// A sentence asks for its whole length on one line, and the menu is as
|
||||
// wide as its widest content: the notice once widened the camera menu from
|
||||
// its set width to 340. It fits the menu rather than setting it, and lines
|
||||
// up with the tiles above it.
|
||||
await expect(Math.round(frame.width)).toBe(296);
|
||||
const tile = menu.querySelector(`.${styles.effectTile}`)!;
|
||||
await expect(Math.round(tile.getBoundingClientRect().left)).toBe(
|
||||
Math.round(box.left),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -966,7 +980,7 @@ export const BackgroundEffectsWithPreview: Story = {
|
||||
|
||||
// And paid for out of the list's share: 60% of 720, less the preview.
|
||||
await expect(getComputedStyle(list).maxBlockSize).toBe(
|
||||
`${Math.round(720 * 0.6) - 176}px`,
|
||||
`${Math.round(720 * 0.6) - 166}px`,
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -1169,13 +1183,13 @@ export const BackgroundEffectsWithALongDeviceName: Story = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A long device name widens the menu past the width at which the preview is
|
||||
* sixteen by nine. The preview still spans it: Safari, given a ratio and a
|
||||
* height cap, kept the ratio by narrowing the box and left a strip of menu at
|
||||
* its right. Checked here in Chromium, which never did — no test tier here
|
||||
* runs WebKit — so this holds the requirement, not the engine.
|
||||
* A long device name, which once widened the menu over more of the picture the
|
||||
* user is judging. The camera menu now holds its width and the name wraps, so
|
||||
* the preview is always the sixteen by nine it is drawn at — and the Safari
|
||||
* failure that came of a widened menu, the preview stopping short of its right
|
||||
* edge, has nothing left to happen to.
|
||||
*/
|
||||
export const BackgroundEffectsWithPreviewInAWideMenu: Story = {
|
||||
export const BackgroundEffectsWithPreviewAndALongDeviceName: Story = {
|
||||
args: {
|
||||
...BackgroundEffectsWithALongDeviceName.args,
|
||||
selfPreview: <img src={cameraStandIn} alt="" />,
|
||||
@@ -1190,11 +1204,10 @@ export const BackgroundEffectsWithPreviewInAWideMenu: Story = {
|
||||
const preview = menu.querySelector<HTMLElement>(`.${styles.selfPreview}`)!;
|
||||
const frame = menu.getBoundingClientRect();
|
||||
const box = preview.getBoundingClientRect();
|
||||
// Wider than the preview's sixteen-by-nine width, or this proves nothing.
|
||||
await expect(frame.width).toBeGreaterThan((176 * 16) / 9 + 8);
|
||||
await expect(Math.round(frame.width)).toBe(296);
|
||||
await expect(frame.right - box.right).toBeLessThanOrEqual(2);
|
||||
await expect(box.left - frame.left).toBeLessThanOrEqual(2);
|
||||
await expect(Math.round(box.height)).toBe(176);
|
||||
await expect(Math.round(box.height)).toBe(166);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -188,13 +188,27 @@ const MIN_LIST_HEIGHT = 160;
|
||||
/**
|
||||
* The most the self-preview may take, which is also what it costs the list.
|
||||
*
|
||||
* Its fixed height: sixteen by nine across the menu at its narrowest, and
|
||||
* cropped wider as the menu widens, so it never costs more than this. The
|
||||
* Its fixed height: sixteen by nine across the camera menu's width, less the
|
||||
* frame, so it never costs more than this. The
|
||||
* preview is paid for out of the list's share rather than on top of it, so the
|
||||
* menu is never taller for having one: where the list could not keep its floor
|
||||
* after paying, there is no preview at all.
|
||||
*/
|
||||
const PREVIEW_BLOCK = 176;
|
||||
const PREVIEW_BLOCK = 166;
|
||||
|
||||
/**
|
||||
* The camera menu's width, as design sets it.
|
||||
*
|
||||
* Fixed rather than sized to its content, so a long device name wraps instead
|
||||
* of widening the menu over more of the picture the user is trying to judge,
|
||||
* and so the self-preview is always the sixteen by nine it is drawn at. Less
|
||||
* only where the call area itself is narrower — the menu has to stay inside
|
||||
* it — which is why it is measured rather than written into the stylesheet.
|
||||
*/
|
||||
const CAMERA_MENU_WIDTH = 296;
|
||||
|
||||
/** Clear space kept between the menu and the call area's sides. */
|
||||
const CAMERA_MENU_MARGIN = 16;
|
||||
|
||||
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
title,
|
||||
@@ -319,6 +333,23 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [menuOpen, rootElement]);
|
||||
|
||||
const [cameraMenuWidth, setCameraMenuWidth] = useState(CAMERA_MENU_WIDTH);
|
||||
useEffect(() => {
|
||||
if (!menuOpen || iconsAndLabels !== "video") return;
|
||||
const subscription = observeElementSize$(rootElement)
|
||||
.pipe(
|
||||
map(({ width }) =>
|
||||
Math.min(
|
||||
CAMERA_MENU_WIDTH,
|
||||
Math.round(width - 2 * CAMERA_MENU_MARGIN),
|
||||
),
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
.subscribe(setCameraMenuWidth);
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [menuOpen, iconsAndLabels, rootElement]);
|
||||
|
||||
// Only while the menu is open, so nothing holds a second capture of the
|
||||
// microphone for the length of a call.
|
||||
// The meter sits over the foot of the scrolling list, so the list has to
|
||||
@@ -780,6 +811,10 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
{
|
||||
"--device-list-max-height":
|
||||
listMaxHeight === undefined ? undefined : `${listMaxHeight}px`,
|
||||
// The menu takes its width from its content, and this is the
|
||||
// content that could otherwise set it. Only the camera menu.
|
||||
"--device-list-inline-size":
|
||||
iconsAndLabels === "video" ? `${cameraMenuWidth}px` : undefined,
|
||||
"--device-list-scroll-padding-end":
|
||||
meterHeight === undefined ? undefined : `${meterHeight}px`,
|
||||
"--device-list-scroll-padding-start":
|
||||
|
||||
Reference in New Issue
Block a user