mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 01:57:41 +02:00
Fix: AMS drying popover no longer renders off the bottom of the viewport (#1447 part 1)
The flame-icon onClick on PrintersPage computed popover position as a
fixed { top: rect.bottom + 4, left: Math.max(8, rect.right - 240) }
with no viewport-overflow check. The flame icon sits at the bottom of
the AMS info section on the printer card, so on most realistic viewports
rect.bottom + 4 + popover_height (~320px) overruns viewport.height and
the popover renders partially or entirely off-screen with the Start
button unreachable. Reporter (kleinweby, P1S + AMS-HT) worked around it
via DevTools to confirm the popover was actually there, just clipped.
Extract a computePopoverPosition() helper in utils/popoverPosition.ts:
- defaults to below + right-aligned to the trigger (preserves the
original visual layout when there's room),
- flips ABOVE the trigger when below would overflow AND above fits,
- stays below in the degraded case (popover taller than the viewport) —
at least the top is visible and the user can scroll inside the
popover; flipping to a top-clipped position would lose the action
buttons too,
- clamps the left coordinate so a trigger near either viewport edge
can't push the popover off-screen horizontally either.
Both PrintersPage callsites (the compact AMS row at :3498 and the
dual-nozzle layout at :4011) route through the helper.
This is part 1 of #1447. The functional drying bug — printer receives
the ams_filament_drying MQTT command, ACKs it, but never starts/stops
drying on Bambuddy's request while the printer's own touchscreen works
— is NOT addressed here. Diagnosing it needs the printer's actual
response payload (whether result: "fail" and the specific reason code),
which bambu_mqtt.py:918 currently doesn't log for ams_filament_drying.
Punted to a follow-up where I'll extend the existing extrusion_cali_* /
ams_filament_setting payload-logging path at :919-920 to cover
ams_filament_drying too, ask the reporter to retry, and fix the
command side based on what the printer actually returns.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { computePopoverPosition } from '../../utils/popoverPosition';
|
||||
|
||||
/**
|
||||
* Tests for #1447: the AMS drying popover was rendering off the bottom of
|
||||
* the viewport with the Start button unreachable. The new helper must:
|
||||
* - keep the popover below when below fits
|
||||
* - flip above when below would overflow AND above fits
|
||||
* - stay below (degraded) when neither side fits
|
||||
* - clamp the horizontal position so a trigger near the viewport's right
|
||||
* edge doesn't push the popover off-screen.
|
||||
*/
|
||||
describe('computePopoverPosition (#1447)', () => {
|
||||
// Trigger positioned in the middle of a 1024x768 viewport.
|
||||
const middleTrigger = { top: 300, bottom: 320, left: 400, right: 440 };
|
||||
const viewport = { viewportWidth: 1024, viewportHeight: 768 };
|
||||
|
||||
it('places the popover below the trigger when below has room', () => {
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: middleTrigger,
|
||||
popoverWidth: 240,
|
||||
estimatedHeight: 320,
|
||||
...viewport,
|
||||
});
|
||||
expect(pos.top).toBe(middleTrigger.bottom + 4); // 324
|
||||
});
|
||||
|
||||
it('right-aligns the popover to the trigger by default', () => {
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: middleTrigger,
|
||||
popoverWidth: 240,
|
||||
estimatedHeight: 320,
|
||||
...viewport,
|
||||
});
|
||||
expect(pos.left).toBe(middleTrigger.right - 240); // 200
|
||||
});
|
||||
|
||||
it('flips above when the popover would overflow the bottom of the viewport', () => {
|
||||
// Trigger near the bottom — bottom=700 + gap 4 + height 320 = 1024 > 768.
|
||||
const bottomTrigger = { top: 680, bottom: 700, left: 400, right: 440 };
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: bottomTrigger,
|
||||
popoverWidth: 240,
|
||||
estimatedHeight: 320,
|
||||
...viewport,
|
||||
});
|
||||
// Above placement: trigger.top - gap - height = 680 - 4 - 320 = 356.
|
||||
expect(pos.top).toBe(356);
|
||||
});
|
||||
|
||||
it('stays below when neither below nor above can fully fit (degraded)', () => {
|
||||
// A popover taller than the viewport itself can never fit anywhere. Stay
|
||||
// below so the user at least sees the top of the popover and can scroll
|
||||
// through it — flipping to a top-clipped position would lose visibility
|
||||
// of the action buttons at the bottom of the popover too.
|
||||
const tallPopover = { estimatedHeight: 900 };
|
||||
const trigger = { top: 380, bottom: 400, left: 400, right: 440 };
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: trigger,
|
||||
popoverWidth: 240,
|
||||
...tallPopover,
|
||||
...viewport,
|
||||
});
|
||||
expect(pos.top).toBe(trigger.bottom + 4);
|
||||
});
|
||||
|
||||
it('clamps horizontally when trigger sits near the right viewport edge', () => {
|
||||
// Trigger.right=1020, popoverWidth=240. Default left would be 780; the
|
||||
// popover would extend to 1020 which is within viewport=1024 minus the
|
||||
// 8px margin -> 1016, so it overflows by 4px. Clamp pushes it left.
|
||||
const rightEdgeTrigger = { top: 100, bottom: 120, left: 980, right: 1020 };
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: rightEdgeTrigger,
|
||||
popoverWidth: 240,
|
||||
estimatedHeight: 320,
|
||||
...viewport,
|
||||
});
|
||||
expect(pos.left).toBeLessThanOrEqual(1024 - 240 - 8); // 776
|
||||
expect(pos.left).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
|
||||
it('clamps horizontally when trigger sits near the left viewport edge', () => {
|
||||
// Trigger.right=120, popoverWidth=240. Default left would be -120 (off
|
||||
// viewport). Clamp to the margin.
|
||||
const leftEdgeTrigger = { top: 100, bottom: 120, left: 80, right: 120 };
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: leftEdgeTrigger,
|
||||
popoverWidth: 240,
|
||||
estimatedHeight: 320,
|
||||
...viewport,
|
||||
});
|
||||
expect(pos.left).toBe(8); // default margin
|
||||
});
|
||||
|
||||
it('respects a custom margin', () => {
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: { top: 100, bottom: 120, left: 80, right: 120 },
|
||||
popoverWidth: 240,
|
||||
estimatedHeight: 320,
|
||||
margin: 16,
|
||||
...viewport,
|
||||
});
|
||||
expect(pos.left).toBe(16);
|
||||
});
|
||||
|
||||
it('respects a custom gap between trigger and popover', () => {
|
||||
const pos = computePopoverPosition({
|
||||
triggerRect: middleTrigger,
|
||||
popoverWidth: 240,
|
||||
estimatedHeight: 320,
|
||||
gap: 12,
|
||||
...viewport,
|
||||
});
|
||||
expect(pos.top).toBe(middleTrigger.bottom + 12); // 332
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user