From c4abbf73ef8496c4f56a2e0f8212a4dd02e559c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ula=C5=9F=20Turan?= Date: Tue, 16 May 2023 18:24:45 +0300 Subject: [PATCH] Refactor #4391 - For ContextMenu --- components/doc/common/apidoc/index.json | 158 ++++++ components/doc/contextmenu/pt/ptdoc.js | 453 ++++++++++++++++++ components/doc/contextmenu/pt/wireframe.js | 15 + components/lib/contextmenu/ContextMenu.js | 29 +- components/lib/contextmenu/ContextMenuBase.js | 10 +- components/lib/contextmenu/ContextMenuSub.js | 86 +++- components/lib/contextmenu/contextmenu.d.ts | 93 +++- pages/contextmenu/index.js | 23 +- 8 files changed, 846 insertions(+), 21 deletions(-) create mode 100644 components/doc/contextmenu/pt/ptdoc.js create mode 100644 components/doc/contextmenu/pt/wireframe.js diff --git a/components/doc/common/apidoc/index.json b/components/doc/common/apidoc/index.json index 6009f5d24f..4ac6589f63 100644 --- a/components/doc/common/apidoc/index.json +++ b/components/doc/common/apidoc/index.json @@ -10217,6 +10217,14 @@ "type": "ReactNode", "default": "", "description": "Used to get the child elements of the component." + }, + { + "name": "pt", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughOptions", + "default": "", + "description": "Uses to pass attributes to DOM elements inside the component." } ] }, @@ -10252,6 +10260,156 @@ ] } } + }, + "interfaces": { + "description": "Defines the custom interfaces used by the module.", + "values": { + "ContextMenuPassThroughMethodOptions": { + "description": "Custom passthrough(pt) option method.", + "relatedProp": "", + "props": [ + { + "name": "props", + "optional": false, + "readonly": false, + "type": "ContextMenuProps" + }, + { + "name": "state", + "optional": false, + "readonly": false, + "type": "ContextMenuState" + }, + { + "name": "context", + "optional": false, + "readonly": false, + "type": "ContextMenuContext" + } + ], + "callbacks": [] + }, + "ContextMenuPassThroughOptions": { + "description": "Custom passthrough(pt) options.", + "relatedProp": "pt", + "props": [ + { + "name": "root", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType>", + "description": "Uses to pass attributes to the root's DOM element." + }, + { + "name": "menu", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType>", + "description": "Uses to pass attributes to the list's DOM element." + }, + { + "name": "menuitem", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType>", + "description": "Uses to pass attributes to the list item's DOM element." + }, + { + "name": "action", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType>", + "description": "Uses to pass attributes to the action's DOM element." + }, + { + "name": "icon", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType | SVGProps>", + "description": "Uses to pass attributes to the icon's DOM element." + }, + { + "name": "label", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType>", + "description": "Uses to pass attributes to the label's DOM element." + }, + { + "name": "submenuIcon", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType | SVGProps>", + "description": "Uses to pass attributes to the submenu icon's DOM element." + }, + { + "name": "separator", + "optional": true, + "readonly": false, + "type": "ContextMenuPassThroughType>", + "description": "Uses to pass attributes to the separator's DOM element." + } + ], + "callbacks": [] + }, + "ContextMenuState": { + "description": "Defines current inline state in ContextMenu component.", + "relatedProp": "", + "props": [ + { + "name": "visible", + "optional": false, + "readonly": false, + "type": "boolean", + "description": "Current visible state as a boolean." + }, + { + "name": "reshow", + "optional": false, + "readonly": false, + "type": "boolean", + "description": "Current reshow state as a boolean." + }, + { + "name": "resetMenu", + "optional": false, + "readonly": false, + "type": "boolean", + "description": "Current resetMenu state as a boolean." + }, + { + "name": "attributeSelector", + "optional": false, + "readonly": false, + "type": "boolean", + "description": "Current attributeSelector visible state as a string." + } + ], + "callbacks": [] + }, + "ContextMenuContext": { + "description": "Defines current options in ContextMenu component.", + "relatedProp": "", + "props": [ + { + "name": "active", + "optional": false, + "readonly": false, + "type": "boolean", + "description": "Current active state of menuitem as a boolean." + } + ], + "callbacks": [] + } + } + }, + "types": { + "description": "Defines the custom types used by the module.", + "values": { + "ContextMenuPassThroughType": { + "values": "PassThroughType" + } + } } }, "csstransition": { diff --git a/components/doc/contextmenu/pt/ptdoc.js b/components/doc/contextmenu/pt/ptdoc.js new file mode 100644 index 0000000000..342c996020 --- /dev/null +++ b/components/doc/contextmenu/pt/ptdoc.js @@ -0,0 +1,453 @@ +import { useRef } from 'react'; +import { ContextMenu } from '../../../lib/contextmenu/ContextMenu'; +import { DocSectionCode } from '../../common/docsectioncode'; +import { DocSectionText } from '../../common/docsectiontext'; + +export function PTDoc(props) { + const cm = useRef(null); + const items = [ + { + label: 'File', + icon: 'pi pi-fw pi-file', + items: [ + { + label: 'New', + icon: 'pi pi-fw pi-plus', + items: [ + { + label: 'Bookmark', + icon: 'pi pi-fw pi-bookmark' + }, + { + label: 'Video', + icon: 'pi pi-fw pi-video' + } + ] + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-trash' + }, + { + separator: true + }, + { + label: 'Export', + icon: 'pi pi-fw pi-external-link' + } + ] + }, + { + label: 'Edit', + icon: 'pi pi-fw pi-pencil', + items: [ + { + label: 'Left', + icon: 'pi pi-fw pi-align-left' + }, + { + label: 'Right', + icon: 'pi pi-fw pi-align-right' + }, + { + label: 'Center', + icon: 'pi pi-fw pi-align-center' + }, + { + label: 'Justify', + icon: 'pi pi-fw pi-align-justify' + } + ] + }, + { + label: 'Users', + icon: 'pi pi-fw pi-user', + items: [ + { + label: 'New', + icon: 'pi pi-fw pi-user-plus' + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-user-minus' + }, + { + label: 'Search', + icon: 'pi pi-fw pi-users', + items: [ + { + label: 'Filter', + icon: 'pi pi-fw pi-filter', + items: [ + { + label: 'Print', + icon: 'pi pi-fw pi-print' + } + ] + }, + { + icon: 'pi pi-fw pi-bars', + label: 'List' + } + ] + } + ] + }, + { + label: 'Events', + icon: 'pi pi-fw pi-calendar', + items: [ + { + label: 'Edit', + icon: 'pi pi-fw pi-pencil', + items: [ + { + label: 'Save', + icon: 'pi pi-fw pi-calendar-plus' + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-calendar-minus' + } + ] + }, + { + label: 'Archieve', + icon: 'pi pi-fw pi-calendar-times', + items: [ + { + label: 'Remove', + icon: 'pi pi-fw pi-calendar-minus' + } + ] + } + ] + }, + { + separator: true + }, + { + label: 'Quit', + icon: 'pi pi-fw pi-power-off' + } + ]; + + const code = { + basic: ` + ({ className: context.active ? 'bg-primary-200' : context.focused ? 'bg-primary-300' : undefined }) + }} +/> +Logo cm.current.show(e)} /> +`, + javascript: ` +import React, { useRef } from 'react'; +import { ContextMenu } from 'primereact/contextmenu'; + +export default function PTDemo() { + const cm = useRef(null); + const items = [ + { + label: 'File', + icon: 'pi pi-fw pi-file', + items: [ + { + label: 'New', + icon: 'pi pi-fw pi-plus', + items: [ + { + label: 'Bookmark', + icon: 'pi pi-fw pi-bookmark' + }, + { + label: 'Video', + icon: 'pi pi-fw pi-video' + } + ] + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-trash' + }, + { + separator: true + }, + { + label: 'Export', + icon: 'pi pi-fw pi-external-link' + } + ] + }, + { + label: 'Edit', + icon: 'pi pi-fw pi-pencil', + items: [ + { + label: 'Left', + icon: 'pi pi-fw pi-align-left' + }, + { + label: 'Right', + icon: 'pi pi-fw pi-align-right' + }, + { + label: 'Center', + icon: 'pi pi-fw pi-align-center' + }, + { + label: 'Justify', + icon: 'pi pi-fw pi-align-justify' + } + ] + }, + { + label: 'Users', + icon: 'pi pi-fw pi-user', + items: [ + { + label: 'New', + icon: 'pi pi-fw pi-user-plus' + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-user-minus' + }, + { + label: 'Search', + icon: 'pi pi-fw pi-users', + items: [ + { + label: 'Filter', + icon: 'pi pi-fw pi-filter', + items: [ + { + label: 'Print', + icon: 'pi pi-fw pi-print' + } + ] + }, + { + icon: 'pi pi-fw pi-bars', + label: 'List' + } + ] + } + ] + }, + { + label: 'Events', + icon: 'pi pi-fw pi-calendar', + items: [ + { + label: 'Edit', + icon: 'pi pi-fw pi-pencil', + items: [ + { + label: 'Save', + icon: 'pi pi-fw pi-calendar-plus' + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-calendar-minus' + } + ] + }, + { + label: 'Archieve', + icon: 'pi pi-fw pi-calendar-times', + items: [ + { + label: 'Remove', + icon: 'pi pi-fw pi-calendar-minus' + } + ] + } + ] + }, + { + separator: true + }, + { + label: 'Quit', + icon: 'pi pi-fw pi-power-off' + } + ]; + + return ( +
+ + Logo cm.current.show(e)} /> +
+ ) +} + `, + typescript: ` +import React, { useRef } from 'react'; +import { ContextMenu } from 'primereact/contextmenu'; +import { MenuItem } from 'primereact/menuitem'; + +export default function PTDemo() { + const cm = useRef(null); + const items: MenuItem = [ + { + label: 'File', + icon: 'pi pi-fw pi-file', + items: [ + { + label: 'New', + icon: 'pi pi-fw pi-plus', + items: [ + { + label: 'Bookmark', + icon: 'pi pi-fw pi-bookmark' + }, + { + label: 'Video', + icon: 'pi pi-fw pi-video' + } + ] + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-trash' + }, + { + separator: true + }, + { + label: 'Export', + icon: 'pi pi-fw pi-external-link' + } + ] + }, + { + label: 'Edit', + icon: 'pi pi-fw pi-pencil', + items: [ + { + label: 'Left', + icon: 'pi pi-fw pi-align-left' + }, + { + label: 'Right', + icon: 'pi pi-fw pi-align-right' + }, + { + label: 'Center', + icon: 'pi pi-fw pi-align-center' + }, + { + label: 'Justify', + icon: 'pi pi-fw pi-align-justify' + } + ] + }, + { + label: 'Users', + icon: 'pi pi-fw pi-user', + items: [ + { + label: 'New', + icon: 'pi pi-fw pi-user-plus' + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-user-minus' + }, + { + label: 'Search', + icon: 'pi pi-fw pi-users', + items: [ + { + label: 'Filter', + icon: 'pi pi-fw pi-filter', + items: [ + { + label: 'Print', + icon: 'pi pi-fw pi-print' + } + ] + }, + { + icon: 'pi pi-fw pi-bars', + label: 'List' + } + ] + } + ] + }, + { + label: 'Events', + icon: 'pi pi-fw pi-calendar', + items: [ + { + label: 'Edit', + icon: 'pi pi-fw pi-pencil', + items: [ + { + label: 'Save', + icon: 'pi pi-fw pi-calendar-plus' + }, + { + label: 'Delete', + icon: 'pi pi-fw pi-calendar-minus' + } + ] + }, + { + label: 'Archieve', + icon: 'pi pi-fw pi-calendar-times', + items: [ + { + label: 'Remove', + icon: 'pi pi-fw pi-calendar-minus' + } + ] + } + ] + }, + { + separator: true + }, + { + label: 'Quit', + icon: 'pi pi-fw pi-power-off' + } + ]; + + return ( +
+ + Logo cm.current.show(e)} /> +
+ ) +} + ` + }; + + return ( + <> + +

+ ContextMenu requires a collection of menuitems as its model and the show method needs to be called explicity using the onContextMenu event of the target to display the menu. +

+
+
+ ({ className: context.active ? 'bg-primary-200' : context.focused ? 'bg-primary-300' : undefined }) + }} + /> + Logo cm.current.show(e)} /> +
+ + + ); +} diff --git a/components/doc/contextmenu/pt/wireframe.js b/components/doc/contextmenu/pt/wireframe.js new file mode 100644 index 0000000000..c26c4050fc --- /dev/null +++ b/components/doc/contextmenu/pt/wireframe.js @@ -0,0 +1,15 @@ + +import React from 'react'; +import { DocSectionText } from '../../common/docsectiontext'; + +export const Wireframe = (props) => { + + return ( + <> + +
+ contextmenu +
+ + ); +}; diff --git a/components/lib/contextmenu/ContextMenu.js b/components/lib/contextmenu/ContextMenu.js index 2ef1ddc639..2cc2406c27 100644 --- a/components/lib/contextmenu/ContextMenu.js +++ b/components/lib/contextmenu/ContextMenu.js @@ -3,7 +3,7 @@ import PrimeReact from '../api/Api'; import { CSSTransition } from '../csstransition/CSSTransition'; import { useEventListener, useMatchMedia, useMountEffect, useResizeListener, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks'; import { Portal } from '../portal/Portal'; -import { classNames, DomHandler, UniqueComponentId, ZIndexUtils } from '../utils/Utils'; +import { classNames, DomHandler, mergeProps, UniqueComponentId, ZIndexUtils } from '../utils/Utils'; import { ContextMenuBase } from './ContextMenuBase'; import { ContextMenuSub } from './ContextMenuSub'; @@ -15,6 +15,15 @@ export const ContextMenu = React.memo( const [reshowState, setReshowState] = React.useState(false); const [resetMenuState, setResetMenuState] = React.useState(false); const [attributeSelectorState, setAttributeSelectorState] = React.useState(null); + const { ptm } = ContextMenuBase.setMetaData({ + props, + state: { + visible: visibleState, + reshow: reshowState, + resetMenu: resetMenuState, + attributeSelector: attributeSelectorState + } + }); const menuRef = React.useRef(null); const currentEvent = React.useRef(null); const styleElementRef = React.useRef(null); @@ -240,12 +249,24 @@ export const ContextMenu = React.memo( })); const createContextMenu = () => { - const otherProps = ContextMenuBase.getOtherProps(props); const className = classNames('p-contextmenu p-component', props.className, { 'p-input-filled': PrimeReact.inputStyle === 'filled', 'p-ripple-disabled': PrimeReact.ripple === false }); + const rootProps = mergeProps( + { + id: props.id, + ref: menuRef, + className, + style: props.style, + onClick: (e) => onMenuClick(e), + onMouseEnter: (e) => onMenuMouseEnter(e) + }, + ContextMenuBase.getOtherProps(props), + ptm('root') + ); + return ( -
- +
+
); diff --git a/components/lib/contextmenu/ContextMenuBase.js b/components/lib/contextmenu/ContextMenuBase.js index 00ad663ef6..b1aa92351a 100644 --- a/components/lib/contextmenu/ContextMenuBase.js +++ b/components/lib/contextmenu/ContextMenuBase.js @@ -1,6 +1,6 @@ -import { ObjectUtils } from '../utils/Utils'; +import { ComponentBase } from '../componentbase/ComponentBase'; -export const ContextMenuBase = { +export const ContextMenuBase = ComponentBase.extend({ defaultProps: { __TYPE: 'ContextMenu', id: null, @@ -18,7 +18,5 @@ export const ContextMenuBase = { onHide: null, submenuIcon: null, children: undefined - }, - getProps: (props) => ObjectUtils.getMergedProps(props, ContextMenuBase.defaultProps), - getOtherProps: (props) => ObjectUtils.getDiffProps(props, ContextMenuBase.defaultProps) -}; + } +}); diff --git a/components/lib/contextmenu/ContextMenuSub.js b/components/lib/contextmenu/ContextMenuSub.js index 213e8680f3..a540d45e7d 100644 --- a/components/lib/contextmenu/ContextMenuSub.js +++ b/components/lib/contextmenu/ContextMenuSub.js @@ -2,7 +2,7 @@ import * as React from 'react'; import { CSSTransition } from '../csstransition/CSSTransition'; import { useUpdateEffect } from '../hooks/Hooks'; import { Ripple } from '../ripple/Ripple'; -import { classNames, DomHandler, IconUtils } from '../utils/Utils'; +import { classNames, DomHandler, IconUtils, mergeProps } from '../utils/Utils'; import { AngleRightIcon } from '../icons/angleright'; export const ContextMenuSub = React.memo((props) => { @@ -10,6 +10,14 @@ export const ContextMenuSub = React.memo((props) => { const submenuRef = React.useRef(null); const active = props.root || !props.resetMenu; + const getPTOptions = (item, key) => { + return props.ptm(key, { + context: { + active: activeItemState === item + } + }); + }; + if (props.resetMenu === true && activeItemState !== null) { setActiveItemState(null); } @@ -84,12 +92,21 @@ export const ContextMenuSub = React.memo((props) => { }); const createSeparator = (index) => { - return
  • ; + const separatorProps = mergeProps( + { + role: "separator", + key: 'separator_' + index, + className: "p-menu-separator", + }, + props.ptm('separator') + ); + + return
  • ; }; const createSubmenu = (item) => { if (item.items) { - return ; + return ; } return null; @@ -105,13 +122,45 @@ export const ContextMenuSub = React.memo((props) => { const className = classNames('p-menuitem', { 'p-menuitem-active': active }, item.className); const linkClassName = classNames('p-menuitem-link', { 'p-disabled': item.disabled }); const iconClassName = 'p-menuitem-icon'; - const icon = IconUtils.getJSXIcon(item.icon, { className: iconClassName }, { props: props.menuProps }); + const iconProps = mergeProps( + { + className: iconClassName + }, + getPTOptions(item, 'icon') + ); + const icon = IconUtils.getJSXIcon(item.icon, { ...iconProps }, { props: props.menuProps }); const submenuIconClassName = 'p-submenu-icon'; - const submenuIcon = item.items && IconUtils.getJSXIcon(props.submenuIcon || , { className: submenuIconClassName }, { props: props.menuProps }); - const label = item.label && {item.label}; + const submenuIconProps = mergeProps( + { + className: submenuIconClassName + }, + getPTOptions(item, 'submenuIcon') + ); + + const labelProps = mergeProps( + { + className: "p-menuitem-text" + }, + getPTOptions(item, 'label') + ); + const submenuIcon = item.items && IconUtils.getJSXIcon(props.submenuIcon || , { ...submenuIconProps }, { props: props.menuProps }); + const label = item.label && {item.label}; const submenu = createSubmenu(item); + const actionProps = mergeProps( + { + href: item.url || '#', + className: linkClassName, + target: item.target, + onClick: (event) => onItemClick(event, item, index), + role: "menuitem", + 'aria-haspopup': item.items != null, + 'aria-disabled': item.disabled + }, + getPTOptions(item, "action") + ); + let content = ( - onItemClick(event, item, index)} role="menuitem" aria-haspopup={item.items != null} aria-disabled={item.disabled}> + {icon} {label} {submenuIcon} @@ -119,8 +168,20 @@ export const ContextMenuSub = React.memo((props) => { ); + const menuitemProps = mergeProps( + { + id: item.id, + role: "none", + className, + style: item.style, + key, + onMouseEnter: (event) => onItemMouseEnter(event, item) + }, + getPTOptions(item, 'menuitem') + ); + return ( -
  • onItemMouseEnter(event, item)}> +
  • {content} {submenu}
  • @@ -139,10 +200,17 @@ export const ContextMenuSub = React.memo((props) => { 'p-submenu-list': !props.root }); const submenu = createMenu(); + const menuProps = mergeProps( + { + ref: submenuRef, + className + }, + props.ptm('menu') + ) return ( -
      +
        {submenu}
      diff --git a/components/lib/contextmenu/contextmenu.d.ts b/components/lib/contextmenu/contextmenu.d.ts index bd83decb4c..bdabaa8d25 100644 --- a/components/lib/contextmenu/contextmenu.d.ts +++ b/components/lib/contextmenu/contextmenu.d.ts @@ -10,7 +10,93 @@ import * as React from 'react'; import { CSSTransitionProps } from '../csstransition'; import { MenuItem } from '../menuitem'; -import { IconType } from '../utils/utils'; +import { IconType, PassThroughType } from '../utils/utils'; + +export declare type ContextMenuPassThroughType = PassThroughType; + +/** + * Custom passthrough(pt) option method. + */ +export interface ContextMenuPassThroughMethodOptions { + props: ContextMenuProps; + state: ContextMenuState; + context: ContextMenuContext; +} + +/** + * Custom passthrough(pt) options. + * @see {@link ContextMenuProps.pt} + */ +export interface ContextMenuPassThroughOptions { + /** + * Uses to pass attributes to the root's DOM element. + */ + root?: ContextMenuPassThroughType>; + /** + * Uses to pass attributes to the list's DOM element. + */ + menu?: ContextMenuPassThroughType>; + /** + * Uses to pass attributes to the list item's DOM element. + */ + menuitem?: ContextMenuPassThroughType>; + /** + * Uses to pass attributes to the action's DOM element. + */ + action?: ContextMenuPassThroughType>; + /** + * Uses to pass attributes to the icon's DOM element. + */ + icon?: ContextMenuPassThroughType | React.HTMLAttributes>; + /** + * Uses to pass attributes to the label's DOM element. + */ + label?: ContextMenuPassThroughType>; + /** + * Uses to pass attributes to the submenu icon's DOM element. + */ + submenuIcon?: ContextMenuPassThroughType | React.HTMLAttributes>; + /** + * Uses to pass attributes to the separator's DOM element. + */ + separator?: ContextMenuPassThroughType>; +} + +/** + * Defines current inline state in ContextMenu component. + */ +export interface ContextMenuState { + /** + * Current visible state as a boolean. + * @defaultValue false + */ + visible: boolean; + /** + * Current reshow state as a boolean. + * @defaultValue false + */ + reshow: boolean; + /** + * Current resetMenu state as a boolean. + * @defaultValue false + */ + resetMenu: boolean; + /** + * Current attributeSelector visible state as a string. + */ + attributeSelector: boolean; +} + +/** + * Defines current options in ContextMenu component. + */ +export interface ContextMenuContext { + /** + * Current active state of menuitem as a boolean. + * @defaultValue false + */ + active: boolean; +} /** * Defines valid properties in ContextMenu component. In addition to these, all properties of HTMLDivElement can be used in this component. @@ -73,6 +159,11 @@ export interface ContextMenuProps extends Omit { component: AccessibilityDoc } ]; +const ptDocs = [ + { + id: 'pt.wireframe', + label: 'Wireframe', + component: Wireframe + }, + { + id: 'pt.contextmenu.options', + label: 'ContextMenu PT Options', + component: DocApiTable + }, + { + id: 'pt.demo', + label: 'Example', + component: PTDoc + } +]; - return ; + + return ; }; export default ContextMenuDemo;