From 2c2fcfc4bf05484a6b75a6bf3247368cee41ded5 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 26 Feb 2026 20:52:24 +0100 Subject: [PATCH 1/2] feat: add triggerRef prop to dropdown --- .../dropdown/__tests__/dropdown.test.tsx | 85 ++++++++++++++++++- .../dropdown/dropdown-fit-handler.ts | 2 +- src/internal/components/dropdown/index.tsx | 40 +++++++-- .../components/dropdown/interfaces.ts | 7 ++ 4 files changed, 124 insertions(+), 10 deletions(-) diff --git a/src/internal/components/dropdown/__tests__/dropdown.test.tsx b/src/internal/components/dropdown/__tests__/dropdown.test.tsx index 31dc37ede1..40a88fb1bb 100644 --- a/src/internal/components/dropdown/__tests__/dropdown.test.tsx +++ b/src/internal/components/dropdown/__tests__/dropdown.test.tsx @@ -1,11 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { act, fireEvent, render, screen } from '@testing-library/react'; +import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; + import Dropdown from '../../../../../lib/components/internal/components/dropdown'; import { calculatePosition } from '../../../../../lib/components/internal/components/dropdown/dropdown-fit-handler'; import customCssProps from '../../../../../lib/components/internal/generated/custom-css-properties'; +import { nodeBelongs } from '../../../../../lib/components/internal/utils/node-belongs'; import DropdownWrapper from '../../../../../lib/components/test-utils/dom/internal/dropdown'; const outsideId = 'outside'; @@ -433,6 +436,86 @@ describe('Dropdown Component', () => { }); }); +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), + warnOnce: jest.fn(), +})); + +afterEach(() => { + (warnOnce as jest.Mock).mockReset(); +}); + +describe('triggerRef prop', () => { + test('renders trigger without a wrapper div when triggerRef is provided', () => { + function TestComponent() { + const ref = useRef(null); + return ( + } triggerRef={ref} open={false} /> + ); + } + const { container } = render(); + // The trigger should be a direct child of the root div, not wrapped in another div + const root = container.firstElementChild!; + expect(root.firstElementChild!.tagName).toBe('BUTTON'); + }); + + test('wraps trigger in a div when triggerRef is not provided', () => { + const { container } = render(} open={false} />); + const root = container.firstElementChild!; + expect(root.firstElementChild!.tagName).toBe('DIV'); + }); + + test('warns when triggerRef is provided but trigger element has no id', async () => { + function TestComponent() { + const ref = useRef(null); + return } triggerRef={ref} open={false} />; + } + render(); + await act(() => Promise.resolve()); + expect(warnOnce).toHaveBeenCalledWith('Dropdown', expect.stringContaining('id')); + }); + + test('does not warn when triggerRef is provided and trigger element has an id', async () => { + function TestComponent() { + const ref = useRef(null); + return } triggerRef={ref} open={false} />; + } + render(); + await act(() => Promise.resolve()); + expect(warnOnce).not.toHaveBeenCalled(); + }); + + test('portal container references the trigger id via data-awsui-referrer-id when expandToViewport is used', () => { + function TestComponent() { + const ref = useRef(null); + const [open, setOpen] = useState(false); + return ( + <> +