From 8202977c74b8e1c7c4f14ffde29e16e2c7bdcb63 Mon Sep 17 00:00:00 2001
From: purnimagarg1 <139125209+purnimagarg1@users.noreply.github.com>
Date: Wed, 27 Aug 2025 03:19:13 +0530
Subject: [PATCH] fix(ui): open links from inside modal in a new tab (#14431)
---
.../components/Modal/Modal.tsx | 4 +-
.../homeV3/module/components/EntityItem.tsx | 8 +-
.../autoCompleteV2/AutoCompleteEntityItem.tsx | 5 +-
.../shared/useEmbeddedProfileLinkProps.tsx | 7 +-
.../src/app/sharedV2/modals/ModalContext.ts | 9 ++
.../__tests__/useGetModalLinkProps.test.tsx | 83 +++++++++++++++++++
.../sharedV2/modals/useGetModalLinkProps.ts | 14 ++++
7 files changed, 126 insertions(+), 4 deletions(-)
create mode 100644 datahub-web-react/src/app/sharedV2/modals/ModalContext.ts
create mode 100644 datahub-web-react/src/app/sharedV2/modals/__tests__/useGetModalLinkProps.test.tsx
create mode 100644 datahub-web-react/src/app/sharedV2/modals/useGetModalLinkProps.ts
diff --git a/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx b/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx
index 6284ac66e2..9efa80535d 100644
--- a/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx
+++ b/datahub-web-react/src/alchemy-components/components/Modal/Modal.tsx
@@ -3,6 +3,8 @@ import { Modal as AntModal, ModalProps as AntModalProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
+import { ModalContext } from '@app/sharedV2/modals/ModalContext';
+
const StyledModal = styled(AntModal)<{ hasChildren: boolean }>`
font-family: ${typography.fonts.body};
@@ -125,7 +127,7 @@ export function Modal({
}
{...props}
>
- {children}
+ {children}
);
}
diff --git a/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx b/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx
index 8354472c88..a35d6ea20c 100644
--- a/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx
+++ b/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx
@@ -4,6 +4,7 @@ import styled from 'styled-components';
import analytics, { EventType } from '@app/analytics';
import AutoCompleteEntityItem from '@app/searchV2/autoCompleteV2/AutoCompleteEntityItem';
+import { useGetModalLinkProps } from '@app/sharedV2/modals/useGetModalLinkProps';
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
import { DataHubPageModuleType, Entity } from '@types';
@@ -34,6 +35,7 @@ export default function EntityItem({
padding,
}: Props) {
const entityRegistry = useEntityRegistryV2();
+ const linkProps = useGetModalLinkProps();
const sendAnalytics = useCallback(
() =>
@@ -60,7 +62,11 @@ export default function EntityItem({
onClick={sendAnalytics}
/>
) : (
-
+
+
{
export const useEmbeddedProfileLinkProps = () => {
const isEmbedded = useIsEmbeddedProfile();
- return useMemo(() => (isEmbedded ? { target: '_blank', rel: 'noreferrer noopener' } : {}), [isEmbedded]);
+ const { isInsideModal } = useModalContext(); // If link is opened from inside a modal
+ return useMemo(
+ () => (isEmbedded || isInsideModal ? { target: '_blank', rel: 'noreferrer noopener' } : {}),
+ [isEmbedded, isInsideModal],
+ );
};
diff --git a/datahub-web-react/src/app/sharedV2/modals/ModalContext.ts b/datahub-web-react/src/app/sharedV2/modals/ModalContext.ts
new file mode 100644
index 0000000000..8e525ce8c0
--- /dev/null
+++ b/datahub-web-react/src/app/sharedV2/modals/ModalContext.ts
@@ -0,0 +1,9 @@
+import { createContext, useContext } from 'react';
+
+export type ModalContextType = {
+ isInsideModal: boolean;
+};
+
+export const ModalContext = createContext({ isInsideModal: false });
+
+export const useModalContext = () => useContext(ModalContext);
diff --git a/datahub-web-react/src/app/sharedV2/modals/__tests__/useGetModalLinkProps.test.tsx b/datahub-web-react/src/app/sharedV2/modals/__tests__/useGetModalLinkProps.test.tsx
new file mode 100644
index 0000000000..b64229b6e8
--- /dev/null
+++ b/datahub-web-react/src/app/sharedV2/modals/__tests__/useGetModalLinkProps.test.tsx
@@ -0,0 +1,83 @@
+import { renderHook } from '@testing-library/react-hooks';
+import React, { PropsWithChildren } from 'react';
+import { describe, expect, it } from 'vitest';
+
+import { ModalContext, ModalContextType } from '@app/sharedV2/modals/ModalContext';
+import { useGetModalLinkProps } from '@app/sharedV2/modals/useGetModalLinkProps';
+
+function wrapperWithContext(value: ModalContextType) {
+ return ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+}
+
+describe('useGetModalLinkProps', () => {
+ it('should return an empty object when not in modal (default context)', () => {
+ const { result } = renderHook(() => useGetModalLinkProps());
+ expect(result.current).toEqual({});
+ });
+
+ it('should return correct props when isInsideModal = true', () => {
+ const { result } = renderHook(() => useGetModalLinkProps(), {
+ wrapper: wrapperWithContext({ isInsideModal: true }),
+ });
+ expect(result.current).toEqual({
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ });
+ });
+
+ it('should return empty object when isInsideModal = false', () => {
+ const { result } = renderHook(() => useGetModalLinkProps(), {
+ wrapper: wrapperWithContext({ isInsideModal: false }),
+ });
+ expect(result.current).toEqual({});
+ });
+
+ it('should memoize result for same context value', () => {
+ const { result, rerender } = renderHook(() => useGetModalLinkProps(), {
+ wrapper: wrapperWithContext({ isInsideModal: true }),
+ });
+ const first = result.current;
+ rerender();
+ expect(result.current).toBe(first); // stable reference
+ });
+
+ it('should change reference when context value changes', () => {
+ const wrapper = ({ children, value }: PropsWithChildren<{ value: boolean }>) => (
+ {children}
+ );
+
+ const { result, rerender } = renderHook(() => useGetModalLinkProps(), {
+ initialProps: { value: false },
+ wrapper,
+ });
+
+ const first = result.current;
+ rerender({ value: true });
+ expect(result.current).not.toBe(first);
+ expect(result.current).toEqual({
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ });
+ });
+
+ it('should have no extra keys when not in modal', () => {
+ const { result } = renderHook(() => useGetModalLinkProps(), {
+ wrapper: wrapperWithContext({ isInsideModal: false }),
+ });
+ expect(Object.keys(result.current)).toHaveLength(0);
+ });
+
+ it('should match snapshot when in modal', () => {
+ const { result } = renderHook(() => useGetModalLinkProps(), {
+ wrapper: wrapperWithContext({ isInsideModal: true }),
+ });
+ expect(result.current).toMatchInlineSnapshot(`
+ {
+ "rel": "noopener noreferrer",
+ "target": "_blank",
+ }
+ `);
+ });
+});
diff --git a/datahub-web-react/src/app/sharedV2/modals/useGetModalLinkProps.ts b/datahub-web-react/src/app/sharedV2/modals/useGetModalLinkProps.ts
new file mode 100644
index 0000000000..1da24f981b
--- /dev/null
+++ b/datahub-web-react/src/app/sharedV2/modals/useGetModalLinkProps.ts
@@ -0,0 +1,14 @@
+import { useMemo } from 'react';
+
+import { useModalContext } from '@app/sharedV2/modals/ModalContext';
+
+export const useGetModalLinkProps = () => {
+ const { isInsideModal } = useModalContext();
+
+ return useMemo(() => {
+ if (isInsideModal) {
+ return { target: '_blank', rel: 'noopener noreferrer' };
+ }
+ return {};
+ }, [isInsideModal]);
+};