Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions semcore/base-components/__tests__/hint.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { expect, test, describe, vi, afterEach } from '@semcore/testing-utils/vitest';
import { render, fireEvent, waitFor, cleanup, act } from '@testing-library/react';
import { render, fireEvent, waitFor, cleanup } from '@testing-library/react';
import React, { useRef } from 'react';
import { userEvent } from 'storybook/test';

import { Hint } from '../src';
import { Hint, PortalProvider } from '../src';

describe('Hint', () => {
afterEach(cleanup);
Expand Down Expand Up @@ -142,4 +142,58 @@ describe('Hint', () => {

vi.useRealTimers();
});

test('Should ignore portal stacking by default and render into document.body', async () => {
const containerRef = React.createRef<HTMLDivElement>();

const TestComponent = () => {
const ref = useRef<HTMLButtonElement>(null);
return (
<>
<div data-testid='portal-container' ref={containerRef} />
<PortalProvider value={containerRef}>
<button ref={ref} data-testid='trigger'>Hover</button>
<Hint triggerRef={ref} defaultVisible={true} data-testid='hint'>
Hint text
</Hint>
</PortalProvider>
</>
);
};

render(<TestComponent />);

await waitFor(() => {
expect(document.body.querySelector('[data-testid="hint"]')).not.toBeNull();
});

const container = document.querySelector('[data-testid="portal-container"]');
expect(container?.querySelector('[data-testid="hint"]')).toBeNull();
});

test('Should respect portal stacking when ignorePortalsStacking is false', async () => {
const containerRef = React.createRef<HTMLDivElement>();

const TestComponent = () => {
const ref = useRef<HTMLButtonElement>(null);
return (
<>
<div data-testid='portal-container' ref={containerRef} />
<PortalProvider value={containerRef}>
<button ref={ref} data-testid='trigger'>Hover</button>
<Hint triggerRef={ref} defaultVisible={true} ignorePortalsStacking={false} data-testid='hint'>
Hint text
</Hint>
</PortalProvider>
</>
);
};

render(<TestComponent />);

await waitFor(() => {
const container = document.querySelector('[data-testid="portal-container"]');
expect(container?.querySelector('[data-testid="hint"]')).not.toBeNull();
});
});
});
11 changes: 9 additions & 2 deletions semcore/base-components/src/components/hint/Hint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,19 @@ export type SimpleHintPopperProps = {
defaultVisible?: boolean;
/** Function called when visibility changes */
onVisibleChange?: (visible: boolean, e?: Event) => boolean | void;
/**
* Set ignore for portal stacking
* @default true
*/
ignorePortalsStacking?: boolean;
};

type DefaultProps = {
defaultVisible?: boolean;
timeout: number | [number, number];
timingFunction: DataType.EasingFunction;
placement?: Placement;
ignorePortalsStacking?: boolean;
};

type State = {
Expand Down Expand Up @@ -100,6 +106,7 @@ class HintPopperRoot extends Component<SimpleHintPopperProps, typeof enhances, H
timeout: [500, 500],
timingFunction: 'ease-out',
placement: 'top',
ignorePortalsStacking: true,
};

constructor(props: SimpleHintPopperProps) {
Expand Down Expand Up @@ -321,7 +328,7 @@ class HintPopperRoot extends Component<SimpleHintPopperProps, typeof enhances, H

render() {
const SHintPopper = Root;
const { visible, Children, parentZIndexStacking, styles, timingFunction } = this.asProps;
const { visible, Children, parentZIndexStacking, styles, timingFunction, ignorePortalsStacking } = this.asProps;
const { innerVisible, calculatedPlacement } = this.state;

if (canUseDOM()) {
Expand All @@ -338,7 +345,7 @@ class HintPopperRoot extends Component<SimpleHintPopperProps, typeof enhances, H
const showHint = (visible && innerVisible === null) || innerVisible === true;

return sstyled(styles)(
<Portal>
<Portal ignorePortalsStacking={ignorePortalsStacking}>
<SHintPopper
render={Box}
ref={this.hintRef}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { expect, userEvent, within, waitFor } from 'storybook/test';

export async function Link({ canvasElement }: { canvasElement: HTMLElement }) {
const canvas = within(canvasElement);

export async function Link(_: { canvasElement: HTMLElement }) {
const trigger = await waitFor(async () => {
const links = await within(document.body).findAllByRole('link');
const first = links[0];
if (!first) throw new Error('link not found');
return first;
});

await userEvent.hover(await trigger);
const box = trigger.getBoundingClientRect();
await userEvent.pointer({
target: trigger,
coords: {
clientX: Math.round(box.left + box.width / 6),
clientY: Math.round(box.top + box.height / 2),
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const Demo = (props: Partial<SimpleHintPopperProps>) => {
timeout={props.timeout}
visible={props.visible}
defaultVisible={props.defaultVisible}
ignorePortalsStacking={props.ignorePortalsStacking}
triggerRef={ref}
onVisibleChange={(visible) => console.log('Hint visibility changed:', visible)}
>
Expand All @@ -33,6 +34,7 @@ export const defaultProps: Partial<SimpleHintPopperProps> = {
timeout: undefined,
visible: undefined,
defaultVisible: undefined,
ignorePortalsStacking: true,
};

Demo.defaultProps = defaultProps;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const Hint: StoryObj<typeof defaultProps> = {
defaultVisible: {
control: { type: 'boolean' },
},
ignorePortalsStacking: {
control: { type: 'boolean' },
},
},
args: defaultProps,
};
Loading