mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(ui): start a sidebar drag from its CSS width, not its border box
ResizableDirective started a drag from offsetWidth, which includes padding and border, but writes the result to style.width. The hosts are content-box, so the first move widened a padded host by its padding plus border: 23px on the workspace categories panel, 1px on the Favorites sidebar. Each drag also saved a width that much larger. A drag now starts from the computed CSS width (the box style.width sizes), falling back to the directive's own width when that is not a length. Border-box hosts (live-layout and groups rails) are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6edec0fc76
commit
b45114e259
5 files changed
+73
-5
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fix
|
||||
area: ui
|
||||
---
|
||||
|
||||
A sidebar no longer jumps wider the moment you start dragging its edge. It now follows the pointer from its current width, so a resized sidebar ends up exactly as wide as you dragged it.
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
* The workspace context panels share one stored width but clamp it to their
|
||||
* own limits. Opening a narrower panel used to save its clamped width, so a
|
||||
* categories panel widened past Settings' 400px maximum came back at 400.
|
||||
* The drag also lands on 520 only if the padded panel does not jump wider
|
||||
* when the drag starts.
|
||||
*/
|
||||
|
||||
const SHARED_WIDTH_KEY = 'sidebar-width';
|
||||
@@ -38,8 +40,9 @@ async function dragPanelTo(
|
||||
if (!box) throw new Error('The panel has no resize handle');
|
||||
const startX = Math.round(box.x + box.width / 2);
|
||||
const y = Math.round(box.y + box.height / 2);
|
||||
// The directive starts a drag from offsetWidth (border box included).
|
||||
const delta = width - (await panel.evaluate((el) => el.offsetWidth));
|
||||
// A drag starts from the CSS width, so the panel's padding and border
|
||||
// must not count toward the distance.
|
||||
const delta = width - (await renderedWidth(panel));
|
||||
|
||||
await page.mouse.move(startX, y);
|
||||
await page.mouse.down();
|
||||
|
||||
@@ -220,6 +220,12 @@ renders the stored width clamped to the host's own limits and never writes
|
||||
that clamped value back: only a drag that changes the width persists, so a
|
||||
narrow sidebar such as Settings cannot shrink the width a wider one saved.
|
||||
|
||||
A drag starts from the host's computed CSS `width`, the box that `style.width`
|
||||
sizes, not from `offsetWidth`. Hosts may be `content-box` (the workspace
|
||||
context panels and the Favorites sidebar) or `border-box` (live-layout and
|
||||
groups rails); starting from the border box widened a padded `content-box`
|
||||
host by its padding and border on the first move.
|
||||
|
||||
## Keyboard Scrolling and Channel Focus
|
||||
|
||||
`ChannelScrollFocusDirective` belongs on the actual channel scroll owner,
|
||||
|
||||
@@ -23,6 +23,18 @@ class Host {
|
||||
handlePosition: 'right' | 'left' = 'right';
|
||||
}
|
||||
|
||||
/** A content-box element's border box: its width plus padding and border. */
|
||||
function borderBoxWidth(el: HTMLElement): number {
|
||||
const style = getComputedStyle(el);
|
||||
return [
|
||||
style.width,
|
||||
style.paddingLeft,
|
||||
style.paddingRight,
|
||||
style.borderLeftWidth,
|
||||
style.borderRightWidth,
|
||||
].reduce((sum, value) => sum + (parseFloat(value) || 0), 0);
|
||||
}
|
||||
|
||||
describe('ResizableDirective', () => {
|
||||
const fixtures: ComponentFixture<Host>[] = [];
|
||||
|
||||
@@ -33,10 +45,11 @@ describe('ResizableDirective', () => {
|
||||
fixtures.push(fixture);
|
||||
|
||||
const aside: HTMLElement = fixture.nativeElement.querySelector('aside');
|
||||
// jsdom has no layout; a drag starts from the rendered width.
|
||||
// jsdom has no layout. Give offsetWidth the border box a browser
|
||||
// reports, so a drag that starts from it is caught widening.
|
||||
Object.defineProperty(aside, 'offsetWidth', {
|
||||
configurable: true,
|
||||
get: () => parseInt(aside.style.width, 10) || 0,
|
||||
get: () => borderBoxWidth(aside),
|
||||
});
|
||||
return aside;
|
||||
}
|
||||
@@ -63,6 +76,7 @@ describe('ResizableDirective', () => {
|
||||
afterEach(() => {
|
||||
fixtures.splice(0).forEach((fixture) => fixture.destroy());
|
||||
localStorage.clear();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('clamps a shared width for rendering without writing it back', () => {
|
||||
@@ -140,6 +154,32 @@ describe('ResizableDirective', () => {
|
||||
expect(localStorage.getItem('sidebar-width')).toBeNull();
|
||||
});
|
||||
|
||||
it('drags a padded host from its CSS width, not its border box', () => {
|
||||
localStorage.setItem('sidebar-width', '300');
|
||||
const panel = render({ maxWidth: 560 });
|
||||
panel.style.padding = '0 10px 0 12px';
|
||||
panel.style.borderRight = '1px solid';
|
||||
expect(panel.offsetWidth).toBe(323);
|
||||
|
||||
drag(panel, 100, 140);
|
||||
|
||||
expect(panel.style.width).toBe('340px');
|
||||
expect(localStorage.getItem('sidebar-width')).toBe('340');
|
||||
});
|
||||
|
||||
it('drags from the width it set when the CSS width is not a length', () => {
|
||||
localStorage.setItem('sidebar-width', '300');
|
||||
const aside = render();
|
||||
jest.spyOn(window, 'getComputedStyle').mockReturnValue({
|
||||
width: 'auto',
|
||||
} as CSSStyleDeclaration);
|
||||
|
||||
drag(aside, 100, 140);
|
||||
|
||||
expect(aside.style.width).toBe('340px');
|
||||
expect(localStorage.getItem('sidebar-width')).toBe('340');
|
||||
});
|
||||
|
||||
it('does not persist the clamped width when the handle is only clicked', () => {
|
||||
localStorage.setItem('sidebar-width', '520');
|
||||
const settings = render({ maxWidth: 400 });
|
||||
|
||||
@@ -253,11 +253,24 @@ export class ResizableDirective implements OnInit, AfterViewInit, OnDestroy {
|
||||
private startResize(clientX: number): void {
|
||||
this.isResizing.set(true);
|
||||
this.startX = clientX;
|
||||
this.startWidth = this.el.nativeElement.offsetWidth;
|
||||
this.startWidth = this.renderedWidth();
|
||||
this.widthBeforeResize = this.currentWidth();
|
||||
this.resizeStart.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* The rendered width in the box `style.width` sizes: the content box for
|
||||
* a content-box host, the border box for a border-box one. `offsetWidth`
|
||||
* always includes padding and border, so starting from it widened padded
|
||||
* hosts by that amount on the first move.
|
||||
*/
|
||||
private renderedWidth(): number {
|
||||
const width = parseFloat(
|
||||
window.getComputedStyle(this.el.nativeElement).width
|
||||
);
|
||||
return Number.isFinite(width) ? width : this.currentWidth();
|
||||
}
|
||||
|
||||
private onMouseMove(e: MouseEvent): void {
|
||||
if (!this.isResizing()) return;
|
||||
this.resize(e.clientX);
|
||||
|
||||
Reference in new issue
Block a user