@elementor/editor-controls 1.3.0 → 3.32.0-20

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 (74) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.d.mts +104 -26
  3. package/dist/index.d.ts +104 -26
  4. package/dist/index.js +2271 -1119
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +2147 -990
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +18 -18
  9. package/src/components/control-toggle-button-group.tsx +78 -14
  10. package/src/components/floating-bar.tsx +45 -0
  11. package/src/components/item-selector.tsx +168 -0
  12. package/src/components/repeater.tsx +23 -12
  13. package/src/components/restricted-link-infotip.tsx +76 -0
  14. package/src/components/size-control/size-input.tsx +4 -3
  15. package/src/components/size-control/text-field-inner-selection.tsx +60 -14
  16. package/src/components/text-field-popover.tsx +30 -7
  17. package/src/components/unstable-repeater/actions/add-item-action.tsx +50 -0
  18. package/src/components/unstable-repeater/actions/disable-item-action.tsx +39 -0
  19. package/src/components/unstable-repeater/actions/duplicate-item-action.tsx +32 -0
  20. package/src/components/unstable-repeater/actions/remove-item-action.tsx +27 -0
  21. package/src/components/unstable-repeater/context/repeater-context.tsx +137 -0
  22. package/src/components/unstable-repeater/header/header.tsx +23 -0
  23. package/src/components/unstable-repeater/index.ts +5 -0
  24. package/src/components/unstable-repeater/items/edit-item-popover.tsx +28 -0
  25. package/src/components/unstable-repeater/items/item.tsx +71 -0
  26. package/src/components/unstable-repeater/items/items-container.tsx +49 -0
  27. package/src/components/unstable-repeater/items/use-popover.tsx +26 -0
  28. package/src/{locations.ts → components/unstable-repeater/locations.ts} +9 -1
  29. package/src/components/unstable-repeater/types.ts +26 -0
  30. package/src/components/unstable-repeater/unstable-repeater.tsx +24 -0
  31. package/src/control-actions/control-actions.tsx +3 -20
  32. package/src/control-replacements.tsx +41 -0
  33. package/src/controls/background-control/background-control.tsx +1 -8
  34. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +17 -16
  35. package/src/controls/color-control.tsx +12 -1
  36. package/src/controls/equal-unequal-sizes-control.tsx +2 -9
  37. package/src/controls/filter-control/drop-shadow-item-content.tsx +67 -0
  38. package/src/controls/filter-control/drop-shadow-item-label.tsx +20 -0
  39. package/src/controls/filter-repeater-control.tsx +214 -88
  40. package/src/controls/font-family-control/font-family-control.tsx +22 -10
  41. package/src/controls/key-value-control.tsx +64 -50
  42. package/src/controls/link-control.tsx +8 -91
  43. package/src/controls/linked-dimensions-control.tsx +3 -16
  44. package/src/controls/number-control.tsx +10 -1
  45. package/src/controls/position-control.tsx +4 -16
  46. package/src/controls/repeatable-control.tsx +56 -34
  47. package/src/controls/select-control.tsx +7 -2
  48. package/src/controls/selection-size-control.tsx +74 -0
  49. package/src/controls/size-control.tsx +189 -121
  50. package/src/controls/stroke-control.tsx +2 -2
  51. package/src/controls/text-control.tsx +33 -18
  52. package/src/controls/toggle-control.tsx +3 -2
  53. package/src/controls/transform-control/functions/axis-row.tsx +4 -2
  54. package/src/controls/transform-control/functions/move.tsx +2 -1
  55. package/src/controls/transform-control/functions/rotate.tsx +48 -0
  56. package/src/controls/transform-control/functions/scale-axis-row.tsx +32 -0
  57. package/src/controls/transform-control/functions/scale.tsx +45 -0
  58. package/src/controls/transform-control/functions/skew.tsx +43 -0
  59. package/src/controls/transform-control/transform-content.tsx +60 -23
  60. package/src/controls/transform-control/transform-icon.tsx +10 -2
  61. package/src/controls/transform-control/transform-label.tsx +39 -2
  62. package/src/controls/transform-control/transform-repeater-control.tsx +2 -10
  63. package/src/controls/transform-control/types.ts +58 -0
  64. package/src/controls/transform-control/use-transform-tabs-history.tsx +107 -0
  65. package/src/controls/transition-control/data.ts +34 -0
  66. package/src/controls/transition-control/transition-repeater-control.tsx +63 -0
  67. package/src/controls/transition-control/transition-selector.tsx +88 -0
  68. package/src/controls/unstable-transform-control/unstable-transform-repeater-control.tsx +35 -0
  69. package/src/hooks/use-filtered-items-list.ts +24 -0
  70. package/src/hooks/use-size-extended-options.ts +1 -6
  71. package/src/index.ts +13 -3
  72. package/src/utils/size-control.ts +10 -2
  73. package/src/components/font-family-selector.tsx +0 -158
  74. package/src/hooks/use-filtered-font-families.ts +0 -24
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "1.3.0",
4
+ "version": "3.32.0-20",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -19,11 +19,11 @@
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/elementor/elementor-packages.git",
22
+ "url": "git+https://github.com/elementor/elementor.git",
23
23
  "directory": "packages/libs/editor-controls"
