@elementor/editor-canvas 3.33.0-220 → 3.33.0-222

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/dist/index.js CHANGED
@@ -137,6 +137,7 @@ var import_react3 = require("react");
137
137
  var import_react4 = require("@floating-ui/react");
138
138
  function useFloatingOnElement({ element, isSelected }) {
139
139
  const [isOpen, setIsOpen] = (0, import_react3.useState)(false);
140
+ const sizeModifier = 2;
140
141
  const { refs, floatingStyles, context } = (0, import_react4.useFloating)({
141
142
  // Must be controlled for interactions (like hover) to work.
142
143
  open: isOpen || isSelected,
@@ -144,13 +145,15 @@ function useFloatingOnElement({ element, isSelected }) {
144
145
  whileElementsMounted: import_react4.autoUpdate,
145
146
  middleware: [
146
147
  // Match the floating element's size to the reference element.
147
- (0, import_react4.size)({
148
- apply({ elements, rects }) {
149
- Object.assign(elements.floating.style, {
150
- width: `${rects.reference.width + 2}px`,
151
- height: `${rects.reference.height + 2}px`
152
- });
153
- }
148
+ (0, import_react4.size)(() => {
149
+ return {
150
+ apply({ elements, rects }) {
151
+ Object.assign(elements.floating.style, {
152
+ width: `${rects.reference.width + sizeModifier}px`,
153
+ height: `${rects.reference.height + sizeModifier}px`
154
+ });
155
+ }
156
+ };
154
157
  }),
155
158
  // Center the floating element on the reference element.
156
159
  (0, import_react4.offset)(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)
@@ -170,6 +173,23 @@ function useFloatingOnElement({ element, isSelected }) {
170
173
  };
171
174
  }
172
175
 
176
+ // src/hooks/use-has-overlapping.ts
177
+ var possibleOverlappingSelectors = [".e-off-canvas"];
178
+ var useHasOverlapping = () => {
179
+ const preview = window.elementor?.$preview?.[0];
180
+ if (!preview) {
181
+ return false;
182
+ }
183
+ const hasOverlapping = possibleOverlappingSelectors.map((selector) => Array.from(preview?.contentWindow?.document.body.querySelectorAll(selector) ?? [])).flat().some(
184
+ (elem) => elem.checkVisibility({
185
+ opacityProperty: true,
186
+ visibilityProperty: true,
187
+ contentVisibilityAuto: true
188
+ })
189
+ );
190
+ return hasOverlapping;
191
+ };
192
+
173
193
  // src/components/element-overlay.tsx
174
194
  var CANVAS_WRAPPER_ID = "elementor-preview-responsive-wrapper";
175
195
  var OverlayBox = (0, import_ui.styled)(import_ui.Box, {
@@ -182,9 +202,10 @@ var OverlayBox = (0, import_ui.styled)(import_ui.Box, {
182
202
  function ElementOverlay({ element, isSelected, id }) {
183
203
  const { context, floating, isVisible } = useFloatingOnElement({ element, isSelected });
184
204
  const { getFloatingProps, getReferenceProps } = (0, import_react5.useInteractions)([(0, import_react5.useHover)(context)]);
205
+ const hasOverlapping = useHasOverlapping();
185
206
  useBindReactPropsToElement(element, getReferenceProps);
186
207
  const isSmallerOffset = element.offsetHeight <= 1;
187
- return isVisible && /* @__PURE__ */ React.createElement(import_react5.FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
208
+ return isVisible && !hasOverlapping && /* @__PURE__ */ React.createElement(import_react5.FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
188
209
  OverlayBox,
189
210
  {
190
211
  ref: floating.setRef,
@@ -281,6 +302,7 @@ function getLinkAttrs(el) {
281
302
  // src/hooks/use-style-items.ts
282
303
  var import_react9 = require("react");
283
304
  var import_editor_responsive2 = require("@elementor/editor-responsive");
305
+ var import_editor_styles3 = require("@elementor/editor-styles");
284
306
  var import_editor_styles_repository2 = require("@elementor/editor-styles-repository");
285
307
  var import_editor_v1_adapters3 = require("@elementor/editor-v1-adapters");
286
308
 
@@ -469,6 +491,7 @@ var import_react8 = require("react");
469
491
  var import_editor_responsive = require("@elementor/editor-responsive");
470
492
 
471
493
  // src/renderers/create-styles-renderer.ts
494
+ var import_editor_styles2 = require("@elementor/editor-styles");
472
495
  var import_utils3 = require("@elementor/utils");
473
496
 
474
497
  // src/renderers/errors.ts
@@ -477,6 +500,10 @@ var UnknownStyleTypeError = (0, import_utils2.createError)({
477
500
  code: "unknown_style_type",
478
501
  message: "Unknown style type"
479
502
  });
503
+ var UnknownStyleStateError = (0, import_utils2.createError)({
504
+ code: "unknown_style_state",
505
+ message: "Unknown style state"
506
+ });
480
507
 
481
508
  // src/renderers/create-styles-renderer.ts
482
509
  var SELECTORS_MAP = {
@@ -494,7 +521,8 @@ function createStylesRenderer({ resolve, breakpoints, selectorPrefix = "" }) {
494
521
  return {
495
522
  id: style.id,
496
523
  breakpoint: style?.variants[0]?.meta?.breakpoint || "desktop",
497
- value: variantsCss.join("")
524
+ value: variantsCss.join(""),
525
+ state: style?.variants[0]?.meta?.state || null
498
526
  };
499
527
  });
500
528
  return await Promise.all(stylesCssPromises);
@@ -510,7 +538,18 @@ function createStyleWrapper(value = "", wrapper) {
510
538
  return createStyleWrapper(`${value}${symbol}${cssName}`, wrapper);
511
539
  },
512
540
  withPrefix: (prefix) => createStyleWrapper([prefix, value].filter(Boolean).join(" "), wrapper),
513
- withState: (state) => createStyleWrapper(state ? `${value}:${state}` : value, wrapper),
541
+ withState: (state) => {
542
+ if (!state) {
543
+ return createStyleWrapper(value, wrapper);
544
+ }
545
+ if ((0, import_editor_styles2.isClassState)(state)) {
546
+ return createStyleWrapper(`${value}.${state}`, wrapper);
547
+ }
548
+ if ((0, import_editor_styles2.isPseudoState)(state)) {
549
+ return createStyleWrapper(`${value}:${state}`, wrapper);
550
+ }
551
+ throw new UnknownStyleStateError({ context: { state } });
552
+ },
514
553
  withMediaQuery: (breakpoint) => {
515
554
  if (!breakpoint?.type) {
516
555
  return createStyleWrapper(value, wrapper);
@@ -591,14 +630,27 @@ function useStyleItems() {
591
630
  });
592
631
  const breakpointsOrder = (0, import_editor_responsive2.getBreakpoints)().map((breakpoint) => breakpoint.id);
593
632
  return (0, import_react9.useMemo)(
594
- () => Object.values(styleItems).sort(({ provider: providerA }, { provider: providerB }) => providerA.priority - providerB.priority).flatMap(({ items }) => items).sort(({ breakpoint: breakpointA }, { breakpoint: breakpointB }) => {
595
- return breakpointsOrder.indexOf(breakpointA) - breakpointsOrder.indexOf(breakpointB);
596
- }),
633
+ () => Object.values(styleItems).sort(sortByProviderPriority).flatMap(({ items }) => items).sort(sortByStateType).sort(sortByBreakpoint(breakpointsOrder)),
597
634
  // eslint-disable-next-line
598
635
  // eslint-disable-next-line react-hooks/exhaustive-deps
599
636
  [styleItems, breakpointsOrder.join("-")]
600
637
  );
601
638
  }
639
+ function sortByProviderPriority({ provider: providerA }, { provider: providerB }) {
640
+ return providerA.priority - providerB.priority;
641
+ }
642
+ function sortByBreakpoint(breakpointsOrder) {
643
+ return ({ breakpoint: breakpointA }, { breakpoint: breakpointB }) => breakpointsOrder.indexOf(breakpointA) - breakpointsOrder.indexOf(breakpointB);
644
+ }
645
+ function sortByStateType({ state: stateA }, { state: stateB }) {
646
+ if ((0, import_editor_styles3.isClassState)(stateA) && !(0, import_editor_styles3.isClassState)(stateB)) {
647
+ return -1;
648
+ }
649
+ if (!(0, import_editor_styles3.isClassState)(stateA) && (0, import_editor_styles3.isClassState)(stateB)) {
650
+ return 1;
651
+ }
652
+ return 0;
653
+ }
602
654
  function createProviderSubscriber({ provider, renderStyles, setStyleItems }) {
603
655
  return abortPreviousRuns(
604
656
  (abortController) => signalizedProcess(abortController.signal).then((_, signal) => {
package/dist/index.mjs CHANGED
@@ -97,6 +97,7 @@ import { useEffect as useEffect3, useState } from "react";
97
97
  import { autoUpdate, offset, size, useFloating } from "@floating-ui/react";
98
98
  function useFloatingOnElement({ element, isSelected }) {
99
99
  const [isOpen, setIsOpen] = useState(false);
100
+ const sizeModifier = 2;
100
101
  const { refs, floatingStyles, context } = useFloating({
101
102
  // Must be controlled for interactions (like hover) to work.
102
103
  open: isOpen || isSelected,
@@ -104,13 +105,15 @@ function useFloatingOnElement({ element, isSelected }) {
104
105
  whileElementsMounted: autoUpdate,
105
106
  middleware: [
106
107
  // Match the floating element's size to the reference element.
107
- size({
108
- apply({ elements, rects }) {
109
- Object.assign(elements.floating.style, {
110
- width: `${rects.reference.width + 2}px`,
111
- height: `${rects.reference.height + 2}px`
112
- });
113
- }
108
+ size(() => {
109
+ return {
110
+ apply({ elements, rects }) {
111
+ Object.assign(elements.floating.style, {
112
+ width: `${rects.reference.width + sizeModifier}px`,
113
+ height: `${rects.reference.height + sizeModifier}px`
114
+ });
115
+ }
116
+ };
114
117
  }),
115
118
  // Center the floating element on the reference element.
116
119
  offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)
@@ -130,6 +133,23 @@ function useFloatingOnElement({ element, isSelected }) {
130
133
  };
131
134
  }
132
135
 
136
+ // src/hooks/use-has-overlapping.ts
137
+ var possibleOverlappingSelectors = [".e-off-canvas"];
138
+ var useHasOverlapping = () => {
139
+ const preview = window.elementor?.$preview?.[0];
140
+ if (!preview) {
141
+ return false;
142
+ }
143
+ const hasOverlapping = possibleOverlappingSelectors.map((selector) => Array.from(preview?.contentWindow?.document.body.querySelectorAll(selector) ?? [])).flat().some(
144
+ (elem) => elem.checkVisibility({
145
+ opacityProperty: true,
146
+ visibilityProperty: true,
147
+ contentVisibilityAuto: true
148
+ })
149
+ );
150
+ return hasOverlapping;
151
+ };
152
+
133
153
  // src/components/element-overlay.tsx
134
154
  var CANVAS_WRAPPER_ID = "elementor-preview-responsive-wrapper";
135
155
  var OverlayBox = styled(Box, {
@@ -142,9 +162,10 @@ var OverlayBox = styled(Box, {
142
162
  function ElementOverlay({ element, isSelected, id }) {
143
163
  const { context, floating, isVisible } = useFloatingOnElement({ element, isSelected });
144
164
  const { getFloatingProps, getReferenceProps } = useInteractions([useHover(context)]);
165
+ const hasOverlapping = useHasOverlapping();
145
166
  useBindReactPropsToElement(element, getReferenceProps);
146
167
  const isSmallerOffset = element.offsetHeight <= 1;
147
- return isVisible && /* @__PURE__ */ React.createElement(FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
168
+ return isVisible && !hasOverlapping && /* @__PURE__ */ React.createElement(FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
148
169
  OverlayBox,
149
170
  {
150
171
  ref: floating.setRef,
@@ -241,6 +262,7 @@ function getLinkAttrs(el) {
241
262
  // src/hooks/use-style-items.ts
242
263
  import { useEffect as useEffect5, useMemo as useMemo3, useState as useState2 } from "react";
243
264
  import { getBreakpoints } from "@elementor/editor-responsive";
265
+ import { isClassState as isClassState2 } from "@elementor/editor-styles";
244
266
  import { stylesRepository as stylesRepository2 } from "@elementor/editor-styles-repository";
245
267
  import { registerDataHook } from "@elementor/editor-v1-adapters";
246
268
 
@@ -431,6 +453,10 @@ import { useMemo as useMemo2 } from "react";
431
453
  import { useBreakpointsMap } from "@elementor/editor-responsive";
432
454
 
433
455
  // src/renderers/create-styles-renderer.ts
456
+ import {
457
+ isClassState,
458
+ isPseudoState
459
+ } from "@elementor/editor-styles";
434
460
  import { decodeString } from "@elementor/utils";
435
461
 
436
462
  // src/renderers/errors.ts
@@ -439,6 +465,10 @@ var UnknownStyleTypeError = createError({
439
465
  code: "unknown_style_type",
440
466
  message: "Unknown style type"
441
467
  });
468
+ var UnknownStyleStateError = createError({
469
+ code: "unknown_style_state",
470
+ message: "Unknown style state"
471
+ });
442
472
 
443
473
  // src/renderers/create-styles-renderer.ts
444
474
  var SELECTORS_MAP = {
@@ -456,7 +486,8 @@ function createStylesRenderer({ resolve, breakpoints, selectorPrefix = "" }) {
456
486
  return {
457
487
  id: style.id,
458
488
  breakpoint: style?.variants[0]?.meta?.breakpoint || "desktop",
459
- value: variantsCss.join("")
489
+ value: variantsCss.join(""),
490
+ state: style?.variants[0]?.meta?.state || null
460
491
  };
461
492
  });
462
493
  return await Promise.all(stylesCssPromises);
@@ -472,7 +503,18 @@ function createStyleWrapper(value = "", wrapper) {
472
503
  return createStyleWrapper(`${value}${symbol}${cssName}`, wrapper);
473
504
  },
474
505
  withPrefix: (prefix) => createStyleWrapper([prefix, value].filter(Boolean).join(" "), wrapper),
475
- withState: (state) => createStyleWrapper(state ? `${value}:${state}` : value, wrapper),
506
+ withState: (state) => {
507
+ if (!state) {
508
+ return createStyleWrapper(value, wrapper);
509
+ }
510
+ if (isClassState(state)) {
511
+ return createStyleWrapper(`${value}.${state}`, wrapper);
512
+ }
513
+ if (isPseudoState(state)) {
514
+ return createStyleWrapper(`${value}:${state}`, wrapper);
515
+ }
516
+ throw new UnknownStyleStateError({ context: { state } });
517
+ },
476
518
  withMediaQuery: (breakpoint) => {
477
519
  if (!breakpoint?.type) {
478
520
  return createStyleWrapper(value, wrapper);
@@ -553,14 +595,27 @@ function useStyleItems() {
553
595
  });
554
596
  const breakpointsOrder = getBreakpoints().map((breakpoint) => breakpoint.id);
555
597
  return useMemo3(
556
- () => Object.values(styleItems).sort(({ provider: providerA }, { provider: providerB }) => providerA.priority - providerB.priority).flatMap(({ items }) => items).sort(({ breakpoint: breakpointA }, { breakpoint: breakpointB }) => {
557
- return breakpointsOrder.indexOf(breakpointA) - breakpointsOrder.indexOf(breakpointB);
558
- }),
598
+ () => Object.values(styleItems).sort(sortByProviderPriority).flatMap(({ items }) => items).sort(sortByStateType).sort(sortByBreakpoint(breakpointsOrder)),
559
599
  // eslint-disable-next-line
560
600
  // eslint-disable-next-line react-hooks/exhaustive-deps
561
601
  [styleItems, breakpointsOrder.join("-")]
562
602
  );
563
603
  }
604
+ function sortByProviderPriority({ provider: providerA }, { provider: providerB }) {
605
+ return providerA.priority - providerB.priority;
606
+ }
607
+ function sortByBreakpoint(breakpointsOrder) {
608
+ return ({ breakpoint: breakpointA }, { breakpoint: breakpointB }) => breakpointsOrder.indexOf(breakpointA) - breakpointsOrder.indexOf(breakpointB);
609
+ }
610
+ function sortByStateType({ state: stateA }, { state: stateB }) {
611
+ if (isClassState2(stateA) && !isClassState2(stateB)) {
612
+ return -1;
613
+ }
614
+ if (!isClassState2(stateA) && isClassState2(stateB)) {
615
+ return 1;
616
+ }
617
+ return 0;
618
+ }
564
619
  function createProviderSubscriber({ provider, renderStyles, setStyleItems }) {
565
620
  return abortPreviousRuns(
566
621
  (abortController) => signalizedProcess(abortController.signal).then((_, signal) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "3.33.0-220",
4
+ "version": "3.33.0-222",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,19 +37,19 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "3.33.0-220",
41
- "@elementor/editor-notifications": "3.33.0-220",
42
- "@elementor/editor-documents": "3.33.0-220",
43
- "@elementor/editor-elements": "3.33.0-220",
44
- "@elementor/editor-props": "3.33.0-220",
45
- "@elementor/editor-responsive": "3.33.0-220",
46
- "@elementor/editor-styles": "3.33.0-220",
47
- "@elementor/editor-styles-repository": "3.33.0-220",
48
- "@elementor/editor-v1-adapters": "3.33.0-220",
49
- "@elementor/twing": "3.33.0-220",
40
+ "@elementor/editor": "3.33.0-222",
41
+ "@elementor/editor-notifications": "3.33.0-222",
42
+ "@elementor/editor-documents": "3.33.0-222",
43
+ "@elementor/editor-elements": "3.33.0-222",
44
+ "@elementor/editor-props": "3.33.0-222",
45
+ "@elementor/editor-responsive": "3.33.0-222",
46
+ "@elementor/editor-styles": "3.33.0-222",
47
+ "@elementor/editor-styles-repository": "3.33.0-222",
48
+ "@elementor/editor-v1-adapters": "3.33.0-222",
49
+ "@elementor/twing": "3.33.0-222",
50
50
  "@elementor/ui": "1.36.12",
51
- "@elementor/utils": "3.33.0-220",
52
- "@elementor/wp-media": "3.33.0-220",
51
+ "@elementor/utils": "3.33.0-222",
52
+ "@elementor/wp-media": "3.33.0-222",
53
53
  "@floating-ui/react": "^0.27.5",
54
54
  "@wordpress/i18n": "^5.13.0"
55
55
  },
@@ -46,8 +46,8 @@ describe( '<StyleRenderer />', () => {
46
46
  const mockContainer = document.createElement( 'div' );
47
47
 
48
48
  const mockCssItems = [
49
- { id: 'style1', value: '.test { color: red; }', breakpoint: 'desktop' },
50
- { id: 'style2', value: '.test2 { color: blue; }', breakpoint: 'desktop' },
49
+ { id: 'style1', value: '.test { color: red; }', breakpoint: 'desktop', state: null },
50
+ { id: 'style2', value: '.test2 { color: blue; }', breakpoint: 'desktop', state: null },
51
51
  ];
52
52
 
53
53
  const mockLinkAttrs = [
@@ -4,6 +4,7 @@ import { FloatingPortal, useHover, useInteractions } from '@floating-ui/react';
4
4
 
5
5
  import { useBindReactPropsToElement } from '../hooks/use-bind-react-props-to-element';
6
6
  import { useFloatingOnElement } from '../hooks/use-floating-on-element';
7
+ import { useHasOverlapping } from '../hooks/use-has-overlapping';
7
8
 
8
9
  export const CANVAS_WRAPPER_ID = 'elementor-preview-responsive-wrapper';
9
10
 
@@ -25,12 +26,14 @@ const OverlayBox = styled( Box, {
25
26
  export function ElementOverlay( { element, isSelected, id }: Props ) {
26
27
  const { context, floating, isVisible } = useFloatingOnElement( { element, isSelected } );
27
28
  const { getFloatingProps, getReferenceProps } = useInteractions( [ useHover( context ) ] );
29
+ const hasOverlapping = useHasOverlapping();
28
30
 
29
31
  useBindReactPropsToElement( element, getReferenceProps );
30
32
  const isSmallerOffset = element.offsetHeight <= 1;
31
33
 
32
34
  return (
33
- isVisible && (
35
+ isVisible &&
36
+ ! hasOverlapping && (
34
37
  <FloatingPortal id={ CANVAS_WRAPPER_ID }>
35
38
  <OverlayBox
36
39
  ref={ floating.setRef }
@@ -0,0 +1,187 @@
1
+ import { createDOMElement } from 'test-utils';
2
+ import { renderHook } from '@testing-library/react';
3
+
4
+ import { type CanvasExtendedWindow } from '../../sync/types';
5
+ import { useHasOverlapping } from '../use-has-overlapping';
6
+
7
+ const OFF_CANVAS_CLASS = 'e-off-canvas';
8
+
9
+ describe( 'useHasOverlapping', () => {
10
+ let mockPreviewFrame: HTMLIFrameElement;
11
+ let mockDocument: Document;
12
+
13
+ const setupPreviewFrame = ( hasOffCanvas: boolean, isVisible: boolean = true ) => {
14
+ // Arrange - Create mock iframe and document
15
+ mockPreviewFrame = createDOMElement( { tag: 'iframe' } ) as HTMLIFrameElement;
16
+ mockDocument = document.implementation.createHTMLDocument( 'Preview' );
17
+
18
+ if ( hasOffCanvas ) {
19
+ const offCanvasElement = createDOMElement( {
20
+ tag: 'div',
21
+ attrs: { class: OFF_CANVAS_CLASS },
22
+ } );
23
+
24
+ // Mock checkVisibility method
25
+ offCanvasElement.checkVisibility = jest.fn().mockReturnValue( isVisible );
26
+
27
+ mockDocument.body.appendChild( offCanvasElement );
28
+ }
29
+
30
+ // Setup the content window with the mock document
31
+ Object.defineProperty( mockPreviewFrame, 'contentWindow', {
32
+ value: {
33
+ document: mockDocument,
34
+ },
35
+ writable: true,
36
+ } );
37
+
38
+ // Setup window.elementor.$preview
39
+ ( window as unknown as CanvasExtendedWindow ).elementor = {
40
+ $preview: [ mockPreviewFrame ],
41
+ };
42
+ };
43
+
44
+ const cleanupPreviewFrame = () => {
45
+ delete ( window as unknown as CanvasExtendedWindow ).elementor;
46
+ };
47
+
48
+ afterEach( () => {
49
+ cleanupPreviewFrame();
50
+ } );
51
+
52
+ it( 'should return false when preview frame is not available', () => {
53
+ // Arrange - No preview frame setup
54
+
55
+ // Act
56
+ const { result } = renderHook( () => useHasOverlapping() );
57
+
58
+ // Assert
59
+ expect( result.current ).toBe( false );
60
+ } );
61
+
62
+ it( 'should return false when off-canvas element does not exist', () => {
63
+ // Arrange
64
+ setupPreviewFrame( false );
65
+
66
+ // Act
67
+ const { result } = renderHook( () => useHasOverlapping() );
68
+
69
+ // Assert
70
+ expect( result.current ).toBe( false );
71
+ } );
72
+
73
+ it( 'should return true when off-canvas element exists and is visible', () => {
74
+ // Arrange
75
+ setupPreviewFrame( true, true );
76
+
77
+ // Act
78
+ const { result } = renderHook( () => useHasOverlapping() );
79
+
80
+ // Assert
81
+ expect( result.current ).toBe( true );
82
+ } );
83
+
84
+ it( 'should return false when off-canvas element exists but is not visible', () => {
85
+ // Arrange
86
+ setupPreviewFrame( true, false );
87
+
88
+ // Act
89
+ const { result } = renderHook( () => useHasOverlapping() );
90
+
91
+ // Assert
92
+ expect( result.current ).toBe( false );
93
+ } );
94
+
95
+ it( 'should check visibility with correct options', () => {
96
+ // Arrange
97
+ setupPreviewFrame( true, true );
98
+
99
+ // Act
100
+ renderHook( () => useHasOverlapping() );
101
+
102
+ // Assert
103
+ // eslint-disable-next-line testing-library/no-node-access
104
+ const offCanvasElement = mockDocument.querySelector( `.${ OFF_CANVAS_CLASS }` );
105
+ expect( offCanvasElement?.checkVisibility ).toHaveBeenCalledWith( {
106
+ opacityProperty: true,
107
+ visibilityProperty: true,
108
+ contentVisibilityAuto: true,
109
+ } );
110
+ } );
111
+
112
+ it( 'should return true when multiple off-canvas elements exist and at least one is visible', () => {
113
+ // Arrange
114
+ mockPreviewFrame = createDOMElement( { tag: 'iframe' } ) as HTMLIFrameElement;
115
+ mockDocument = document.implementation.createHTMLDocument( 'Preview' );
116
+
117
+ const offCanvasElement1 = createDOMElement( {
118
+ tag: 'div',
119
+ attrs: { class: OFF_CANVAS_CLASS },
120
+ } );
121
+ offCanvasElement1.checkVisibility = jest.fn().mockReturnValue( false );
122
+
123
+ const offCanvasElement2 = createDOMElement( {
124
+ tag: 'div',
125
+ attrs: { class: OFF_CANVAS_CLASS },
126
+ } );
127
+ offCanvasElement2.checkVisibility = jest.fn().mockReturnValue( true );
128
+
129
+ mockDocument.body.appendChild( offCanvasElement1 );
130
+ mockDocument.body.appendChild( offCanvasElement2 );
131
+
132
+ Object.defineProperty( mockPreviewFrame, 'contentWindow', {
133
+ value: {
134
+ document: mockDocument,
135
+ },
136
+ writable: true,
137
+ } );
138
+
139
+ ( window as unknown as CanvasExtendedWindow ).elementor = {
140
+ $preview: [ mockPreviewFrame ],
141
+ };
142
+
143
+ // Act
144
+ const { result } = renderHook( () => useHasOverlapping() );
145
+
146
+ // Assert
147
+ expect( result.current ).toBe( true );
148
+ } );
149
+
150
+ it( 'should return false when multiple off-canvas elements exist but none are visible', () => {
151
+ // Arrange
152
+ mockPreviewFrame = createDOMElement( { tag: 'iframe' } ) as HTMLIFrameElement;
153
+ mockDocument = document.implementation.createHTMLDocument( 'Preview' );
154
+
155
+ const offCanvasElement1 = createDOMElement( {
156
+ tag: 'div',
157
+ attrs: { class: OFF_CANVAS_CLASS },
158
+ } );
159
+ offCanvasElement1.checkVisibility = jest.fn().mockReturnValue( false );
160
+
161
+ const offCanvasElement2 = createDOMElement( {
162
+ tag: 'div',
163
+ attrs: { class: OFF_CANVAS_CLASS },
164
+ } );
165
+ offCanvasElement2.checkVisibility = jest.fn().mockReturnValue( false );
166
+
167
+ mockDocument.body.appendChild( offCanvasElement1 );
168
+ mockDocument.body.appendChild( offCanvasElement2 );
169
+
170
+ Object.defineProperty( mockPreviewFrame, 'contentWindow', {
171
+ value: {
172
+ document: mockDocument,
173
+ },
174
+ writable: true,
175
+ } );
176
+
177
+ ( window as unknown as CanvasExtendedWindow ).elementor = {
178
+ $preview: [ mockPreviewFrame ],
179
+ };
180
+
181
+ // Act
182
+ const { result } = renderHook( () => useHasOverlapping() );
183
+
184
+ // Assert
185
+ expect( result.current ).toBe( false );
186
+ } );
187
+ } );
@@ -8,6 +8,7 @@ type Options = {
8
8
 
9
9
  export function useFloatingOnElement( { element, isSelected }: Options ) {
10
10
  const [ isOpen, setIsOpen ] = useState( false );
11
+ const sizeModifier = 2;
11
12
 
12
13
  const { refs, floatingStyles, context } = useFloating( {
13
14
  // Must be controlled for interactions (like hover) to work.
@@ -18,15 +19,17 @@ export function useFloatingOnElement( { element, isSelected }: Options ) {
18
19
 
19
20
  middleware: [
20
21
  // Match the floating element's size to the reference element.
21
- size( {
22
- apply( { elements, rects } ) {
23
- Object.assign( elements.floating.style, {
24
- width: `${ rects.reference.width + 2 }px`,
25
- height: `${ rects.reference.height + 2 }px`,
26
- } );
27
- },
28
- } ),
29
22
 
23
+ size( () => {
24
+ return {
25
+ apply( { elements, rects } ) {
26
+ Object.assign( elements.floating.style, {
27
+ width: `${ rects.reference.width + sizeModifier }px`,
28
+ height: `${ rects.reference.height + sizeModifier }px`,
29
+ } );
30
+ },
31
+ };
32
+ } ),
30
33
  // Center the floating element on the reference element.
31
34
  offset( ( { rects } ) => -rects.reference.height / 2 - rects.floating.height / 2 ),
32
35
  ],
@@ -0,0 +1,21 @@
1
+ import { type CanvasExtendedWindow } from '../sync/types';
2
+
3
+ const possibleOverlappingSelectors = [ '.e-off-canvas' ]; // can add more selectors here if needed, make sure to loop through them to check classList
4
+
5
+ export const useHasOverlapping = () => {
6
+ const preview = ( window as unknown as CanvasExtendedWindow ).elementor?.$preview?.[ 0 ];
7
+ if ( ! preview ) {
8
+ return false;
9
+ }
10
+ const hasOverlapping = possibleOverlappingSelectors
11
+ .map( ( selector ) => Array.from( preview?.contentWindow?.document.body.querySelectorAll( selector ) ?? [] ) )
12
+ .flat()
13
+ .some( ( elem ) =>
14
+ elem.checkVisibility( {
15
+ opacityProperty: true,
16
+ visibilityProperty: true,
17
+ contentVisibilityAuto: true,
18
+ } )
19
+ );
20
+ return hasOverlapping;
21
+ };
@@ -1,5 +1,6 @@
1
1
  import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
2
2
  import { type BreakpointId, getBreakpoints } from '@elementor/editor-responsive';
3
+ import { isClassState, type StyleDefinitionClassState } from '@elementor/editor-styles';
3
4
  import { type StylesProvider, stylesRepository } from '@elementor/editor-styles-repository';
4
5
  import { registerDataHook } from '@elementor/editor-v1-adapters';
5
6
 
@@ -58,19 +59,45 @@ export function useStyleItems() {
58
59
  return useMemo(
59
60
  () =>
60
61
  Object.values( styleItems )
61
- .sort( ( { provider: providerA }, { provider: providerB } ) => providerA.priority - providerB.priority )
62
+ .sort( sortByProviderPriority )
62
63
  .flatMap( ( { items } ) => items )
63
- .sort( ( { breakpoint: breakpointA }, { breakpoint: breakpointB } ) => {
64
- return (
65
- breakpointsOrder.indexOf( breakpointA as BreakpointId ) -
66
- breakpointsOrder.indexOf( breakpointB as BreakpointId )
67
- );
68
- } ),
64
+ .sort( sortByStateType )
65
+ .sort( sortByBreakpoint( breakpointsOrder ) ),
69
66
  // eslint-disable-next-line
70
67
  // eslint-disable-next-line react-hooks/exhaustive-deps
71
68
  [ styleItems, breakpointsOrder.join( '-' ) ]
72
69
  );
73
70
  }
71
+ function sortByProviderPriority(
72
+ { provider: providerA }: ProviderAndStyleItems,
73
+ { provider: providerB }: ProviderAndStyleItems
74
+ ) {
75
+ return providerA.priority - providerB.priority;
76
+ }
77
+
78
+ function sortByBreakpoint( breakpointsOrder: BreakpointId[] ) {
79
+ return ( { breakpoint: breakpointA }: StyleItem, { breakpoint: breakpointB }: StyleItem ) =>
80
+ breakpointsOrder.indexOf( breakpointA as BreakpointId ) -
81
+ breakpointsOrder.indexOf( breakpointB as BreakpointId );
82
+ }
83
+
84
+ function sortByStateType( { state: stateA }: StyleItem, { state: stateB }: StyleItem ) {
85
+ if (
86
+ isClassState( stateA as StyleDefinitionClassState ) &&
87
+ ! isClassState( stateB as StyleDefinitionClassState )
88
+ ) {
89
+ return -1;
90
+ }
91
+
92
+ if (
93
+ ! isClassState( stateA as StyleDefinitionClassState ) &&
94
+ isClassState( stateB as StyleDefinitionClassState )
95
+ ) {
96
+ return 1;
97
+ }
98
+
99
+ return 0;
100
+ }
74
101
 
75
102
  type CreateProviderSubscriberArgs = {
76
103
  provider: StylesProvider;
@@ -5,11 +5,13 @@ exports[`renderStyles should render styles 1`] = `
5
5
  {
6
6
  "breakpoint": "desktop",
7
7
  "id": "test",
8
+ "state": null,
8
9
  "value": ".test{font-size:10px;}.test:hover{font-size:20px;}@media(max-width:992px){.test{font-size:30px;}}@media(max-width:768px){.test:focus{font-size:40px;}}",
9
10
  },
10
11
  {
11
12
  "breakpoint": "desktop",
12
13
  "id": "test-2",
14
+ "state": null,
13
15
  "value": ".custom-name{font-size:50px;}",
14
16
  },
15
17
  ]
@@ -112,6 +112,7 @@ describe( 'renderStyles', () => {
112
112
  breakpoint: 'desktop',
113
113
  id: 'test',
114
114
  value: '.elementor-prefix .test{font-size:24px;}',
115
+ state: null,
115
116
  },
116
117
  ] );
117
118
  } );
@@ -200,4 +201,28 @@ describe( 'custom_css rendering', () => {
200
201
  // Assert.
201
202
  expect( result[ 0 ].value ).toContain( css );
202
203
  } );
204
+
205
+ it( 'should render class state with selector', async () => {
206
+ // Arrange.
207
+ const styleDef: RendererStyleDefinition = {
208
+ id: 'test',
209
+ type: 'class',
210
+ cssName: 'test',
211
+ label: 'Test',
212
+ variants: [
213
+ {
214
+ meta: { breakpoint: null, state: 'e--selected' },
215
+ props: {},
216
+ custom_css: null,
217
+ },
218
+ ],
219
+ };
220
+
221
+ // Act.
222
+ const renderStyles = createStylesRenderer( { breakpoints: {} as BreakpointsMap, resolve: async () => ( {} ) } );
223
+ const result = await renderStyles( { styles: [ styleDef ] } );
224
+
225
+ // Assert.
226
+ expect( result[ 0 ].value ).toContain( '.test.e--selected{}' );
227
+ } );
203
228
  } );
@@ -2,6 +2,8 @@ import type { Props } from '@elementor/editor-props';
2
2
  import { type Breakpoint, type BreakpointsMap } from '@elementor/editor-responsive';
3
3
  import {
4
4
  type CustomCss,
5
+ isClassState,
6
+ isPseudoState,
5
7
  type StyleDefinition,
6
8
  type StyleDefinitionState,
7
9
  type StyleDefinitionType,
@@ -9,12 +11,13 @@ import {
9
11
  import { decodeString } from '@elementor/utils';
10
12
 
11
13
  import { type PropsResolver } from './create-props-resolver';
12
- import { UnknownStyleTypeError } from './errors';
14
+ import { UnknownStyleStateError, UnknownStyleTypeError } from './errors';
13
15
 
14
16
  export type StyleItem = {
15
17
  id: string;
16
18
  value: string;
17
19
  breakpoint: string;
20
+ state: StyleDefinitionState | null;
18
21
  };
19
22
 
20
23
  export type StyleRenderer = ReturnType< typeof createStylesRenderer >;
@@ -65,6 +68,7 @@ export function createStylesRenderer( { resolve, breakpoints, selectorPrefix = '
65
68
  id: style.id,
66
69
  breakpoint: style?.variants[ 0 ]?.meta?.breakpoint || 'desktop',
67
70
  value: variantsCss.join( '' ),
71
+ state: style?.variants[ 0 ]?.meta?.state || null,
68
72
  };
69
73
  } );
70
74
 
@@ -87,9 +91,21 @@ function createStyleWrapper( value: string = '', wrapper?: ( css: string ) => st
87
91
  withPrefix: ( prefix: string ) =>
88
92
  createStyleWrapper( [ prefix, value ].filter( Boolean ).join( ' ' ), wrapper ),
89
93
 
90
- withState: ( state: StyleDefinitionState ) =>
91
- createStyleWrapper( state ? `${ value }:${ state }` : value, wrapper ),
94
+ withState: ( state: StyleDefinitionState ) => {
95
+ if ( ! state ) {
96
+ return createStyleWrapper( value, wrapper );
97
+ }
98
+
99
+ if ( isClassState( state ) ) {
100
+ return createStyleWrapper( `${ value }.${ state }`, wrapper );
101
+ }
92
102
 
103
+ if ( isPseudoState( state ) ) {
104
+ return createStyleWrapper( `${ value }:${ state }`, wrapper );
105
+ }
106
+
107
+ throw new UnknownStyleStateError( { context: { state } } );
108
+ },
93
109
  withMediaQuery: ( breakpoint: Breakpoint | null ) => {
94
110
  if ( ! breakpoint?.type ) {
95
111
  return createStyleWrapper( value, wrapper );
@@ -1,6 +1,12 @@
1
+ import { type StyleDefinitionState } from '@elementor/editor-styles';
1
2
  import { createError } from '@elementor/utils';
2
3
 
3
4
  export const UnknownStyleTypeError = createError< { type: string } >( {
4
5
  code: 'unknown_style_type',
5
6
  message: 'Unknown style type',
6
7
  } );
8
+
9
+ export const UnknownStyleStateError = createError< { state: StyleDefinitionState } >( {
10
+ code: 'unknown_style_state',
11
+ message: 'Unknown style state',
12
+ } );