@elementor/editor-controls 0.15.0 → 0.17.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.15.0",
4
+ "version": "0.17.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,17 +40,19 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-props": "0.9.4",
43
+ "@elementor/editor-current-user": "0.3.0",
44
+ "@elementor/editor-elements": "0.6.3",
45
+ "@elementor/editor-props": "0.11.0",
44
46
  "@elementor/env": "0.3.5",
45
47
  "@elementor/http": "0.1.4",
46
- "@elementor/icons": "1.35.0",
48
+ "@elementor/icons": "1.37.0",
47
49
  "@elementor/query": "0.2.4",
48
50
  "@elementor/session": "0.1.0",
49
51
  "@elementor/ui": "1.26.0",
50
52
  "@elementor/utils": "0.4.0",
51
- "@elementor/wp-media": "0.5.0",
52
- "@wordpress/i18n": "^5.13.0",
53
- "@tanstack/react-virtual": "3.13.3"
53
+ "@elementor/wp-media": "0.6.0",
54
+ "@tanstack/react-virtual": "3.13.3",
55
+ "@wordpress/i18n": "^5.13.0"
54
56
  },
55
57
  "devDependencies": {
56
58
  "tsup": "^8.3.5"
@@ -43,6 +43,8 @@ type Props< TValue > = {
43
43
  }
44
44
  );
45
45
 
