Skip to content

Commit 04440e6

Browse files
fix(aiLabel): link on top of popover fails (#21928)
* fix(aiLabel): link on top over fails * fix(toggletip): update test and logic to include nested dom * fix(AIlabel): test story for the change * fix(Ailabel): story loadig issue * fix(Ailabel): story loadig issue 2 * fix(AILabel): move storyspecific component to story folder * fix(AILabel): refactor outside click logic * refactor(toggletip): clean up comments * fix(AILabel): shadowDOM test case update * fix(tooltip): refactor * fix(Ailabel): remove test only updates
1 parent 597001a commit 04440e6

2 files changed

Lines changed: 139 additions & 4 deletions

File tree

packages/react/src/components/Toggletip/__tests__/Toggletip-test.js

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2025
2+
* Copyright IBM Corp. 2016, 2026
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -514,6 +514,132 @@ describe('Toggletip', () => {
514514
expect(onKeyDown).toHaveBeenCalledTimes(1);
515515
});
516516
});
517+
518+
describe('Shadow DOM Support', () => {
519+
it('should not close when clicking inside the toggletip in Shadow DOM context', async () => {
520+
const shadowHost = document.createElement('div');
521+
document.body.appendChild(shadowHost);
522+
523+
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
524+
525+
const shadowContainer = document.createElement('div');
526+
shadowRoot.appendChild(shadowContainer);
527+
528+
const { container, getByTestId, getByRole } = render(
529+
<Toggletip data-testid="toggletip" defaultOpen>
530+
<ToggletipButton label="Show information">test</ToggletipButton>
531+
<ToggletipContent>
532+
<div data-testid="inner-content">Content</div>
533+
</ToggletipContent>
534+
</Toggletip>,
535+
{ container: shadowContainer }
536+
);
537+
538+
const innerContent = getByTestId('inner-content');
539+
const toggletip = getByTestId('toggletip');
540+
541+
const mockEvent = new MouseEvent('mousedown', { bubbles: true });
542+
Object.defineProperty(mockEvent, 'composedPath', {
543+
value: () => [
544+
innerContent,
545+
toggletip,
546+
shadowContainer,
547+
shadowRoot,
548+
shadowHost,
549+
document.body,
550+
],
551+
});
552+
Object.defineProperty(mockEvent, 'target', {
553+
value: shadowHost,
554+
});
555+
556+
fireEvent(document, mockEvent);
557+
558+
expect(getByRole('button')).toHaveAttribute('aria-expanded', 'true');
559+
expect(toggletip).toHaveClass(`${prefix}--toggletip--open`);
560+
561+
document.body.removeChild(shadowHost);
562+
});
563+
564+
it('should close when clicking outside the toggletip in Shadow DOM context', async () => {
565+
const shadowHost = document.createElement('div');
566+
document.body.appendChild(shadowHost);
567+
568+
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
569+
570+
const shadowContainer = document.createElement('div');
571+
shadowRoot.appendChild(shadowContainer);
572+
573+
const { getByTestId, getByRole } = render(
574+
<Toggletip data-testid="toggletip" defaultOpen>
575+
<ToggletipButton label="Show information">test</ToggletipButton>
576+
<ToggletipContent>
577+
<div data-testid="inner-content">Content</div>
578+
</ToggletipContent>
579+
</Toggletip>,
580+
{ container: shadowContainer }
581+
);
582+
583+
const toggletip = getByTestId('toggletip');
584+
585+
const outsideElement = document.createElement('div');
586+
document.body.appendChild(outsideElement);
587+
588+
const mockEvent = new MouseEvent('mousedown', { bubbles: true });
589+
Object.defineProperty(mockEvent, 'composedPath', {
590+
value: () => [outsideElement, document.body],
591+
});
592+
Object.defineProperty(mockEvent, 'target', {
593+
value: outsideElement,
594+
});
595+
596+
fireEvent(document, mockEvent);
597+
598+
expect(getByRole('button')).toHaveAttribute('aria-expanded', 'false');
599+
expect(toggletip).not.toHaveClass(`${prefix}--toggletip--open`);
600+
601+
document.body.removeChild(outsideElement);
602+
document.body.removeChild(shadowHost);
603+
});
604+
605+
it('should handle clicks when composedPath is not available (fallback to event.target)', async () => {
606+
const shadowHost = document.createElement('div');
607+
document.body.appendChild(shadowHost);
608+
609+
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
610+
611+
const shadowContainer = document.createElement('div');
612+
shadowRoot.appendChild(shadowContainer);
613+
614+
const { getByTestId, getByRole } = render(
615+
<Toggletip data-testid="toggletip" defaultOpen>
616+
<ToggletipButton label="Show information">test</ToggletipButton>
617+
<ToggletipContent>
618+
<div data-testid="inner-content">Content</div>
619+
</ToggletipContent>
620+
</Toggletip>,
621+
{ container: shadowContainer }
622+
);
623+
624+
const innerContent = getByTestId('inner-content');
625+
const toggletip = getByTestId('toggletip');
626+
627+
const mockEvent = new MouseEvent('mousedown', { bubbles: true });
628+
Object.defineProperty(mockEvent, 'composedPath', {
629+
value: undefined,
630+
});
631+
Object.defineProperty(mockEvent, 'target', {
632+
value: innerContent,
633+
});
634+
635+
fireEvent(document, mockEvent);
636+
637+
expect(getByRole('button')).toHaveAttribute('aria-expanded', 'true');
638+
expect(toggletip).toHaveClass(`${prefix}--toggletip--open`);
639+
640+
document.body.removeChild(shadowHost);
641+
});
642+
});
517643
});
518644
});
519645

packages/react/src/components/Toggletip/index.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,25 @@ export function Toggletip<E extends ElementType = 'span'>({
172172
});
173173

174174
useEffect(() => {
175-
if (!ref.current) return;
175+
if (!open || !ref.current) return;
176176

177177
const targetDocument = ref.current.ownerDocument || document;
178178
const eventType: 'pointerdown' | 'mousedown' =
179179
'PointerEvent' in window ? 'pointerdown' : 'mousedown';
180180

181181
const handleOutsideClick = (event: MouseEvent | PointerEvent) => {
182-
const node = event.target as Node | null;
182+
const { current } = ref;
183+
if (!current) return;
183184

184-
if (open && node && !ref.current?.contains(node)) {
185+
const isInsideCurrent = (target: EventTarget | null): target is Node =>
186+
target instanceof Node && current.contains(target);
187+
188+
const isInside =
189+
isInsideCurrent(event.target) ||
190+
(typeof event.composedPath === 'function' &&
191+
event.composedPath().some(isInsideCurrent));
192+
193+
if (!isInside) {
185194
setOpen(false);
186195
}
187196
};

0 commit comments

Comments
 (0)