diff --git a/.changeset/healthy-avocados-give.md b/.changeset/healthy-avocados-give.md
new file mode 100644
index 0000000000..54a9de5908
--- /dev/null
+++ b/.changeset/healthy-avocados-give.md
@@ -0,0 +1,5 @@
+---
+"@ultraviolet/ui": minor
+---
+
+New prop `searchable` and `hideOnClickItem` in `` component
diff --git a/packages/ui/src/components/Breadcrumbs/__stories__/AdvancedUsage.stories.tsx b/packages/ui/src/components/Breadcrumbs/__stories__/AdvancedUsage.stories.tsx
index 1e83b9e608..98c6d38bac 100644
--- a/packages/ui/src/components/Breadcrumbs/__stories__/AdvancedUsage.stories.tsx
+++ b/packages/ui/src/components/Breadcrumbs/__stories__/AdvancedUsage.stories.tsx
@@ -38,6 +38,8 @@ export const AdvancedUsage: StoryFn<
diff --git a/packages/ui/src/components/MenuV2/MenuContent.tsx b/packages/ui/src/components/MenuV2/MenuContent.tsx
new file mode 100644
index 0000000000..f15f1ab7f0
--- /dev/null
+++ b/packages/ui/src/components/MenuV2/MenuContent.tsx
@@ -0,0 +1,198 @@
+import styled from '@emotion/styled'
+import type { ButtonHTMLAttributes, MouseEvent, ReactNode, Ref } from 'react'
+import {
+ cloneElement,
+ forwardRef,
+ isValidElement,
+ useCallback,
+ useEffect,
+ useId,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import { Popup } from '../Popup'
+import { SearchInput } from '../SearchInput'
+import { Stack } from '../Stack'
+import { useMenu } from './MenuProvider'
+import { SIZES } from './constants'
+import { searchChildren } from './helpers'
+import type { MenuProps } from './types'
+
+const StyledPopup = styled(Popup, {
+ shouldForwardProp: prop => !['size', 'searchable'].includes(prop),
+})<{ size: keyof typeof SIZES; searchable: boolean }>`
+ background-color: ${({ theme }) => theme.colors.other.elevation.background.raised};
+ box-shadow: ${({ theme }) => `${theme.shadows.raised[0]}, ${theme.shadows.raised[1]}`};
+ padding: 0;
+
+ &[data-has-arrow='true'] {
+ &::after {
+ border-color: ${({ theme }) => theme.colors.other.elevation.background.raised}
+ transparent transparent transparent;
+ }
+ }
+
+ width: ${({ size }) => SIZES[size]};
+ max-width: none;
+ ${({ searchable }) => (searchable ? `min-width: 20rem` : null)};
+ padding: ${({ theme }) => `${theme.space['0.25']} 0`};
+`
+
+const MenuList = styled(Stack)`
+ overflow-y: auto;
+ overflow-x: hidden;
+ &:after,
+ &:before {
+ border: solid transparent;
+ border-width: 9px;
+ content: ' ';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ }
+
+ &:after {
+ border-color: transparent;
+ }
+ &:before {
+ border-color: transparent;
+ }
+ background-color: ${({ theme }) =>
+ theme.colors.other.elevation.background.raised};
+ color: ${({ theme }) => theme.colors.neutral.text};
+ border-radius: ${({ theme }) => theme.radii.default};
+ position: relative;
+`
+
+const StyledSearchInput = styled(SearchInput)`
+ padding: ${({ theme }) => theme.space['1']};
+`
+
+export const Menu = forwardRef(
+ (
+ {
+ id,
+ ariaLabel = 'Menu',
+ children,
+ disclosure,
+ hasArrow = false,
+ placement = 'bottom',
+ className,
+ 'data-testid': dataTestId,
+ maxHeight,
+ maxWidth,
+ portalTarget = document.body,
+ size = 'small',
+ triggerMethod = 'click',
+ dynamicDomRendering,
+ align,
+ searchable = false,
+ }: MenuProps,
+ ref: Ref,
+ ) => {
+ const { isVisible, setIsVisible } = useMenu()
+ const searchInputRef = useRef(null)
+ const [localChild, setLocalChild] = useState()
+ const popupRef = useRef(null)
+ const disclosureRef = useRef(null)
+ const tempId = useId()
+ const finalId = `menu-${id ?? tempId}`
+
+ // if you need dialog inside your component, use function, otherwise component is fine
+ const target = isValidElement>(
+ disclosure,
+ )
+ ? disclosure
+ : disclosure({ visible: isVisible })
+ const innerRef = useRef(target as unknown as HTMLButtonElement)
+ useImperativeHandle(ref, () => innerRef.current)
+
+ const finalDisclosure = cloneElement(target, {
+ onClick: (event: MouseEvent) => {
+ target.props.onClick?.(event)
+ setIsVisible(!isVisible)
+ },
+ 'aria-haspopup': 'dialog',
+ 'aria-expanded': isVisible,
+ // @ts-expect-error not sure how to fix this
+ ref: disclosureRef,
+ })
+
+ const onSearch = useCallback(
+ (value: string) => {
+ if (typeof children === 'object') {
+ setLocalChild(searchChildren(children, value))
+ }
+ },
+ [children],
+ )
+
+ useEffect(() => {
+ if (isVisible && searchable) {
+ setTimeout(() => {
+ searchInputRef.current?.focus()
+ }, 50)
+ }
+ }, [isVisible, searchable])
+
+ const finalChild = useMemo(() => {
+ if (typeof children === 'function') {
+ return children({ toggle: () => setIsVisible(!isVisible) })
+ }
+
+ if (searchable && localChild) {
+ return localChild
+ }
+
+ return children
+ }, [children, isVisible, localChild, searchable, setIsVisible])
+
+ return (
+ setIsVisible(false)}
+ tabIndex={-1}
+ maxHeight={maxHeight ?? '480px'}
+ maxWidth={maxWidth}
+ searchable={searchable}
+ size={size}
+ text={
+
+ {searchable && typeof children !== 'function' ? (
+
+ ) : null}
+
+ {finalChild}
+
+
+ }
+ portalTarget={portalTarget}
+ dynamicDomRendering={dynamicDomRendering}
+ align={align}
+ >
+ {finalDisclosure}
+
+ )
+ },
+)
diff --git a/packages/ui/src/components/MenuV2/MenuProvider.tsx b/packages/ui/src/components/MenuV2/MenuProvider.tsx
new file mode 100644
index 0000000000..7d288bc08c
--- /dev/null
+++ b/packages/ui/src/components/MenuV2/MenuProvider.tsx
@@ -0,0 +1,46 @@
+import type { Dispatch, ReactNode, SetStateAction } from 'react'
+import { createContext, useContext, useMemo, useState } from 'react'
+
+type MenuContextProps = {
+ hideOnClickItem: boolean
+ isVisible: boolean
+ setIsVisible: Dispatch>
+}
+
+const MenuContext = createContext({
+ hideOnClickItem: false,
+ isVisible: false,
+ setIsVisible: () => {},
+})
+
+export const useMenu = () => {
+ const context = useContext(MenuContext)
+ if (!context) throw new Error('useMenu must be used in MenuProvider')
+
+ return context
+}
+
+type MenuProviderProps = {
+ hideOnClickItem?: boolean
+ children: ReactNode
+ visible?: boolean
+}
+
+export const MenuProvider = ({
+ hideOnClickItem = false,
+ children,
+ visible = false,
+}: MenuProviderProps) => {
+ const [isVisible, setIsVisible] = useState(visible)
+
+ const values = useMemo(
+ () => ({
+ hideOnClickItem,
+ isVisible,
+ setIsVisible,
+ }),
+ [hideOnClickItem, isVisible],
+ )
+
+ return {children}
+}
diff --git a/packages/ui/src/components/MenuV2/__stories__/FunctionDisclosure.stories.tsx b/packages/ui/src/components/MenuV2/__stories__/FunctionDisclosure.stories.tsx
index bfe512166f..1137bb1bc9 100644
--- a/packages/ui/src/components/MenuV2/__stories__/FunctionDisclosure.stories.tsx
+++ b/packages/ui/src/components/MenuV2/__stories__/FunctionDisclosure.stories.tsx
@@ -1,7 +1,7 @@
import type { StoryFn } from '@storybook/react'
-import type { DisclosureProps } from '..'
import { MenuV2 } from '..'
import { Button } from '../../Button'
+import type { DisclosureProps } from '../types'
const CustomDisclosure = ({ visible }: DisclosureProps) => (
diff --git a/packages/ui/src/components/MenuV2/__stories__/Searchable.stories.tsx b/packages/ui/src/components/MenuV2/__stories__/Searchable.stories.tsx
new file mode 100644
index 0000000000..8f09d2e620
--- /dev/null
+++ b/packages/ui/src/components/MenuV2/__stories__/Searchable.stories.tsx
@@ -0,0 +1,63 @@
+import type { StoryFn } from '@storybook/react'
+import { MenuV2 } from '..'
+import { AvatarV2 } from '../../AvatarV2'
+import { Button } from '../../Button'
+import { Stack } from '../../Stack'
+
+export const Searchable: StoryFn = () => (
+
+ }
+ >
+
+
+
+ Default Project
+
+
+
+
+
+ Project 1
+
+
+
+
+
+ Project 2
+
+
+
+)
+
+Searchable.decorators = [
+ StoryComponent => (
+
+
+
+ ),
+]
diff --git a/packages/ui/src/components/MenuV2/__stories__/index.stories.tsx b/packages/ui/src/components/MenuV2/__stories__/index.stories.tsx
index 2ddd99d1ec..02e7de9273 100644
--- a/packages/ui/src/components/MenuV2/__stories__/index.stories.tsx
+++ b/packages/ui/src/components/MenuV2/__stories__/index.stories.tsx
@@ -30,6 +30,7 @@ export { Sentiments } from './Sentiments.stories'
export { Borderless } from './Borderless.stories'
export { Group } from './Group.stories'
export { Active } from './Active.stories'
+export { Searchable } from './Searchable.stories'
export { LongMenu } from './LongMenu.stories'
export { TriggerMethod } from './TriggerMethod.stories'
export { WithModal } from './WithModal.stories'
diff --git a/packages/ui/src/components/MenuV2/__tests__/__snapshots__/index.test.tsx.snap b/packages/ui/src/components/MenuV2/__tests__/__snapshots__/index.test.tsx.snap
index 23868f11b9..277ba62a88 100644
--- a/packages/ui/src/components/MenuV2/__tests__/__snapshots__/index.test.tsx.snap
+++ b/packages/ui/src/components/MenuV2/__tests__/__snapshots__/index.test.tsx.snap
@@ -800,8 +800,8 @@ exports[`Menu > placement > renders bottom 1`] = `
data-testid="testing"
>
@@ -847,8 +847,8 @@ exports[`Menu > placement > renders left 1`] = `
data-testid="testing"
>
@@ -894,8 +894,8 @@ exports[`Menu > placement > renders right 1`] = `
data-testid="testing"
>
@@ -941,8 +941,8 @@ exports[`Menu > placement > renders top 1`] = `
data-testid="testing"
>
@@ -1144,6 +1144,84 @@ exports[`Menu > renders with visible=false 1`] = `
`;
+exports[`Menu > should search on simple childs 1`] = `
+
+ .emotion-0 {
+ display: inherit;
+}
+
+.emotion-0[data-container-full-width="true"] {
+ width: 100%;
+}
+
+.emotion-0 {
+ display: inherit;
+}
+
+.emotion-0[data-container-full-width="true"] {
+ width: 100%;
+}
+
+
+
+`;
+
+exports[`Menu > should search on simple complex childs 1`] = `
+
+ .emotion-0 {
+ display: inherit;
+}
+
+.emotion-0[data-container-full-width="true"] {
+ width: 100%;
+}
+
+.emotion-0 {
+ display: inherit;
+}
+
+.emotion-0[data-container-full-width="true"] {
+ width: 100%;
+}
+
+
+
+`;
+
exports[`Menu > sizes > renders large 1`] = `
.emotion-0 {
@@ -1174,8 +1252,8 @@ exports[`Menu > sizes > renders large 1`] = `
data-testid="testing"
>
@@ -1221,8 +1299,8 @@ exports[`Menu > sizes > renders medium 1`] = `
data-testid="testing"
>
@@ -1268,8 +1346,8 @@ exports[`Menu > sizes > renders small 1`] = `
data-testid="testing"
>
diff --git a/packages/ui/src/components/MenuV2/__tests__/index.test.tsx b/packages/ui/src/components/MenuV2/__tests__/index.test.tsx
index d11300ba2f..c2baef9fe0 100644
--- a/packages/ui/src/components/MenuV2/__tests__/index.test.tsx
+++ b/packages/ui/src/components/MenuV2/__tests__/index.test.tsx
@@ -101,6 +101,103 @@ describe('Menu', () => {
})
})
+ test('should hideOnClickItem', async () => {
+ renderWithTheme(
+ ,
+ )
+ const menuButton = screen.getByRole
('button')
+ // Open Menu
+ await userEvent.click(menuButton)
+ const dialog = screen.getByRole('dialog')
+
+ await waitFor(() => {
+ expect(dialog).toBeVisible()
+ })
+
+ const item = screen.getByRole('menuitem')
+ await userEvent.click(item)
+
+ await waitFor(() => {
+ expect(dialog).not.toBeVisible()
+ })
+ })
+
+ test('should search on simple childs', async () => {
+ const { asFragment } = renderWithTheme(
+ ,
+ )
+ const menuButton = screen.getByRole('button')
+ // Open Menu
+ await userEvent.click(menuButton)
+ const dialog = screen.getByRole('dialog')
+
+ await waitFor(() => {
+ expect(dialog).toBeVisible()
+ })
+
+ expect(asFragment()).toMatchSnapshot()
+
+ const searchInput = screen.getByRole('textbox')
+ await userEvent.type(searchInput, 'Disk')
+
+ const items = screen.getAllByRole('menuitem')
+ expect(items.length).toBe(1)
+ expect(items[0]).toHaveTextContent('Disk')
+ })
+
+ test('should search on simple complex childs', async () => {
+ const { asFragment } = renderWithTheme(
+ ,
+ )
+ const menuButton = screen.getByRole('button')
+ // Open Menu
+ await userEvent.click(menuButton)
+ const dialog = screen.getByRole('dialog')
+
+ await waitFor(() => {
+ expect(dialog).toBeVisible()
+ })
+
+ expect(asFragment()).toMatchSnapshot()
+
+ const searchInput = screen.getByRole('textbox')
+ await userEvent.type(searchInput, 'Disk')
+
+ const items = screen.getAllByRole('menuitem')
+ expect(items.length).toBe(1)
+ expect(items[0]).toHaveTextContent('Disk')
+ })
+
describe('placement', () => {
test('renders top', () =>
shouldMatchEmotionSnapshot(
diff --git a/packages/ui/src/components/MenuV2/Group.tsx b/packages/ui/src/components/MenuV2/components/Group.tsx
similarity index 94%
rename from packages/ui/src/components/MenuV2/Group.tsx
rename to packages/ui/src/components/MenuV2/components/Group.tsx
index 417b489b90..e3ad31512a 100644
--- a/packages/ui/src/components/MenuV2/Group.tsx
+++ b/packages/ui/src/components/MenuV2/components/Group.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled'
import type { ReactNode } from 'react'
-import { Text } from '../Text'
+import { Text } from '../../Text'
const Container = styled.span`
padding: ${({ theme }) => `${theme.space['0.5']} ${theme.space['1.5']}`};
diff --git a/packages/ui/src/components/MenuV2/Item.tsx b/packages/ui/src/components/MenuV2/components/Item.tsx
similarity index 84%
rename from packages/ui/src/components/MenuV2/Item.tsx
rename to packages/ui/src/components/MenuV2/components/Item.tsx
index c131264a1d..24b0cb9b3c 100644
--- a/packages/ui/src/components/MenuV2/Item.tsx
+++ b/packages/ui/src/components/MenuV2/components/Item.tsx
@@ -1,8 +1,9 @@
import type { Theme } from '@emotion/react'
import styled from '@emotion/styled'
-import type { MouseEventHandler, ReactNode, Ref } from 'react'
-import { forwardRef } from 'react'
-import { Tooltip } from '../Tooltip'
+import type { MouseEvent, MouseEventHandler, ReactNode, Ref } from 'react'
+import { forwardRef, useCallback } from 'react'
+import { Tooltip } from '../../Tooltip'
+import { useMenu } from '../MenuProvider'
type MenuItemSentiment = 'neutral' | 'primary' | 'danger'
@@ -130,6 +131,21 @@ const Item = forwardRef(
},
ref,
) => {
+ const { hideOnClickItem, setIsVisible } = useMenu()
+
+ const onClickHandle = useCallback(
+ (event: MouseEvent) => {
+ if (disabled) return undefined
+ onClick?.(event)
+ if (hideOnClickItem) {
+ setIsVisible(false)
+ }
+
+ return undefined
+ },
+ [disabled, hideOnClickItem, onClick, setIsVisible],
+ )
+
if (href && !disabled) {
return (
@@ -139,11 +155,7 @@ const Item = forwardRef(
borderless
href={href}
ref={ref as Ref}
- onClick={
- disabled
- ? undefined
- : (onClick as MouseEventHandler)
- }
+ onClick={onClickHandle}
role="menuitem"
disabled={disabled}
sentiment={sentiment}
@@ -165,7 +177,12 @@ const Item = forwardRef(
ref={ref as Ref}
role="menuitem"
disabled={disabled}
- onClick={onClick}
+ onClick={event => {
+ onClick?.(event)
+ if (hideOnClickItem) {
+ setIsVisible(false)
+ }
+ }}
borderless={borderless}
sentiment={sentiment}
className={className}
diff --git a/packages/ui/src/components/MenuV2/constants.ts b/packages/ui/src/components/MenuV2/constants.ts
new file mode 100644
index 0000000000..e0473ad9c0
--- /dev/null
+++ b/packages/ui/src/components/MenuV2/constants.ts
@@ -0,0 +1,5 @@
+export const SIZES = {
+ small: '180px',
+ medium: '280px',
+ large: '380px',
+}
diff --git a/packages/ui/src/components/MenuV2/helpers.ts b/packages/ui/src/components/MenuV2/helpers.ts
new file mode 100644
index 0000000000..778244b748
--- /dev/null
+++ b/packages/ui/src/components/MenuV2/helpers.ts
@@ -0,0 +1,30 @@
+import { Children, type ReactNode, isValidElement } from 'react'
+
+/**
+ * Search inside a children (React Element) recursively until a result is found
+ */
+export const searchChildren = (
+ children: ReactNode,
+ searchString: string,
+): ReactNode[] => {
+ const matches: ReactNode[] = []
+ const searchRegex = new RegExp(searchString, 'i')
+
+ Children.forEach(children, child => {
+ if (typeof child === 'string' && child.match(searchRegex)) {
+ matches.push(child)
+ } else if (isValidElement(child)) {
+ const childProps = child.props as { children: ReactNode }
+
+ // Recursively search the children of this element
+ const childMatches = searchChildren(childProps.children, searchString)
+
+ if (childMatches.length > 0) {
+ // If any matches are found within this child's children, push the entire child element
+ matches.push(child)
+ }
+ }
+ })
+
+ return matches
+}
diff --git a/packages/ui/src/components/MenuV2/index.tsx b/packages/ui/src/components/MenuV2/index.tsx
index 56c89c753a..1c7e7a6d17 100644
--- a/packages/ui/src/components/MenuV2/index.tsx
+++ b/packages/ui/src/components/MenuV2/index.tsx
@@ -1,209 +1,22 @@
-import styled from '@emotion/styled'
-import type {
- ButtonHTMLAttributes,
- ComponentProps,
- MouseEvent,
- ReactElement,
- ReactNode,
- Ref,
-} from 'react'
-import {
- cloneElement,
- forwardRef,
- isValidElement,
- useId,
- useImperativeHandle,
- useRef,
- useState,
-} from 'react'
-import { Popup } from '../Popup'
-import { Stack } from '../Stack'
-import { Group } from './Group'
-import Item from './Item'
-
-const SIZES = {
- small: '180px',
- medium: '280px',
- large: '380px',
-}
-
-export type DisclosureProps = { visible: boolean }
-
-type DisclosureElement =
- | ((
- disclosure: DisclosureProps,
- ) => ReactElement>)
- | (ReactElement> & {
- ref?: Ref
- })
-
-const StyledPopup = styled(Popup, {
- shouldForwardProp: prop => !['size'].includes(prop),
-})<{ size: keyof typeof SIZES }>`
- background-color: ${({ theme }) => theme.colors.other.elevation.background.raised};
- box-shadow: ${({ theme }) => `${theme.shadows.raised[0]}, ${theme.shadows.raised[1]}`};
- padding: 0;
-
- &[data-has-arrow='true'] {
- &::after {
- border-color: ${({ theme }) => theme.colors.other.elevation.background.raised}
- transparent transparent transparent;
- }
- }
-
- width: ${({ size }) => SIZES[size]};
- max-width: none;
- padding: ${({ theme }) => `${theme.space['0.25']} 0`};
-`
-
-const MenuList = styled(Stack)`
- overflow-y: auto;
- overflow-x: hidden;
- &:after,
- &:before {
- border: solid transparent;
- border-width: 9px;
- content: ' ';
- height: 0;
- width: 0;
- position: absolute;
- pointer-events: none;
- }
-
- &:after {
- border-color: transparent;
- }
- &:before {
- border-color: transparent;
- }
- background-color: ${({ theme }) =>
- theme.colors.other.elevation.background.raised};
- color: ${({ theme }) => theme.colors.neutral.text};
- border-radius: ${({ theme }) => theme.radii.default};
- position: relative;
-`
-
-type ChildMenuProps = {
- toggle: () => void
-}
-
-type MenuProps = {
- id?: string
- ariaLabel?: string
- children?: ReactNode | (({ toggle }: ChildMenuProps) => ReactNode)
- className?: string
- disclosure: DisclosureElement
- hasArrow?: boolean
- visible?: boolean
- 'data-testid'?: string
- maxHeight?: string
- /**
- * @deprecated: use `size` instead
- */
- maxWidth?: string
- /**
- * By default, the portal target is children container or document.body if children is a function. You can override this
- * behavior by setting a portalTarget prop.
- */
- portalTarget?: HTMLElement
- size?: keyof typeof SIZES
- /**
- * The behavior of the menu when it is opened. If set to `click`, the menu will open when the user clicks on the disclosure.
- * If set to `hover`, the menu will open when the user hovers over the disclosure.
- */
- triggerMethod?: 'click' | 'hover'
-} & Pick<
- ComponentProps,
- 'placement' | 'dynamicDomRendering' | 'align'
->
-
-const FwdMenu = forwardRef(
+import type { Ref } from 'react'
+import { forwardRef } from 'react'
+import { Menu } from './MenuContent'
+import { MenuProvider } from './MenuProvider'
+import { Group } from './components/Group'
+import Item from './components/Item'
+import type { MenuProps } from './types'
+
+const Component = forwardRef(
(
- {
- id,
- ariaLabel = 'Menu',
- children,
- disclosure,
- hasArrow = false,
- placement = 'bottom',
- visible = false,
- className,
- 'data-testid': dataTestId,
- maxHeight,
- maxWidth,
- portalTarget = document.body,
- size = 'small',
- triggerMethod = 'click',
- dynamicDomRendering,
- align,
- }: MenuProps,
- ref: Ref,
- ) => {
- const [isVisible, setIsVisible] = useState(visible)
- const popupRef = useRef(null)
- const disclosureRef = useRef(null)
- const tempId = useId()
- const finalId = `menu-${id ?? tempId}`
-
- // if you need dialog inside your component, use function, otherwise component is fine
- const target = isValidElement>(
- disclosure,
- )
- ? disclosure
- : disclosure({ visible: isVisible })
- const innerRef = useRef(target as unknown as HTMLButtonElement)
- useImperativeHandle(ref, () => innerRef.current)
-
- const finalDisclosure = cloneElement(target, {
- onClick: (event: MouseEvent) => {
- target.props.onClick?.(event)
- setIsVisible(!isVisible)
- },
- 'aria-haspopup': 'dialog',
- 'aria-expanded': isVisible,
- // @ts-expect-error not sure how to fix this
- ref: disclosureRef,
- })
-
- return (
- setIsVisible(false)}
- tabIndex={-1}
- maxHeight={maxHeight ?? '480px'}
- maxWidth={maxWidth}
- size={size}
- text={
-
- {typeof children === 'function'
- ? children({ toggle: () => setIsVisible(!isVisible) })
- : children}
-
- }
- portalTarget={portalTarget}
- dynamicDomRendering={dynamicDomRendering}
- align={align}
- >
- {finalDisclosure}
-
- )
- },
+ { children, visible, hideOnClickItem, ...props }: MenuProps,
+ ref: Ref,
+ ) => (
+
+
+
+ ),
)
-/**
- * A menu is a widget that offers a list of choices to the user, such as a set of actions or functions.
- * A menu is usually opened, or made visible, by activating a menu button, choosing an item in a menu that opens a
- * sub menu, or by invoking a command, such as `Shift + F10` on Windows, that opens a context specific menu.
- * When a user activates a choice in a menu, the menu usually closes unless the choice opened a submenu.
- */
-export const MenuV2 = Object.assign(FwdMenu, { Item, Group })
+export const MenuV2 = Object.assign(Component, { Item, Group })
diff --git a/packages/ui/src/components/MenuV2/types.ts b/packages/ui/src/components/MenuV2/types.ts
new file mode 100644
index 0000000000..aa8fdcad7b
--- /dev/null
+++ b/packages/ui/src/components/MenuV2/types.ts
@@ -0,0 +1,65 @@
+import type {
+ ButtonHTMLAttributes,
+ ComponentProps,
+ ReactElement,
+ ReactNode,
+ Ref,
+} from 'react'
+import type { Popup } from '../Popup'
+import type { SIZES } from './constants'
+
+type ChildMenuProps = {
+ toggle: () => void
+}
+
+type DisclosureElement =
+ | ((
+ disclosure: DisclosureProps,
+ ) => ReactElement>)
+ | (ReactElement> & {
+ ref?: Ref
+ })
+
+export type DisclosureProps = { visible: boolean }
+
+export type MenuProps = {
+ id?: string
+ ariaLabel?: string
+ /*
+ * CHILDREN AS FUNCTION IS DEPRECATED: use `hideOnClickWithin` prop instead
+ */
+ children?: ReactNode | (({ toggle }: ChildMenuProps) => ReactNode)
+ className?: string
+ disclosure: DisclosureElement
+ hasArrow?: boolean
+ visible?: boolean
+ 'data-testid'?: string
+ maxHeight?: string
+ /**
+ * @deprecated: use `size` instead
+ */
+ maxWidth?: string
+ /**
+ * By default, the portal target is children container or document.body if children is a function. You can override this
+ * behavior by setting a portalTarget prop.
+ */
+ portalTarget?: HTMLElement
+ size?: keyof typeof SIZES
+ /**
+ * The behavior of the menu when it is opened. If set to `click`, the menu will open when the user clicks on the disclosure.
+ * If set to `hover`, the menu will open when the user hovers over the disclosure.
+ */
+ triggerMethod?: 'click' | 'hover'
+ /**
+ * If set to true, the menu will be searchable. This will add a search input at the top of the menu.
+ * This doesn't work when children is a function.
+ */
+ searchable?: boolean
+ /**
+ * When set to true the menu will automatically close when a `MenuV2.Item` is clicked.
+ */
+ hideOnClickItem?: boolean
+} & Pick<
+ ComponentProps,
+ 'placement' | 'dynamicDomRendering' | 'align'
+>
diff --git a/packages/ui/src/components/Tabs/TabMenuItem.tsx b/packages/ui/src/components/Tabs/TabMenuItem.tsx
index 938fbcb451..9ccdb69162 100644
--- a/packages/ui/src/components/Tabs/TabMenuItem.tsx
+++ b/packages/ui/src/components/Tabs/TabMenuItem.tsx
@@ -1,10 +1,10 @@
import styled from '@emotion/styled'
import type { ComponentProps } from 'react'
import { useMemo } from 'react'
-import Item from '../MenuV2/Item'
+import { MenuV2 } from '../MenuV2'
import { useTabsContext } from './TabsContext'
-const StyledMenuItem = styled(Item)`
+const StyledMenuItem = styled(MenuV2.Item)`
&[aria-selected='true'] {
color: ${({ theme }) => theme.colors.primary.text};
}
diff --git a/packages/ui/src/components/Tabs/__tests__/__snapshots__/index.test.tsx.snap b/packages/ui/src/components/Tabs/__tests__/__snapshots__/index.test.tsx.snap
index cd5d25fe22..aafa6f9cdd 100644
--- a/packages/ui/src/components/Tabs/__tests__/__snapshots__/index.test.tsx.snap
+++ b/packages/ui/src/components/Tabs/__tests__/__snapshots__/index.test.tsx.snap
@@ -2435,31 +2435,33 @@ exports[`Tabs > renders correctly with Tabs with prop 1`] = `
role="dialog"
style="opacity: 1; animation: none;"
>
-
+
-
-
-
-
+
+
- Test 2
-
+
+
@@ -2984,399 +2986,401 @@ exports[`Tabs > renders correctly with Tabs with prop 1`] = `
role="dialog"
style="opacity: 1; animation: none;"
>
-
+
-
-
-
-
-
-
+
- Counter
-
- 12
-
+ Counter
+
+ 12
+
+
-
-
-
-
+
- Counter no items
-
- 0
-
+ Counter no items
+
+ 0
+
+
-
-
-
-
+
- Conter and badge
-
- 12
-
-
- Badge
-
+ Conter and badge
+
+ 12
+
+
+ Badge
+
+
-
-
-
-
+
- Badge
-
Badge
-
+
+ Badge
+
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
-
+
- Very long tab name
+
+ Very long tab name
+
-
-
-
+
-
- Blabla
-
-
+ Blabla
+
+
+
@@ -3575,31 +3579,33 @@ exports[`Tabs > renders correctly with Tabs with prop 1`] = `
role="dialog"
style="opacity: 1; animation: none;"
>
-
+
-
- Test
-
-
-
-
+ Test
+
+
+
- Test 2
-
+
+ Test 2
+
+