46
+ // The maximum number of buttons that can be displayed in the group
47
+ const MAX_VISIBLE_ITEMS = 4;
46
48
  export const ControlToggleButtonGroup = < TValue, >( {
47
49
  justify = 'end',
48
50
  size = 'tiny',
@@ -69,6 +71,9 @@ export const ControlToggleButtonGroup = < TValue, >( {
69
71
  exclusive={ exclusive }
70
72
  sx={ {
71
73
  direction: isRtl ? 'rtl /* @noflip */' : 'ltr /* @noflip */',
74
+ display: 'grid',
75
+ gridTemplateColumns: `repeat(${ items.length }, 1fr)`,
76
+ width: `${ ( items.length / MAX_VISIBLE_ITEMS ) * 100 }%`,
72
77
  } }
73
78
  >
74
79
  { items.map( ( { label, value: buttonValue, renderContent: Content, showTooltip } ) =>
@@ -0,0 +1,130 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { useCurrentUserCapabilities } from '@elementor/editor-current-user';
4
+ import {
5
+ Button,
6
+ CircularProgress,
7
+ Dialog,
8
+ DialogActions,
9
+ DialogContent,
10
+ DialogContentText,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ Divider,
14
+ } from '@elementor/ui';
15
+ import { __ } from '@wordpress/i18n';
16
+
17
+ import { useUpdateUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
18
+
19
+ type EnableUnfilteredModalProps = {
20
+ open: boolean;
21
+ onClose: ( enabled: boolean ) => void;
22
+ };
23
+
24
+ type LocalModalProps = {
25
+ open: boolean;
26
+ onClose: ( enabled: boolean ) => void;
27
+ isPending?: boolean;
28
+ isError?: boolean;
29
+ handleEnable: () => void;
30
+ };
31
+
32
+ const ADMIN_TITLE_TEXT = __( 'Enable Unfiltered Uploads', 'elementor' );
33
+ const ADMIN_CONTENT_TEXT = __(
34
+ 'Before you enable unfiltered files upload, note that such files include a security risk. Elementor does run a process to remove possible malicious code, but there is still risk involved when using such files.',
35
+ 'elementor'
36
+ );
37
+ const NON_ADMIN_TITLE_TEXT = __( "Sorry, you can't upload that file yet", 'elementor' );
38
+ const NON_ADMIN_CONTENT_TEXT = __(
39
+ 'This is because this file type may pose a security risk. To upload them anyway, ask the site administrator to enable unfiltered file uploads.',
40
+ 'elementor'
41
+ );
42
+
43
+ const ADMIN_FAILED_CONTENT_TEXT_PT1 = __( 'Failed to enable unfiltered files upload.', 'elementor' );
44
+
45
+ const ADMIN_FAILED_CONTENT_TEXT_PT2 = __(
46
+ 'You can try again, if the problem persists, please contact support.',
47
+ 'elementor'
48
+ );
49
+
50
+ const WAIT_FOR_CLOSE_TIMEOUT_MS = 300;
51
+
52
+ export const EnableUnfilteredModal = ( props: EnableUnfilteredModalProps ) => {
53
+ const { mutateAsync, isPending } = useUpdateUnfilteredFilesUpload();
54
+ const { canUser } = useCurrentUserCapabilities();
55
+ const [ isError, setIsError ] = useState( false );
56
+ const canManageOptions = canUser( 'manage_options' );
57
+
58
+ const onClose = ( enabled: boolean ) => {
59
+ props.onClose( enabled );
60
+ setTimeout( () => setIsError( false ), WAIT_FOR_CLOSE_TIMEOUT_MS );
61
+ };
62
+
63
+ const handleEnable = async () => {
64
+ try {
65
+ const response = await mutateAsync( { allowUnfilteredFilesUpload: true } );
66
+ if ( response?.data?.success === false ) {
67
+ setIsError( true );
68
+ } else {
69
+ props.onClose( true );
70
+ }
71
+ } catch {
72
+ setIsError( true );
73
+ }
74
+ };
75
+
76
+ const dialogProps = { ...props, isPending, handleEnable, isError, onClose };
77
+
78
+ return canManageOptions ? <AdminDialog { ...dialogProps } /> : <NonAdminDialog { ...dialogProps } />;
79
+ };
80
+
81
+ const AdminDialog = ( { open, onClose, handleEnable, isPending, isError }: LocalModalProps ) => (
82
+ <Dialog open={ open } maxWidth={ 'sm' } onClose={ () => onClose( false ) }>
83
+ <DialogHeader logo={ false }>
84
+ <DialogTitle>{ ADMIN_TITLE_TEXT }</DialogTitle>
85
+ </DialogHeader>
86
+ <Divider />
87
+ <DialogContent>
88
+ <DialogContentText>
89
+ { isError ? (
90
+ <>
91
+ { ADMIN_FAILED_CONTENT_TEXT_PT1 } <br /> { ADMIN_FAILED_CONTENT_TEXT_PT2 }
92
+ </>
93
+ ) : (
94
+ ADMIN_CONTENT_TEXT
95
+ ) }
96
+ </DialogContentText>
97
+ </DialogContent>
98
+ <DialogActions>
99
+ <Button size={ 'medium' } color="secondary" onClick={ () => onClose( false ) }>
100
+ { __( 'Cancel', 'elementor' ) }
101
+ </Button>
102
+ <Button
103
+ size={ 'medium' }
104
+ onClick={ () => handleEnable() }
105
+ variant="contained"
106
+ color="primary"
107
+ disabled={ isPending }
108
+ >
109
+ { isPending ? <CircularProgress size={ 24 } /> : __( 'Enable', 'elementor' ) }
110
+ </Button>
111
+ </DialogActions>
112
+ </Dialog>
113
+ );
114
+
115
+ const NonAdminDialog = ( { open, onClose }: LocalModalProps ) => (
116
+ <Dialog open={ open } maxWidth={ 'sm' } onClose={ () => onClose( false ) }>
117
+ <DialogHeader logo={ false }>
118
+ <DialogTitle>{ NON_ADMIN_TITLE_TEXT }</DialogTitle>
119
+ </DialogHeader>
120
+ <Divider />
121
+ <DialogContent>
122
+ <DialogContentText>{ NON_ADMIN_CONTENT_TEXT }</DialogContentText>
123
+ </DialogContent>
124
+ <DialogActions>
125
+ <Button size={ 'medium' } onClick={ () => onClose( false ) } variant="contained" color="primary">
126
+ { __( 'Got it', 'elementor' ) }
127
+ </Button>
128
+ </DialogActions>
129
+ </Dialog>
130
+ );
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useRef, useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { type PropKey } from '@elementor/editor-props';
4
4
  import { CopyIcon, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from '@elementor/icons';
5
5
  import {
@@ -33,6 +33,7 @@ type RepeaterProps< T > = {
33
33
  label: string;
34
34
  values?: T[];
35
35
  addToBottom?: boolean;
36
+ openOnAdd?: boolean;
36
37
  setValues: ( newValue: T[] ) => void;
37
38
  itemSettings: {
38
39
  initialValues: T;
@@ -49,10 +50,13 @@ type RepeaterProps< T > = {
49
50
  export const Repeater = < T, >( {
50
51
  label,
51
52
  itemSettings,
53
+ openOnAdd = false,
52
54
  addToBottom = false,
53
55
  values: repeaterValues = [],
54
56
  setValues: setRepeaterValues,
55
57
  }: RepeaterProps< Item< T > > ) => {
58
+ const [ openItem, setOpenItem ] = useState( -1 );
59
+
56
60
  const [ items, setItems ] = useSyncExternalState( {
57
61
  external: repeaterValues,
58
62
  // @ts-expect-error - as long as persistWhen => true, value will never be null
@@ -77,6 +81,10 @@ export const Repeater = < T, >( {
77
81
  setItems( [ newItem, ...items ] );
78
82
  setUniqueKeys( [ newKey, ...uniqueKeys ] );
79
83
  }
84
+
85
+ if ( openOnAdd ) {
86
+ setOpenItem( newKey );
87
+ }
80
88
  };
81
89
 
82
90
  const duplicateRepeaterItem = ( index: number ) => {
@@ -158,6 +166,7 @@ export const Repeater = < T, >( {
158
166
  removeItem={ () => removeRepeaterItem( index ) }
159
167
  duplicateItem={ () => duplicateRepeaterItem( index ) }
160
168
  toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
169
+ openOnMount={ openOnAdd && openItem === key }
161
170
  >
162
171
  { ( props ) => (
163
172
  <itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
@@ -181,25 +190,21 @@ type RepeaterItemProps = {
181
190
  duplicateItem: () => void;
182
191
  toggleDisableItem: () => void;
183
192
  children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
193
+ openOnMount: boolean;
184
194
  };
185
195
 
186
196
  const RepeaterItem = ( {
187
197
  label,
188
- bind,
189
198
  disabled,
190
199
  startIcon,
191
200
  children,
192
201
  removeItem,
193
202
  duplicateItem,
194
203
  toggleDisableItem,
204
+ openOnMount,
195
205
  }: RepeaterItemProps ) => {
196
- const popupId = `repeater-popup-${ bind }`;
197
- const controlRef = useRef< HTMLElement >( null );
198
206
  const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
199
-
200
- const popoverState = usePopupState( { popupId, variant: 'popover' } );
201
-
202
- const popoverProps = bindPopover( popoverState );
207
+ const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount );
203
208
 
204
209
  const duplicateLabel = __( 'Duplicate', 'elementor' );
205
210
  const toggleLabel = disabled ? __( 'Show', 'elementor' ) : __( 'Hide', 'elementor' );
@@ -211,7 +216,7 @@ const RepeaterItem = ( {
211
216
  label={ label }
212
217
  showActionsOnHover
213
218
  fullWidth
214
- ref={ controlRef }
219
+ ref={ setRef }
215
220
  variant="outlined"
216
221
  aria-label={ __( 'Open item', 'elementor' ) }
217
222
  { ...bindTrigger( popoverState ) }
@@ -242,14 +247,38 @@ const RepeaterItem = ( {
242
247
  slotProps={ {
243
248
  paper: {
244
249
  ref: setAnchorEl,
245
- sx: { mt: 0.5, width: controlRef.current?.getBoundingClientRect().width },
250
+ sx: { mt: 0.5, width: ref?.getBoundingClientRect().width },
246
251
  },
247
252
  } }
248
253
  anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
249
254
  { ...popoverProps }
255
+ anchorEl={ ref }
250
256
  >
251
257
  <Box>{ children( { anchorEl } ) }</Box>
252
258
  </Popover>
253
259
  </>
254
260
  );
255
261
  };
262
+
263
+ const usePopover = ( openOnMount: boolean ) => {
264
+ const [ ref, setRef ] = useState< HTMLElement | null >( null );
265
+
266
+ const popoverState = usePopupState( { variant: 'popover' } );
267
+
268
+ const popoverProps = bindPopover( popoverState );
269
+
270
+ useEffect( () => {
271
+ if ( openOnMount && ref ) {
272
+ popoverState.open( ref );
273
+ }
274
+ // eslint-disable-next-line react-compiler/react-compiler
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ }, [ ref ] );
277
+
278
+ return {
279
+ popoverState,
280
+ ref,
281
+ setRef,
282
+ popoverProps,
283
+ };
284
+ };
@@ -14,13 +14,8 @@ import {
14
14
 
15
15
  export const SortableProvider = < T extends number >( props: UnstableSortableProviderProps< T > ) => {
16
16
  return (
17
- <List sx={ { p: 0, m: 0 } }>
18
- <UnstableSortableProvider
19
- restrictAxis={ true }
20
- disableDragOverlay={ false }
21
- variant={ 'static' }
22
- { ...props }
23
- />
17
+ <List sx={ { p: 0, my: -0.5, mx: 0 } }>
18
+ <UnstableSortableProvider restrictAxis disableDragOverlay={ false } variant={ 'static' } { ...props } />
24
19
  </List>
25
20
  );
26
21
  };
@@ -0,0 +1,101 @@
1
+ import * as React from 'react';
2
+ import {
3
+ backgroundGradientOverlayPropTypeUtil,
4
+ type BackgroundGradientOverlayPropValue,
5
+ type BackgroundOverlayItemPropValue,
6
+ colorPropTypeUtil,
7
+ type ColorPropValue,
8
+ colorStopPropTypeUtil,
9
+ gradientColorStopPropTypeUtil,
10
+ numberPropTypeUtil,
11
+ type NumberPropValue,
12
+ stringPropTypeUtil,
13
+ type TransformablePropValue,
14
+ } from '@elementor/editor-props';
15
+ import { UnstableGradientBox } from '@elementor/ui';
16
+
17
+ import { useBoundProp } from '../../bound-prop-context';
18
+ import ControlActions from '../../control-actions/control-actions';
19
+ import { createControl } from '../../create-control';
20
+
21
+ export type ColorStop = TransformablePropValue<
22
+ 'color-stop',
23
+ {
24
+ color: ColorPropValue;
25
+ offset: NumberPropValue;
26
+ }
27
+ >;
28
+
29
+ export const BackgroundGradientColorControl = createControl( () => {
30
+ const { value, setValue } = useBoundProp( backgroundGradientOverlayPropTypeUtil );
31
+
32
+ const handleChange = ( newValue: BackgroundGradientOverlayPropValue[ 'value' ] ) => {
33
+ const transformedValue = createTransformableValue( newValue );
34
+
35
+ if ( transformedValue.positions ) {
36
+ transformedValue.positions = stringPropTypeUtil.create( newValue.positions.join( ' ' ) );
37
+ }
38
+
39
+ setValue( transformedValue );
40
+ };
41
+
42
+ // TODO: To support Global variables this won't be needed when we have a flexible Gradient Box
43
+ const createTransformableValue = ( newValue: BackgroundGradientOverlayPropValue[ 'value' ] ) => ( {
44
+ ...newValue,
45
+ type: stringPropTypeUtil.create( newValue.type ),
46
+ angle: numberPropTypeUtil.create( newValue.angle ),
47
+ stops: gradientColorStopPropTypeUtil.create(
48
+ newValue.stops.map( ( { color, offset }: { color: string; offset: number } ) =>
49
+ colorStopPropTypeUtil.create( {
50
+ color: colorPropTypeUtil.create( color ),
51
+ offset: numberPropTypeUtil.create( offset ),
52
+ } )
53
+ )
54
+ ),
55
+ } );
56
+
57
+ // TODO: To support Global variables this won't be needed when we have a flexible Gradient Box
58
+ const normalizeValue = () => {
59
+ if ( ! value ) {
60
+ return;
61
+ }
62
+
63
+ const { type, angle, stops, positions } = value;
64
+
65
+ return {
66
+ type: type.value,
67
+ angle: angle.value,
68
+ stops: stops.value.map( ( { value: { color, offset } }: ColorStop ) => ( {
69
+ color: color.value,
70
+ offset: offset.value,
71
+ } ) ),
72
+ positions: positions?.value.split( ' ' ),
73
+ };
74
+ };
75
+
76
+ return (
77
+ <ControlActions>
78
+ <UnstableGradientBox
79
+ sx={ { width: 'auto', padding: 1.5 } }
80
+ value={ normalizeValue() }
81
+ onChange={ handleChange }
82
+ />
83
+ </ControlActions>
84
+ );
85
+ } );
86
+
87
+ export const initialBackgroundGradientOverlay: BackgroundOverlayItemPropValue =
88
+ backgroundGradientOverlayPropTypeUtil.create( {
89
+ type: stringPropTypeUtil.create( 'linear' ),
90
+ angle: numberPropTypeUtil.create( 180 ),
91
+ stops: gradientColorStopPropTypeUtil.create( [
92
+ colorStopPropTypeUtil.create( {
93
+ color: colorPropTypeUtil.create( 'rgb(0,0,0)' ),
94
+ offset: numberPropTypeUtil.create( 0 ),
95
+ } ),
96
+ colorStopPropTypeUtil.create( {
97
+ color: colorPropTypeUtil.create( 'rgb(255,255,255)' ),
98
+ offset: numberPropTypeUtil.create( 100 ),
99
+ } ),
100
+ ] ),
101
+ } );
@@ -4,6 +4,7 @@ import {
4
4
  backgroundImageOverlayPropTypeUtil,
5
5
  type BackgroundOverlayItemPropValue,
6
6
  backgroundOverlayPropTypeUtil,
7
+ colorPropTypeUtil,
7
8
  type PropKey,
8
9
  } from '@elementor/editor-props';
9
10
  import { Box, CardMedia, Grid, Tab, TabPanel, Tabs, UnstableColorIndicator } from '@elementor/ui';
@@ -17,6 +18,11 @@ import { createControl } from '../../../create-control';
17
18
  import { env } from '../../../env';
18
19
  import { ColorControl } from '../../color-control';
19
20
  import { ImageControl } from '../../image-control';
21
+ import {
22
+ BackgroundGradientColorControl,
23
+ type ColorStop,
24
+ initialBackgroundGradientOverlay,
25
+ } from '../background-gradient-color-control';
20
26
  import { BackgroundImageOverlayAttachment } from './background-image-overlay/background-image-overlay-attachment';
21
27
  import { BackgroundImageOverlayPosition } from './background-image-overlay/background-image-overlay-position';
22
28
  import { BackgroundImageOverlayRepeat } from './background-image-overlay/background-image-overlay-repeat';
@@ -24,10 +30,13 @@ import { BackgroundImageOverlaySize } from './background-image-overlay/backgroun
24
30
  import { type BackgroundImageOverlay } from './types';
25
31
  import { useBackgroundTabsHistory } from './use-background-tabs-history';
26
32
 
27
- export const initialBackgroundColorOverlay: BackgroundOverlayItemPropValue = {
28
- $$type: 'background-color-overlay',
29
- value: '#00000033',
30
- };
33
+ const DEFAULT_BACKGROUND_COLOR_OVERLAY_COLOR = '#00000033';
34
+
35
+ export const initialBackgroundColorOverlay: BackgroundOverlayItemPropValue = backgroundColorOverlayPropTypeUtil.create(
36
+ {
37
+ color: colorPropTypeUtil.create( DEFAULT_BACKGROUND_COLOR_OVERLAY_COLOR ),
38
+ }
39
+ );
31
40
 
32
41
  export const getInitialBackgroundOverlay = (): BackgroundOverlayItemPropValue => ( {
33
42
  $$type: 'background-image-overlay',
@@ -67,6 +76,7 @@ export const BackgroundOverlayRepeaterControl = createControl( () => {
67
76
  return (
68
77
  <PropProvider propType={ propType } value={ overlayValues } setValue={ setValue }>
69
78
  <Repeater
79
+ openOnAdd
70
80
  values={ overlayValues ?? [] }
71
81
  setValues={ setValue }
72
82
  label={ __( 'Overlay', 'elementor' ) }
@@ -93,6 +103,7 @@ const Content = () => {
93
103
  const { getTabsProps, getTabProps, getTabPanelProps } = useBackgroundTabsHistory( {
94
104
  image: getInitialBackgroundOverlay().value,
95
105
  color: initialBackgroundColorOverlay.value,
106
+ gradient: initialBackgroundGradientOverlay.value,
96
107
  } );
97
108
 
98
109
  return (
@@ -100,6 +111,7 @@ const Content = () => {
100
111
  <Box sx={ { borderBottom: 1, borderColor: 'divider' } }>
101
112
  <Tabs { ...getTabsProps() } aria-label={ __( 'Background Overlay', 'elementor' ) }>
102
113
  <Tab label={ __( 'Image', 'elementor' ) } { ...getTabProps( 'image' ) } />
114
+ <Tab label={ __( 'Gradient', 'elementor' ) } { ...getTabProps( 'gradient' ) } />
103
115
  <Tab label={ __( 'Color', 'elementor' ) } { ...getTabProps( 'color' ) } />
104
116
  </Tabs>
105
117
  </Box>
@@ -108,12 +120,13 @@ const Content = () => {
108
120
  <ImageOverlayContent />
109
121
  </PopoverContent>
110
122
  </TabPanel>
111
- <TabPanel { ...getTabPanelProps( 'color' ) } sx={ { p: 1.5 } }>
112
- <Grid container spacing={ 1 } alignItems="center">
113
- <Grid item xs={ 12 }>
114
- <ColorControl propTypeUtil={ backgroundColorOverlayPropTypeUtil } />
115
- </Grid>
116
- </Grid>
123
+ <TabPanel sx={ { p: 1.5 } } { ...getTabPanelProps( 'gradient' ) }>
124
+ <BackgroundGradientColorControl />
125
+ </TabPanel>
126
+ <TabPanel sx={ { p: 1.5 } } { ...getTabPanelProps( 'color' ) }>
127
+ <PopoverContent>
128
+ <ColorOverlayContent />
129
+ </PopoverContent>
117
130
  </TabPanel>
118
131
  </Box>
119
132
  );
@@ -125,13 +138,24 @@ const ItemIcon = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
125
138
  return <ItemIconImage value={ value as BackgroundImageOverlay } />;
126
139
  case 'background-color-overlay':
127
140
  return <ItemIconColor value={ value } />;
141
+ case 'background-gradient-overlay':
142
+ return <ItemIconGradient value={ value } />;
128
143
  default:
129
144
  return null;
130
145
  }
131
146
  };
132
147
 
133
- const ItemIconColor = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
134
- return <UnstableColorIndicator size="inherit" component="span" value={ value.value } />;
148
+ const extractColorFrom = ( prop: BackgroundOverlayItemPropValue ) => {
149
+ if ( prop?.value?.color?.value ) {
150
+ return prop.value.color.value;
151
+ }
152
+
153
+ return '';
154
+ };
155
+
156
+ const ItemIconColor = ( { value: prop }: { value: BackgroundOverlayItemPropValue } ) => {
157
+ const color = extractColorFrom( prop );
158
+ return <UnstableColorIndicator size="inherit" component="span" value={ color } />;
135
159
  };
136
160
 
137
161
  const ItemIconImage = ( { value }: { value: BackgroundImageOverlay } ) => {
@@ -140,19 +164,28 @@ const ItemIconImage = ( { value }: { value: BackgroundImageOverlay } ) => {
140
164
  return <CardMedia image={ imageUrl } sx={ { height: 13, width: 13, borderRadius: '4px' } } />;
141
165
  };
142
166
 
167
+ const ItemIconGradient = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
168
+ const gradient = getGradientValue( value );
169
+
170
+ return <UnstableColorIndicator size="inherit" component="span" value={ gradient } />;
171
+ };
172
+
143
173
  const ItemLabel = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
144
174
  switch ( value.$$type ) {
145
175
  case 'background-image-overlay':
146
176
  return <ItemLabelImage value={ value as BackgroundImageOverlay } />;
147
177
  case 'background-color-overlay':
148
178
  return <ItemLabelColor value={ value } />;
179
+ case 'background-gradient-overlay':
180
+ return <ItemLabelGradient value={ value } />;
149
181
  default:
150
182
  return null;
151
183
  }
152
184
  };
153
185
 
154
- const ItemLabelColor = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
155
- return <span>{ value.value }</span>;
186
+ const ItemLabelColor = ( { value: prop }: { value: BackgroundOverlayItemPropValue } ) => {
187
+ const color = extractColorFrom( prop );
188
+ return <span>{ color }</span>;
156
189
  };
157
190
 
158
191
  const ItemLabelImage = ( { value }: { value: BackgroundImageOverlay } ) => {
@@ -161,6 +194,25 @@ const ItemLabelImage = ( { value }: { value: BackgroundImageOverlay } ) => {
161
194
  return <span>{ imageTitle }</span>;
162
195
  };
163
196
 
197
+ const ItemLabelGradient = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
198
+ if ( value.value.type.value === 'linear' ) {
199
+ return <span>{ __( 'Linear Gradient', 'elementor' ) }</span>;
200
+ }
201
+
202
+ return <span>{ __( 'Radial Gradient', 'elementor' ) }</span>;
203
+ };
204
+
205
+ const ColorOverlayContent = () => {
206
+ const propContext = useBoundProp( backgroundColorOverlayPropTypeUtil );
207
+ return (
208
+ <PropProvider { ...propContext }>
209
+ <PropKeyProvider bind={ 'color' }>
210
+ <ColorControl />
211
+ </PropKeyProvider>
212
+ </PropProvider>
213
+ );
214
+ };
215
+
164
216
  const ImageOverlayContent = () => {
165
217
  const propContext = useBoundProp( backgroundImageOverlayPropTypeUtil );
166
218
 
@@ -210,3 +262,17 @@ const useImage = ( image: BackgroundImageOverlay ) => {
210
262
 
211
263
  return { imageTitle, imageUrl };
212
264
  };
265
+
266
+ const getGradientValue = ( value: BackgroundOverlayItemPropValue ) => {
267
+ const gradient = value.value;
268
+
269
+ const stops = gradient.stops.value
270
+ ?.map( ( { value: { color, offset } }: ColorStop ) => `${ color.value } ${ offset.value ?? 0 }%` )
271
+ ?.join( ',' );
272
+
273
+ if ( gradient.type.value === 'linear' ) {
274
+ return `linear-gradient(${ gradient.angle.value }deg, ${ stops })`;
275
+ }
276
+
277
+ return `radial-gradient(circle at ${ gradient.positions.value }, ${ stops })`;
278
+ };
@@ -1,6 +1,7 @@
1
1
  import { useRef } from 'react';
2
2
  import {
3
3
  backgroundColorOverlayPropTypeUtil,
4
+ backgroundGradientOverlayPropTypeUtil,
4
5
  backgroundImageOverlayPropTypeUtil,
5
6
  type BackgroundOverlayItemPropValue,
6
7
  } from '@elementor/editor-props';
@@ -9,25 +10,41 @@ import { useTabs } from '@elementor/ui';
9
10
  import { useBoundProp } from '../../../bound-prop-context';
10
11
  import { type BackgroundImageOverlay } from './types';
11
12
 
12
- type OverlayType = 'image' | 'color';
13
+ type OverlayType = 'image' | 'gradient' | 'color';
13
14
 
14
15
  type InitialBackgroundValues = {
15
16
  color: BackgroundOverlayItemPropValue[ 'value' ];
16
17
  image: BackgroundImageOverlay[ 'value' ];
18
+ gradient: BackgroundOverlayItemPropValue[ 'value' ];
17
19
  };
18
20
 
19
21
  export const useBackgroundTabsHistory = ( {
20
22
  color: initialBackgroundColorOverlay,
21
23
  image: initialBackgroundImageOverlay,
24
+ gradient: initialBackgroundGradientOverlay,
22
25
  }: InitialBackgroundValues ) => {
23
26
  const { value: imageValue, setValue: setImageValue } = useBoundProp( backgroundImageOverlayPropTypeUtil );
24
27
  const { value: colorValue, setValue: setColorValue } = useBoundProp( backgroundColorOverlayPropTypeUtil );
28
+ const { value: gradientValue, setValue: setGradientValue } = useBoundProp( backgroundGradientOverlayPropTypeUtil );
25
29
 
26
- const { getTabsProps, getTabProps, getTabPanelProps } = useTabs< OverlayType >( colorValue ? 'color' : 'image' );
30
+ const getCurrentOverlayType = (): OverlayType => {
31
+ if ( colorValue ) {
32
+ return 'color';
33
+ }
34
+
35
+ if ( gradientValue ) {
36
+ return 'gradient';
37
+ }
38
+
39
+ return 'image';
40
+ };
41
+
42
+ const { getTabsProps, getTabProps, getTabPanelProps } = useTabs< OverlayType >( getCurrentOverlayType() );
27
43
 
28
44
  const valuesHistory = useRef< InitialBackgroundValues >( {
29
45
  image: initialBackgroundImageOverlay,
30
46
  color: initialBackgroundColorOverlay,
47
+ gradient: initialBackgroundGradientOverlay,
31
48
  } );
32
49
 
33
50
  const saveToHistory = ( key: keyof InitialBackgroundValues, value: BackgroundOverlayItemPropValue[ 'value' ] ) => {
@@ -42,6 +59,15 @@ export const useBackgroundTabsHistory = ( {
42
59
  setImageValue( valuesHistory.current.image );
43
60
 
44
61
  saveToHistory( 'color', colorValue );
62
+ saveToHistory( 'gradient', gradientValue );
63
+
64
+ break;
65
+
66
+ case 'gradient':
67
+ setGradientValue( valuesHistory.current.gradient );
68
+
69
+ saveToHistory( 'color', colorValue );
70
+ saveToHistory( 'image', imageValue );
45
71
 
46
72
  break;
47
73
 
@@ -49,6 +75,7 @@ export const useBackgroundTabsHistory = ( {
49
75
  setColorValue( valuesHistory.current.color );
50
76
 
51
77
  saveToHistory( 'image', imageValue );
78
+ saveToHistory( 'gradient', gradientValue );
52
79
  }
53
80
 
54
81
  return getTabsProps().onChange( e, tabName );