@elementor/editor-responsive 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.
- package/LICENSE +674 -0
- package/README.md +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +237 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/src/components/__tests__/breakpoints-switcher.test.tsx +120 -0
- package/src/components/breakpoints-switcher.tsx +81 -0
- package/src/hooks/__tests__/use-breakpoints-actions.test.tsx +23 -0
- package/src/hooks/__tests__/use-breakpoints.test.tsx +77 -0
- package/src/hooks/use-breakpoints-actions.ts +13 -0
- package/src/hooks/use-breakpoints.ts +12 -0
- package/src/index.ts +3 -0
- package/src/init.ts +26 -0
- package/src/store/index.ts +42 -0
- package/src/store/selectors.ts +37 -0
- package/src/sync/__tests__/sync-store.test.ts +147 -0
- package/src/sync/sync-store.ts +74 -0
- package/src/types.ts +36 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Breakpoint } from '../../types';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import useBreakpoints from '../../hooks/use-breakpoints';
|
|
5
|
+
import BreakpointsSwitcher from '../breakpoints-switcher';
|
|
6
|
+
import useBreakpointsActions from '../../hooks/use-breakpoints-actions';
|
|
7
|
+
|
|
8
|
+
jest.mock( '../../hooks/use-breakpoints', () => jest.fn() );
|
|
9
|
+
jest.mock( '../../hooks/use-breakpoints-actions', () => jest.fn().mockReturnValue( {
|
|
10
|
+
activate: jest.fn(),
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
describe( '@elementor/editor-responsive - Breakpoints Switcher', () => {
|
|
14
|
+
it( 'should not render when there are no breakpoints', () => {
|
|
15
|
+
// Arrange.
|
|
16
|
+
jest.mocked( useBreakpoints ).mockReturnValue( {
|
|
17
|
+
all: [],
|
|
18
|
+
active: {
|
|
19
|
+
id: 'desktop',
|
|
20
|
+
label: 'Desktop',
|
|
21
|
+
},
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
// Act.
|
|
25
|
+
const { container } = render( <BreakpointsSwitcher /> );
|
|
26
|
+
|
|
27
|
+
// Assert.
|
|
28
|
+
expect( container ).toBeEmptyDOMElement();
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
it( 'should not render when there is no active breakpoint', () => {
|
|
32
|
+
// Arrange.
|
|
33
|
+
jest.mocked( useBreakpoints ).mockReturnValue( {
|
|
34
|
+
active: null,
|
|
35
|
+
all: [ {
|
|
36
|
+
id: 'desktop',
|
|
37
|
+
label: 'Desktop',
|
|
38
|
+
} ],
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
// Act.
|
|
42
|
+
const { container } = render( <BreakpointsSwitcher /> );
|
|
43
|
+
|
|
44
|
+
// Assert.
|
|
45
|
+
expect( container ).toBeEmptyDOMElement();
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'should render all of the breakpoints', () => {
|
|
49
|
+
// Arrange.
|
|
50
|
+
const sortedBreakpoints: Breakpoint[] = [
|
|
51
|
+
{ id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
|
|
52
|
+
{ id: 'desktop', label: 'Desktop' },
|
|
53
|
+
{ id: 'laptop', label: 'Laptop', width: 1366, type: 'max-width' },
|
|
54
|
+
{ id: 'tablet_extra', label: 'Tablet Landscape', width: 1200, type: 'max-width' },
|
|
55
|
+
{ id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
|
|
56
|
+
{ id: 'mobile_extra', label: 'Mobile Landscape', width: 880, type: 'max-width' },
|
|
57
|
+
{ id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
jest.mocked( useBreakpoints ).mockReturnValue( {
|
|
61
|
+
all: sortedBreakpoints,
|
|
62
|
+
active: { id: 'desktop', label: 'Desktop' },
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
// Act.
|
|
66
|
+
const { getAllByRole } = render( <BreakpointsSwitcher /> );
|
|
67
|
+
|
|
68
|
+
// Assert.
|
|
69
|
+
const tabs = getAllByRole( 'tab' );
|
|
70
|
+
const expectedLabels = [
|
|
71
|
+
'Widescreen (2400px and up)',
|
|
72
|
+
'Desktop',
|
|
73
|
+
'Laptop (up to 1366px)',
|
|
74
|
+
'Tablet Landscape (up to 1200px)',
|
|
75
|
+
'Tablet Portrait (up to 1024px)',
|
|
76
|
+
'Mobile Landscape (up to 880px)',
|
|
77
|
+
'Mobile Portrait (up to 767px)',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
expect( tabs ).toHaveLength( 7 );
|
|
81
|
+
|
|
82
|
+
tabs.forEach( ( tab, index ) => {
|
|
83
|
+
expect( tab ).toHaveAttribute( 'aria-label', expectedLabels[ index ] );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
// Desktop should be active.
|
|
87
|
+
expect( tabs[ 1 ] ).toHaveAttribute( 'aria-selected', 'true' );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
it( 'should activate a breakpoint on click', () => {
|
|
91
|
+
// Arrange.
|
|
92
|
+
const activate = jest.fn();
|
|
93
|
+
|
|
94
|
+
jest.mocked( useBreakpointsActions ).mockReturnValue( {
|
|
95
|
+
activate,
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
jest.mocked( useBreakpoints ).mockReturnValue( {
|
|
99
|
+
active: {
|
|
100
|
+
id: 'desktop',
|
|
101
|
+
label: 'Desktop',
|
|
102
|
+
},
|
|
103
|
+
all: [
|
|
104
|
+
{ id: 'mobile', label: 'Mobile Portrait' },
|
|
105
|
+
{ id: 'desktop', label: 'Desktop' },
|
|
106
|
+
],
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
// Act.
|
|
110
|
+
const { getByLabelText } = render( <BreakpointsSwitcher /> );
|
|
111
|
+
const mobileTab = getByLabelText( 'Mobile Portrait', {
|
|
112
|
+
selector: 'button',
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
mobileTab.click();
|
|
116
|
+
|
|
117
|
+
// Assert.
|
|
118
|
+
expect( activate ).toHaveBeenCalledWith( 'mobile' );
|
|
119
|
+
} );
|
|
120
|
+
} );
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { __ } from '@wordpress/i18n';
|
|
3
|
+
import { BreakpointId } from '../types';
|
|
4
|
+
import useBreakpoints from '../hooks/use-breakpoints';
|
|
5
|
+
import { Tab, Tabs, Tooltip as BaseTooltip, TooltipProps } from '@elementor/ui';
|
|
6
|
+
import {
|
|
7
|
+
DesktopIcon,
|
|
8
|
+
TabletPortraitIcon,
|
|
9
|
+
MobilePortraitIcon,
|
|
10
|
+
WidescreenIcon,
|
|
11
|
+
LaptopIcon,
|
|
12
|
+
TabletLandscapeIcon,
|
|
13
|
+
MobileLandscapeIcon,
|
|
14
|
+
} from '@elementor/icons';
|
|
15
|
+
import useBreakpointsActions from '../hooks/use-breakpoints-actions';
|
|
16
|
+
|
|
17
|
+
export default function BreakpointsSwitcher() {
|
|
18
|
+
const { all, active } = useBreakpoints();
|
|
19
|
+
const { activate } = useBreakpointsActions();
|
|
20
|
+
|
|
21
|
+
if ( ! all.length || ! active ) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const onChange = ( _: unknown, value: BreakpointId ) => activate( value );
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Tabs value={ active.id } onChange={ onChange } aria-label={ __( 'Switch Device', 'elementor' ) }>
|
|
29
|
+
{
|
|
30
|
+
all.map( ( { id, label, type, width } ) => {
|
|
31
|
+
const Icon = iconsMap[ id ];
|
|
32
|
+
|
|
33
|
+
const title = labelsMap[ type || 'default' ]
|
|
34
|
+
.replace( '%s', label )
|
|
35
|
+
.replace( '%d', width?.toString() || '' );
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Tab value={ id }
|
|
39
|
+
key={ id }
|
|
40
|
+
aria-label={ title }
|
|
41
|
+
icon={ <Tooltip title={ title }><Icon /></Tooltip> }
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
} )
|
|
45
|
+
}
|
|
46
|
+
</Tabs>
|
|
47
|
+
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function Tooltip( props: TooltipProps ) {
|
|
52
|
+
return <BaseTooltip
|
|
53
|
+
PopperProps={ {
|
|
54
|
+
sx: {
|
|
55
|
+
'&.MuiTooltip-popper .MuiTooltip-tooltip.MuiTooltip-tooltipPlacementBottom': {
|
|
56
|
+
mt: 7,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} }
|
|
60
|
+
{ ...props }
|
|
61
|
+
/>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const iconsMap = {
|
|
65
|
+
widescreen: WidescreenIcon,
|
|
66
|
+
desktop: DesktopIcon,
|
|
67
|
+
laptop: LaptopIcon,
|
|
68
|
+
tablet_extra: TabletLandscapeIcon,
|
|
69
|
+
tablet: TabletPortraitIcon,
|
|
70
|
+
mobile_extra: MobileLandscapeIcon,
|
|
71
|
+
mobile: MobilePortraitIcon,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const labelsMap = {
|
|
75
|
+
default: '%s',
|
|
76
|
+
// translators: %s: Breakpoint label, %d: Breakpoint size.
|
|
77
|
+
'min-width': __( '%s (%dpx and up)', 'elementor' ),
|
|
78
|
+
|
|
79
|
+
// translators: %s: Breakpoint label, %d: Breakpoint size.
|
|
80
|
+
'max-width': __( '%s (up to %dpx)', 'elementor' ),
|
|
81
|
+
} as const;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { runCommand } from '@elementor/editor-v1-adapters';
|
|
2
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
3
|
+
import useBreakpointsActions from '../use-breakpoints-actions';
|
|
4
|
+
|
|
5
|
+
jest.mock( '@elementor/editor-v1-adapters', () => ( {
|
|
6
|
+
runCommand: jest.fn(),
|
|
7
|
+
} ) );
|
|
8
|
+
|
|
9
|
+
describe( '@elementor/editor-responsive - useBreakpointsActions', () => {
|
|
10
|
+
it( 'should activate a breakpoint', () => {
|
|
11
|
+
// Act.
|
|
12
|
+
const { result } = renderHook( () => useBreakpointsActions() );
|
|
13
|
+
|
|
14
|
+
result.current.activate( 'tablet' );
|
|
15
|
+
|
|
16
|
+
// Assert.
|
|
17
|
+
expect( jest.mocked( runCommand ) ).toHaveBeenCalledTimes( 1 );
|
|
18
|
+
expect( jest.mocked( runCommand ) ).toHaveBeenCalledWith(
|
|
19
|
+
'panel/change-device-mode',
|
|
20
|
+
{ device: 'tablet' }
|
|
21
|
+
);
|
|
22
|
+
} );
|
|
23
|
+
} );
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Slice } from '../../types';
|
|
3
|
+
import { PropsWithChildren } from 'react';
|
|
4
|
+
import { createSlice } from '../../store';
|
|
5
|
+
import useBreakpoints from '../use-breakpoints';
|
|
6
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
7
|
+
import { createStore, dispatch, SliceState, Store, StoreProvider } from '@elementor/store';
|
|
8
|
+
|
|
9
|
+
jest.mock( '@elementor/editor-v1-adapters', () => ( {
|
|
10
|
+
runCommand: jest.fn(),
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
describe( '@elementor/editor-responsive - useBreakpoints', () => {
|
|
14
|
+
let store: Store<SliceState<Slice>>;
|
|
15
|
+
let slice: Slice;
|
|
16
|
+
|
|
17
|
+
beforeEach( () => {
|
|
18
|
+
slice = createSlice();
|
|
19
|
+
store = createStore();
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
it( 'should return all breakpoints sorted by size', () => {
|
|
23
|
+
// Arrange.
|
|
24
|
+
dispatch( slice.actions.init( {
|
|
25
|
+
activeId: null,
|
|
26
|
+
entities: [
|
|
27
|
+
{ id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
|
|
28
|
+
{ id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
|
|
29
|
+
{ id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
|
|
30
|
+
{ id: 'desktop', label: 'Desktop' },
|
|
31
|
+
],
|
|
32
|
+
} ) );
|
|
33
|
+
|
|
34
|
+
// Act.
|
|
35
|
+
const { result } = renderHookWithStore( () => useBreakpoints(), store );
|
|
36
|
+
|
|
37
|
+
// Assert.
|
|
38
|
+
expect( result.current.all ).toEqual( [
|
|
39
|
+
{ id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
|
|
40
|
+
{ id: 'desktop', label: 'Desktop' },
|
|
41
|
+
{ id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
|
|
42
|
+
{ id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
|
|
43
|
+
] );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
it( 'should return the active breakpoint', () => {
|
|
47
|
+
// Arrange.
|
|
48
|
+
dispatch( slice.actions.init( {
|
|
49
|
+
activeId: 'tablet',
|
|
50
|
+
entities: [
|
|
51
|
+
{ id: 'desktop', label: 'Desktop' },
|
|
52
|
+
{ id: 'tablet', label: 'Tablet Portrait', type: 'max-width', width: 1024 },
|
|
53
|
+
],
|
|
54
|
+
} ) );
|
|
55
|
+
|
|
56
|
+
// Act.
|
|
57
|
+
const { result } = renderHookWithStore( () => useBreakpoints(), store );
|
|
58
|
+
|
|
59
|
+
// Assert.
|
|
60
|
+
expect( result.current.active ).toEqual( {
|
|
61
|
+
id: 'tablet',
|
|
62
|
+
label: 'Tablet Portrait',
|
|
63
|
+
type: 'max-width',
|
|
64
|
+
width: 1024,
|
|
65
|
+
} );
|
|
66
|
+
} );
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
function renderHookWithStore<T>( hook: () => T, store: Store ) {
|
|
70
|
+
const wrapper = ( { children }: PropsWithChildren<unknown> ) => (
|
|
71
|
+
<StoreProvider store={ store }>
|
|
72
|
+
{ children }
|
|
73
|
+
</StoreProvider>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return renderHook( hook, { wrapper } );
|
|
77
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { BreakpointId } from '../types';
|
|
3
|
+
import { runCommand } from '@elementor/editor-v1-adapters';
|
|
4
|
+
|
|
5
|
+
export default function useBreakpointsActions() {
|
|
6
|
+
const activate = useCallback( ( device: BreakpointId ) => {
|
|
7
|
+
return runCommand( 'panel/change-device-mode', { device } );
|
|
8
|
+
}, [] );
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
activate,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useSelector } from '@elementor/store';
|
|
2
|
+
import { selectActiveBreakpoint, selectSortedBreakpoints } from '../store/selectors';
|
|
3
|
+
|
|
4
|
+
export default function useBreakpoints() {
|
|
5
|
+
const all = useSelector( selectSortedBreakpoints );
|
|
6
|
+
const active = useSelector( selectActiveBreakpoint );
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
all,
|
|
10
|
+
active,
|
|
11
|
+
};
|
|
12
|
+
}
|
package/src/index.ts
ADDED
package/src/init.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createSlice } from './store';
|
|
2
|
+
import syncStore from './sync/sync-store';
|
|
3
|
+
import { injectIntoResponsive } from '@elementor/editor-app-bar';
|
|
4
|
+
import BreakpointsSwitcher from './components/breakpoints-switcher';
|
|
5
|
+
|
|
6
|
+
export default function init() {
|
|
7
|
+
initStore();
|
|
8
|
+
|
|
9
|
+
registerAppBarUI();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function initStore() {
|
|
13
|
+
const slice = createSlice();
|
|
14
|
+
|
|
15
|
+
syncStore( slice );
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function registerAppBarUI() {
|
|
19
|
+
injectIntoResponsive( {
|
|
20
|
+
name: 'responsive-breakpoints-switcher',
|
|
21
|
+
filler: BreakpointsSwitcher,
|
|
22
|
+
options: {
|
|
23
|
+
priority: 20, // After document indication.
|
|
24
|
+
},
|
|
25
|
+
} );
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { addSlice, PayloadAction } from '@elementor/store';
|
|
2
|
+
import { Breakpoint, BreakpointId } from '../types';
|
|
3
|
+
|
|
4
|
+
export type State = {
|
|
5
|
+
entities: Record<BreakpointId, Breakpoint>,
|
|
6
|
+
activeId: BreakpointId | null,
|
|
7
|
+
}
|
|
8
|
+
const initialState: State = {
|
|
9
|
+
entities: {} as State['entities'],
|
|
10
|
+
activeId: null,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createSlice() {
|
|
14
|
+
return addSlice( {
|
|
15
|
+
name: 'breakpoints',
|
|
16
|
+
initialState,
|
|
17
|
+
reducers: {
|
|
18
|
+
init( state, action: PayloadAction<{
|
|
19
|
+
entities: Breakpoint[],
|
|
20
|
+
activeId: State['activeId'],
|
|
21
|
+
}> ) {
|
|
22
|
+
state.activeId = action.payload.activeId;
|
|
23
|
+
state.entities = normalizeEntities( action.payload.entities );
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
activateBreakpoint( state, action: PayloadAction<BreakpointId> ) {
|
|
27
|
+
if ( state.entities[ action.payload ] ) {
|
|
28
|
+
state.activeId = action.payload;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} );
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeEntities( entities: Breakpoint[] ) {
|
|
36
|
+
return entities.reduce( ( acc, breakpoint ) => {
|
|
37
|
+
return {
|
|
38
|
+
...acc,
|
|
39
|
+
[ breakpoint.id ]: breakpoint,
|
|
40
|
+
};
|
|
41
|
+
}, {} as State['entities'] );
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Breakpoint, Slice } from '../types';
|
|
2
|
+
import { createSelector, SliceState } from '@elementor/store';
|
|
3
|
+
|
|
4
|
+
type State = SliceState<Slice>;
|
|
5
|
+
|
|
6
|
+
export const selectEntities = ( state: State ) => state.breakpoints.entities;
|
|
7
|
+
export const selectActiveId = ( state: State ) => state.breakpoints.activeId;
|
|
8
|
+
|
|
9
|
+
export const selectActiveBreakpoint = createSelector(
|
|
10
|
+
selectEntities,
|
|
11
|
+
selectActiveId,
|
|
12
|
+
( entities, activeId ) => activeId && entities[ activeId ]
|
|
13
|
+
? entities[ activeId ]
|
|
14
|
+
: null,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const selectSortedBreakpoints = createSelector(
|
|
18
|
+
selectEntities,
|
|
19
|
+
( entities ) => {
|
|
20
|
+
const byWidth = ( a: Breakpoint, b: Breakpoint ) => {
|
|
21
|
+
return ( a.width && b.width ) ? b.width - a.width : 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const all = Object.values( entities );
|
|
25
|
+
|
|
26
|
+
const defaults = all.filter( ( breakpoint ) => ! breakpoint.width ); // AKA Desktop.
|
|
27
|
+
const minWidth = all.filter( ( breakpoint ) => breakpoint.type === 'min-width' );
|
|
28
|
+
const maxWidth = all.filter( ( breakpoint ) => breakpoint.type === 'max-width' );
|
|
29
|
+
|
|
30
|
+
// Sort by size, big to small.
|
|
31
|
+
return [
|
|
32
|
+
...minWidth.sort( byWidth ),
|
|
33
|
+
...defaults,
|
|
34
|
+
...maxWidth.sort( byWidth ),
|
|
35
|
+
];
|
|
36
|
+
},
|
|
37
|
+
);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import syncStore from '../sync-store';
|
|
2
|
+
import { createSlice } from '../../store';
|
|
3
|
+
import { BreakpointId, ExtendedWindow, Slice } from '../../types';
|
|
4
|
+
import { createStore, dispatch, SliceState, Store } from '@elementor/store';
|
|
5
|
+
import { selectActiveBreakpoint, selectEntities } from '../../store/selectors';
|
|
6
|
+
|
|
7
|
+
describe( '@elementor/editor-responsive - Sync Store', () => {
|
|
8
|
+
let store: Store<SliceState<Slice>>;
|
|
9
|
+
let slice: Slice;
|
|
10
|
+
let extendedWindow: ExtendedWindow;
|
|
11
|
+
|
|
12
|
+
beforeEach( () => {
|
|
13
|
+
slice = createSlice();
|
|
14
|
+
store = createStore();
|
|
15
|
+
|
|
16
|
+
syncStore( slice );
|
|
17
|
+
|
|
18
|
+
extendedWindow = ( window as unknown as ExtendedWindow );
|
|
19
|
+
} );
|
|
20
|
+
|
|
21
|
+
it( 'should initialize the store when V1 is ready', () => {
|
|
22
|
+
// Arrange.
|
|
23
|
+
mockV1BreakpointsConfig();
|
|
24
|
+
|
|
25
|
+
// Act.
|
|
26
|
+
dispatchEvent( new CustomEvent( 'elementor/initialized' ) );
|
|
27
|
+
|
|
28
|
+
// Assert.
|
|
29
|
+
expect( selectEntities( store.getState() ) ).toEqual( {
|
|
30
|
+
desktop: { id: 'desktop', label: 'Desktop' },
|
|
31
|
+
mobile: { id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
|
|
32
|
+
tablet: { id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
|
|
33
|
+
laptop: { id: 'laptop', label: 'Laptop', width: 1366, type: 'max-width' },
|
|
34
|
+
widescreen: { id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
|
|
35
|
+
mobile_extra: { id: 'mobile_extra', label: 'Mobile Landscape', width: 880, type: 'max-width' },
|
|
36
|
+
tablet_extra: { id: 'tablet_extra', label: 'Tablet Landscape', width: 1200, type: 'max-width' },
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
expect( extendedWindow.elementor.channels.deviceMode.request ).toHaveBeenCalledTimes( 1 );
|
|
40
|
+
expect( extendedWindow.elementor.channels.deviceMode.request ).toHaveBeenCalledWith( 'currentMode' );
|
|
41
|
+
|
|
42
|
+
expect( selectActiveBreakpoint( store.getState() ) ).toEqual( {
|
|
43
|
+
id: 'mobile',
|
|
44
|
+
label: 'Mobile Portrait',
|
|
45
|
+
width: 767,
|
|
46
|
+
type: 'max-width',
|
|
47
|
+
} );
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
it( 'should initialize an empty store when V1 breakpoints config is not available', () => {
|
|
51
|
+
// Act.
|
|
52
|
+
dispatchEvent( new CustomEvent( 'elementor/initialized' ) );
|
|
53
|
+
|
|
54
|
+
// Assert.
|
|
55
|
+
expect( selectEntities( store.getState() ) ).toEqual( {} );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'should sync the active breakpoint on change', () => {
|
|
59
|
+
// Arrange.
|
|
60
|
+
mockV1BreakpointsConfig();
|
|
61
|
+
|
|
62
|
+
dispatch( slice.actions.init( {
|
|
63
|
+
entities: [
|
|
64
|
+
{ id: 'desktop', label: 'Desktop' },
|
|
65
|
+
{ id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
|
|
66
|
+
],
|
|
67
|
+
activeId: 'mobile',
|
|
68
|
+
} ) );
|
|
69
|
+
|
|
70
|
+
// Act - Mock a change.
|
|
71
|
+
jest.mocked( extendedWindow.elementor.channels.deviceMode.request ).mockReturnValue( 'desktop' );
|
|
72
|
+
dispatchEvent( new CustomEvent( 'elementor/device-mode/change' ) );
|
|
73
|
+
|
|
74
|
+
// Assert.
|
|
75
|
+
expect( selectActiveBreakpoint( store.getState() ) ).toEqual( { id: 'desktop', label: 'Desktop' } );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( "should not change the active breakpoint when it's empty", () => {
|
|
79
|
+
// Arrange.
|
|
80
|
+
mockV1BreakpointsConfig();
|
|
81
|
+
|
|
82
|
+
dispatch( slice.actions.init( {
|
|
83
|
+
entities: [
|
|
84
|
+
{ id: 'desktop', label: 'Desktop' },
|
|
85
|
+
{ id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
|
|
86
|
+
],
|
|
87
|
+
activeId: 'desktop',
|
|
88
|
+
} ) );
|
|
89
|
+
|
|
90
|
+
// Act - Mock a change.
|
|
91
|
+
jest.mocked( extendedWindow.elementor.channels.deviceMode.request ).mockReturnValue( '' as BreakpointId );
|
|
92
|
+
dispatchEvent( new CustomEvent( 'elementor/device-mode/change' ) );
|
|
93
|
+
|
|
94
|
+
// Assert.
|
|
95
|
+
expect( selectActiveBreakpoint( store.getState() ) ).toEqual( { id: 'desktop', label: 'Desktop' } );
|
|
96
|
+
} );
|
|
97
|
+
} );
|
|
98
|
+
|
|
99
|
+
function mockV1BreakpointsConfig() {
|
|
100
|
+
( window as unknown as ExtendedWindow ).elementor = {
|
|
101
|
+
channels: {
|
|
102
|
+
deviceMode: { request: jest.fn( () => 'mobile' ) },
|
|
103
|
+
},
|
|
104
|
+
config: {
|
|
105
|
+
responsive: {
|
|
106
|
+
breakpoints: {
|
|
107
|
+
mobile: {
|
|
108
|
+
label: 'Mobile Portrait',
|
|
109
|
+
value: 767,
|
|
110
|
+
direction: 'max',
|
|
111
|
+
is_enabled: true,
|
|
112
|
+
},
|
|
113
|
+
mobile_extra: {
|
|
114
|
+
label: 'Mobile Landscape',
|
|
115
|
+
value: 880,
|
|
116
|
+
direction: 'max',
|
|
117
|
+
is_enabled: true,
|
|
118
|
+
},
|
|
119
|
+
tablet: {
|
|
120
|
+
label: 'Tablet Portrait',
|
|
121
|
+
value: 1024,
|
|
122
|
+
direction: 'max',
|
|
123
|
+
is_enabled: true,
|
|
124
|
+
},
|
|
125
|
+
tablet_extra: {
|
|
126
|
+
label: 'Tablet Landscape',
|
|
127
|
+
value: 1200,
|
|
128
|
+
direction: 'max',
|
|
129
|
+
is_enabled: true,
|
|
130
|
+
},
|
|
131
|
+
laptop: {
|
|
132
|
+
label: 'Laptop',
|
|
133
|
+
value: 1366,
|
|
134
|
+
direction: 'max',
|
|
135
|
+
is_enabled: true,
|
|
136
|
+
},
|
|
137
|
+
widescreen: {
|
|
138
|
+
label: 'Widescreen',
|
|
139
|
+
value: 2400,
|
|
140
|
+
direction: 'min',
|
|
141
|
+
is_enabled: true,
|
|
142
|
+
},
|
|
143
|
+
} as const,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { dispatch } from '@elementor/store';
|
|
2
|
+
import { Breakpoint, ExtendedWindow, Slice } from '../types';
|
|
3
|
+
import { listenTo, v1ReadyEvent, windowEvent } from '@elementor/editor-v1-adapters';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
export default function syncStore( slice: Slice ) {
|
|
7
|
+
syncInitialization( slice );
|
|
8
|
+
syncOnChange( slice );
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function syncInitialization( slice: Slice ) {
|
|
12
|
+
const { init } = slice.actions;
|
|
13
|
+
|
|
14
|
+
listenTo(
|
|
15
|
+
v1ReadyEvent(),
|
|
16
|
+
() => {
|
|
17
|
+
dispatch( init( {
|
|
18
|
+
entities: getBreakpoints(),
|
|
19
|
+
activeId: getActiveBreakpoint(),
|
|
20
|
+
} ) );
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function syncOnChange( slice: Slice ) {
|
|
26
|
+
const { activateBreakpoint } = slice.actions;
|
|
27
|
+
|
|
28
|
+
listenTo(
|
|
29
|
+
deviceModeChangeEvent(),
|
|
30
|
+
() => {
|
|
31
|
+
const activeBreakpoint = getActiveBreakpoint();
|
|
32
|
+
|
|
33
|
+
dispatch( activateBreakpoint( activeBreakpoint ) );
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getBreakpoints() {
|
|
39
|
+
const { breakpoints } = ( window as unknown as ExtendedWindow ).elementor?.config?.responsive || {};
|
|
40
|
+
|
|
41
|
+
if ( ! breakpoints ) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entities = Object
|
|
46
|
+
.entries( breakpoints )
|
|
47
|
+
.filter( ( [ , breakpoint ] ) => breakpoint.is_enabled )
|
|
48
|
+
.map( ( [ id, { value, direction, label } ] ) => {
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
label,
|
|
52
|
+
width: value,
|
|
53
|
+
type: direction === 'min' ? 'min-width' : 'max-width',
|
|
54
|
+
} as Breakpoint;
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
// Desktop breakpoint is not included in V1 config.
|
|
58
|
+
entities.push( {
|
|
59
|
+
id: 'desktop',
|
|
60
|
+
label: __( 'Desktop', 'elementor' ),
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
return entities;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getActiveBreakpoint() {
|
|
67
|
+
const extendedWindow = window as unknown as ExtendedWindow;
|
|
68
|
+
|
|
69
|
+
return extendedWindow.elementor?.channels?.deviceMode?.request?.( 'currentMode' ) || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function deviceModeChangeEvent() {
|
|
73
|
+
return windowEvent( 'elementor/device-mode/change' );
|
|
74
|
+
}
|