@elementor/editor-app-bar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +5 -0
  3. package/dist/index.d.ts +234 -0
  4. package/dist/index.js +1047 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +1017 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +50 -0
  9. package/src/components/__tests__/top-bar.test.tsx +15 -0
  10. package/src/components/actions/action.tsx +33 -0
  11. package/src/components/actions/link.tsx +33 -0
  12. package/src/components/actions/toggle-action.tsx +35 -0
  13. package/src/components/app-bar.tsx +37 -0
  14. package/src/components/locations/__tests__/locations-components.test.tsx +37 -0
  15. package/src/components/locations/__tests__/menus.test.tsx +89 -0
  16. package/src/components/locations/main-menu-location.tsx +50 -0
  17. package/src/components/locations/page-indication-location.tsx +9 -0
  18. package/src/components/locations/primary-action-location.tsx +9 -0
  19. package/src/components/locations/responsive-location.tsx +9 -0
  20. package/src/components/locations/tools-menu-location.tsx +26 -0
  21. package/src/components/locations/utilities-menu-location.tsx +35 -0
  22. package/src/components/ui/popover-menu-item.tsx +39 -0
  23. package/src/components/ui/popover-menu.tsx +23 -0
  24. package/src/components/ui/toolbar-logo.tsx +80 -0
  25. package/src/components/ui/toolbar-menu-item.tsx +18 -0
  26. package/src/components/ui/toolbar-menu-more.tsx +29 -0
  27. package/src/components/ui/toolbar-menu-toggle-item.tsx +18 -0
  28. package/src/components/ui/toolbar-menu.tsx +15 -0
  29. package/src/contexts/menu-context.tsx +20 -0
  30. package/src/extensions/documents-indicator/components/__tests__/settings-button.test.tsx +46 -0
  31. package/src/extensions/documents-indicator/components/settings-button.tsx +43 -0
  32. package/src/extensions/documents-indicator/index.ts +12 -0
  33. package/src/extensions/documents-preview/hooks/__tests__/use-document-preview-props.test.ts +43 -0
  34. package/src/extensions/documents-preview/hooks/use-action-props.ts +17 -0
  35. package/src/extensions/documents-preview/index.ts +10 -0
  36. package/src/extensions/documents-save/components/__tests__/primary-action-menu.test.tsx +41 -0
  37. package/src/extensions/documents-save/components/__tests__/primary-action.test.tsx +176 -0
  38. package/src/extensions/documents-save/components/primary-action-menu.tsx +48 -0
  39. package/src/extensions/documents-save/components/primary-action.tsx +79 -0
  40. package/src/extensions/documents-save/hooks/__tests__/use-document-save-draft-props.test.ts +64 -0
  41. package/src/extensions/documents-save/hooks/__tests__/use-document-save-template-props.test.ts +27 -0
  42. package/src/extensions/documents-save/hooks/use-document-save-draft-props.ts +16 -0
  43. package/src/extensions/documents-save/hooks/use-document-save-template-props.ts +14 -0
  44. package/src/extensions/documents-save/index.ts +26 -0
  45. package/src/extensions/elements/hooks/__tests__/use-action-props.test.ts +33 -0
  46. package/src/extensions/elements/hooks/use-action-props.ts +15 -0
  47. package/src/extensions/elements/index.ts +13 -0
  48. package/src/extensions/elements/sync/__tests__/sync-panel-title.test.ts +92 -0
  49. package/src/extensions/elements/sync/sync-panel-title.ts +47 -0
  50. package/src/extensions/finder/hooks/__tests__/use-action-props.test.ts +36 -0
  51. package/src/extensions/finder/hooks/use-action-props.ts +18 -0
  52. package/src/extensions/finder/index.ts +10 -0
  53. package/src/extensions/help/index.ts +18 -0
  54. package/src/extensions/history/hooks/__tests__/use-action-props.test.ts +33 -0
  55. package/src/extensions/history/hooks/use-action-props.ts +15 -0
  56. package/src/extensions/history/index.ts +10 -0
  57. package/src/extensions/index.ts +34 -0
  58. package/src/extensions/keyboard-shortcuts/hooks/__tests__/use-action-props.test.ts +20 -0
  59. package/src/extensions/keyboard-shortcuts/hooks/use-action-props.ts +12 -0
  60. package/src/extensions/keyboard-shortcuts/index.ts +11 -0
  61. package/src/extensions/site-settings/components/__tests__/portalled-primary-action.test.tsx +94 -0
  62. package/src/extensions/site-settings/components/__tests__/primary-action.test.tsx +100 -0
  63. package/src/extensions/site-settings/components/portal.tsx +27 -0
  64. package/src/extensions/site-settings/components/portalled-primary-action.tsx +11 -0
  65. package/src/extensions/site-settings/components/primary-action.tsx +32 -0
  66. package/src/extensions/site-settings/hooks/__tests__/use-action-props.test.ts +49 -0
  67. package/src/extensions/site-settings/hooks/use-action-props.ts +22 -0
  68. package/src/extensions/site-settings/index.ts +18 -0
  69. package/src/extensions/structure/hooks/__tests__/use-action-props.test.ts +33 -0
  70. package/src/extensions/structure/hooks/use-action-props.ts +16 -0
  71. package/src/extensions/structure/index.ts +10 -0
  72. package/src/extensions/theme-builder/hooks/__tests__/use-action-props.test.ts +22 -0
  73. package/src/extensions/theme-builder/hooks/use-action-props.ts +12 -0
  74. package/src/extensions/theme-builder/index.ts +9 -0
  75. package/src/extensions/user-preferences/hooks/__tests__/use-action-props.test.ts +34 -0
  76. package/src/extensions/user-preferences/hooks/use-action-props.ts +16 -0
  77. package/src/extensions/user-preferences/index.ts +10 -0
  78. package/src/extensions/wordpress/index.ts +21 -0
  79. package/src/index.ts +13 -0
  80. package/src/init.ts +15 -0
  81. package/src/locations/__tests__/menus.test.tsx +229 -0
  82. package/src/locations/consts.ts +4 -0
  83. package/src/locations/index.ts +29 -0
  84. package/src/locations/menus.tsx +141 -0
  85. package/src/sync/__tests__/redirect-old-menus.test.ts +34 -0
  86. package/src/sync/redirect-old-menus.ts +9 -0
  87. package/src/types.ts +3 -0
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { Fragment } from 'react';
3
+ import ToolbarMenu from '../ui/toolbar-menu';
4
+ import { utilitiesMenu } from '../../locations';
5
+ import { Divider } from '@elementor/ui';
6
+ import ToolbarMenuMore from '../ui/toolbar-menu-more';
7
+
8
+ const MAX_TOOLBAR_ACTIONS = 3;
9
+
10
+ const { useMenuItems } = utilitiesMenu;
11
+
12
+ export default function UtilitiesMenuLocation() {
13
+ const menuItems = useMenuItems();
14
+
15
+ const toolbarMenuItems = menuItems.default.slice( 0, MAX_TOOLBAR_ACTIONS );
16
+ const popoverMenuItems = menuItems.default.slice( MAX_TOOLBAR_ACTIONS );
17
+
18
+ return (
19
+ <ToolbarMenu>
20
+ { toolbarMenuItems.map(
21
+ ( { MenuItem, id }, index ) => (
22
+ <Fragment key={ id }>
23
+ { index === 0 && <Divider orientation="vertical" /> }
24
+ <MenuItem />
25
+ </Fragment>
26
+ )
27
+ ) }
28
+ { popoverMenuItems.length > 0 && (
29
+ <ToolbarMenuMore id="elementor-editor-app-bar-utilities-more">
30
+ { popoverMenuItems.map( ( { MenuItem, id } ) => <MenuItem key={ id } /> ) }
31
+ </ToolbarMenuMore>
32
+ ) }
33
+ </ToolbarMenu>
34
+ );
35
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from 'react';
2
+ import {
3
+ MenuItem,
4
+ MenuItemProps,
5
+ ListItemText,
6
+ ListItemIcon,
7
+ withDirection,
8
+ } from '@elementor/ui';
9
+ import { ArrowUpRightIcon } from '@elementor/icons';
10
+
11
+ type ExtraProps = {
12
+ href?: string;
13
+ target?: string;
14
+ text?: string;
15
+ icon?: JSX.Element;
16
+ }
17
+
18
+ export type PopoverMenuItemProps = MenuItemProps & ExtraProps;
19
+
20
+ const DirectionalArrowIcon = withDirection( ArrowUpRightIcon );
21
+
22
+ export default function PopoverMenuItem( { text, icon, onClick, href, target, disabled, ...props }: PopoverMenuItemProps ) {
23
+ const isExternalLink = href && target === '_blank';
24
+
25
+ return (
26
+ <MenuItem
27
+ { ...props }
28
+ disabled={ disabled }
29
+ onClick={ onClick }
30
+ component={ href ? 'a' : 'div' }
31
+ href={ href }
32
+ target={ target }
33
+ >
34
+ <ListItemIcon>{ icon }</ListItemIcon>
35
+ <ListItemText primary={ text } />
36
+ { isExternalLink && <DirectionalArrowIcon /> }
37
+ </MenuItem>
38
+ );
39
+ }
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import { Menu, MenuProps } from '@elementor/ui';
3
+ import { MenuContextProvider } from '../../contexts/menu-context';
4
+
5
+ export type PopoverMenuProps = MenuProps;
6
+
7
+ export default function PopoverMenu( { children, ...props }: PopoverMenuProps ) {
8
+ return (
9
+ <MenuContextProvider type={ 'popover' }>
10
+ <Menu
11
+ PaperProps={ {
12
+ sx: { mt: 4 },
13
+ } }
14
+ { ...props }
15
+ MenuListProps={ {
16
+ component: 'div',
17
+ } }
18
+ >
19
+ { children }
20
+ </Menu>
21
+ </MenuContextProvider>
22
+ );
23
+ }
@@ -0,0 +1,80 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { __ } from '@wordpress/i18n';
4
+ import { ToggleButton, ToggleButtonProps, SvgIconProps, SvgIcon, styled } from '@elementor/ui';
5
+
6
+ interface StyledElementorLogoProps extends SvgIconProps {
7
+ showMenuIcon?: boolean;
8
+ }
9
+
10
+ type ToolbarLogoProps = Omit<ToggleButtonProps, 'value'>;
11
+
12
+ const ElementorLogo = ( props: SvgIconProps ) => {
13
+ return (
14
+ <SvgIcon viewBox="0 0 32 32" { ...props }>
15
+ <g>
16
+ <circle cx="16" cy="16" r="16" />
17
+ <path d="M11.7 9H9V22.3H11.7V9Z" />
18
+ <path d="M22.4 9H9V11.7H22.4V9Z" />
19
+ <path d="M22.4 14.4004H9V17.1004H22.4V14.4004Z" />
20
+ <path d="M22.4 19.6992H9V22.3992H22.4V19.6992Z" />
21
+ </g>
22
+ </SvgIcon>
23
+ );
24
+ };
25
+
26
+ const StyledToggleButton = styled( ToggleButton )( () => ( {
27
+ padding: 0,
28
+ '&.MuiToggleButton-root:hover': {
29
+ backgroundColor: 'initial',
30
+ },
31
+ '&.MuiToggleButton-root.Mui-selected': {
32
+ backgroundColor: 'initial',
33
+ },
34
+ } ) );
35
+
36
+ const StyledElementorLogo = styled( ElementorLogo, {
37
+ shouldForwardProp: ( prop ) => prop !== 'showMenuIcon',
38
+ } )<StyledElementorLogoProps>( ( { theme, showMenuIcon } ) => ( {
39
+ width: 'auto',
40
+ height: '100%',
41
+ '& path': {
42
+ fill: 'initial',
43
+ transition: 'all 0.2s linear',
44
+ transformOrigin: 'bottom left',
45
+ '&:first-of-type': {
46
+ transitionDelay: ! showMenuIcon && '0.2s',
47
+ transform: showMenuIcon && 'translateY(-9px) scaleY(0)',
48
+ },
49
+ '&:not(:first-of-type)': {
50
+ // Emotion automatically change 4 to -4 in RTL moode.
51
+ transform: ! showMenuIcon && `translateX(${ theme.direction === 'rtl' ? '4' : '9' }px) scaleX(0.6)`,
52
+ },
53
+ '&:nth-of-type(2)': {
54
+ transitionDelay: showMenuIcon ? '0' : '0.2s',
55
+ },
56
+ '&:nth-of-type(3)': {
57
+ transitionDelay: '0.1s',
58
+ },
59
+ '&:nth-of-type(4)': {
60
+ transitionDelay: showMenuIcon ? '0.2s' : '0',
61
+ },
62
+ },
63
+ } ) );
64
+
65
+ export default function ToolbarLogo( props: ToolbarLogoProps ) {
66
+ const [ isHoverState, setIsHoverState ] = useState( false );
67
+ const showMenuIcon = props.selected || isHoverState;
68
+
69
+ return (
70
+ <StyledToggleButton
71
+ { ...props }
72
+ value="selected"
73
+ size="small"
74
+ onMouseEnter={ () => setIsHoverState( true ) }
75
+ onMouseLeave={ () => setIsHoverState( false ) }
76
+ >
77
+ <StyledElementorLogo titleAccess={ __( 'Elementor Logo', 'elementor' ) } showMenuIcon={ showMenuIcon } />
78
+ </StyledToggleButton>
79
+ );
80
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import { Box, IconButton, IconButtonProps, Tooltip } from '@elementor/ui';
3
+
4
+ export type ToolbarMenuItemProps = IconButtonProps & {
5
+ title?: string;
6
+ selected?: boolean;
7
+ }
8
+
9
+ export default function ToolbarMenuItem( { title, ...props }: ToolbarMenuItemProps ) {
10
+ return (
11
+ <Tooltip title={ title }>
12
+ { /* @see https://mui.com/material-ui/react-tooltip/#disabled-elements */ }
13
+ <Box component="span" aria-label={ undefined }>
14
+ <IconButton { ...props } aria-label={ title } size="small" />
15
+ </Box>
16
+ </Tooltip>
17
+ );
18
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+ import { PropsWithChildren } from 'react';
3
+ import { bindMenu, bindTrigger, usePopupState } from '@elementor/ui';
4
+ import ToolbarMenuItem from './toolbar-menu-item';
5
+ import { __ } from '@wordpress/i18n';
6
+ import { DotsVerticalIcon } from '@elementor/icons';
7
+ import PopoverMenu from './popover-menu';
8
+
9
+ export type ToolbarMenuMoreProps = PropsWithChildren<{
10
+ id: string;
11
+ }>
12
+
13
+ export default function ToolbarMenuMore( { children, id }: ToolbarMenuMoreProps ) {
14
+ const popupState = usePopupState( {
15
+ variant: 'popover',
16
+ popupId: id,
17
+ } );
18
+
19
+ return (
20
+ <>
21
+ <ToolbarMenuItem { ...bindTrigger( popupState ) } title={ __( 'More', 'elementor' ) }>
22
+ <DotsVerticalIcon />
23
+ </ToolbarMenuItem>
24
+ <PopoverMenu onClick={ popupState.close } { ...bindMenu( popupState ) }>
25
+ { children }
26
+ </PopoverMenu>
27
+ </>
28
+ );
29
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import { Box, ToggleButton, ToggleButtonProps, Tooltip } from '@elementor/ui';
3
+
4
+ export type ToolbarMenuToggleItemProps = Omit<ToggleButtonProps, 'onChange'> & {
5
+ title?: string;
6
+ onClick?: () => void;
7
+ }
8
+
9
+ export default function ToolbarMenuToggleItem( { title, onClick, ...props }: ToolbarMenuToggleItemProps ) {
10
+ return (
11
+ <Tooltip title={ title }>
12
+ { /* @see https://mui.com/material-ui/react-tooltip/#disabled-elements */ }
13
+ <Box component="span" aria-label={ undefined }>
14
+ <ToggleButton { ...props } onChange={ onClick } aria-label={ title } size="small" />
15
+ </Box>
16
+ </Tooltip>
17
+ );
18
+ }
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import { Stack, StackProps } from '@elementor/ui';
3
+ import { MenuContextProvider } from '../../contexts/menu-context';
4
+
5
+ export type ToolbarMenuProps = StackProps;
6
+
7
+ export default function ToolbarMenu( { children, ...props }: ToolbarMenuProps ) {
8
+ return (
9
+ <MenuContextProvider type={ 'toolbar' }>
10
+ <Stack sx={ { px: 4 } } spacing={ 4 } direction="row" alignItems="center" { ...props }>
11
+ { children }
12
+ </Stack>
13
+ </MenuContextProvider>
14
+ );
15
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from 'react';
2
+ import { createContext, PropsWithChildren, useContext } from 'react';
3
+
4
+ type MenuContextValue = {
5
+ type: 'toolbar' | 'popover';
6
+ }
7
+
8
+ const MenuContext = createContext<MenuContextValue>( { type: 'toolbar' } );
9
+
10
+ export function MenuContextProvider( { type, children }: PropsWithChildren<MenuContextValue> ) {
11
+ return (
12
+ <MenuContext.Provider value={ { type } }>
13
+ { children }
14
+ </MenuContext.Provider>
15
+ );
16
+ }
17
+
18
+ export function useMenuContext() {
19
+ return useContext( MenuContext );
20
+ }
@@ -0,0 +1,46 @@
1
+ import * as React from 'react';
2
+ import SettingsButton from '../settings-button';
3
+ import { openRoute, useRouteStatus } from '@elementor/editor-v1-adapters';
4
+ import { render } from '@testing-library/react';
5
+
6
+ jest.mock( '@elementor/editor-v1-adapters', () => ( {
7
+ openRoute: jest.fn(),
8
+ useRouteStatus: jest.fn( () => ( { isActive: false, isBlocked: false } ) ),
9
+ } ) );
10
+
11
+ jest.mock( '@elementor/editor-documents', () => ( {
12
+ useActiveDocument: jest.fn( () => ( {
13
+ type: { value: 'wp-page', label: 'Page' },
14
+ } ) ),
15
+ useHostDocument: jest.fn(),
16
+ } ) );
17
+
18
+ describe( '@elementor/editor-app-bar - App bar document settings button', () => {
19
+ it( 'should open the document settings panel on click', () => {
20
+ // Arrange.
21
+ const { getByRole } = render( <SettingsButton /> );
22
+
23
+ // Act.
24
+ getByRole( 'button' ).click();
25
+
26
+ // Assert.
27
+ expect( openRoute ).toHaveBeenCalledTimes( 1 );
28
+ expect( openRoute ).toHaveBeenCalledWith( 'panel/page-settings/settings' );
29
+ } );
30
+
31
+ it( 'should have the correct props for disabled and selected', () => {
32
+ // Arrange.
33
+ jest.mocked( useRouteStatus ).mockReturnValue( { isActive: true, isBlocked: true } );
34
+
35
+ // Act.
36
+ const { getByRole } = render( <SettingsButton /> );
37
+
38
+ // Assert.
39
+ const button = getByRole( 'button' );
40
+
41
+ expect( button ).toHaveAttribute( 'disabled' );
42
+ expect( button ).toHaveAttribute( 'aria-pressed', 'true' );
43
+ expect( useRouteStatus ).toHaveBeenCalledTimes( 1 );
44
+ expect( useRouteStatus ).toHaveBeenCalledWith( 'panel/page-settings' );
45
+ } );
46
+ } );
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ import { Box, ToggleButton, Tooltip } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+ import { openRoute, useRouteStatus } from '@elementor/editor-v1-adapters';
5
+ import { SettingsIcon } from '@elementor/icons';
6
+ import { useActiveDocument, useHostDocument } from '@elementor/editor-documents';
7
+
8
+ export default function SettingsButton() {
9
+ const activeDocument = useActiveDocument();
10
+ const hostDocument = useHostDocument();
11
+
12
+ const document = activeDocument && activeDocument.type.value !== 'kit'
13
+ ? activeDocument
14
+ : hostDocument;
15
+
16
+ const { isActive, isBlocked } = useRouteStatus( 'panel/page-settings' );
17
+
18
+ if ( ! document ) {
19
+ return null;
20
+ }
21
+
22
+ /* translators: %s: Post type label. */
23
+ const title = __( '%s Settings', 'elementor' )
24
+ .replace( '%s', document.type.label );
25
+
26
+ return (
27
+ <Tooltip title={ title }>
28
+ { /* @see https://mui.com/material-ui/react-tooltip/#disabled-elements */ }
29
+ <Box component="span" aria-label={ undefined }>
30
+ <ToggleButton
31
+ value="document-settings"
32
+ selected={ isActive }
33
+ disabled={ isBlocked }
34
+ onChange={ () => openRoute( 'panel/page-settings/settings' ) }
35
+ aria-label={ title }
36
+ size="small"
37
+ >
38
+ <SettingsIcon />
39
+ </ToggleButton>
40
+ </Box>
41
+ </Tooltip>
42
+ );
43
+ }
@@ -0,0 +1,12 @@
1
+ import { injectIntoPageIndication } from '../../locations';
2
+ import SettingsButton from './components/settings-button';
3
+
4
+ export function init() {
5
+ injectIntoPageIndication( {
6
+ name: 'document-settings-button',
7
+ filler: SettingsButton,
8
+ options: {
9
+ priority: 20, // After document indicator.
10
+ },
11
+ } );
12
+ }
@@ -0,0 +1,43 @@
1
+ import { renderHook } from '@testing-library/react-hooks';
2
+ import useActionProps from '../use-action-props';
3
+ import { runCommand } from '@elementor/editor-v1-adapters';
4
+ import { createMockDocument } from 'test-utils';
5
+ import { useActiveDocument } from '@elementor/editor-documents';
6
+
7
+ jest.mock( '@elementor/editor-documents', () => ( {
8
+ useActiveDocument: jest.fn(),
9
+ } ) );
10
+
11
+ jest.mock( '@elementor/editor-v1-adapters', () => ( {
12
+ runCommand: jest.fn(),
13
+ } ) );
14
+
15
+ describe( '@elementor/editor-app-bar - useDocumentPreviewProps', () => {
16
+ it( 'should open the document preview', () => {
17
+ // Arrange.
18
+ jest.mocked( useActiveDocument ).mockReturnValue( createMockDocument() );
19
+
20
+ const command = 'editor/documents/preview';
21
+ const args = { id: 1, force: true };
22
+
23
+ // Act.
24
+ const { result } = renderHook( () => useActionProps() );
25
+ result.current.onClick();
26
+
27
+ // Assert.
28
+ expect( runCommand ).toBeCalledTimes( 1 );
29
+ expect( runCommand ).toHaveBeenCalledWith( command, args );
30
+ } );
31
+
32
+ it( 'should not run the command when there is no document', () => {
33
+ // Arrange.
34
+ jest.mocked( useActiveDocument ).mockReturnValue( null );
35
+
36
+ // Act.
37
+ const { result } = renderHook( () => useActionProps() );
38
+ result.current.onClick();
39
+
40
+ // Assert.
41
+ expect( runCommand ).toBeCalledTimes( 0 );
42
+ } );
43
+ } );
@@ -0,0 +1,17 @@
1
+ import { __ } from '@wordpress/i18n';
2
+ import { EyeIcon } from '@elementor/icons';
3
+ import { runCommand } from '@elementor/editor-v1-adapters';
4
+ import { useActiveDocument } from '@elementor/editor-documents';
5
+
6
+ export default function useActionProps() {
7
+ const document = useActiveDocument();
8
+
9
+ return {
10
+ icon: EyeIcon,
11
+ title: __( 'Preview Changes', 'elementor' ),
12
+ onClick: () => document && runCommand( 'editor/documents/preview', {
13
+ id: document.id,
14
+ force: true,
15
+ } ),
16
+ };
17
+ }
@@ -0,0 +1,10 @@
1
+ import { utilitiesMenu } from '../../locations';
2
+ import useDocumentPreviewProps from './hooks/use-action-props';
3
+
4
+ export function init() {
5
+ utilitiesMenu.registerAction( {
6
+ name: 'document-preview-button',
7
+ priority: 30, // After help.
8
+ useProps: useDocumentPreviewProps,
9
+ } );
10
+ }
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import PrimaryActionMenu from '../primary-action-menu';
4
+ import { documentOptionsMenu } from '../../../../locations';
5
+
6
+ jest.mock( '@elementor/editor-documents', () => ( {
7
+ useActiveDocument: jest.fn(),
8
+ } ) );
9
+
10
+ describe( '@elementor/editor-app-bar - Primary action menu', () => {
11
+ it( 'should render the actions ordered properly (save, default)', () => {
12
+ // Arrange.
13
+ documentOptionsMenu.registerAction( {
14
+ name: 'test--first-action',
15
+ group: 'save',
16
+ props: {
17
+ title: 'First action',
18
+ icon: () => <span>a</span>,
19
+ },
20
+ } );
21
+
22
+ documentOptionsMenu.registerAction( {
23
+ name: 'test--second-action',
24
+ group: 'default',
25
+ props: {
26
+ title: 'Second action',
27
+ icon: () => <span>a</span>,
28
+ },
29
+ } );
30
+
31
+ // Act.
32
+ const { getAllByRole } = render( <PrimaryActionMenu open={ true } anchorEl={ document.body } /> );
33
+
34
+ // Assert.
35
+ const menuItems = getAllByRole( 'menuitem' );
36
+
37
+ expect( menuItems ).toHaveLength( 2 );
38
+ expect( menuItems[ 0 ] ).toHaveTextContent( 'First action' );
39
+ expect( menuItems[ 1 ] ).toHaveTextContent( 'Second action' );
40
+ } );
41
+ } );
@@ -0,0 +1,176 @@
1
+ import * as React from 'react';
2
+ import PrimaryAction from '../primary-action';
3
+ import { render } from '@testing-library/react';
4
+ import { createMockDocument } from 'test-utils';
5
+ import { useActiveDocument, useActiveDocumentActions } from '@elementor/editor-documents';
6
+
7
+ jest.mock( '@elementor/editor-documents', () => ( {
8
+ useActiveDocument: jest.fn(),
9
+ useActiveDocumentActions: jest.fn(),
10
+ } ) );
11
+
12
+ const actionsMock = {
13
+ save: jest.fn(),
14
+ saveDraft: jest.fn(),
15
+ saveTemplate: jest.fn(),
16
+ };
17
+
18
+ jest.mocked( useActiveDocumentActions ).mockReturnValue( actionsMock );
19
+
20
+ describe( '@elementor/editor-app-bar - Top Bar Primary Action', () => {
21
+ it( 'should not render when there is no active document', () => {
22
+ // Arrange.
23
+ jest.mocked( useActiveDocument ).mockReturnValue( null );
24
+
25
+ // Act.
26
+ const { container } = render( <PrimaryAction /> );
27
+
28
+ // Assert.
29
+ expect( container ).toBeEmptyDOMElement();
30
+ } );
31
+
32
+ it( 'should have "Submit" text when the user cannot publish the document', () => {
33
+ // Arrange.
34
+ const mockDocument = createMockDocument( {
35
+ userCan: {
36
+ publish: false,
37
+ },
38
+ } );
39
+
40
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
41
+
42
+ // Act.
43
+ const { getAllByRole } = render( <PrimaryAction /> );
44
+
45
+ // Assert.
46
+ expect( getAllByRole( 'button' )[ 0 ] ).toHaveTextContent( 'Submit' );
47
+ } );
48
+
49
+ it( 'should have "Publish" text when the user can publish the document', () => {
50
+ // Arrange.
51
+ const mockDocument = createMockDocument( {
52
+ userCan: {
53
+ publish: true,
54
+ },
55
+ } );
56
+
57
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
58
+
59
+ // Act.
60
+ const { getAllByRole } = render( <PrimaryAction /> );
61
+
62
+ // Assert.
63
+ expect( getAllByRole( 'button' )[ 0 ] ).toHaveTextContent( 'Publish' );
64
+ } );
65
+
66
+ it( 'should be disabled when the document is a Kit', () => {
67
+ // Arrange.
68
+ const mockDocument = createMockDocument( {
69
+ isDirty: true,
70
+ type: {
71
+ value: 'kit',
72
+ label: 'Kit',
73
+ },
74
+ } );
75
+
76
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
77
+
78
+ // Act.
79
+ const { getAllByRole } = render( <PrimaryAction /> );
80
+
81
+ // Assert.
82
+ expect( getAllByRole( 'button' )[ 0 ] ).toBeDisabled();
83
+ } );
84
+
85
+ it( 'should be disabled when the document is pristine', () => {
86
+ // Arrange.
87
+ const mockDocument = createMockDocument( { isDirty: false } );
88
+
89
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
90
+
91
+ // Act.
92
+ const { getAllByRole } = render( <PrimaryAction /> );
93
+
94
+ // Assert.
95
+ expect( getAllByRole( 'button' )[ 0 ] ).toBeDisabled();
96
+ } );
97
+
98
+ it( 'should always be enabled when the document status is draft', () => {
99
+ // Arrange.
100
+ const mockDocument = createMockDocument( {
101
+ isDirty: false,
102
+ status: {
103
+ value: 'draft',
104
+ label: 'Draft',
105
+ },
106
+ } );
107
+
108
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
109
+
110
+ // Act.
111
+ const { getAllByRole } = render( <PrimaryAction /> );
112
+
113
+ // Assert.
114
+ expect( getAllByRole( 'button' )[ 0 ] ).toBeEnabled();
115
+ } );
116
+
117
+ it( 'should save the active document on click', () => {
118
+ // Arrange.
119
+ const mockDocument = createMockDocument( { isDirty: true } );
120
+
121
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
122
+
123
+ // Act.
124
+ const { getAllByRole } = render( <PrimaryAction /> );
125
+
126
+ getAllByRole( 'button' )[ 0 ]?.click();
127
+
128
+ // Assert.
129
+ expect( actionsMock.save ).toHaveBeenCalledTimes( 1 );
130
+ } );
131
+
132
+ it( 'should show a loader & not save when there is save in progress', () => {
133
+ // Arrange.
134
+ const mockDocument = createMockDocument( {
135
+ isDirty: true,
136
+ isSaving: true,
137
+ } );
138
+
139
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
140
+
141
+ // Act.
142
+ const { getAllByRole } = render( <PrimaryAction /> );
143
+
144
+ const button = getAllByRole( 'button' )[ 0 ];
145
+ const loader = getAllByRole( 'progressbar' )[ 0 ];
146
+
147
+ button?.click();
148
+
149
+ // Assert.
150
+ expect( actionsMock.save ).not.toHaveBeenCalled();
151
+ expect( loader ).toBeInTheDocument();
152
+ expect( button.textContent ).toBe( '' );
153
+ } );
154
+
155
+ it( 'should not show a loader when the button is disabled', () => {
156
+ // Arrange.
157
+ const mockDocument = createMockDocument( {
158
+ type: {
159
+ value: 'kit',
160
+ label: 'Kit',
161
+ }, // Disables the button.
162
+ isSaving: true,
163
+ } );
164
+
165
+ jest.mocked( useActiveDocument ).mockReturnValue( mockDocument );
166
+
167
+ // Act.
168
+ const { getAllByRole, queryByRole } = render( <PrimaryAction /> );
169
+ const button = getAllByRole( 'button' )[ 0 ];
170
+ const loader = queryByRole( 'progressbar' );
171
+
172
+ // Assert.
173
+ expect( loader ).not.toBeInTheDocument();
174
+ expect( button.textContent ).not.toBe( '' );
175
+ } );
176
+ } );