From db3f6563bda37e5f273493ea853e88a4be926681 Mon Sep 17 00:00:00 2001
From: Kevin Cormier <kcormier@redhat.com>
Date: Fri, 24 Mar 2023 17:13:34 -0400
Subject: [PATCH] WIP: Convert AcmDropdown to use Menu

Signed-off-by: Kevin Cormier <kcormier@redhat.com>
---
 frontend/src/components/Rbac.tsx              |  42 ++-
 frontend/src/routes/Applications/Overview.tsx |   6 +-
 .../components/PolicyActionDropdown.tsx       | 302 +++++++++---------
 .../components/ClusterActionDropdown.tsx      |   3 +-
 .../DownloadConfigurationDropdown.tsx         |   2 +
 .../AcmDropdown/AcmDropdown.stories.tsx       |   2 +-
 .../ui-components/AcmDropdown/AcmDropdown.tsx | 269 ++++++++++------
 7 files changed, 364 insertions(+), 262 deletions(-)

diff --git a/frontend/src/components/Rbac.tsx b/frontend/src/components/Rbac.tsx
index 798aa3e79be..357908d0403 100644
--- a/frontend/src/components/Rbac.tsx
+++ b/frontend/src/components/Rbac.tsx
@@ -2,9 +2,11 @@
 
 import { makeStyles } from '@mui/styles'
 import { createSubjectAccessReview, ResourceAttributes } from '../resources'
-import { AcmButton, AcmDropdown } from '../ui-components'
-import { useEffect, useState } from 'react'
+import { AcmButton, AcmDropdown, AcmDropdownItems } from '../ui-components'
+import { useCallback, useEffect, useMemo, useState } from 'react'
 import { useTranslation } from '../lib/acm-i18next'
