diff --git a/packages/demo-app-ts/src/App.tsx b/packages/demo-app-ts/src/App.tsx index ab373867..ee3014a0 100755 --- a/packages/demo-app-ts/src/App.tsx +++ b/packages/demo-app-ts/src/App.tsx @@ -10,25 +10,24 @@ import { Avatar, Brand, Radio, - Masthead, - MastheadMain, - MastheadToggle, - MastheadBrand, NavExpandable, PageSidebarBody, - MastheadContent, - PageToggleButton, Toolbar, - ToolbarContent, ToolbarGroup, - ToolbarItem + ToolbarItem, + Masthead, + MastheadToggle, + PageToggleButton, + MastheadContent, + MastheadBrand, + MastheadMain, + ToolbarContent } from '@patternfly/react-core'; +import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon'; import imgBrand from './assets/images/imgBrand.svg'; import imgAvatar from './assets/images/imgAvatar.svg'; import Demos from './Demos'; import './App.css'; -import { BarsIcon } from '@patternfly/react-icons'; - interface AppState { activeItem: number | string; isNavOpen: boolean; @@ -153,13 +152,18 @@ class App extends React.Component<{}, AppState> { const AppHeader = ( - + this.setState({ isNavOpen: !isNavOpen })} + > - - + + {AppToolbar} diff --git a/packages/demo-app-ts/src/components/actionsComponentFactory.tsx b/packages/demo-app-ts/src/components/actionsComponentFactory.tsx index 503d0108..6b4d3625 100644 --- a/packages/demo-app-ts/src/components/actionsComponentFactory.tsx +++ b/packages/demo-app-ts/src/components/actionsComponentFactory.tsx @@ -22,7 +22,7 @@ import CustomPathNode from './CustomPathNode'; const contextMenuItem = (label: string, i: number): React.ReactElement => { if (label === '-') { - return ; + return ; } if (label.includes('->')) { const parent = label.slice(0, label.indexOf('->')); diff --git a/packages/demo-app-ts/src/components/pipelineComponentFactory.tsx b/packages/demo-app-ts/src/components/pipelineComponentFactory.tsx index 7d029799..a33ef892 100644 --- a/packages/demo-app-ts/src/components/pipelineComponentFactory.tsx +++ b/packages/demo-app-ts/src/components/pipelineComponentFactory.tsx @@ -25,7 +25,7 @@ export const GROUPED_EDGE_TYPE = 'GROUPED_EDGE'; const contextMenuItem = (label: string, i: number): React.ReactElement => { if (label === '-') { - return ; + return ; } return ( // eslint-disable-next-line no-alert diff --git a/packages/demo-app-ts/src/components/stylesComponentFactory.tsx b/packages/demo-app-ts/src/components/stylesComponentFactory.tsx index 084d0b90..abc1ef7d 100644 --- a/packages/demo-app-ts/src/components/stylesComponentFactory.tsx +++ b/packages/demo-app-ts/src/components/stylesComponentFactory.tsx @@ -41,7 +41,7 @@ interface EdgeProps { const contextMenuItem = (label: string, i: number): React.ReactElement => { if (label === '-') { - return ; + return ; } return ( // eslint-disable-next-line no-alert diff --git a/packages/demo-app-ts/src/demos/ContextMenus.tsx b/packages/demo-app-ts/src/demos/ContextMenus.tsx index 6c55ff05..ec9c58f5 100644 --- a/packages/demo-app-ts/src/demos/ContextMenus.tsx +++ b/packages/demo-app-ts/src/demos/ContextMenus.tsx @@ -20,7 +20,7 @@ import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; const contextMenuItem = (label: string, i: number): React.ReactElement => { if (label === '-') { - return ; + return ; } return ( // eslint-disable-next-line no-alert diff --git a/packages/demo-app-ts/src/utils/useTopologyOptions.tsx b/packages/demo-app-ts/src/utils/useTopologyOptions.tsx index 7b3f36ce..851761e9 100644 --- a/packages/demo-app-ts/src/utils/useTopologyOptions.tsx +++ b/packages/demo-app-ts/src/utils/useTopologyOptions.tsx @@ -1,31 +1,24 @@ import React from 'react'; import * as _ from 'lodash'; import { - Button, - Flex, - Split, - SplitItem, - TextInput, - ToolbarItem, - Tooltip + Button, + Dropdown, + DropdownItem, + DropdownList, + Flex, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + Split, + SplitItem, + TextInput, + ToolbarItem, + Tooltip } from '@patternfly/react-core'; -import { - Dropdown as DropdownDeprecated, - DropdownItem as DropdownItemDeprecated, - DropdownPosition as DropdownPositionDeprecated, - DropdownToggle as DropdownToggleDeprecated, - Select, - SelectOption, - SelectVariant -} from '@patternfly/react-core/deprecated'; import { DefaultEdgeOptions, DefaultNodeOptions, GeneratorEdgeOptions, GeneratorNodeOptions } from '../data/generator'; -import { - EDGE_ANIMATION_SPEEDS, - EDGE_STYLES, - EDGE_TERMINAL_TYPES, - NODE_SHAPES, - NODE_STATUSES -} from './styleUtils'; +import { EDGE_ANIMATION_SPEEDS, EDGE_STYLES, EDGE_TERMINAL_TYPES, NODE_SHAPES, NODE_STATUSES } from './styleUtils'; import { Controller, Model, NodeShape } from '@patternfly/react-topology'; const GRAPH_LAYOUT_OPTIONS = ['x', 'y', 'visible', 'style', 'layout', 'scale', 'scaleExtent', 'layers']; @@ -74,115 +67,156 @@ export const useTopologyOptions = ( - setLayoutDropdownOpen(!layoutDropdownOpen)}>{layout}} + ) => ( + setLayoutDropdownOpen(!layoutDropdownOpen)}> + {layout} + + )} isOpen={layoutDropdownOpen} - dropdownItems={[ - updateLayout('Force')}> + onOpenChange={(isOpen) => setLayoutDropdownOpen(isOpen)} + > + + updateLayout('Force')}> Force - , - updateLayout('Dagre')}> + + updateLayout('Dagre')}> Dagre - , - updateLayout('Cola')}> + + updateLayout('Cola')}> Cola - , - updateLayout('ColaGroups')}> + + updateLayout('ColaGroups')}> ColaGroups - , - updateLayout('ColaNoForce')}> + + updateLayout('ColaNoForce')}> ColaNoForce - , - updateLayout('Grid')}> + + updateLayout('Grid')}> Grid - , - updateLayout('Concentric')}> + + updateLayout('Concentric')}> Concentric - , - updateLayout('BreadthFirst')}> + + updateLayout('BreadthFirst')}> BreadthFirst - - ]} - /> + + + ); const renderNodeOptionsDropdown = () => { const selectContent = ( -
+ setNodeOptions(prev => ({ ...prev, nodeLabels: !prev.nodeLabels }))} - /> + isSelected={nodeOptions.nodeLabels} + onClick={() => setNodeOptions((prev) => ({ ...prev, nodeLabels: !prev.nodeLabels }))} + > + Labels + setNodeOptions(prev => ({ ...prev, nodeSecondaryLabels: !prev.nodeSecondaryLabels }))} - /> + isSelected={nodeOptions.nodeSecondaryLabels} + onClick={() => setNodeOptions((prev) => ({ ...prev, nodeSecondaryLabels: !prev.nodeSecondaryLabels }))} + > + Secondary Labels + 1} + isSelected={nodeOptions.statuses.length > 1} onClick={() => - setNodeOptions(prev => ({ + setNodeOptions((prev) => ({ ...prev, statuses: prev.statuses.length > 1 ? DefaultNodeOptions.statuses : NODE_STATUSES })) } - /> + > + Status + - setNodeOptions(prev => ({ + setNodeOptions((prev) => ({ ...prev, statusDecorators: !prev.statusDecorators, showDecorators: !prev.showDecorators })) } - /> + > + Decorators + setNodeOptions(prev => ({ ...prev, nodeBadges: !prev.nodeBadges }))} - /> + isSelected={nodeOptions.nodeBadges} + onClick={() => setNodeOptions((prev) => ({ ...prev, nodeBadges: !prev.nodeBadges }))} + > + Badges + setNodeOptions(prev => ({ ...prev, nodeIcons: !prev.nodeIcons }))} - /> + isSelected={nodeOptions.nodeIcons} + onClick={() => setNodeOptions((prev) => ({ ...prev, nodeIcons: !prev.nodeIcons }))} + > + Icons + setNodeOptions(prev => ({ ...prev, contextMenus: !prev.contextMenus }))} - /> -
+ isSelected={nodeOptions.contextMenus} + onClick={() => setNodeOptions((prev) => ({ ...prev, contextMenus: !prev.contextMenus }))} + > + Context Menus + + + ); + + const nodeOptionsToggle = (toggleRef: React.Ref) => ( + setNodeOptionsOpen((prev) => !prev)} + isExpanded={nodeOptionsOpen} + style={ + { + width: '250px' + } as React.CSSProperties + } + > + Node options + ); return ( ); }; const toggleNodeShape = (shape: NodeShape): void => { const index = nodeOptions.shapes.indexOf(shape); if (index >= 0) { - setNodeOptions(prev => ({ + setNodeOptions((prev) => ({ ...prev, shapes: [...prev.shapes.slice(0, index), ...prev.shapes.slice(index + 1)] })); } else { - setNodeOptions(prev => ({ + setNodeOptions((prev) => ({ ...prev, shapes: [...prev.shapes, shape] })); @@ -191,92 +225,137 @@ export const useTopologyOptions = ( const renderNodeShapesDropdown = () => { const selectContent = ( -
- {NODE_SHAPES.map(shape => ( + + {NODE_SHAPES.map((shape) => ( toggleNodeShape(shape)} - /> + > + {shape} + ))} -
+ + ); + + const nodeShapesToggle = (toggleRef: React.Ref) => ( + setNodeShapesOpen((prev) => !prev)} + isExpanded={nodeShapesOpen} + style={ + { + width: '250px' + } as React.CSSProperties + } + > + Node shapes + ); return ( ); }; const renderEdgeOptionsDropdown = () => { const selectContent = ( -
+ 1} + hasCheckbox + isSelected={edgeOptions.edgeStatuses.length > 1} onClick={() => - setEdgeOptions(prev => ({ + setEdgeOptions((prev) => ({ ...prev, edgeStatuses: prev.edgeStatuses.length > 1 ? DefaultEdgeOptions.edgeStatuses : NODE_STATUSES })) } - /> + > + Status + 1} + hasCheckbox + isSelected={edgeOptions.edgeStyles.length > 1} onClick={() => - setEdgeOptions(prev => ({ + setEdgeOptions((prev) => ({ ...prev, edgeStyles: prev.edgeStyles.length > 1 ? DefaultEdgeOptions.edgeStyles : EDGE_STYLES })) } - /> + > + Styles + 1} + hasCheckbox + isSelected={edgeOptions.edgeAnimations.length > 1} onClick={() => - setEdgeOptions(prev => ({ + setEdgeOptions((prev) => ({ ...prev, edgeAnimations: prev.edgeAnimations.length > 1 ? DefaultEdgeOptions.edgeAnimations : EDGE_ANIMATION_SPEEDS })) } - /> + > + Animations + 1} + hasCheckbox + isSelected={edgeOptions.terminalTypes.length > 1} onClick={() => - setEdgeOptions(prev => ({ + setEdgeOptions((prev) => ({ ...prev, terminalTypes: prev.terminalTypes.length > 1 ? DefaultEdgeOptions.terminalTypes : EDGE_TERMINAL_TYPES })) } - /> + > + Terminal type + setEdgeOptions(prev => ({ ...prev, edgeTags: !prev.edgeTags }))} - /> -
+ hasCheckbox + isSelected={edgeOptions.edgeTags} + onClick={() => setEdgeOptions((prev) => ({ ...prev, edgeTags: !prev.edgeTags }))} + > + Tags + + + ); + const edgeOptionsToggle = (toggleRef: React.Ref) => ( + setEdgeOptionsOpen((prev) => !prev)} + isExpanded={edgeOptionsOpen} + style={ + { + width: '250px' + } as React.CSSProperties + } + > + Edge options + ); return ( ); }; @@ -295,8 +374,8 @@ export const useTopologyOptions = ( ...currentModel.graph, ..._.pick(savedModel.graph, GRAPH_LAYOUT_OPTIONS) }; - currentModel.nodes = currentModel.nodes.map(n => { - const savedNode = savedModel.nodes.find(sn => sn.id === n.id); + currentModel.nodes = currentModel.nodes.map((n) => { + const savedNode = savedModel.nodes.find((sn) => sn.id === n.id); if (!savedNode) { return n; } @@ -341,28 +420,36 @@ export const useTopologyOptions = ( aria-label="nodes" type="number" value={numNodes || ''} - onChange={(_event, val: string) => (val ? updateValue(parseInt(val), 0, 9999, setNumNodes) : setNumNodes(null))} + onChange={(_event, val: string) => + val ? updateValue(parseInt(val), 0, 9999, setNumNodes) : setNumNodes(null) + } /> Edges: (val ? updateValue(parseInt(val), 0, 200, setNumEdges) : setNumEdges(null))} + onChange={(_event, val: string) => + val ? updateValue(parseInt(val), 0, 200, setNumEdges) : setNumEdges(null) + } /> Groups: (val ? updateValue(parseInt(val), 0, 100, setNumGroups) : setNumGroups(null))} + onChange={(_event, val: string) => + val ? updateValue(parseInt(val), 0, 100, setNumGroups) : setNumGroups(null) + } /> Nesting Depth: (val ? updateValue(parseInt(val), 0, 5, setNestedLevel) : setNestedLevel(null))} + onChange={(_event, val: string) => + val ? updateValue(parseInt(val), 0, 5, setNestedLevel) : setNestedLevel(null) + } /> - } - /> + ref={nodeRef} + // prevent this DropdownItem from executing like a normal action item + onClick={(e) => e.stopPropagation()} + // mouse enter will open the sub menu + onMouseEnter={() => setOpen(true)} + onMouseLeave={(e) => { + // if the mouse leaves this item, close the sub menu only if the mouse did not enter the sub menu itself + if (!subMenuRef.current || !subMenuRef.current.contains(e.relatedTarget as Node)) { + setOpen(false); + } + }} + onKeyDown={(e) => { + // open the sub menu on enter or right arrow + if (e.key === 'ArrowRight' || e.key === 'Enter') { + setOpen(true); + e.stopPropagation(); + } + }} + > + {label} + + { + onRequestClose={(e) => { // only close the sub menu if clicking anywhere outside the menu item that owns the sub menu if (!e || !nodeRef.current || !nodeRef.current.contains(e.target as Node)) { setOpen(false); @@ -72,13 +64,13 @@ const ContextSubMenuItem: React.FunctionComponent = ({ ref={subMenuRef} role="presentation" className="pf-v5-c-dropdown pf-m-expanded" - onMouseLeave={e => { + onMouseLeave={(e) => { // only close the sub menu if the mouse does not enter the item if (!nodeRef.current || !nodeRef.current.contains(e.relatedTarget as Node)) { setOpen(false); } }} - onKeyDown={e => { + onKeyDown={(e) => { // close the sub menu on left arrow if (e.key === 'ArrowLeft') { setOpen(false); @@ -86,9 +78,9 @@ const ContextSubMenuItem: React.FunctionComponent = ({ } }} > - + <>} className={css(topologyStyles.topologyContextMenuCDropdownMenu)}> {children} - + diff --git a/packages/module/src/components/contextmenu/index.ts b/packages/module/src/components/contextmenu/index.ts index 9b863a60..5489acdf 100644 --- a/packages/module/src/components/contextmenu/index.ts +++ b/packages/module/src/components/contextmenu/index.ts @@ -2,4 +2,4 @@ export { default as ContextMenu } from './ContextMenu'; export { default as ContextSubMenuItem } from './ContextSubMenuItem'; // re-export dropdown components as context menu components -export { DropdownItem as ContextMenuItem, DropdownSeparator as ContextMenuSeparator } from '@patternfly/react-core/deprecated'; +export { DropdownItem as ContextMenuItem, Divider as ContextMenuSeparator } from '@patternfly/react-core';