24
24
  },
25
25
  "bugs": {
26
- "url": "https://github.com/elementor/elementor-packages/issues"
26
+ "url": "https://github.com/elementor/elementor/issues"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
@@ -40,21 +40,21 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "0.6.0",
44
- "@elementor/editor-elements": "0.9.0",
45
- "@elementor/editor-props": "0.16.0",
46
- "@elementor/editor-responsive": "0.13.6",
47
- "@elementor/editor-ui": "0.14.0",
48
- "@elementor/editor-v1-adapters": "0.12.1",
49
- "@elementor/env": "0.3.5",
50
- "@elementor/http-client": "0.3.0",
51
- "@elementor/icons": "1.46.0",
52
- "@elementor/locations": "0.8.0",
53
- "@elementor/query": "0.2.4",
54
- "@elementor/session": "0.1.0",
55
- "@elementor/ui": "1.36.0",
56
- "@elementor/utils": "0.5.0",
57
- "@elementor/wp-media": "0.6.1",
43
+ "@elementor/editor-current-user": "3.32.0-20",
44
+ "@elementor/editor-elements": "3.32.0-20",
45
+ "@elementor/editor-props": "3.32.0-20",
46
+ "@elementor/editor-responsive": "3.32.0-20",
47
+ "@elementor/editor-ui": "3.32.0-20",
48
+ "@elementor/editor-v1-adapters": "3.32.0-20",
49
+ "@elementor/env": "3.32.0-20",
50
+ "@elementor/http-client": "3.32.0-20",
51
+ "@elementor/icons": "^1.51.1",
52
+ "@elementor/locations": "3.32.0-20",
53
+ "@elementor/query": "3.32.0-20",
54
+ "@elementor/session": "3.32.0-20",
55
+ "@elementor/ui": "1.36.2",
56
+ "@elementor/utils": "3.32.0-20",
57
+ "@elementor/wp-media": "3.32.0-20",
58
58
  "@wordpress/i18n": "^5.13.0"
59
59
  },
