Skip to content

Commit 673f109

Browse files
feat(uve): Fix same-page navigation handling with anchors with id (#35326)
- Added utility to determine if a URL change is a same-page navigation (hash-only or query-only). - Updated to prevent triggering page load for same-page navigations. - Enhanced tests in to cover various same-page navigation scenarios. - Modified to utilize the new utility for navigation updates. - Updated to exclude hash-only links from triggering navigation actions. This change improves user experience by preventing unnecessary page reloads during internal navigation events. https://github.com/user-attachments/assets/e4c9c2c5-3422-4054-9040-167a70ed5731 This PR fixes: #35324
1 parent 0c035c5 commit 673f109

7 files changed

Lines changed: 466 additions & 10 deletions

File tree

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-iframe/dot-uve-iframe.component.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import {
44
ChangeDetectionStrategy,
55
Component,
66
DestroyRef,
7-
ElementRef,
8-
OnDestroy,
9-
ViewChild,
107
effect,
8+
ElementRef,
119
inject,
1210
input,
13-
output
11+
OnDestroy,
12+
output,
13+
ViewChild
1414
} from '@angular/core';
1515
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1616

@@ -196,7 +196,13 @@ export class DotUveIframeComponent implements OnDestroy {
196196
.pipe(
197197
filter((e) => {
198198
const target = e.target as HTMLElement;
199-
const hasLink = !!target.closest('a')?.getAttribute('href');
199+
const linkElement = target.closest('a');
200+
const href = linkElement?.getAttribute('href');
201+
202+
// Exclude hash-only links (e.g., #sectionA) - these are same-page anchors
203+
const isHashOnly = href?.startsWith('#');
204+
205+
const hasLink = !!href && !isHashOnly;
200206
const hasInlineEditTarget =
201207
!!target.closest('[data-mode]') || !!target.dataset?.mode;
202208
return hasLink || hasInlineEditTarget;

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ import { EditEmaEditorComponent } from './edit-ema-editor.component';
9292
import { DotBlockEditorSidebarComponent } from '../components/dot-block-editor-sidebar/dot-block-editor-sidebar.component';
9393
import { DotEmaDialogComponent } from '../components/dot-ema-dialog/dot-ema-dialog.component';
9494
import { DotActionUrlService } from '../services/dot-action-url/dot-action-url.service';
95-
import { DotPageApiService } from '../services/dot-page-api/dot-page-api.service';
95+
import { DotPageApiParams, DotPageApiService } from '../services/dot-page-api/dot-page-api.service';
9696
import { DotUveActionsHandlerService } from '../services/dot-uve-actions-handler/dot-uve-actions-handler.service';
9797
import { DotUveDragDropService } from '../services/dot-uve-drag-drop/dot-uve-drag-drop.service';
9898
import { InlineEditService } from '../services/inline-edit/inline-edit.service';
@@ -152,7 +152,7 @@ const mockGlobalStore = {
152152
};
153153

154154
const mockDotUveActionsHandlerService = {
155-
handleAction: jest.fn(() => of({}))
155+
handleAction: jest.fn((_message: unknown, _deps: unknown) => of({}))
156156
};
157157

158158
const mockDotUveDragDropService = {
@@ -2343,6 +2343,107 @@ describe('EditEmaEditorComponent', () => {
23432343
expect(mockEvent.preventDefault).toHaveBeenCalled();
23442344
});
23452345

2346+
describe('same-page navigation (same pathname)', () => {
2347+
const samePathPageParams = (): DotPageApiParams => ({
2348+
url: '/current-page',
2349+
language_id: '1',
2350+
[PERSONA_KEY]: DEFAULT_PERSONA.identifier
2351+
});
2352+
2353+
beforeEach(() => {
2354+
jest.spyOn(store, 'pageParams').mockReturnValue(samePathPageParams());
2355+
});
2356+
2357+
it('should not trigger pageLoad for hash-only navigation on same page', () => {
2358+
const hashUrl = 'http://localhost:3000/current-page#sectionA';
2359+
const mockEvent = createMockEvent(hashUrl);
2360+
2361+
spectator.component.handleInternalNav(mockEvent);
2362+
2363+
expect(pageLoadSpy).not.toHaveBeenCalled();
2364+
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
2365+
});
2366+
2367+
it('should not trigger pageLoad for hash-only with complex id', () => {
2368+
const hashUrl = 'http://localhost:3000/current-page#section-123_complex';
2369+
const mockEvent = createMockEvent(hashUrl);
2370+
2371+
spectator.component.handleInternalNav(mockEvent);
2372+
2373+
expect(pageLoadSpy).not.toHaveBeenCalled();
2374+
});
2375+
2376+
it('should not trigger pageLoad for query-only navigation on same page', () => {
2377+
const queryUrl = 'http://localhost:3000/current-page?tab=2';
2378+
const mockEvent = createMockEvent(queryUrl);
2379+
2380+
spectator.component.handleInternalNav(mockEvent);
2381+
2382+
expect(pageLoadSpy).not.toHaveBeenCalled();
2383+
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
2384+
});
2385+
2386+
it('should not trigger pageLoad for multiple query params on same page', () => {
2387+
const queryUrl =
2388+
'http://localhost:3000/current-page?filter=value&sort=date';
2389+
const mockEvent = createMockEvent(queryUrl);
2390+
2391+
spectator.component.handleInternalNav(mockEvent);
2392+
2393+
expect(pageLoadSpy).not.toHaveBeenCalled();
2394+
});
2395+
2396+
it('should not trigger pageLoad when both hash and query are present on same path', () => {
2397+
const combinedUrl = 'http://localhost:3000/current-page?tab=2#section';
2398+
const mockEvent = createMockEvent(combinedUrl);
2399+
2400+
spectator.component.handleInternalNav(mockEvent);
2401+
2402+
expect(pageLoadSpy).not.toHaveBeenCalled();
2403+
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
2404+
});
2405+
2406+
it('should trigger pageLoad when navigating to different page with hash', () => {
2407+
const differentPageUrl = 'http://localhost:3000/other-page#section';
2408+
const mockEvent = createMockEvent(differentPageUrl);
2409+
2410+
spectator.component.handleInternalNav(mockEvent);
2411+
2412+
expect(pageLoadSpy).toHaveBeenCalledWith({
2413+
url: '/other-page'
2414+
});
2415+
expect(mockEvent.preventDefault).toHaveBeenCalled();
2416+
});
2417+
2418+
it('should trigger pageLoad when navigating to different page with query', () => {
2419+
const differentPageUrl = 'http://localhost:3000/other-page?tab=1';
2420+
const mockEvent = createMockEvent(differentPageUrl);
2421+
2422+
spectator.component.handleInternalNav(mockEvent);
2423+
2424+
expect(pageLoadSpy).toHaveBeenCalledWith({
2425+
url: '/other-page',
2426+
tab: '1'
2427+
});
2428+
expect(mockEvent.preventDefault).toHaveBeenCalled();
2429+
});
2430+
2431+
it('should handle root path hash navigation', () => {
2432+
jest.spyOn(store, 'pageParams').mockReturnValue({
2433+
url: '/',
2434+
language_id: '1',
2435+
[PERSONA_KEY]: DEFAULT_PERSONA.identifier
2436+
});
2437+
2438+
const hashUrl = 'http://localhost:3000/#top';
2439+
const mockEvent = createMockEvent(hashUrl);
2440+
2441+
spectator.component.handleInternalNav(mockEvent);
2442+
2443+
expect(pageLoadSpy).not.toHaveBeenCalled();
2444+
});
2445+
});
2446+
23462447
afterEach(() => {
23472448
jest.clearAllMocks();
23482449
});

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import {
111111
getTargetUrl,
112112
getWrapperMeasures,
113113
insertContentletInContainer,
114+
isSamePageNavigation,
114115
shouldNavigate
115116
} from '../utils';
116117

@@ -629,6 +630,11 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy, AfterViewInit
629630
return;
630631
}
631632

633+
// Same pathname (any hash/query): let the browser handle it (anchors, query-driven UI)
634+
if (isSamePageNavigation(href, this.uveStore.pageParams()?.url)) {
635+
return;
636+
}
637+
632638
this.uveStore.pageLoad({ url: url.pathname, ...urlQueryParams });
633639
e.preventDefault();
634640
}

0 commit comments

Comments
 (0)