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]); +};