@elementor/editor-site-navigation 0.7.0 → 0.8.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.
@@ -0,0 +1,48 @@
1
+ import * as React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { Page } from '../../../../types';
4
+ import PagesCollapsibleList from '../pages-collapsible-list';
5
+
6
+ jest.mock( '@elementor/editor-documents' );
7
+
8
+ describe( '@elementor/editor-site-navigation - PagesCollapsibleList', () => {
9
+ const mockPosts: Page[] = [
10
+ { id: 1, type: 'page', title: 'Home', status: 'draft' },
11
+ { id: 2, type: 'page', title: 'About', status: 'publish' },
12
+ { id: 3, type: 'page', title: 'Services', status: 'publish', isHome: true },
13
+ { id: 4, type: 'page', title: 'Contact', status: 'draft' },
14
+ { id: 5, type: 'page', title: 'FAQ', status: 'publish' },
15
+ ];
16
+
17
+ afterAll( () => {
18
+ jest.clearAllMocks();
19
+ } );
20
+
21
+ it( 'should render closed list', () => {
22
+ // Act.
23
+ const { getByText, queryByText } = render(
24
+ <PagesCollapsibleList pages={ mockPosts } isOpenByDefault={ false } />,
25
+ );
26
+
27
+ // Assert.
28
+ const label = getByText( `Pages (${ mockPosts.length })` );
29
+ expect( label ).toBeInTheDocument();
30
+
31
+ const postInList = queryByText( 'Services' );
32
+ expect( postInList ).toBeNull();
33
+ } );
34
+
35
+ it( 'should render open list', () => {
36
+ // Act.
37
+ const { getByText } = render(
38
+ <PagesCollapsibleList pages={ mockPosts } isOpenByDefault={ true } />,
39
+ );
40
+
41
+ // Assert.
42
+ const label = getByText( `Pages (${ mockPosts.length })` );
43
+ expect( label ).toBeInTheDocument();
44
+
45
+ const postInList = getByText( 'Services' );
46
+ expect( postInList ).toBeInTheDocument();
47
+ } );
48
+ } );
@@ -0,0 +1,63 @@
1
+ import * as React from 'react';
2
+ import { ComponentType, PropsWithChildren, useState } from 'react';
3
+ import { Collapse, IconButton, List, ListItem, ListItemIcon, ListItemText, styled, SvgIconProps } from '@elementor/ui';
4
+ import { ChevronDownIcon } from '@elementor/icons';
5
+
6
+ type Props = {
7
+ label: string
8
+ Icon: ComponentType;
9
+ isOpenByDefault?: boolean;
10
+ }
11
+
12
+ interface RotateIconProps extends SvgIconProps {
13
+ isOpen: boolean;
14
+ }
15
+
16
+ // TODO 21/06/2023 : Should replace this with future Rotate component that will be implemented in elementor-ui
17
+ const RotateIcon = styled( ChevronDownIcon, {
18
+ shouldForwardProp: ( prop ) => prop !== 'isOpen',
19
+ } )<RotateIconProps>( ( { theme, isOpen } ) => ( {
20
+ transform: isOpen ? 'rotate(0deg)' : 'rotate(-90deg)',
21
+ transition: theme.transitions.create( 'transform', {
22
+ duration: theme.transitions.duration.standard,
23
+ } ),
24
+ } ) );
25
+
26
+ export default function CollapsibleList(
27
+ {
28
+ label,
29
+ Icon,
30
+ isOpenByDefault = false,
31
+ children,
32
+ }: PropsWithChildren<Props>
33
+ ) {
34
+ const [ isOpen, setIsOpen ] = useState( isOpenByDefault );
35
+
36
+ return (
37
+ <>
38
+ <ListItem disableGutters>
39
+ <ListItemIcon>
40
+ <IconButton
41
+ onClick={ () => setIsOpen( ( prev ) => ! prev ) }
42
+ sx={ { color: 'inherit' } }
43
+ size="small"
44
+ >
45
+ <RotateIcon fontSize="small" isOpen={ isOpen } />
46
+ </IconButton>
47
+ </ListItemIcon>
48
+ <ListItemIcon>
49
+ <Icon />
50
+ </ListItemIcon>
51
+ <ListItemText>{ label }</ListItemText>
52
+ </ListItem>
53
+ <Collapse
54
+ in={ isOpen }
55
+ timeout="auto"
56
+ unmountOnExit>
57
+ <List dense>
58
+ { children }
59
+ </List>
60
+ </Collapse>
61
+ </>
62
+ );
63
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import {
3
+ bindMenu,
4
+ bindTrigger,
5
+ ListItem,
6
+ ListItemButton,
7
+ ListItemIcon,
8
+ ListItemText,
9
+ ToggleButton,
10
+ usePopupState,
11
+ } from '@elementor/ui';
12
+ import { DotsVerticalIcon, HomeIcon } from '@elementor/icons';
13
+ import { Page } from '../../../types';
14
+ import { useActiveDocument, useNavigateToDocument } from '@elementor/editor-documents';
15
+ import PageTitleAndStatus from '../../shared/page-title-and-status';
16
+ import PageActionsMenu from '../actions-menu/page-actions-menu';
17
+
18
+ export default function PageListItem( { page }: { page: Page } ) {
19
+ const popupState = usePopupState( {
20
+ variant: 'popover',
21
+ popupId: 'page-actions',
22
+ } );
23
+
24
+ const activeDocument = useActiveDocument();
25
+ const navigateToDocument = useNavigateToDocument();
26
+
27
+ const isActive = activeDocument?.id === page.id;
28
+
29
+ return (
30
+ <>
31
+ <ListItem
32
+ disablePadding
33
+ secondaryAction={
34
+ <ToggleButton
35
+ value
36
+ color="secondary"
37
+ size="small"
38
+ selected={ popupState.isOpen }
39
+ { ...bindTrigger( popupState ) }
40
+ >
41
+ <DotsVerticalIcon fontSize="small" />
42
+ </ToggleButton>
43
+ }
44
+ >
45
+ <ListItemButton
46
+ selected={ isActive }
47
+ onClick={ () => navigateToDocument( page.id ) }
48
+ dense
49
+ >
50
+ <ListItemIcon />
51
+ <ListItemText
52
+ disableTypography={ true }
53
+ >
54
+ <PageTitleAndStatus page={ page } />
55
+ </ListItemText>
56
+ { page.isHome &&
57
+ <ListItemIcon>
58
+ <HomeIcon color="disabled" />
59
+ </ListItemIcon>
60
+ }
61
+ </ListItemButton>
62
+ </ListItem>
63
+ <PageActionsMenu page={ page } { ...bindMenu( popupState ) } />
64
+ </>
65
+ );
66
+ }
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import CollapsibleList from './collapsible-list';
3
+ import { Page } from '../../../types';
4
+ import { PageTypeIcon } from '@elementor/icons';
5
+ import PageListItem from './page-list-item';
6
+
7
+ type Props = {
8
+ pages: Page[];
9
+ isOpenByDefault?: boolean;
10
+ }
11
+
12
+ export default function PagesCollapsibleList( { pages, isOpenByDefault = false }: Props ) {
13
+ const label = `Pages (${ pages.length })`; // TODO 21/06/2023 : This label should come from the backend
14
+
15
+ return (
16
+ <CollapsibleList
17
+ label={ label }
18
+ Icon={ PageTypeIcon }
19
+ isOpenByDefault={ isOpenByDefault }
20
+ >
21
+ { pages.map( ( page ) => <PageListItem key={ page.id } page={ page } /> ) }
22
+ </CollapsibleList>
23
+ );
24
+ }
@@ -0,0 +1,58 @@
1
+ import * as React from 'react';
2
+ import { Box, Button, Divider, Grid, List, Paper, ThemeProvider, Typography } from '@elementor/ui';
3
+ import { PlusIcon } from '@elementor/icons';
4
+ import { Page } from '../../types';
5
+ import PagesCollapsibleList from './pages-list/pages-collapsible-list';
6
+
7
+ // TODO: Remove once connected to real data fetching.
8
+ const mockPages: Page[] = [
9
+ {
10
+ id: 1,
11
+ type: 'page',
12
+ title: 'This is a very long title that somebody wrote, a very very long line',
13
+ status: 'pending approval',
14
+ },
15
+ { id: 2, type: 'page', title: 'About', status: 'publish' },
16
+ { id: 3, type: 'page', title: 'Services', status: 'publish', isHome: true },
17
+ { id: 4, type: 'page', title: 'Contact', status: 'draft' },
18
+ { id: 5, type: 'page', title: 'FAQ', status: 'publish' },
19
+ ];
20
+
21
+ const Shell = () => {
22
+ return (
23
+ <ThemeProvider colorScheme="light">
24
+ <Box sx={ { width: '100%', maxWidth: 360 } }>
25
+ <Paper>
26
+ <Grid
27
+ container
28
+ justifyContent="center"
29
+ alignItems="center"
30
+ sx={ { height: 51 } }>
31
+ <Typography variant="h6">Pages</Typography>
32
+ </Grid>
33
+ <Divider />
34
+ <Box
35
+ display="flex"
36
+ justifyContent="flex-end"
37
+ alignItems="center"
38
+ >
39
+ <Button
40
+ sx={ { mt: 4, mb: 4, mr: 5 } }
41
+ startIcon={ <PlusIcon /> }
42
+ >
43
+ Add New
44
+ </Button>
45
+ </Box>
46
+ <Box sx={ { width: '100%', maxWidth: 360 } }>
47
+ <List dense>
48
+ <PagesCollapsibleList pages={ mockPages } isOpenByDefault={ true } />
49
+ </List>
50
+ </Box>
51
+ <Divider />
52
+ </Paper>
53
+ </Box>
54
+ </ThemeProvider>
55
+ );
56
+ };
57
+
58
+ export default Shell;
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { Box, Typography } from '@elementor/ui';
3
+ import { Page } from '../../types';
4
+ import { useReverseHtmlEntities } from '../../hooks/use-reverse-html-entities';
5
+
6
+ const PageStatus = ( { status }: { status: string } ) => {
7
+ if ( 'publish' === status ) {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ <Typography
13
+ component="span"
14
+ variant="body1"
15
+ sx={ {
16
+ textTransform: 'capitalize',
17
+ fontStyle: 'italic',
18
+ whiteSpace: 'nowrap',
19
+ flexBasis: 'content',
20
+ } }
21
+ >
22
+ ({ status })
23
+ </Typography>
24
+ );
25
+ };
26
+
27
+ const PageTitle = ( { title }: { title: string } ) => {
28
+ const modifiedTitle = useReverseHtmlEntities( title );
29
+
30
+ return (
31
+ <Typography
32
+ component="span"
33
+ variant="body1"
34
+ noWrap
35
+ sx={ {
36
+ flexBasis: 'auto',
37
+ } }
38
+ >
39
+ { modifiedTitle }
40
+ </Typography>
41
+ );
42
+ };
43
+
44
+ export default function PageTitleAndStatus( { page }: { page: Page } ) {
45
+ return (
46
+ <Box display="flex">
47
+ <PageTitle title={ page.title } />&nbsp;<PageStatus status={ page.status } />
48
+ </Box>
49
+ );
50
+ }
package/src/init.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import RecentlyEdited from './components/top-bar/recently-edited';
2
2
  import { injectIntoPageIndication } from '@elementor/editor-app-bar';
3
+ import { injectIntoTop } from '@elementor/editor';
4
+ import Shell from './components/panel/shell';
3
5
 
4
6
  export default function init() {
5
7
  registerTopBarMenuItems();
8
+ // TODO 06/06/2023 : uncomment registerPanel() when we are ready to release
9
+ registerPanel();
6
10
  }
7
11
 
8
12
  function registerTopBarMenuItems() {
@@ -11,3 +15,11 @@ function registerTopBarMenuItems() {
11
15
  filler: RecentlyEdited,
12
16
  } );
13
17
  }
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ function registerPanel() {
21
+ injectIntoTop( {
22
+ id: 'navigation-panel',
23
+ filler: Shell,
24
+ } );
25
+ }
package/src/types.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type Page = {
2
+ isHome?: boolean;
3
+ id: number;
4
+ title: string;
5
+ status: string;
6
+ type: string;
7
+ };