+import { Actions } from '../routes/Infrastructure/Clusters/ManagedClusters/CreateCluster/components/assisted-installer/hypershift/common/common'
+import { DropdownPosition } from '@patternfly/react-core'
 
 type RbacDropdownProps<T = unknown> = {
   actions: Actions<T>[]
@@ -14,17 +16,23 @@ type RbacDropdownProps<T = unknown> = {
   id: string
   isDisabled?: boolean
   tooltip?: string
+  dropdownPosition?: DropdownPosition
 }
 
-type Actions<T = unknown> = {
-  id: string
-  text: React.ReactNode
-  isAriaDisabled?: boolean
-  tooltip?: string
-  click: (item: T) => void
+type Actions<T = unknown> = AcmDropdownItems & {
+  flyoutMenu?: Actions<T>[]
+  click?: (item: T) => void
   rbac?: ResourceAttributes[] | Promise<ResourceAttributes>[]
 }
 
+function flattenAction<T>(action: Actions<T>): Actions<T> | Actions<T>[] {
+  return action.flyoutMenu ? flattenActions(action.flyoutMenu) : action
+}
+
+function flattenActions<T>(actions: Actions<T>[]): Actions<T>[] {
+  return actions.flatMap(flattenAction)
+}
+
 export function RbacDropdown<T = unknown>(props: RbacDropdownProps<T>) {
   const { t } = useTranslation()
   const [actions, setActions] = useState<Actions<T>[]>([])
@@ -36,10 +44,19 @@ export function RbacDropdown<T = unknown>(props: RbacDropdownProps<T>) {
     }
   }, [actions, props.actions])
 
-  const onSelect = (id: string) => {
-    const action = props.actions.find((a) => a.id === id)
-    return action?.click(props.item)
-  }
+  const actionsWithFlyoutActions = useMemo(() => {
+    return flattenActions<T>(actions)
+  }, [actions])
+
+  const onSelect = useCallback(
+    (id: string) => {
+      const action = actionsWithFlyoutActions.find((a) => a.id === id)
+      if (action?.click) {
+        action.click(props.item)
+      }
+    },
+    [actionsWithFlyoutActions, props.item]
+  )
 
   const onToggle = async (isOpen?: boolean) => {
     if (isOpen) {
@@ -75,6 +92,7 @@ export function RbacDropdown<T = unknown>(props: RbacDropdownProps<T>) {
       id={props.id}
       onSelect={onSelect}
       dropdownItems={actions}
+      dropdownPosition={props.dropdownPosition}
       isKebab={props.isKebab}
       isPlain={true}
       text={props.text}
diff --git a/frontend/src/routes/Applications/Overview.tsx b/frontend/src/routes/Applications/Overview.tsx
index bbe36b4bde2..c7a9e85ab52 100644
--- a/frontend/src/routes/Applications/Overview.tsx
+++ b/frontend/src/routes/Applications/Overview.tsx
@@ -1,6 +1,5 @@
 /* Copyright Contributors to the Open Cluster Management project */
-
-import { PageSection, Text, TextContent, TextVariants, Tooltip } from '@patternfly/react-core'
+import { DropdownPosition, PageSection, Text, TextContent, TextVariants, Tooltip } from '@patternfly/react-core'
 import { ExternalLinkAltIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons'
 import { cellWidth } from '@patternfly/react-table'
 import {
@@ -1023,8 +1022,7 @@ export default function ApplicationsOverview() {
         isKebab={false}
         isPlain={false}
         isPrimary={true}
-        // tooltipPosition={tableDropdown.tooltipPosition}
-        // dropdownPosition={DropdownPosition.left}
+        dropdownPosition={DropdownPosition.left}
       />
     ),
     [canCreateApplication, history, t]
diff --git a/frontend/src/routes/Governance/components/PolicyActionDropdown.tsx b/frontend/src/routes/Governance/components/PolicyActionDropdown.tsx
index e8aa2db58b5..4164ee66eab 100644
--- a/frontend/src/routes/Governance/components/PolicyActionDropdown.tsx
+++ b/frontend/src/routes/Governance/components/PolicyActionDropdown.tsx
@@ -74,159 +74,175 @@ export function PolicyActionDropdown(props: {
         rbac: [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)],
       },
       {
-        id: 'enable-policy',
-        text: t('Enable'),
-        tooltip: item.policy.spec.disabled ? t('Enable policy') : t('Policy is already enabled'),
-        isAriaDisabled: item.policy.spec.disabled === false,
-        click: (item: PolicyTableItem) => {
-          setModalProps({
-            open: true,
-            title: t('policy.modal.title.enable'),
-            action: t('policy.table.actions.enable'),
-            processing: t('policy.table.actions.enabling'),
-            items: [item],
-            emptyState: undefined, // there is always 1 item supplied
-            description: t('policy.modal.message.enable'),
-            columns: bulkModalStatusColumns,
-            keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
-            actionFn: (item: PolicyTableItem) => {
-              return patchResource(
-                {
-                  apiVersion: PolicyApiVersion,
-                  kind: PolicyKind,
-                  metadata: {
-                    name: item.policy.metadata.name,
-                    namespace: item.policy.metadata.namespace,
+        id: 'status-policy',
+        text: t('Status'),
+        rbac: [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)],
+        flyoutMenu: [
+          {
+            id: 'enable-policy',
+            text: t('Enable'),
+            tooltip: item.policy.spec.disabled ? t('Enable policy') : t('Policy is already enabled'),
+            isSelected: !item.policy.spec.disabled,
+            click: (item: PolicyTableItem) => {
+              if (item.policy.spec.disabled) {
+                setModalProps({
+                  open: true,
+                  title: t('policy.modal.title.enable'),
+                  action: t('policy.table.actions.enable'),
+                  processing: t('policy.table.actions.enabling'),
+                  items: [item],
+                  emptyState: undefined, // there is always 1 item supplied
+                  description: t('policy.modal.message.enable'),
+                  columns: bulkModalStatusColumns,
+                  keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
+                  actionFn: (item: PolicyTableItem) => {
+                    return patchResource(
+                      {
+                        apiVersion: PolicyApiVersion,
+                        kind: PolicyKind,
+                        metadata: {
+                          name: item.policy.metadata.name,
+                          namespace: item.policy.metadata.namespace,
+                        },
+                      } as Policy,
+                      [{ op: 'replace', path: '/spec/disabled', value: false }]
+                    )
                   },
-                } as Policy,
-                [{ op: 'replace', path: '/spec/disabled', value: false }]
-              )
-            },
-            close: () => {
-              setModalProps({ open: false })
-            },
-            hasExternalResources: item.source !== 'Local',
-          })
-        },
-        rbac: item.policy.spec.disabled ? [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)] : undefined,
-      },
-      {
-        id: 'disable-policy',
-        text: t('policy.table.actions.disable'),
-        tooltip: item.policy.spec.disabled ? t('Policy is already disabled') : t('Disable policy'),
-        isAriaDisabled: item.policy.spec.disabled === true,
-        click: (item: PolicyTableItem) => {
-          setModalProps({
-            open: true,
-            title: t('policy.modal.title.disable'),
-            action: t('policy.table.actions.disable'),
-            processing: t('policy.table.actions.disabling'),
-            items: [item],
-            emptyState: undefined, // there is always 1 item supplied
-            description: t('policy.modal.message.disable'),
-            columns: bulkModalStatusColumns,
-            keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
-            actionFn: (item) => {
-              return patchResource(
-                {
-                  apiVersion: PolicyApiVersion,
-                  kind: PolicyKind,
-                  metadata: {
-                    name: item.policy.metadata.name,
-                    namespace: item.policy.metadata.namespace,
+                  close: () => {
+                    setModalProps({ open: false })
                   },
-                } as Policy,
-                [{ op: 'replace', path: '/spec/disabled', value: true }]
-              )
-            },
-            close: () => {
-              setModalProps({ open: false })
+                  hasExternalResources: item.source !== 'Local',
+                })
+              }
             },
-            hasExternalResources: item.source !== 'Local',
-          })
-        },
-        rbac: item.policy.spec.disabled ? undefined : [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)],
-      },
-      {
-        id: 'inform-policy',
-        text: t('policy.table.actions.inform'),
-        tooltip: policyRemediationAction === 'inform' ? t('Already informing') : t('Inform policy'),
-        addSeparator: true,
-        isAriaDisabled: policyRemediationAction === 'inform',
-        click: (item: PolicyTableItem) => {
-          setModalProps({
-            open: true,
-            title: t('policy.modal.title.inform'),
-            action: t('policy.table.actions.inform'),
-            processing: t('policy.table.actions.informing'),
-            items: [item],
-            emptyState: undefined, // there is always 1 item supplied
-            description: t('policy.modal.message.inform'),
-            columns: bulkModalRemediationColumns,
-            keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
-            actionFn: (item) => {
-              return patchResource(
-                {
-                  apiVersion: PolicyApiVersion,
-                  kind: PolicyKind,
-                  metadata: {
-                    name: item.policy.metadata.name,
-                    namespace: item.policy.metadata.namespace,
+          },
+          {
+            id: 'disable-policy',
+            text: t('policy.table.actions.disable'),
+            tooltip: item.policy.spec.disabled ? t('Policy is already disabled') : t('Disable policy'),
+            isSelected: item.policy.spec.disabled,
+            click: (item: PolicyTableItem) => {
+              if (!item.policy.spec.disabled) {
+                setModalProps({
+                  open: true,
+                  title: t('policy.modal.title.disable'),
+                  action: t('policy.table.actions.disable'),
+                  processing: t('policy.table.actions.disabling'),
+                  items: [item],
+                  emptyState: undefined, // there is always 1 item supplied
+                  description: t('policy.modal.message.disable'),
+                  columns: bulkModalStatusColumns,
+                  keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
+                  actionFn: (item) => {
+                    return patchResource(
+                      {
+                        apiVersion: PolicyApiVersion,
+                        kind: PolicyKind,
+                        metadata: {
+                          name: item.policy.metadata.name,
+                          namespace: item.policy.metadata.namespace,
+                        },
+                      } as Policy,
+                      [{ op: 'replace', path: '/spec/disabled', value: true }]
+                    )
                   },
-                } as Policy,
-                [{ op: 'replace', path: '/spec/remediationAction', value: 'inform' }]
-              )
-            },
-            close: () => {
-              setModalProps({ open: false })
+                  close: () => {
+                    setModalProps({ open: false })
+                  },
+                  hasExternalResources: item.source !== 'Local',
+                })
+              }
             },
-            hasExternalResources: item.source !== 'Local',
-          })
-        },
-        rbac:
-          policyRemediationAction === 'inform'
-            ? undefined
-            : [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)],
+          },
+        ],
       },
       {
-        id: 'enforce-policy',
-        text: t('policy.table.actions.enforce'),
-        tooltip: policyRemediationAction === 'enforce' ? t('Already enforcing') : t('Enforce policy'),
-        isAriaDisabled: policyRemediationAction === 'enforce',
-        click: (item: PolicyTableItem) => {
-          setModalProps({
-            open: true,
-            title: t('policy.modal.title.enforce'),
-            action: t('policy.table.actions.enforce'),
-            processing: t('policy.table.actions.enforcing'),
-            items: [item],
-            emptyState: undefined, // there is always 1 item supplied
-            description: t('policy.modal.message.enforce'),
-            columns: bulkModalRemediationColumns,
-            keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
-            actionFn: (item) => {
-              return patchResource(
-                {
-                  apiVersion: PolicyApiVersion,
-                  kind: PolicyKind,
-                  metadata: {
-                    name: item.policy.metadata.name,
-                    namespace: item.policy.metadata.namespace,
+        id: 'remediation-policy',
+        text: t('Remediation'),
+        rbac: [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)],
+        flyoutMenu: [
+          {
+            id: 'inform-policy',
+            text: t('policy.table.actions.inform'),
+            tooltip: policyRemediationAction === 'inform' ? t('Already informing') : t('Inform policy'),
+            addSeparator: true,
+            isSelected: policyRemediationAction === 'inform',
+            click: (item: PolicyTableItem) => {
+              if (policyRemediationAction !== 'inform') {
+                setModalProps({
+                  open: true,
+                  title: t('policy.modal.title.inform'),
+                  action: t('policy.table.actions.inform'),
+                  processing: t('policy.table.actions.informing'),
+                  items: [item],
+                  emptyState: undefined, // there is always 1 item supplied
+                  description: t('policy.modal.message.inform'),
+                  columns: bulkModalRemediationColumns,
+                  keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
+                  actionFn: (item) => {
+                    return patchResource(
+                      {
+                        apiVersion: PolicyApiVersion,
+                        kind: PolicyKind,
+                        metadata: {
+                          name: item.policy.metadata.name,
+                          namespace: item.policy.metadata.namespace,
+                        },
+                      } as Policy,
+                      [{ op: 'replace', path: '/spec/remediationAction', value: 'inform' }]
+                    )
+                  },
+                  close: () => {
+                    setModalProps({ open: false })
                   },
-                } as Policy,
-                [{ op: 'replace', path: '/spec/remediationAction', value: 'enforce' }]
-              )
+                  hasExternalResources: item.source !== 'Local',
+                })
+              }
             },
-            close: () => {
-              setModalProps({ open: false })
+            rbac:
+              policyRemediationAction === 'inform'
+                ? undefined
+                : [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)],
+          },
+          {
+            id: 'enforce-policy',
+            text: t('policy.table.actions.enforce'),
+            tooltip: policyRemediationAction === 'enforce' ? t('Already enforcing') : t('Enforce policy'),
+            isSelected: policyRemediationAction === 'enforce',
+            click: (item: PolicyTableItem) => {
+              if (policyRemediationAction !== 'enforce') {
+                setModalProps({
+                  open: true,
+                  title: t('policy.modal.title.enforce'),
+                  action: t('policy.table.actions.enforce'),
+                  processing: t('policy.table.actions.enforcing'),
+                  items: [item],
+                  emptyState: undefined, // there is always 1 item supplied
+                  description: t('policy.modal.message.enforce'),
+                  columns: bulkModalRemediationColumns,
+                  keyFn: (item: PolicyTableItem) => item.policy.metadata.uid as string,
+                  actionFn: (item) => {
+                    return patchResource(
+                      {
+                        apiVersion: PolicyApiVersion,
+                        kind: PolicyKind,
+                        metadata: {
+                          name: item.policy.metadata.name,
+                          namespace: item.policy.metadata.namespace,
+                        },
+                      } as Policy,
+                      [{ op: 'replace', path: '/spec/remediationAction', value: 'enforce' }]
+                    )
+                  },
+                  close: () => {
+                    setModalProps({ open: false })
+                  },
+                  hasExternalResources: item.source !== 'Local',
+                })
+              }
             },
-            hasExternalResources: item.source !== 'Local',
-          })
-        },
-        rbac:
-          policyRemediationAction === 'enforce'
-            ? undefined
-            : [rbacPatch(PolicyDefinition, item.policy.metadata.namespace)],
+          },
+        ],
       },
       {
         id: 'edit-policy',
diff --git a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/ClusterActionDropdown.tsx b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/ClusterActionDropdown.tsx
index 87ed5a8141c..eca70cdcc47 100644
--- a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/ClusterActionDropdown.tsx
+++ b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/ClusterActionDropdown.tsx
@@ -1,6 +1,6 @@
 /* Copyright Contributors to the Open Cluster Management project */
 
-import { Text, TextContent, TextVariants } from '@patternfly/react-core'
+import { DropdownPosition, Text, TextContent, TextVariants } from '@patternfly/react-core'
 import { AcmInlineProvider } from '../../../../../ui-components'
 import { useContext, useMemo, useState } from 'react'
 import { useHistory } from 'react-router'
@@ -410,6 +410,7 @@ export function ClusterActionDropdown(props: { cluster: Cluster; isKebab: boolea
           isKebab={props.isKebab}
           text={t('actions')}
           actions={actions}
+          dropdownPosition={DropdownPosition.right}
         />
       )}
       <ScaleUpDialog
diff --git a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/DownloadConfigurationDropdown.tsx b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/DownloadConfigurationDropdown.tsx
index e99416dbac5..c215dc678a1 100644
--- a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/DownloadConfigurationDropdown.tsx
+++ b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/DownloadConfigurationDropdown.tsx
@@ -5,6 +5,7 @@ import { AcmDropdown } from '../../../../../ui-components'
 import { useContext } from 'react'
 import { useTranslation } from '../../../../../lib/acm-i18next'
 import { ClusterContext } from '../ClusterDetails/ClusterDetails'
+import { DropdownPosition } from '@patternfly/react-core'
 
 export function DownloadConfigurationDropdown(props: { canGetSecret: boolean }) {
   const { cluster } = useContext(ClusterContext)
@@ -54,6 +55,7 @@ export function DownloadConfigurationDropdown(props: { canGetSecret: boolean })
       onSelect={(id: string) => downloadConfig(id)}
       text={t('configuration.download')}
       id="download-configuration"
+      dropdownPosition={DropdownPosition.right}
     />
   )
 }
diff --git a/frontend/src/ui-components/AcmDropdown/AcmDropdown.stories.tsx b/frontend/src/ui-components/AcmDropdown/AcmDropdown.stories.tsx
index a14426ce71c..5ab3629cff1 100644
--- a/frontend/src/ui-components/AcmDropdown/AcmDropdown.stories.tsx
+++ b/frontend/src/ui-components/AcmDropdown/AcmDropdown.stories.tsx
@@ -22,7 +22,7 @@ export const Dropdown = (args: any) => {
     { id: 'other-config', text: 'Other config', isAriaDisabled: true, tooltip: 'Forbidden' },
     { id: 'launch-out', text: 'Launch page', icon: <ExternalLinkAltIcon /> },
     { id: 'link item', text: 'Link item', href: 'www.google.com', component: 'a' },
-    { id: 'new-feature', text: 'New feature', label: 'Technology Preview', labelColor: 'orange' },
+    { id: 'new-feature', text: 'New feature', label: 'Technology Preview', labelColor: 'orange' as const },
   ]
   const onSelect = (id: string) => alert(`clicked: ${id}`)
   return (
diff --git a/frontend/src/ui-components/AcmDropdown/AcmDropdown.tsx b/frontend/src/ui-components/AcmDropdown/AcmDropdown.tsx
index 247905f979d..f4d4de7f56e 100644
--- a/frontend/src/ui-components/AcmDropdown/AcmDropdown.tsx
+++ b/frontend/src/ui-components/AcmDropdown/AcmDropdown.tsx
@@ -1,19 +1,24 @@
 /* Copyright Contributors to the Open Cluster Management project */
 
-import { useState } from 'react'
+import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
 import {
-  Dropdown,
-  DropdownToggle,
-  DropdownItem,
   DropdownPosition,
-  KebabToggle,
   Label,
   LabelProps,
   DropdownProps,
   TooltipPosition,
+  Menu,
+  MenuContent,
+  MenuItem,
+  Tooltip,
+  MenuProps,
+  MenuList,
+  Popper,
+  MenuToggle,
 } from '@patternfly/react-core'
-import { makeStyles } from '@mui/styles'
+import { ClassNameMap, makeStyles } from '@mui/styles'
 import { TooltipWrapper } from '../utils'
+import { EllipsisVIcon } from '@patternfly/react-icons'
 
 type Props = Omit<DropdownProps, 'toggle' | 'onSelect' | 'dropdownItems'>
 
@@ -47,22 +52,15 @@ export type AcmDropdownItems = {
   tooltipPosition?: TooltipPosition
   label?: string
   labelColor?: LabelProps['color']
+  isSelected?: boolean
+  flyoutMenu?: AcmDropdownItems[]
 }
 
 const useStyles = makeStyles({
   button: {
     '& button': {
-      backgroundColor: (props: AcmDropdownProps) => {
-        if (!props.isKebab) {
-          if (props.isDisabled) {
-            return 'var(--pf-global--disabled-color--200)'
-          } else if (!props.isDisabled && props.isPrimary) {
-            return 'var(--pf-c-dropdown__toggle--BackgroundColor)'
-          } else {
-            return 'transparent'
-          }
-        }
-        return undefined
+      '--pf-c-menu-toggle--PaddingRight': (props: AcmDropdownProps) => {
+        return props.isPlain ? '0' : 'var(--pf-global--spacer--sm)'
       },
       '& span': {
         color: (props: AcmDropdownProps) => {
@@ -76,26 +74,6 @@ const useStyles = makeStyles({
           return 'var(--pf-global--primary-color--100)'
         },
       },
-      '&:hover, &:focus': {
-        '& span': {
-          color: (props: AcmDropdownProps) => (props.isKebab ? undefined : 'var(--pf-global--primary-color--100)'),
-        },
-        '& span.pf-c-dropdown__toggle-text': {
-          color: (props: AcmDropdownProps) => props.isPrimary && 'var(--pf-global--Color--light-100)',
-        },
-        '& span.pf-c-dropdown__toggle-icon': {
-          color: (props: AcmDropdownProps) => props.isPrimary && 'var(--pf-global--Color--light-100)',
-        },
-      },
-      '& span.pf-c-dropdown__toggle-text': {
-        // centers dropdown text in plain dropdown button
-        paddingLeft: (props: AcmDropdownProps) => {
-          if (props.isPlain) {
-            return '8px'
-          }
-          return undefined
-        },
-      },
     },
   },
   label: {
@@ -103,77 +81,166 @@ const useStyles = makeStyles({
   },
 })
 
+type MenuItemProps = {
+  menuItems: AcmDropdownItems[]
+  onSelect: MenuProps['onSelect']
+  classes: ClassNameMap<'label'>
+} & MenuProps
+
+const MenuItems = forwardRef<HTMLDivElement, MenuItemProps>((props, ref) => {
+  const { menuItems, onSelect, classes, ...menuProps } = props
+  return (
+    <Menu ref={ref} onSelect={onSelect} containsFlyout={menuItems.some((mi) => mi.flyoutMenu)} {...menuProps}>
+      <MenuContent>
+        <MenuList>
+          {menuItems.map((item) => {
+            const menuItem = (
+              <MenuItem
+                id={item.id}
+                key={item.id}
+                itemId={item.id}
+                component="a"
+                isDisabled={item.isAriaDisabled}
+                isSelected={item.isSelected}
+                flyoutMenu={
+                  item.flyoutMenu && item.flyoutMenu.length ? (
+                    <MenuItems menuItems={item.flyoutMenu} classes={classes} onSelect={onSelect} />
+                  ) : undefined
+                }
+              >
+                {item.text}
+                {item.label && item.labelColor && (
+                  <Label className={classes.label} color={item.labelColor}>
+                    {item.label}
+                  </Label>
+                )}
+              </MenuItem>
+            )
+            return item.tooltip ? (
+              <Tooltip key={item.id} position={item.tooltipPosition} content={item.tooltip}>
+                <div>{menuItem}</div>
+              </Tooltip>
+            ) : (
+              menuItem
+            )
+          })}
+        </MenuList>
+      </MenuContent>
+    </Menu>
+  )
+})
+
 export function AcmDropdown(props: AcmDropdownProps) {
+  const {
+    dropdownItems,
+    dropdownPosition,
+    id,
+    isDisabled,
+    isKebab,
+    isPlain,
+    isPrimary,
+    onHover,
+    onSelect,
+    onToggle,
+    text,
+    tooltip,
+    tooltipPosition,
+  } = props
   const [isOpen, setOpen] = useState<boolean>(false)
+  const popperContainer = useRef<HTMLDivElement>(null)
+  const toggleRef = useRef<HTMLButtonElement>(null)
+  const menuRef = useRef<HTMLDivElement>(null)
   const classes = useStyles(props)
 
-  const onSelect = (id: string) => {
-    props.onSelect(id)
-    setOpen(false)
-    const element = document.getElementById(props.id)
-    /* istanbul ignore else */
-    if (element) element.focus()
-  }
+  const toggleMenu = useCallback(() => {
+    if (onToggle) {
+      onToggle(!isOpen)
+    }
+    setOpen(!isOpen)
+  }, [isOpen, onToggle])
+
+  const handleSelect = useCallback(
+    (_event?: React.MouseEvent, itemId?: string | number) => {
+      onSelect((itemId || '').toString())
+      setOpen(false)
+      const element = document.getElementById(id)
+      /* istanbul ignore else */
+      if (element) element.focus()
+    },
+    [id, onSelect]
+  )
+
+  const handleToggleClick = useCallback(() => {
+    setTimeout(() => {
+      if (menuRef.current) {
+        const firstElement = menuRef.current.querySelector('li > button:not(:disabled), li > a:not(:disabled)')
+        firstElement && (firstElement as HTMLElement).focus()
+      }
+    }, 0)
+    toggleMenu()
+  }, [toggleMenu])
+
+  const handleMenuKeys = useCallback(
+    (event: KeyboardEvent) => {
+      if (!isOpen) {
+        return
+      }
+      if (menuRef.current?.contains(event.target as Node) || toggleRef.current?.contains(event.target as Node)) {
+        if (event.key === 'Escape' || event.key === 'Tab') {
+          toggleMenu()
+          toggleRef.current?.focus()
+        }
+      }
+    },
+    [isOpen, toggleMenu]
+  )
+
+  const handleClickOutside = useCallback(
+    (event: MouseEvent) => {
+      if (
+        isOpen &&
+        !toggleRef.current?.contains(event.target as Node) &&
+        !menuRef.current?.contains(event.target as Node)
+      ) {
+        toggleMenu()
+      }
+    },
+    [isOpen, toggleMenu]
+  )
+
+  useEffect(() => {
+    window.addEventListener('keydown', handleMenuKeys)
+    window.addEventListener('click', handleClickOutside)
+    return () => {
+      window.removeEventListener('keydown', handleMenuKeys)
+      window.removeEventListener('click', handleClickOutside)
+    }
+  }, [handleMenuKeys, handleClickOutside])
 
   return (
-    <TooltipWrapper showTooltip={!!props.tooltip} tooltip={props.tooltip} tooltipPosition={props.tooltipPosition}>
-      <Dropdown
-        className={classes.button}
-        onMouseOver={props.onHover}
-        position={props.dropdownPosition || DropdownPosition.right}
-        dropdownItems={props.dropdownItems.map((item) => {
-          return (
-            <DropdownItem
-              key={item.id}
-              tooltip={item.tooltip}
-              tooltipProps={{ position: item.tooltipPosition }}
-              href={item.href}
-              id={item.id}
-              isAriaDisabled={item.isAriaDisabled}
-              icon={item.icon}
-              onClick={() => onSelect(item.id)}
-            >
-              {item.text}
-              {item.label && item.labelColor && (
-                <Label className={classes.label} color={item.labelColor}>
-                  {item.label}
-                </Label>
-              )}
-            </DropdownItem>
-          )
-        })}
-        toggle={
-          props.isKebab ? (
-            <KebabToggle
-              id={props.id}
-              isDisabled={props.isDisabled}
-              onToggle={() => {
-                /* istanbul ignore next */
-                if (props.onToggle) {
-                  props.onToggle(!isOpen)
-                }
-                setOpen(!isOpen)
-              }}
-            />
-          ) : (
-            <DropdownToggle
-              isPrimary={props.isPrimary}
-              id={props.id}
-              isDisabled={props.isDisabled}
-              onToggle={() => {
-                if (props.onToggle) {
-                  props.onToggle(!isOpen)
-                }
-                setOpen(!isOpen)
-              }}
+    <TooltipWrapper showTooltip={!!tooltip} tooltip={tooltip} tooltipPosition={tooltipPosition}>
+      <div ref={popperContainer} className={classes.button}>
+        <Popper
+          trigger={
+            <MenuToggle
+              ref={toggleRef}
+              variant={isKebab ? 'plain' : isPlain ? 'plainText' : isPrimary ? 'primary' : 'default'}
+              id={id}
+              isDisabled={isDisabled}
+              onClick={handleToggleClick}
+              onMouseOver={onHover}
+              isExpanded={isOpen}
+              aria-label={text}
             >
-              {props.text}
-            </DropdownToggle>
-          )
-        }
-        isOpen={isOpen}
-        isPlain={props.isPlain}
-      />
+              {isKebab ? <EllipsisVIcon /> : text}
+            </MenuToggle>
+          }
+          popperMatchesTriggerWidth={false}
+          isVisible={isOpen}
+          position={dropdownPosition || (isKebab ? DropdownPosition.right : DropdownPosition.left)}
+          popper={<MenuItems ref={menuRef} menuItems={dropdownItems} onSelect={handleSelect} classes={classes} />}
+        />
+      </div>
     </TooltipWrapper>
   )
 }