60
60
  "devDependencies": {
@@ -42,6 +42,21 @@ const StyledToggleButtonGroup = styled( ToggleButtonGroup )`
42
42
  }
43
43
  `;
44
44
 
45
+ const StyledToggleButton = styled( ToggleButton, {
46
+ shouldForwardProp: ( prop ) => prop !== 'isPlaceholder',
47
+ } )< { isPlaceholder: boolean } >`
48
+ ${ ( { theme, isPlaceholder } ) =>
49
+ isPlaceholder &&
50
+ `
51
+ color: ${ theme.palette.text.tertiary };
52
+ background-color: ${ theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.02)' };
53
+
54
+ &:hover {
55
+ background-color: ${ theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)' };
56
+ }
57
+ ` }
58
+ `;
59
+
45
60
  type ExclusiveValue< TValue > = TValue;
46
61
  type NonExclusiveValue< TValue > = TValue[];
47
62
 
@@ -52,6 +67,7 @@ type Props< TValue > = {
52
67
  items: ToggleButtonGroupItem< TValue | null >[];
53
68
  maxItems?: number;
54
69
  fullWidth?: boolean;
70
+ placeholder?: TValue | TValue[];
55
71
  } & (
56
72
  | {
57
73
  exclusive?: false;
@@ -75,12 +91,15 @@ export const ControlToggleButtonGroup = < TValue, >( {
75
91
  exclusive = false,
76
92
  fullWidth = false,
77
93
  disabled,
94
+ placeholder,
78
95
  }: Props< TValue > ) => {
79
96
  const shouldSliceItems = exclusive && maxItems !== undefined && items.length > maxItems;
80
97
  const menuItems = shouldSliceItems ? items.slice( maxItems - 1 ) : [];
81
98
  const fixedItems = shouldSliceItems ? items.slice( 0, maxItems - 1 ) : items;
82
99
 
83
- const isRtl = 'rtl' === useTheme().direction;
100
+ const theme = useTheme();
101
+ const isRtl = 'rtl' === theme.direction;
102
+
84
103
  const handleChange = (
85
104
  _: React.MouseEvent< HTMLElement >,
86
105
  newValue: typeof exclusive extends true ? ExclusiveValue< TValue > : NonExclusiveValue< TValue >
@@ -92,10 +111,42 @@ export const ControlToggleButtonGroup = < TValue, >( {
92
111
  const isOffLimits = menuItems?.length;
93
112
  const itemsCount = isOffLimits ? fixedItems.length + 1 : fixedItems.length;
94
113
  const templateColumnsSuffix = isOffLimits ? 'auto' : '';
95
-
96
114
  return `repeat(${ itemsCount }, minmax(0, 25%)) ${ templateColumnsSuffix }`;
97
115
  }, [ menuItems?.length, fixedItems.length ] );
98
116
 
117
+ const shouldShowExclusivePlaceholder = exclusive && ( value === null || value === undefined || value === '' );
118
+
119
+ const nonExclusiveSelectedValues =
120
+ ! exclusive && Array.isArray( value )
121
+ ? value
122
+ .map( ( v ) => ( typeof v === 'string' ? v : '' ) )
123
+ .join( ' ' )
124
+ .trim()
125
+ .split( /\s+/ )
126
+ .filter( Boolean )
127
+ : [];
128
+
129
+ const shouldShowNonExclusivePlaceholder = ! exclusive && nonExclusiveSelectedValues.length === 0;
130
+
131
+ const getPlaceholderArray = ( placeholderValue: TValue | TValue[] | undefined ): string[] => {
132
+ if ( Array.isArray( placeholderValue ) ) {
133
+ return placeholderValue.flatMap( ( p ) => {
134
+ if ( typeof p === 'string' ) {
135
+ return p.trim().split( /\s+/ ).filter( Boolean );
136
+ }
137
+ return [];
138
+ } );
139
+ }
140
+
141
+ if ( typeof placeholderValue === 'string' ) {
142
+ return placeholderValue.trim().split( /\s+/ ).filter( Boolean );
143
+ }
144
+
145
+ return [];
146
+ };
147
+
148
+ const placeholderArray = getPlaceholderArray( placeholder );
149
+
99
150
  return (
100
151
  <ControlActions>
101
152
  <StyledToggleButtonGroup
@@ -111,17 +162,30 @@ export const ControlToggleButtonGroup = < TValue, >( {
111
162
  width: `100%`,
112
163
  } }
113
164
  >
114
- { fixedItems.map( ( { label, value: buttonValue, renderContent: Content, showTooltip } ) => (
115
- <ConditionalTooltip
116
- key={ buttonValue as string }
117
- label={ label }
118
- showTooltip={ showTooltip || false }
119
- >
120
- <ToggleButton value={ buttonValue } aria-label={ label } size={ size } fullWidth={ fullWidth }>
121
- <Content size={ size } />
122
- </ToggleButton>
123
- </ConditionalTooltip>
124
- ) ) }
165
+ { fixedItems.map( ( { label, value: buttonValue, renderContent: Content, showTooltip } ) => {
166
+ const isPlaceholder =
167
+ placeholderArray.length > 0 &&
168
+ placeholderArray.includes( buttonValue as string ) &&
169
+ ( shouldShowExclusivePlaceholder || shouldShowNonExclusivePlaceholder );
170
+
171
+ return (
172
+ <ConditionalTooltip
173
+ key={ buttonValue as string }
174
+ label={ label }
175
+ showTooltip={ showTooltip || false }
176
+ >
177
+ <StyledToggleButton
178
+ value={ buttonValue }
179
+ aria-label={ label }
180
+ size={ size }
181
+ fullWidth={ fullWidth }
182
+ isPlaceholder={ isPlaceholder }
183
+ >
184
+ <Content size={ size } />
185
+ </StyledToggleButton>
186
+ </ConditionalTooltip>
187
+ );
188
+ } ) }
125
189
 
126
190
  { menuItems.length && exclusive && (
127
191
  <SplitButtonGroup
@@ -194,7 +258,7 @@ const SplitButtonGroup = < TValue, >( {
194
258
  aria-pressed={ undefined }
195
259
  onClick={ onMenuToggle }
196
260
  ref={ menuButtonRef }
197
- value={ '__chevron-icon-button__' }
261
+ value="__chevron-icon-button__"
198
262
  >
199
263
  <ChevronDownIcon fontSize={ size } />
200
264
  </ToggleButton>
@@ -0,0 +1,45 @@
1
+ import * as React from 'react';
2
+ import { createContext, type PropsWithChildren, type ReactElement, useContext, useState } from 'react';
3
+ import { styled, UnstableFloatingActionBar } from '@elementor/ui';
4
+
5
+ // CSS hack to hide empty floating bars.
6
+ const FloatingBarContainer = styled( 'span' )`
7
+ display: contents;
8
+
9
+ .MuiFloatingActionBar-popper:has( .MuiFloatingActionBar-actions:empty ) {
10
+ display: none;
11
+ }
12
+
13
+ .MuiFloatingActionBar-popper {
14
+ z-index: 1000;
15
+ }
16
+ `;
17
+
18
+ const FloatingActionsContext = createContext< null | {
19
+ open: boolean;
20
+ setOpen: React.Dispatch< React.SetStateAction< boolean > >;
21
+ } >( null );
22
+
23
+ export function FloatingActionsBar( { actions, children }: PropsWithChildren< { actions: ReactElement[] } > ) {
24
+ const [ open, setOpen ] = useState< boolean >( false );
25
+
26
+ return (
27
+ <FloatingActionsContext.Provider value={ { open, setOpen } }>
28
+ <FloatingBarContainer>
29
+ <UnstableFloatingActionBar actions={ actions } open={ open || undefined }>
30
+ { children as ReactElement }
31
+ </UnstableFloatingActionBar>
32
+ </FloatingBarContainer>
33
+ </FloatingActionsContext.Provider>
34
+ );
35
+ }
36
+
37
+ export function useFloatingActionsBar() {
38
+ const context = useContext( FloatingActionsContext );
39
+
40
+ if ( ! context ) {
41
+ throw new Error( 'useFloatingActions must be used within a FloatingActionsBar' );
42
+ }
43
+
44
+ return context;
45
+ }
@@ -0,0 +1,168 @@
1
+ import * as React from 'react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { PopoverBody, PopoverHeader, PopoverMenuList, PopoverSearch } from '@elementor/editor-ui';
4
+ import { Box, Divider, Link, Stack, Typography } from '@elementor/ui';
5
+ import { debounce } from '@elementor/utils';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { type SelectableItem, useFilteredItemsList } from '../hooks/use-filtered-items-list';
9
+
10
+ export type Category = {
11
+ label: string;
12
+ items: string[];
13
+ };
14
+
15
+ type ItemSelectorProps = {
16
+ itemsList: Category[];
17
+ selectedItem: string | null;
18
+ onItemChange: ( item: string ) => void;
19
+ onClose: () => void;
20
+ sectionWidth: number;
21
+ title: string;
22
+ itemStyle?: ( item: SelectableItem ) => React.CSSProperties;
23
+ onDebounce?: ( name: string ) => void;
24
+ icon: React.ElementType< { fontSize: string } >;
25
+ };
26
+
27
+ export const ItemSelector = ( {
28
+ itemsList,
29
+ selectedItem,
30
+ onItemChange,
31
+ onClose,
32
+ sectionWidth,
33
+ title,
34
+ itemStyle = () => ( {} ),
35
+ onDebounce = () => {},
36
+ icon,
37
+ }: ItemSelectorProps ) => {
38
+ const [ searchValue, setSearchValue ] = useState( '' );
39
+
40
+ const filteredItemsList = useFilteredItemsList( itemsList, searchValue );
41
+
42
+ const IconComponent = icon;
43
+
44
+ const handleSearch = ( value: string ) => {
45
+ setSearchValue( value );
46
+ };
47
+
48
+ const handleClose = () => {
49
+ setSearchValue( '' );
50
+ onClose();
51
+ };
52
+
53
+ return (
54
+ <PopoverBody width={ sectionWidth }>
55
+ <PopoverHeader title={ title } onClose={ handleClose } icon={ <IconComponent fontSize="tiny" /> } />
56
+ <PopoverSearch
57
+ value={ searchValue }
58
+ onSearch={ handleSearch }
59
+ placeholder={ __( 'Search', 'elementor' ) }
60
+ />
61
+
62
+ <Divider />
63
+
64
+ { filteredItemsList.length > 0 ? (
65
+ <ItemList
66
+ itemListItems={ filteredItemsList }
67
+ setSelectedItem={ onItemChange }
68
+ handleClose={ handleClose }
69
+ selectedItem={ selectedItem }
70
+ itemStyle={ itemStyle }
71
+ onDebounce={ onDebounce }
72
+ />
73
+ ) : (
74
+ <Stack
75
+ alignItems="center"
76
+ justifyContent="center"
77
+ height="100%"
78
+ p={ 2.5 }
79
+ gap={ 1.5 }
80
+ overflow="hidden"
81
+ >
82
+ <IconComponent fontSize="large" />
83
+ <Box sx={ { maxWidth: 160, overflow: 'hidden' } }>
84
+ <Typography align="center" variant="subtitle2" color="text.secondary">
85
+ { __( 'Sorry, nothing matched', 'elementor' ) }
86
+ </Typography>
87
+ <Typography
88
+ variant="subtitle2"
89
+ color="text.secondary"
90
+ sx={ { display: 'flex', width: '100%', justifyContent: 'center' } }
91
+ >
92
+ <span>&ldquo;</span>
93
+ <span style={ { maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis' } }>
94
+ { searchValue }
95
+ </span>
96
+ <span>&rdquo;.</span>
97
+ </Typography>
98
+ </Box>
99
+ <Typography
100
+ align="center"
101
+ variant="caption"
102
+ color="text.secondary"
103
+ sx={ { display: 'flex', flexDirection: 'column' } }
104
+ >
105
+ { __( 'Try something else.', 'elementor' ) }
106
+ <Link
107
+ color="secondary"
108
+ variant="caption"
109
+ component="button"
110
+ onClick={ () => setSearchValue( '' ) }
111
+ >
112
+ { __( 'Clear & try again', 'elementor' ) }
113
+ </Link>
114
+ </Typography>
115
+ </Stack>
116
+ ) }
117
+ </PopoverBody>
118
+ );
119
+ };
120
+
121
+ type ItemListProps = {
122
+ itemListItems: SelectableItem[];
123
+ setSelectedItem: ( item: string ) => void;
124
+ handleClose: () => void;
125
+ selectedItem: string | null;
126
+ itemStyle?: ( item: SelectableItem ) => React.CSSProperties;
127
+ onDebounce?: ( name: string ) => void;
128
+ };
129
+
130
+ const ItemList = ( {
131
+ itemListItems,
132
+ setSelectedItem,
133
+ handleClose,
134
+ selectedItem,
135
+ itemStyle = () => ( {} ),
136
+ onDebounce = () => {},
137
+ }: ItemListProps ) => {
138
+ const selectedItemFound = itemListItems.find( ( item ) => item.value === selectedItem );
139
+
140
+ const debouncedVirtualizeChange = useDebounce( ( { getVirtualIndexes }: { getVirtualIndexes: () => number[] } ) => {
141
+ getVirtualIndexes().forEach( ( index ) => {
142
+ const item = itemListItems[ index ];
143
+ if ( item && item.type === 'item' ) {
144
+ onDebounce( item.value );
145
+ }
146
+ } );
147
+ }, 100 );
148
+
149
+ const memoizedItemStyle = useCallback( ( item: SelectableItem ) => itemStyle( item ), [ itemStyle ] );
150
+
151
+ return (
152
+ <PopoverMenuList
153
+ items={ itemListItems }
154
+ selectedValue={ selectedItemFound?.value }
155
+ onChange={ debouncedVirtualizeChange }
156
+ onSelect={ setSelectedItem }
157
+ onClose={ handleClose }
158
+ itemStyle={ memoizedItemStyle }
159
+ data-testid="item-list"
160
+ />
161
+ );
162
+ };
163
+
164
+ const useDebounce = < TArgs extends unknown[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
165
+ const [ debouncedFn ] = useState( () => debounce( fn, delay ) );
166
+ useEffect( () => () => debouncedFn.cancel(), [ debouncedFn ] );
167
+ return debouncedFn;
168
+ };
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { useEffect, useState } from 'react';
3
- import { type PropKey } from '@elementor/editor-props';
3
+ import { type PropKey, type PropTypeUtil } from '@elementor/editor-props';
4
4
  import { CopyIcon, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from '@elementor/icons';
5
5
  import {
6
6
  bindPopover,
@@ -19,9 +19,9 @@ import { __ } from '@wordpress/i18n';
19
19
 
20
20
  import { ControlAdornments } from '../control-adornments/control-adornments';
21
21
  import { useSyncExternalState } from '../hooks/use-sync-external-state';
22
- import { RepeaterItemIconSlot, RepeaterItemLabelSlot } from '../locations';
23
22
  import { SectionContent } from './section-content';
24
23
  import { SortableItem, SortableProvider } from './sortable';
24
+ import { RepeaterItemIconSlot, RepeaterItemLabelSlot } from './unstable-repeater/locations';
25
25
 
26
26
  const SIZE = 'tiny';
27
27
 
@@ -30,6 +30,16 @@ type AnchorEl = HTMLElement | null;
30
30
  type Item< T > = {
31
31
  disabled?: boolean;
32
32
  } & T;
33
+ export type CollectionPropUtil< T > = PropTypeUtil< PropKey, T[] >;
34
+
35
+ type RepeaterItemContentProps< T > = {
36
+ anchorEl: AnchorEl;
37
+ bind: PropKey;
38
+ value: T;
39
+ collectionPropUtil?: CollectionPropUtil< T >;
40
+ };
41
+
42
+ type RepeaterItemContent< T > = React.ComponentType< RepeaterItemContentProps< T > >;
33
43
 
34
44
  type RepeaterProps< T > = {
35
45
  label: string;
@@ -42,15 +52,12 @@ type RepeaterProps< T > = {
42
52
  initialValues: T;
43
53
  Label: React.ComponentType< { value: T } >;
44
54
  Icon: React.ComponentType< { value: T } >;
45
- Content: React.ComponentType< {
46
- anchorEl: AnchorEl;
47
- bind: PropKey;
48
- value: T;
49
- } >;
55
+ Content: RepeaterItemContent< T >;
50
56
  };
51
57
  showDuplicate?: boolean;
52
58
  showToggle?: boolean;
53
59
  isSortable?: boolean;
60
+ collectionPropUtil?: CollectionPropUtil< T >;
54
61
  };
55
62
 
56
63
  const EMPTY_OPEN_ITEM = -1;
@@ -66,6 +73,7 @@ export const Repeater = < T, >( {
66
73
  showDuplicate = true,
67
74
  showToggle = true,
68
75
  isSortable = true,
76
+ collectionPropUtil,
69
77
  }: RepeaterProps< Item< T > > ) => {
70
78
  const [ openItem, setOpenItem ] = useState( EMPTY_OPEN_ITEM );
71
79
 
@@ -203,6 +211,7 @@ export const Repeater = < T, >( {
203
211
  onOpen={ () => setOpenItem( EMPTY_OPEN_ITEM ) }
204
212
  showDuplicate={ showDuplicate }
205
213
  showToggle={ showToggle }
214
+ collectionPropUtil={ collectionPropUtil }
206
215
  >
207
216
  { ( props ) => (
208
217
  <itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
@@ -217,22 +226,23 @@ export const Repeater = < T, >( {
217
226
  );
218
227
  };
219
228
 
220
- type RepeaterItemProps = {
229
+ type RepeaterItemProps< T > = {
221
230
  label: React.ReactNode;
222
231
  propDisabled?: boolean;
223
232
  startIcon: UnstableTagProps[ 'startIcon' ];
224
233
  removeItem: () => void;
225
234
  duplicateItem: () => void;
226
235
  toggleDisableItem: () => void;
227
- children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
236
+ children: ( props: Pick< RepeaterItemContentProps< T >, 'anchorEl' | 'collectionPropUtil' > ) => React.ReactNode;
228
237
  openOnMount: boolean;
229
238
  onOpen: () => void;
230
239
  showDuplicate: boolean;
231
240
  showToggle: boolean;
232
241
  disabled?: boolean;
242
+ collectionPropUtil?: CollectionPropUtil< T >;
233
243
  };
234
244
 
235
- const RepeaterItem = ( {
245
+ const RepeaterItem = < T, >( {
236
246
  label,
237
247
  propDisabled,
238
248
  startIcon,
@@ -245,7 +255,8 @@ const RepeaterItem = ( {
245
255
  showDuplicate,
246
256
  showToggle,
247
257
  disabled,
248
- }: RepeaterItemProps ) => {
258
+ collectionPropUtil,
259
+ }: RepeaterItemProps< T > ) => {
249
260
  const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
250
261
  const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount, onOpen );
251
262
 
@@ -301,7 +312,7 @@ const RepeaterItem = ( {
301
312
  { ...popoverProps }
302
313
  anchorEl={ ref }
303
314
  >
304
- <Box>{ children( { anchorEl } ) }</Box>
315
+ <Box>{ children( { anchorEl, collectionPropUtil } ) }</Box>
305
316
  </Popover>
306
317
  </>
307
318
  );
@@ -0,0 +1,76 @@
1
+ import * as React from 'react';
2
+ import { type PropsWithChildren } from 'react';
3
+ import { type LinkInLinkRestriction, selectElement } from '@elementor/editor-elements';
4
+ import { InfoCircleFilledIcon } from '@elementor/icons';
5
+ import { Alert, AlertAction, AlertTitle, Box, Infotip, Link } from '@elementor/ui';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ const learnMoreButton = {
9
+ label: __( 'Learn More', 'elementor' ),
10
+ href: 'https://go.elementor.com/element-link-inside-link-infotip',
11
+ };
12
+
13
+ const INFOTIP_CONTENT = {
14
+ descendant: __(
15
+ 'To add a link to this element, first remove the link from the elements inside of it.',
16
+ 'elementor'
17
+ ),
18
+ ancestor: __( 'To add a link to this element, first remove the link from its parent container.', 'elementor' ),
19
+ };
20
+
21
+ type RestrictedLinkInfotipProps = PropsWithChildren< {
22
+ linkInLinkRestriction: LinkInLinkRestriction;
23
+ isVisible: boolean;
24
+ } >;
25
+
26
+ export const RestrictedLinkInfotip: React.FC< RestrictedLinkInfotipProps > = ( {
27
+ linkInLinkRestriction,
28
+ isVisible,
29
+ children,
30
+ } ) => {
31
+ const { shouldRestrict, reason, elementId } = linkInLinkRestriction;
32
+
33
+ const handleTakeMeClick = () => {
34
+ if ( elementId ) {
35
+ selectElement( elementId );
36
+ }
37
+ };
38
+
39
+ const content = (
40
+ <Alert
41
+ severity="secondary"
42
+ icon={ <InfoCircleFilledIcon /> }
43
+ action={
44
+ <AlertAction
45
+ sx={ { width: 'fit-content' } }
46
+ variant="contained"
47
+ color="secondary"
48
+ onClick={ handleTakeMeClick }
49
+ >
50
+ { __( 'Take me there', 'elementor' ) }
51
+ </AlertAction>
52
+ }
53
+ >
54
+ <AlertTitle>{ __( 'Nested links', 'elementor' ) }</AlertTitle>
55
+ <Box component="span">
56
+ { INFOTIP_CONTENT[ reason ?? 'descendant' ] }{ ' ' }
57
+ <Link href={ learnMoreButton.href } target="_blank" color="info.main">
58
+ { learnMoreButton.label }
59
+ </Link>
60
+ </Box>
61
+ </Alert>
62
+ );
63
+
64
+ return shouldRestrict && isVisible ? (
65
+ <Infotip
66
+ placement="right"
67
+ content={ content }
68
+ color="secondary"
69
+ slotProps={ { popper: { sx: { width: 300 } } } }
70
+ >
71
+ <Box>{ children }</Box>
72
+ </Infotip>
73
+ ) : (
74
+ <>{ children }</>
75
+ );
76
+ };
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { useRef } from 'react';
3
- import { PencilIcon } from '@elementor/icons';
3
+ import { MathFunctionIcon } from '@elementor/icons';
4
4
  import { Box, InputAdornment, type PopupState } from '@elementor/ui';
5
5
 
6
6
  import ControlActions from '../../control-actions/control-actions';
@@ -72,6 +72,7 @@ export const SizeInput = ( {
72
72
 
73
73
  const inputProps = {
74
74
  ...popupAttributes,
75
+ readOnly: isUnitExtendedOption( unit ),
75
76
  autoComplete: 'off',
76
77
  onClick,
77
78
  onFocus,
@@ -87,7 +88,7 @@ export const SizeInput = ( {
87
88
  onClick={ handleUnitChange }
88
89
  value={ unit }
89
90
  alternativeOptionLabels={ {
90
- custom: <PencilIcon fontSize="small" />,
91
+ custom: <MathFunctionIcon fontSize="tiny" />,
91
92
  } }
92
93
  menuItemsAttributes={
93
94
  units.includes( 'custom' )
@@ -116,8 +117,8 @@ export const SizeInput = ( {
116
117
  } }
117
118
  onKeyUp={ handleKeyUp }
118
119
  onBlur={ onBlur }
119
- shouldBlockInput={ isUnitExtendedOption( unit ) }
120
120
  inputProps={ inputProps }
121
+ isPopoverOpen={ popupState.isOpen }
121
122
  />
122
123
  </Box>
123
124
  </ControlActions>