@elementor/editor-controls 0.28.2 → 0.30.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/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": "0.28.2",
4
+ "version": "0.30.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -47,6 +47,7 @@
47
47
  "@elementor/env": "0.3.5",
48
48
  "@elementor/http-client": "0.3.0",
49
49
  "@elementor/icons": "1.40.1",
50
+ "@elementor/locations": "0.8.0",
50
51
  "@elementor/query": "0.2.4",
51
52
  "@elementor/session": "0.1.0",
52
53
  "@elementor/ui": "1.34.2",
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+ import { Tooltip } from '@elementor/ui';
3
+
4
+ export const ConditionalTooltip = ( {
5
+ showTooltip,
6
+ children,
7
+ label,
8
+ }: React.PropsWithChildren< { showTooltip: boolean; label: string } > ) => {
9
+ return showTooltip && label ? (
10
+ <Tooltip title={ label } disableFocusListener={ true } placement="top">
11
+ { children }
12
+ </Tooltip>
13
+ ) : (
14
+ children
15
+ );
16
+ };
@@ -1,14 +1,21 @@
1
1
  import * as React from 'react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { ChevronDownIcon } from '@elementor/icons';
2
4
  import {
5
+ ListItemText,
6
+ Menu,
7
+ MenuItem,
3
8
  type StackProps,
4
9
  styled,
5
10
  ToggleButton,
6
11
  ToggleButtonGroup,
7
12
  type ToggleButtonProps,
8
- Tooltip,
13
+ Typography,
9
14
  useTheme,
10
15
  } from '@elementor/ui';
11
16
 
17
+ import { ConditionalTooltip } from './conditional-tooltip';
18
+
12
19
  type RenderContentProps = { size: ToggleButtonProps[ 'size' ] };
13
20
 
