fix(ui): open links from inside modal in a new tab (#14431)

This commit is contained in:
purnimagarg1 2025-08-27 03:19:13 +05:30 committed by GitHub
parent 4e83f951be
commit 8202977c74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 126 additions and 4 deletions

View File

@ -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}
<ModalContext.Provider value={{ isInsideModal: true }}>{children}</ModalContext.Provider>
</StyledModal>
);
}

View File

@ -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}
/>
) : (
<StyledLink to={entityRegistry.getEntityUrl(entity.type, entity.urn)} onClick={sendAnalytics}>
<StyledLink
to={entityRegistry.getEntityUrl(entity.type, entity.urn)}
onClick={sendAnalytics}
{...linkProps}
>
<AutoCompleteEntityItem
entity={entity}
key={entity.urn}

View File

@ -12,6 +12,7 @@ import EntitySubtitle from '@app/searchV2/autoCompleteV2/components/subtitle/Ent
import { VARIANT_STYLES } from '@app/searchV2/autoCompleteV2/constants';
import { EntityItemVariant } from '@app/searchV2/autoCompleteV2/types';
import { getEntityDisplayType } from '@app/searchV2/autoCompleteV2/utils';
import { useGetModalLinkProps } from '@app/sharedV2/modals/useGetModalLinkProps';
import { Text } from '@src/alchemy-components';
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
import { Entity, MatchedField } from '@src/types.generated';
@ -131,6 +132,8 @@ export default function AutoCompleteEntityItem({
}: EntityAutocompleteItemProps) {
const theme = useTheme();
const entityRegistry = useEntityRegistryV2();
const linkProps = useGetModalLinkProps();
const displayName = entityRegistry.getDisplayName(entity.type, entity);
const displayType = getEntityDisplayType(entity, entityRegistry);
const variantProps = VARIANT_STYLES.get(variant ?? 'default');
@ -140,7 +143,7 @@ export default function AutoCompleteEntityItem({
: DisplayNameHoverFromContainer;
const displayNameContent = variantProps?.nameCanBeHovered ? (
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)}>
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)} {...linkProps}>
<DisplayNameHoverComponent
displayName={displayName}
highlight={query}

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useModalContext } from '@app/sharedV2/modals/ModalContext';
import { PageRoutes } from '@conf/Global';
// Function to check if the current page is an embedded profile
@ -11,5 +12,9 @@ export const useIsEmbeddedProfile = () => {
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],
);
};

View File

@ -0,0 +1,9 @@
import { createContext, useContext } from 'react';
export type ModalContextType = {
isInsideModal: boolean;
};
export const ModalContext = createContext<ModalContextType>({ isInsideModal: false });
export const useModalContext = () => useContext(ModalContext);

View File

@ -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 }) => (
<ModalContext.Provider value={value}>{children}</ModalContext.Provider>
);
}
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 }>) => (
<ModalContext.Provider value={{ isInsideModal: value }}>{children}</ModalContext.Provider>
);
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",
}
`);
});
});

View File

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