feat(quick-260916-dyv): Bearbeiten-Schalter unten rechts (Rand oben 28 px), ganze Kachel als Griff mit Kopfleiste, cancel-Selektor, kein Ueberlappen beim Ablegen
- page.tsx: feste Aktionsleiste `fixed bottom-6 right-6 z-20` mit "Widget
hinzufuegen" (nur Bearbeitungsmodus) links neben dem Stift/Haekchen; Block
oben rechts und mt-8-Wrapper entfernt, Grid direkt im Container p-2
(12 + 8 + 8 = 28 px statt 60 px); page.test.tsx NEU mit 3 Tests
- widget-wrapper.tsx: Karte ist im Bearbeitungsmodus der Griff (cursor-grab),
20-px-Overlay-Kopfleiste mit Griff-Symbol und Tooltip dragHint, Loesch-Knopf
in der Kopfleiste mit data-no-drag; Rumpf h-full unveraendert (Hoehenkette)
- dashboard-grid.tsx: WIDGET_DRAG_HANDLE_SELECTOR, WIDGET_DRAG_CANCEL_SELECTOR
(input, textarea, select, button, a, [contenteditable], [data-no-drag],
.widgetNoDrag), threshold 3; FREE_PLACEMENT_COMPACTOR = noCompactor +
preventCollision: true (freie Platzierung bleibt, Commit c8f3361)
- dashboard-grid.test.tsx: Mock per importOriginal (echter noCompactor),
Tests 6-8 (dragConfig-Pin, Compactor-Pin, cancel/handle-Semantik im DOM)
- edit-mode-toggle.tsx: schwebend (shadow-lg, inaktiv border + bg-card)
- de.json/en.json: widgets.dragHint
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -16,6 +16,7 @@ vi.mock('next-intl', () => ({
|
||||
addWidget: 'Add widget',
|
||||
catalogTitle: 'Add widget',
|
||||
deleteTooltip: 'Remove widget',
|
||||
dragHint: 'Drag the tile to move it',
|
||||
'clock.name': 'Clock',
|
||||
'clock.description': 'Shows the current time',
|
||||
'search.name': 'Search',
|
||||
@@ -38,13 +39,15 @@ vi.mock('next-intl', () => ({
|
||||
// Mock react-grid-layout Responsive to a simple passthrough for jsdom stability.
|
||||
// quick-260916-bwo: der Mock faengt zusaetzlich die Props ein, damit die
|
||||
// Raster-Konstanten (cols, rowHeight, margin, breakpoints) pruefbar sind.
|
||||
// quick-260916-dyv: Mock per importOriginal, damit `noCompactor` das echte
|
||||
// Objekt ist (Test 7 prueft compact als Identitaets-Kopie), nur Responsive ersetzt.
|
||||
const captured = vi.hoisted(() => ({ props: null as Record<string, unknown> | null }));
|
||||
vi.mock('react-grid-layout', () => ({
|
||||
vi.mock('react-grid-layout', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('react-grid-layout')>()),
|
||||
Responsive: (props: Record<string, unknown> & { children: React.ReactNode }) => {
|
||||
captured.props = props;
|
||||
return <div data-testid="responsive-grid">{props.children}</div>;
|
||||
},
|
||||
noCompactor: () => null,
|
||||
}));
|
||||
|
||||
// Mock dashboard-api
|
||||
@@ -273,4 +276,115 @@ describe('DashboardGrid', () => {
|
||||
expect(layouts.lg[0].h).toBe(8);
|
||||
expect(layouts.lg[0].minH).toBe(8);
|
||||
});
|
||||
|
||||
const CANCEL = 'input, textarea, select, button, a, [contenteditable], [data-no-drag], .widgetNoDrag';
|
||||
|
||||
it('quick-260916-dyv Test 6: dragConfig-Pin — handle Karte, cancel fuer Interaktives, threshold 3; resizeConfig folgt dem Bearbeitungsmodus', async () => {
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
const widgets = [{ id: 'inst-2', widgetType: 'clock', config: {} }];
|
||||
const layouts = { lg: [{ i: 'inst-2', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [] };
|
||||
|
||||
captured.props = null;
|
||||
render(
|
||||
<DashboardGrid layouts={layouts} widgets={widgets} isEditMode={true} onLayoutChange={vi.fn()} onRemoveWidget={vi.fn()} />,
|
||||
);
|
||||
expect(captured.props?.dragConfig).toEqual({
|
||||
enabled: true,
|
||||
handle: '.widget-drag-handle',
|
||||
cancel: CANCEL,
|
||||
threshold: 3,
|
||||
});
|
||||
expect(captured.props?.resizeConfig).toEqual({ enabled: true });
|
||||
cleanup();
|
||||
|
||||
captured.props = null;
|
||||
render(
|
||||
<DashboardGrid layouts={layouts} widgets={widgets} isEditMode={false} onLayoutChange={vi.fn()} onRemoveWidget={vi.fn()} />,
|
||||
);
|
||||
expect(captured.props?.dragConfig).toEqual({
|
||||
enabled: false,
|
||||
handle: '.widget-drag-handle',
|
||||
cancel: CANCEL,
|
||||
threshold: 3,
|
||||
});
|
||||
expect(captured.props?.resizeConfig).toEqual({ enabled: false });
|
||||
});
|
||||
|
||||
it('quick-260916-dyv Test 7: Compactor-Pin — echter noCompactor plus preventCollision: true, compact ist Identitaets-Kopie (freie Platzierung)', async () => {
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
render(
|
||||
<DashboardGrid
|
||||
layouts={{ lg: [{ i: 'inst-2', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [] }}
|
||||
widgets={[{ id: 'inst-2', widgetType: 'clock', config: {} }]}
|
||||
isEditMode={true}
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const compactor = captured.props?.compactor as {
|
||||
type: unknown;
|
||||
allowOverlap: boolean;
|
||||
preventCollision?: boolean;
|
||||
compact: (layout: Array<Record<string, unknown>>, cols: number) => Array<Record<string, unknown>>;
|
||||
};
|
||||
expect(compactor).toMatchObject({ type: null, allowOverlap: false, preventCollision: true });
|
||||
expect(typeof compactor.compact).toBe('function');
|
||||
const input = [{ i: 'a', x: 0, y: 0, w: 2, h: 2 }];
|
||||
const out = compactor.compact(input, 24);
|
||||
// Gemessen (RGL 2.2.3): noCompactor.compact = cloneLayout -> cloneLayoutItem
|
||||
// kopiert i/x/y/w/h unveraendert und normalisiert moved/static zu false
|
||||
// (minW/maxW/... als undefined). Keine Verschiebung, keine Kompaktierung.
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0]).toMatchObject({ i: 'a', x: 0, y: 0, w: 2, h: 2, moved: false, static: false });
|
||||
expect(out).not.toBe(input);
|
||||
expect(out[0]).not.toBe(input[0]);
|
||||
expect(input[0]).toEqual({ i: 'a', x: 0, y: 0, w: 2, h: 2 });
|
||||
});
|
||||
|
||||
it('quick-260916-dyv Test 8: cancel/handle-Semantik im DOM — Karte ist Griff, Loesch-Knopf/Eingaben/widgetNoDrag passen auf cancel, Kopfleiste als Overlay nur im Bearbeitungsmodus', async () => {
|
||||
const mod = await import('./dashboard-grid');
|
||||
const { DashboardGrid, WIDGET_DRAG_HANDLE_SELECTOR, WIDGET_DRAG_CANCEL_SELECTOR } = mod;
|
||||
expect(WIDGET_DRAG_HANDLE_SELECTOR).toBe('.widget-drag-handle');
|
||||
expect(WIDGET_DRAG_CANCEL_SELECTOR).toBe(CANCEL);
|
||||
|
||||
const widgets = [{ id: 'inst-2', widgetType: 'clock', config: {} }];
|
||||
const layouts = { lg: [{ i: 'inst-2', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [] };
|
||||
|
||||
render(
|
||||
<DashboardGrid layouts={layouts} widgets={widgets} isEditMode={true} onLayoutChange={vi.fn()} onRemoveWidget={vi.fn()} />,
|
||||
);
|
||||
|
||||
const card = document.querySelector('[data-widget-id="inst-2"]') as HTMLElement;
|
||||
expect(card).not.toBeNull();
|
||||
expect(card.matches(WIDGET_DRAG_HANDLE_SELECTOR)).toBe(true);
|
||||
// Die Karte selbst darf NICHT auf cancel passen, sonst zieht nichts mehr.
|
||||
expect(card.closest(WIDGET_DRAG_CANCEL_SELECTOR)).toBeNull();
|
||||
|
||||
const button = screen.getByLabelText('Remove widget');
|
||||
expect(button.closest(WIDGET_DRAG_CANCEL_SELECTOR)).toBe(button);
|
||||
expect(button.hasAttribute('data-no-drag')).toBe(true);
|
||||
|
||||
const input = document.createElement('input');
|
||||
card.appendChild(input);
|
||||
expect(input.closest(WIDGET_DRAG_CANCEL_SELECTOR)).toBe(input);
|
||||
|
||||
const noDrag = document.createElement('div');
|
||||
noDrag.className = 'widgetNoDrag';
|
||||
card.appendChild(noDrag);
|
||||
expect(noDrag.closest(WIDGET_DRAG_CANCEL_SELECTOR)).toBe(noDrag);
|
||||
|
||||
const head = screen.getByTitle('Drag the tile to move it');
|
||||
expect(card.contains(head)).toBe(true);
|
||||
expect(head.classList.contains('absolute')).toBe(true);
|
||||
expect(head.classList.contains('h-5')).toBe(true);
|
||||
cleanup();
|
||||
|
||||
render(
|
||||
<DashboardGrid layouts={layouts} widgets={widgets} isEditMode={false} onLayoutChange={vi.fn()} onRemoveWidget={vi.fn()} />,
|
||||
);
|
||||
expect(screen.queryByTitle('Drag the tile to move it')).toBeNull();
|
||||
expect(document.querySelector('.widget-drag-handle')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user