14
21
  export type ToggleButtonGroupItem< TValue > = {
@@ -20,6 +27,18 @@ export type ToggleButtonGroupItem< TValue > = {
20
27
 
21
28
  const StyledToggleButtonGroup = styled( ToggleButtonGroup )`
22
29
  ${ ( { justify } ) => `justify-content: ${ justify };` }
30
+ button:not( :last-of-type ) {
31
+ border-start-end-radius: 0;
32
+ border-end-end-radius: 0;
33
+ }
34
+ button:not( :first-of-type ) {
35
+ border-start-start-radius: 0;
36
+ border-end-start-radius: 0;
37
+ }
38
+ button:last-of-type {
39
+ border-start-end-radius: 8px;
40
+ border-end-end-radius: 8px;
41
+ }
23
42
  `;
24
43
 
25
44
  type ExclusiveValue< TValue > = TValue;
@@ -29,6 +48,7 @@ type Props< TValue > = {
29
48
  justify?: StackProps[ 'justifyContent' ];
30
49
  size?: ToggleButtonProps[ 'size' ];
31
50
  items: ToggleButtonGroupItem< TValue | null >[];
51
+ maxItems?: number;
32
52
  fullWidth?: boolean;
33
53
  } & (
34
54
  | {
@@ -49,11 +69,15 @@ export const ControlToggleButtonGroup = < TValue, >( {
49
69
  value,
50
70
  onChange,
51
71
  items,
72
+ maxItems,
52
73
  exclusive = false,
53
74
  fullWidth = false,
54
75
  }: Props< TValue > ) => {
55
- const isRtl = 'rtl' === useTheme().direction;
76
+ const shouldSliceItems = exclusive && maxItems !== undefined && items.length > maxItems;
77
+ const menuItems = shouldSliceItems ? items.slice( maxItems - 1 ) : [];
78
+ const fixedItems = shouldSliceItems ? items.slice( 0, maxItems - 1 ) : items;
56
79
 
80
+ const isRtl = 'rtl' === useTheme().direction;
57
81
  const handleChange = (
58
82
  _: React.MouseEvent< HTMLElement >,
59
83
  newValue: typeof exclusive extends true ? ExclusiveValue< TValue > : NonExclusiveValue< TValue >
@@ -61,38 +85,159 @@ export const ControlToggleButtonGroup = < TValue, >( {
61
85
  onChange( newValue as never );
62
86
  };
63
87
 
88
+ const getGridTemplateColumns = useMemo( () => {
89
+ const isOffLimits = menuItems?.length;
90
+ const itemsCount = isOffLimits ? fixedItems.length + 1 : fixedItems.length;
91
+ const templateColumnsSuffix = isOffLimits ? 'auto' : '';
92
+
93
+ return `repeat(${ itemsCount }, minmax(0, 25%)) ${ templateColumnsSuffix }`;
94
+ }, [ menuItems?.length, fixedItems.length ] );
95
+
64
96
  return (
65
- <StyledToggleButtonGroup
66
- justify={ justify }
67
- value={ value }
68
- onChange={ handleChange }
69
- exclusive={ exclusive }
70
- sx={ {
71
- direction: isRtl ? 'rtl /* @noflip */' : 'ltr /* @noflip */',
72
- display: 'grid',
73
- gridTemplateColumns: `repeat(${ items.length }, minmax(0, 25%))`,
74
- width: `100%`,
75
- } }
76
- >
77
- { items.map( ( { label, value: buttonValue, renderContent: Content, showTooltip } ) =>
78
- showTooltip ? (
79
- <Tooltip key={ buttonValue } title={ label } disableFocusListener={ true } placement="top">
97
+ <>
98
+ <StyledToggleButtonGroup
99
+ justify={ justify }
100
+ value={ value }
101
+ onChange={ handleChange }
102
+ exclusive={ exclusive }
103
+ sx={ {
104
+ direction: isRtl ? 'rtl /* @noflip */' : 'ltr /* @noflip */',
105
+ display: 'grid',
106
+ gridTemplateColumns: getGridTemplateColumns,
107
+ width: `100%`,
108
+ } }
109
+ >
110
+ { fixedItems.map( ( { label, value: buttonValue, renderContent: Content, showTooltip } ) => (
111
+ <ConditionalTooltip
112
+ key={ buttonValue as string }
113
+ label={ label }
114
+ showTooltip={ showTooltip || false }
115
+ >
80
116
  <ToggleButton value={ buttonValue } aria-label={ label } size={ size } fullWidth={ fullWidth }>
81
117
  <Content size={ size } />
82
118
  </ToggleButton>
83
- </Tooltip>
84
- ) : (
85
- <ToggleButton
86
- key={ buttonValue }
87
- value={ buttonValue }
88
- aria-label={ label }
119
+ </ConditionalTooltip>
120
+ ) ) }
121
+
122
+ { menuItems.length && exclusive && (
123
+ <SplitButtonGroup
89
124
  size={ size }
125
+ value={ ( value as ExclusiveValue< TValue > ) || null }
126
+ onChange={ onChange as ( v: ExclusiveValue< TValue > ) => void }
127
+ items={ menuItems }
90
128
  fullWidth={ fullWidth }
129
+ />
130
+ ) }
131
+ </StyledToggleButtonGroup>
132
+ </>
133
+ );
134
+ };
135
+
136
+ type SplitButtonGroup< TValue > = {
137
+ size: ToggleButtonProps[ 'size' ];
138
+ items: ToggleButtonGroupItem< TValue | null >[];
139
+ fullWidth: boolean;
140
+ value: ExclusiveValue< TValue > | null;
141
+ onChange: ( value: ExclusiveValue< TValue > ) => void;
142
+ };
143
+
144
+ const SplitButtonGroup = < TValue, >( {
145
+ size = 'tiny',
146
+ onChange,
147
+ items,
148
+ fullWidth,
149
+ value,
150
+ }: SplitButtonGroup< TValue > ) => {
151
+ const previewButton = usePreviewButton( items, value );
152
+ const [ isMenuOpen, setIsMenuOpen ] = useState( false );
153
+ const menuButtonRef = useRef( null );
154
+
155
+ const onMenuToggle = ( ev: React.MouseEvent ) => {
156
+ setIsMenuOpen( ( prev ) => ! prev );
157
+ ev.preventDefault();
158
+ };
159
+
160
+ const onMenuItemClick = ( newValue: TValue | null ) => {
161
+ setIsMenuOpen( false );
162
+ onToggleItem( newValue );
163
+ };
164
+
165
+ const onToggleItem = ( newValue: TValue | null ) => {
166
+ const shouldRemove = newValue === value;
167
+
168
+ onChange( ( shouldRemove ? null : newValue ) as never );
169
+ };
170
+
171
+ return (
172
+ <>
173
+ <ToggleButton
174
+ value={ previewButton.value }
175
+ aria-label={ previewButton.label }
176
+ size={ size }
177
+ fullWidth={ fullWidth }
178
+ onClick={ ( ev: React.MouseEvent ) => {
179
+ ev.preventDefault();
180
+ onMenuItemClick( previewButton.value );
181
+ } }
182
+ ref={ menuButtonRef }
183
+ >
184
+ { previewButton.renderContent( { size } ) }
185
+ </ToggleButton>
186
+ <ToggleButton
187
+ size={ size }
188
+ aria-expanded={ isMenuOpen ? 'true' : undefined }
189
+ aria-haspopup="menu"
190
+ aria-pressed={ undefined }
191
+ onClick={ onMenuToggle }
192
+ ref={ menuButtonRef }
193
+ value={ '__chevron-icon-button__' }
194
+ >
195
+ <ChevronDownIcon fontSize={ size } />
196
+ </ToggleButton>
197
+ <Menu
198
+ open={ isMenuOpen }
199
+ onClose={ () => setIsMenuOpen( false ) }
200
+ anchorEl={ menuButtonRef.current }
201
+ anchorOrigin={ {
202
+ vertical: 'bottom',
203
+ horizontal: 'right',
204
+ } }
205
+ transformOrigin={ {
206
+ vertical: 'top',
207
+ horizontal: 'right',
208
+ } }
209
+ sx={ {
210
+ mt: 0.5,
211
+ } }
212
+ >
213
+ { items.map( ( { label, value: buttonValue } ) => (
214
+ <MenuItem
215
+ key={ buttonValue }
216
+ selected={ buttonValue === value }
217
+ onClick={ () => onMenuItemClick( buttonValue ) }
91
218
  >
92
- <Content size={ size } />
93
- </ToggleButton>
94
- )
95
- ) }
96
- </StyledToggleButtonGroup>
219
+ <ListItemText>
220
+ <Typography sx={ { fontSize: '14px' } }>{ label }</Typography>
221
+ </ListItemText>
222
+ </MenuItem>
223
+ ) ) }
224
+ </Menu>
225
+ </>
226
+ );
227
+ };
228
+
229
+ const usePreviewButton = < TValue, >( items: ToggleButtonGroupItem< TValue >[], value: TValue ) => {
230
+ const [ previewButton, setPreviewButton ] = useState(
231
+ items.find( ( item ) => item.value === value ) ?? items[ 0 ]
97
232
  );
233
+
234
+ useEffect( () => {
235
+ const selectedButton = items.find( ( item ) => item.value === value );
236
+
237
+ if ( selectedButton ) {
238
+ setPreviewButton( selectedButton );
239
+ }
240
+ }, [ items, value ] );
241
+
242
+ return previewButton;
98
243
  };
@@ -19,6 +19,7 @@ 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';
22
23
  import { SectionContent } from './section-content';
23
24
  import { SortableItem, SortableProvider } from './sortable';
24
25
 
@@ -175,8 +176,16 @@ export const Repeater = < T, >( {
175
176
  <SortableItem id={ key } key={ `sortable-${ key }` }>
176
177
  <RepeaterItem
177
178
  disabled={ value?.disabled }
178
- label={ <itemSettings.Label value={ value } /> }
179
- startIcon={ <itemSettings.Icon value={ value } /> }
179
+ label={
180
+ <RepeaterItemLabelSlot value={ value }>
181
+ <itemSettings.Label value={ value } />
182
+ </RepeaterItemLabelSlot>
183
+ }
184
+ startIcon={
185
+ <RepeaterItemIconSlot value={ value }>
186
+ <itemSettings.Icon value={ value } />
187
+ </RepeaterItemIconSlot>
188
+ }
180
189
  removeItem={ () => removeRepeaterItem( index ) }
181
190
  duplicateItem={ () => duplicateRepeaterItem( index ) }
182
191
  toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
@@ -34,7 +34,7 @@ export const BoxShadowRepeaterControl = createControl( () => {
34
34
  } );
35
35
 
36
36
  const ItemIcon = ( { value }: { value: ShadowPropValue } ) => (
37
- <UnstableColorIndicator size="inherit" component="span" value={ value.value.color.value } />
37
+ <UnstableColorIndicator size="inherit" component="span" value={ value.value.color?.value } />
38
38
  );
39
39
 
40
40
  const ItemContent = ( { anchorEl, bind }: { anchorEl: HTMLElement | null; bind: PropKey } ) => {
@@ -25,6 +25,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
25
25
  import { __ } from '@wordpress/i18n';
26
26
 
27
27
  import { useBoundProp } from '../../bound-prop-context';
28
+ import ControlActions from '../../control-actions/control-actions';
28
29
  import { createControl } from '../../create-control';
29
30
  import { type FontListItem, useFilteredFontFamilies } from '../../hooks/use-filtered-font-families';
30
31
  import { enqueueFont } from './enqueue-font';
@@ -60,14 +61,15 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
60
61
 
61
62
  return (
62
63
  <>
63
- <UnstableTag
64
- variant="outlined"
65
- label={ fontFamily }
66
- endIcon={ <ChevronDownIcon fontSize="tiny" /> }
67
- { ...bindTrigger( popoverState ) }
68
- fullWidth
69
- />
70
-
64
+ <ControlActions>
65
+ <UnstableTag
66
+ variant="outlined"
67
+ label={ fontFamily }
68
+ endIcon={ <ChevronDownIcon fontSize="tiny" /> }
69
+ { ...bindTrigger( popoverState ) }
70
+ fullWidth
71
+ />
72
+ </ControlActions>
71
73
  <Popover
72
74
  disablePortal
73
75
  disableScrollLock
@@ -11,6 +11,7 @@ export type ToggleControlProps< T extends PropValue > = {
11
11
  fullWidth?: boolean;
12
12
  size?: ToggleButtonProps[ 'size' ];
13
13
  exclusive?: boolean;
14
+ maxItems?: number;
14
15
  };
15
16
 
16
17
  export const ToggleControl = createControl(
@@ -19,6 +20,7 @@ export const ToggleControl = createControl(
19
20
  fullWidth = false,
20
21
  size = 'tiny',
21
22
  exclusive = true,
23
+ maxItems,
22
24
  }: ToggleControlProps< StringPropValue[ 'value' ] > ) => {
23
25
  const { value, setValue, placeholder } = useBoundProp( stringPropTypeUtil );
24
26
 
@@ -37,6 +39,7 @@ export const ToggleControl = createControl(
37
39
 
38
40
  const toggleButtonGroupProps = {
39
41
  items: options,
42
+ maxItems,
40
43
  fullWidth,
41
44
  size,
42
45
  };
package/src/index.ts CHANGED
@@ -40,5 +40,7 @@ export { useBoundProp, PropProvider, PropKeyProvider } from './bound-prop-contex
40
40
  export { ControlAdornmentsProvider } from './control-adornments/control-adornments-context';
41
41
  export { ControlAdornments } from './control-adornments/control-adornments';
42
42
 
43
+ export { injectIntoRepeaterItemIcon, injectIntoRepeaterItemLabel } from './locations';
44
+
43
45
  // hooks
44
46
  export { useSyncExternalState } from './hooks/use-sync-external-state';
@@ -0,0 +1,11 @@
1
+ import { type PropValue } from '@elementor/editor-props';
2
+ import { createReplaceableLocation } from '@elementor/locations';
3
+
4
+ // Repeaters
5
+ export const { Slot: RepeaterItemIconSlot, inject: injectIntoRepeaterItemIcon } = createReplaceableLocation< {
6
+ value: PropValue;
7
+ } >();
8
+
9
+ export const { Slot: RepeaterItemLabelSlot, inject: injectIntoRepeaterItemLabel } = createReplaceableLocation< {
10
+ value: PropValue;
11
+ } >();