@elementor/editor-responsive 0.10.6 → 0.11.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/src/types.ts CHANGED
@@ -11,36 +11,25 @@ export type Breakpoint = {
11
11
  type?: 'min-width' | 'max-width';
12
12
  };
13
13
 
14
+ export type V1Breakpoint = {
15
+ direction: 'min' | 'max';
16
+ is_enabled: boolean;
17
+ value: BreakpointSize;
18
+ label: BreakpointLabel;
19
+ };
20
+
21
+ export type V1Breakpoints = Record< Exclude< BreakpointId, 'desktop' >, V1Breakpoint >;
22
+
14
23
  export type ExtendedWindow = Window & {
15
- elementor: {
16
- config: {
17
- responsive: {
18
- breakpoints: Record<
19
- Exclude< BreakpointId, 'desktop' >,
20
- {
21
- direction: 'min' | 'max';
22
- is_enabled: boolean;
23
- value: BreakpointSize;
24
- label: BreakpointLabel;
25
- }
26
- >;
27
- };
28
- };
29
- channels: {
30
- deviceMode: {
31
- request: ( request: 'currentMode' ) => BreakpointId;
24
+ elementor?: {
25
+ config?: {
26
+ responsive?: {
27
+ breakpoints?: V1Breakpoints;
32
28
  };
33
29
  };
34
- editorEvents: {
35
- dispatchEvent: ( name: string, data: Record< string, string > ) => void;
36
- config: {
37
- locations: Record< string, string >;
38
- secondaryLocations: Record< string, string >;
39
- triggers: Record< string, string >;
40
- elements: Record< string, string >;
41
- names: {
42
- topBar: Record< string, string >;
43
- };
30
+ channels?: {
31
+ deviceMode?: {
32
+ request?: ( request: 'currentMode' ) => BreakpointId;
44
33
  };
45
34
  };
46
35
  };
@@ -1,125 +0,0 @@
1
- import * as React from 'react';
2
- import { Breakpoint } from '../../types';
3
- import { fireEvent, render, screen } 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', () =>
10
- jest.fn().mockReturnValue( {
11
- activate: jest.fn(),
12
- } )
13
- );
14
-
15
- describe( '@elementor/editor-responsive - Breakpoints Switcher', () => {
16
- it( 'should not render when there are no breakpoints', () => {
17
- // Arrange.
18
- jest.mocked( useBreakpoints ).mockReturnValue( {
19
- all: [],
20
- active: {
21
- id: 'desktop',
22
- label: 'Desktop',
23
- },
24
- } );
25
-
26
- // Act.
27
- const { container } = render( <BreakpointsSwitcher /> );
28
-
29
- // Assert.
30
- expect( container ).toBeEmptyDOMElement();
31
- } );
32
-
33
- it( 'should not render when there is no active breakpoint', () => {
34
- // Arrange.
35
- jest.mocked( useBreakpoints ).mockReturnValue( {
36
- active: null,
37
- all: [
38
- {
39
- id: 'desktop',
40
- label: 'Desktop',
41
- },
42
- ],
43
- } );
44
-
45
- // Act.
46
- const { container } = render( <BreakpointsSwitcher /> );
47
-
48
- // Assert.
49
- expect( container ).toBeEmptyDOMElement();
50
- } );
51
-
52
- it( 'should render all of the breakpoints', () => {
53
- // Arrange.
54
- const sortedBreakpoints: Breakpoint[] = [
55
- { id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
56
- { id: 'desktop', label: 'Desktop' },
57
- { id: 'laptop', label: 'Laptop', width: 1366, type: 'max-width' },
58
- { id: 'tablet_extra', label: 'Tablet Landscape', width: 1200, type: 'max-width' },
59
- { id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
60
- { id: 'mobile_extra', label: 'Mobile Landscape', width: 880, type: 'max-width' },
61
- { id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
62
- ];
63
-
64
- jest.mocked( useBreakpoints ).mockReturnValue( {
65
- all: sortedBreakpoints,
66
- active: { id: 'desktop', label: 'Desktop' },
67
- } );
68
-
69
- // Act.
70
- render( <BreakpointsSwitcher /> );
71
-
72
- // Assert.
73
- const tabs = screen.getAllByRole( 'tab' );
74
- const expectedLabels = [
75
- 'Widescreen (2400px and up)',
76
- 'Desktop',
77
- 'Laptop (up to 1366px)',
78
- 'Tablet Landscape (up to 1200px)',
79
- 'Tablet Portrait (up to 1024px)',
80
- 'Mobile Landscape (up to 880px)',
81
- 'Mobile Portrait (up to 767px)',
82
- ];
83
-
84
- expect( tabs ).toHaveLength( 7 );
85
-
86
- tabs.forEach( ( tab, index ) => {
87
- expect( tab ).toHaveAttribute( 'aria-label', expectedLabels[ index ] );
88
- } );
89
-
90
- // Desktop should be active.
91
- expect( tabs[ 1 ] ).toHaveAttribute( 'aria-selected', 'true' );
92
- } );
93
-
94
- it( 'should activate a breakpoint on click', () => {
95
- // Arrange.
96
- const activate = jest.fn();
97
-
98
- jest.mocked( useBreakpointsActions ).mockReturnValue( {
99
- activate,
100
- } );
101
-
102
- jest.mocked( useBreakpoints ).mockReturnValue( {
103
- active: {
104
- id: 'desktop',
105
- label: 'Desktop',
106
- },
107
- all: [
108
- { id: 'mobile', label: 'Mobile Portrait' },
109
- { id: 'desktop', label: 'Desktop' },
110
- ],
111
- } );
112
-
113
- // Act.
114
- render( <BreakpointsSwitcher /> );
115
-
116
- const mobileTab = screen.getByLabelText( 'Mobile Portrait', {
117
- selector: 'button',
118
- } );
119
-
120
- fireEvent.click( mobileTab );
121
-
122
- // Assert.
123
- expect( activate ).toHaveBeenCalledWith( 'mobile' );
124
- } );
125
- } );
@@ -1,113 +0,0 @@
1
- import * as React from 'react';
2
- import { __ } from '@wordpress/i18n';
3
- import { BreakpointId, ExtendedWindow } 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 ) => {
26
- const extendedWindow = window as unknown as ExtendedWindow;
27
- const config = extendedWindow?.elementor?.editorEvents?.config;
28
-
29
- if ( config ) {
30
- extendedWindow.elementor.editorEvents.dispatchEvent( config.names.topBar.responsiveControls, {
31
- location: config.locations.topBar,
32
- secondaryLocation: config.secondaryLocations.responsiveControls,
33
- trigger: config.triggers.click,
34
- element: config.elements.buttonIcon,
35
- mode: value,
36
- } );
37
- }
38
-
39
- activate( value );
40
- };
41
-
42
- return (
43
- <Tabs
44
- textColor="inherit"
45
- indicatorColor="secondary"
46
- value={ active.id }
47
- onChange={ onChange }
48
- aria-label={ __( 'Switch Device', 'elementor' ) }
49
- sx={ {
50
- '& .MuiTabs-indicator': {
51
- backgroundColor: 'text.primary',
52
- },
53
- } }
54
- >
55
- { all.map( ( { id, label, type, width } ) => {
56
- const Icon = iconsMap[ id ];
57
-
58
- const title = labelsMap[ type || 'default' ]
59
- .replace( '%s', label )
60
- .replace( '%d', width?.toString() || '' );
61
-
62
- return (
63
- <Tab
64
- value={ id }
65
- key={ id }
66
- aria-label={ title }
67
- icon={
68
- <Tooltip title={ title }>
69
- <Icon />
70
- </Tooltip>
71
- }
72
- sx={ { minWidth: 'auto' } }
73
- data-testid={ `switch-device-to-${ id }` }
74
- />
75
- );
76
- } ) }
77
- </Tabs>
78
- );
79
- }
80
-
81
- function Tooltip( props: TooltipProps ) {
82
- return (
83
- <BaseTooltip
84
- PopperProps={ {
85
- sx: {
86
- '&.MuiTooltip-popper .MuiTooltip-tooltip.MuiTooltip-tooltipPlacementBottom': {
87
- mt: 2.5,
88
- },
89
- },
90
- } }
91
- { ...props }
92
- />
93
- );
94
- }
95
-
96
- const iconsMap = {
97
- widescreen: WidescreenIcon,
98
- desktop: DesktopIcon,
99
- laptop: LaptopIcon,
100
- tablet_extra: TabletLandscapeIcon,
101
- tablet: TabletPortraitIcon,
102
- mobile_extra: MobileLandscapeIcon,
103
- mobile: MobilePortraitIcon,
104
- };
105
-
106
- const labelsMap = {
107
- default: '%s',
108
- // translators: %s: Breakpoint label, %d: Breakpoint size.
109
- 'min-width': __( '%s (%dpx and up)', 'elementor' ),
110
-
111
- // translators: %s: Breakpoint label, %d: Breakpoint size.
112
- 'max-width': __( '%s (up to %dpx)', 'elementor' ),
113
- } as const;
@@ -1,84 +0,0 @@
1
- import * as React from 'react';
2
- import { PropsWithChildren } from 'react';
3
- import { slice } from '../../store';
4
- import useBreakpoints from '../use-breakpoints';
5
- import { renderHook } from '@testing-library/react';
6
- import {
7
- __createStore,
8
- __dispatch,
9
- __registerSlice,
10
- SliceState,
11
- Store,
12
- __StoreProvider as StoreProvider,
13
- } from '@elementor/store';
14
-
15
- jest.mock( '@elementor/editor-v1-adapters', () => ( {
16
- __privateRunCommand: jest.fn(),
17
- } ) );
18
-
19
- describe( '@elementor/editor-responsive - useBreakpoints', () => {
20
- let store: Store< SliceState< typeof slice > >;
21
-
22
- beforeEach( () => {
23
- __registerSlice( slice );
24
- store = __createStore();
25
- } );
26
-
27
- it( 'should return all breakpoints sorted by size', () => {
28
- // Arrange.
29
- __dispatch(
30
- slice.actions.init( {
31
- activeId: null,
32
- entities: [
33
- { id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
34
- { id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
35
- { id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
36
- { id: 'desktop', label: 'Desktop' },
37
- ],
38
- } )
39
- );
40
-
41
- // Act.
42
- const { result } = renderHookWithStore( () => useBreakpoints(), store );
43
-
44
- // Assert.
45
- expect( result.current.all ).toEqual( [
46
- { id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
47
- { id: 'desktop', label: 'Desktop' },
48
- { id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
49
- { id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
50
- ] );
51
- } );
52
-
53
- it( 'should return the active breakpoint', () => {
54
- // Arrange.
55
- __dispatch(
56
- slice.actions.init( {
57
- activeId: 'tablet',
58
- entities: [
59
- { id: 'desktop', label: 'Desktop' },
60
- { id: 'tablet', label: 'Tablet Portrait', type: 'max-width', width: 1024 },
61
- ],
62
- } )
63
- );
64
-
65
- // Act.
66
- const { result } = renderHookWithStore( () => useBreakpoints(), store );
67
-
68
- // Assert.
69
- expect( result.current.active ).toEqual( {
70
- id: 'tablet',
71
- label: 'Tablet Portrait',
72
- type: 'max-width',
73
- width: 1024,
74
- } );
75
- } );
76
- } );
77
-
78
- function renderHookWithStore< T >( hook: () => T, store: Store ) {
79
- const wrapper = ( { children }: PropsWithChildren< unknown > ) => (
80
- <StoreProvider store={ store }>{ children }</StoreProvider>
81
- );
82
-
83
- return renderHook( hook, { wrapper } );
84
- }
@@ -1,13 +0,0 @@
1
- import { useCallback } from 'react';
2
- import { BreakpointId } from '../types';
3
- import { __privateRunCommand as 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
- }
package/src/init.ts DELETED
@@ -1,27 +0,0 @@
1
- import { slice } 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
- import { __registerSlice } from '@elementor/store';
6
-
7
- export default function init() {
8
- initStore();
9
-
10
- registerAppBarUI();
11
- }
12
-
13
- function initStore() {
14
- __registerSlice( slice );
15
-
16
- syncStore();
17
- }
18
-
19
- function registerAppBarUI() {
20
- injectIntoResponsive( {
21
- id: 'responsive-breakpoints-switcher',
22
- component: BreakpointsSwitcher,
23
- options: {
24
- priority: 20, // After document indication.
25
- },
26
- } );
27
- }
@@ -1,46 +0,0 @@
1
- import { __createSlice, 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 const slice = __createSlice( {
14
- name: 'breakpoints',
15
- initialState,
16
- reducers: {
17
- init(
18
- state,
19
- action: PayloadAction< {
20
- entities: Breakpoint[];
21
- activeId: State[ 'activeId' ];
22
- } >
23
- ) {
24
- state.activeId = action.payload.activeId;
25
- state.entities = normalizeEntities( action.payload.entities );
26
- },
27
-
28
- activateBreakpoint( state, action: PayloadAction< BreakpointId > ) {
29
- if ( state.entities[ action.payload ] ) {
30
- state.activeId = action.payload;
31
- }
32
- },
33
- },
34
- } );
35
-
36
- function normalizeEntities( entities: Breakpoint[] ) {
37
- return entities.reduce(
38
- ( acc, breakpoint ) => {
39
- return {
40
- ...acc,
41
- [ breakpoint.id ]: breakpoint,
42
- };
43
- },
44
- {} as State[ 'entities' ]
45
- );
46
- }
@@ -1,27 +0,0 @@
1
- import { slice } from './index';
2
- import { Breakpoint } from '../types';
3
- import { __createSelector, SliceState } from '@elementor/store';
4
-
5
- type State = SliceState< typeof slice >;
6
-
7
- export const selectEntities = ( state: State ) => state.breakpoints.entities;
8
- export const selectActiveId = ( state: State ) => state.breakpoints.activeId;
9
-
10
- export const selectActiveBreakpoint = __createSelector( selectEntities, selectActiveId, ( entities, activeId ) =>
11
- activeId && entities[ activeId ] ? entities[ activeId ] : null
12
- );
13
-
14
- export const selectSortedBreakpoints = __createSelector( selectEntities, ( entities ) => {
15
- const byWidth = ( a: Breakpoint, b: Breakpoint ) => {
16
- return a.width && b.width ? b.width - a.width : 0;
17
- };
18
-
19
- const all = Object.values( entities );
20
-
21
- const defaults = all.filter( ( breakpoint ) => ! breakpoint.width ); // AKA Desktop.
22
- const minWidth = all.filter( ( breakpoint ) => breakpoint.type === 'min-width' );
23
- const maxWidth = all.filter( ( breakpoint ) => breakpoint.type === 'max-width' );
24
-
25
- // Sort by size, big to small.
26
- return [ ...minWidth.sort( byWidth ), ...defaults, ...maxWidth.sort( byWidth ) ];
27
- } );
@@ -1,162 +0,0 @@
1
- import syncStore from '../sync-store';
2
- import { slice } from '../../store';
3
- import { BreakpointId, ExtendedWindow } from '../../types';
4
- import { __createStore, __dispatch, __registerSlice, 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< typeof slice > >;
9
- let extendedWindow: ExtendedWindow;
10
-
11
- beforeEach( () => {
12
- __registerSlice( slice );
13
- store = __createStore();
14
-
15
- syncStore();
16
-
17
- extendedWindow = window as unknown as ExtendedWindow;
18
- } );
19
-
20
- it( 'should initialize the store when V1 is ready', () => {
21
- // Arrange.
22
- mockV1BreakpointsConfig();
23
-
24
- // Act.
25
- dispatchEvent( new CustomEvent( 'elementor/initialized' ) );
26
-
27
- // Assert.
28
- expect( selectEntities( store.getState() ) ).toEqual( {
29
- desktop: { id: 'desktop', label: 'Desktop' },
30
- mobile: { id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
31
- tablet: { id: 'tablet', label: 'Tablet Portrait', width: 1024, type: 'max-width' },
32
- laptop: { id: 'laptop', label: 'Laptop', width: 1366, type: 'max-width' },
33
- widescreen: { id: 'widescreen', label: 'Widescreen', width: 2400, type: 'min-width' },
34
- mobile_extra: { id: 'mobile_extra', label: 'Mobile Landscape', width: 880, type: 'max-width' },
35
- tablet_extra: { id: 'tablet_extra', label: 'Tablet Landscape', width: 1200, type: 'max-width' },
36
- } );
37
-
38
- expect( extendedWindow.elementor.channels.deviceMode.request ).toHaveBeenCalledTimes( 1 );
39
- expect( extendedWindow.elementor.channels.deviceMode.request ).toHaveBeenCalledWith( 'currentMode' );
40
-
41
- expect( selectActiveBreakpoint( store.getState() ) ).toEqual( {
42
- id: 'mobile',
43
- label: 'Mobile Portrait',
44
- width: 767,
45
- type: 'max-width',
46
- } );
47
- } );
48
-
49
- it( 'should initialize an empty store when V1 breakpoints config is not available', () => {
50
- // Act.
51
- dispatchEvent( new CustomEvent( 'elementor/initialized' ) );
52
-
53
- // Assert.
54
- expect( selectEntities( store.getState() ) ).toEqual( {} );
55
- } );
56
-
57
- it( 'should sync the active breakpoint on change', () => {
58
- // Arrange.
59
- mockV1BreakpointsConfig();
60
-
61
- __dispatch(
62
- 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
-
71
- // Act - Mock a change.
72
- jest.mocked( extendedWindow.elementor.channels.deviceMode.request ).mockReturnValue( 'desktop' );
73
- dispatchEvent( new CustomEvent( 'elementor/device-mode/change' ) );
74
-
75
- // Assert.
76
- expect( selectActiveBreakpoint( store.getState() ) ).toEqual( { id: 'desktop', label: 'Desktop' } );
77
- } );
78
-
79
- it( "should not change the active breakpoint when it's empty", () => {
80
- // Arrange.
81
- mockV1BreakpointsConfig();
82
-
83
- __dispatch(
84
- slice.actions.init( {
85
- entities: [
86
- { id: 'desktop', label: 'Desktop' },
87
- { id: 'mobile', label: 'Mobile Portrait', width: 767, type: 'max-width' },
88
- ],
89
- activeId: 'desktop',
90
- } )
91
- );
92
-
93
- // Act - Mock a change.
94
- jest.mocked( extendedWindow.elementor.channels.deviceMode.request ).mockReturnValue( '' as BreakpointId );
95
- dispatchEvent( new CustomEvent( 'elementor/device-mode/change' ) );
96
-
97
- // Assert.
98
- expect( selectActiveBreakpoint( store.getState() ) ).toEqual( { id: 'desktop', label: 'Desktop' } );
99
- } );
100
- } );
101
-
102
- function mockV1BreakpointsConfig() {
103
- ( window as unknown as ExtendedWindow ).elementor = {
104
- channels: {
105
- deviceMode: { request: jest.fn( () => 'mobile' ) },
106
- },
107
- config: {
108
- responsive: {
109
- breakpoints: {
110
- mobile: {
111
- label: 'Mobile Portrait',
112
- value: 767,
113
- direction: 'max',
114
- is_enabled: true,
115
- },
116
- mobile_extra: {
117
- label: 'Mobile Landscape',
118
- value: 880,
119
- direction: 'max',
120
- is_enabled: true,
121
- },
122
- tablet: {
123
- label: 'Tablet Portrait',
124
- value: 1024,
125
- direction: 'max',
126
- is_enabled: true,
127
- },
128
- tablet_extra: {
129
- label: 'Tablet Landscape',
130
- value: 1200,
131
- direction: 'max',
132
- is_enabled: true,
133
- },
134
- laptop: {
135
- label: 'Laptop',
136
- value: 1366,
137
- direction: 'max',
138
- is_enabled: true,
139
- },
140
- widescreen: {
141
- label: 'Widescreen',
142
- value: 2400,
143
- direction: 'min',
144
- is_enabled: true,
145
- },
146
- } as const,
147
- },
148
- },
149
- editorEvents: {
150
- config: {
151
- elements: {},
152
- locations: {},
153
- names: {
154
- topBar: {},
155
- },
156
- secondaryLocations: {},
157
- triggers: {},
158
- },
159
- dispatchEvent: () => null,
160
- },
161
- };
162
- }