@elementor/editor-editing-panel 0.17.0 → 0.18.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/index.d.mts +26 -9
  3. package/dist/index.d.ts +26 -9
  4. package/dist/index.js +859 -367
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +830 -329
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +8 -7
  9. package/src/components/settings-tab.tsx +5 -2
  10. package/src/components/style-sections/effects-section/box-shadow-repeater.tsx +224 -0
  11. package/src/components/style-sections/effects-section/effects-section.tsx +18 -0
  12. package/src/components/style-sections/position-section/z-index-control.tsx +11 -7
  13. package/src/components/style-sections/size-section.tsx +23 -20
  14. package/src/components/style-sections/spacing-section/linked-dimensions-control.tsx +62 -47
  15. package/src/components/style-sections/typography-section/font-size-control.tsx +10 -6
  16. package/src/components/style-sections/typography-section/font-weight-control.tsx +16 -12
  17. package/src/components/style-sections/typography-section/letter-spacing-control.tsx +10 -6
  18. package/src/components/style-sections/typography-section/text-alignment-control.tsx +12 -8
  19. package/src/components/style-sections/typography-section/text-color-control.tsx +10 -6
  20. package/src/components/style-sections/typography-section/text-direction-control.tsx +37 -0
  21. package/src/components/style-sections/typography-section/text-style-control.tsx +37 -34
  22. package/src/components/style-sections/typography-section/transform-control.tsx +14 -12
  23. package/src/components/style-sections/typography-section/typography-section.tsx +2 -0
  24. package/src/components/style-sections/typography-section/word-spacing-control.tsx +10 -6
  25. package/src/components/style-tab.tsx +5 -1
  26. package/src/controls/components/control-type-container.tsx +28 -0
  27. package/src/controls/components/repeater.tsx +197 -0
  28. package/src/controls/control-actions/actions/popover-action.tsx +58 -0
  29. package/src/controls/control-actions/control-actions-menu.ts +8 -0
  30. package/src/controls/control-actions/control-actions.tsx +43 -0
  31. package/src/controls/control-replacement.ts +15 -7
  32. package/src/controls/control-types/color-control.tsx +21 -18
  33. package/src/controls/control-types/image-control.tsx +56 -59
  34. package/src/controls/control-types/image-media-control.tsx +73 -0
  35. package/src/controls/control-types/number-control.tsx +13 -9
  36. package/src/controls/control-types/select-control.tsx +13 -9
  37. package/src/controls/control-types/size-control.tsx +17 -13
  38. package/src/controls/control-types/text-area-control.tsx +15 -11
  39. package/src/controls/control-types/text-control.tsx +9 -3
  40. package/src/controls/control-types/toggle-control.tsx +3 -2
  41. package/src/controls/control.tsx +1 -7
  42. package/src/controls/controls-registry.tsx +19 -10
  43. package/src/controls/create-control.tsx +31 -0
  44. package/src/controls/settings-control.tsx +2 -9
  45. package/src/dynamics/components/dynamic-selection-control.tsx +1 -1
  46. package/src/dynamics/components/dynamic-selection.tsx +1 -1
  47. package/src/dynamics/dynamic-control.tsx +1 -1
  48. package/src/dynamics/hooks/use-prop-dynamic-action.tsx +23 -0
  49. package/src/dynamics/hooks/use-prop-dynamic-tags.ts +4 -4
  50. package/src/dynamics/init.ts +9 -0
  51. package/src/dynamics/types.ts +6 -3
  52. package/src/dynamics/utils.ts +16 -3
  53. package/src/index.ts +2 -0
  54. package/src/types.ts +35 -14
  55. package/src/controls/components/control-container.tsx +0 -18
@@ -1,16 +1,20 @@
1
1
  import * as React from 'react';
2
- import { StyleControl } from '../../../controls/style-control';
3
- import { ControlContainer } from '../../../controls/components/control-container';
4
2
  import { __ } from '@wordpress/i18n';
3
+ import { Grid } from '@elementor/ui';
4
+ import { StyleControl } from '../../../controls/style-control';
5
5
  import { ColorControl } from '../../../controls/control-types/color-control';
6
6
 
7
7
  export const TextColorControl = () => {
8
8
  return (
9
9
  <StyleControl bind="color">
10
- <ControlContainer>
11
- <StyleControl.Label>{ __( 'Text Color', 'elementor' ) }</StyleControl.Label>
12
- <ColorControl />
13
- </ControlContainer>
10
+ <Grid container spacing={ 1 } alignItems="center">
11
+ <Grid item xs={ 6 }>
12
+ <StyleControl.Label>{ __( 'Text Color', 'elementor' ) }</StyleControl.Label>
13
+ </Grid>
14
+ <Grid item xs={ 6 }>
15
+ <ColorControl />
16
+ </Grid>
17
+ </Grid>
14
18
  </StyleControl>
15
19
  );
16
20
  };
@@ -0,0 +1,37 @@
1
+ import * as React from 'react';
2
+ import { __ } from '@wordpress/i18n';
3
+ import { Grid } from '@elementor/ui';
4
+ import { TextDirectionLtrIcon, TextDirectionRtlIcon } from '@elementor/icons';
5
+ import { ToggleButtonGroupItem } from '../../../controls/components/control-toggle-button-group';
6
+ import { StyleControl } from '../../../controls/style-control';
7
+ import { ToggleControl } from '../../../controls/control-types/toggle-control';
8
+
9
+ type Direction = 'ltr' | 'rtl';
10
+
11
+ const options: ToggleButtonGroupItem< Direction >[] = [
12
+ {
13
+ value: 'ltr',
14
+ label: __( 'Left to Right', 'elementor' ),
15
+ icon: TextDirectionLtrIcon,
16
+ },
17
+ {
18
+ value: 'rtl',
19
+ label: __( 'Right to Left', 'elementor' ),
20
+ icon: TextDirectionRtlIcon,
21
+ },
22
+ ];
23
+
24
+ export const TextDirectionControl = () => {
25
+ return (
26
+ <StyleControl bind={ 'direction' }>
27
+ <Grid container spacing={ 1 } alignItems="center">
28
+ <Grid item xs={ 6 }>
29
+ <StyleControl.Label>{ __( 'Direction', 'elementor' ) }</StyleControl.Label>
30
+ </Grid>
31
+ <Grid item xs={ 6 }>
32
+ <ToggleControl options={ options } />
33
+ </Grid>
34
+ </Grid>
35
+ </StyleControl>
36
+ );
37
+ };
@@ -1,49 +1,52 @@
1
1
  import * as React from 'react';
2
- import { ToggleButton as ToggleButtonBase, ToggleButtonGroup, ToggleButtonProps } from '@elementor/ui';
2
+ import { __ } from '@wordpress/i18n';
3
+ import { Grid, ToggleButton as ToggleButtonBase, ToggleButtonGroup, ToggleButtonProps } from '@elementor/ui';
3
4
  import { ItalicIcon, StrikethroughIcon, UnderlineIcon } from '@elementor/icons';
4
5
  import { ControlLabel } from '../../control-label';
5
6
  import { useStyleControl } from '../../../controls/hooks/use-style-control';
6
- import { __ } from '@wordpress/i18n';
7
- import { ControlContainer } from '../../../controls/components/control-container';
8
7
 
9
8
  const buttonSize = 'tiny';
10
9
 
11
10
  export const TextStyleControl = () => {
12
- const [ fontStyle, setFontStyle ] = useStyleControl< string | null >( 'fontStyle' );
13
- const [ textDecoration, setTextDecoration ] = useStyleControl< string | null >( 'textDecoration' );
11
+ const [ fontStyle, setFontStyle ] = useStyleControl< string | null >( 'font-style' );
12
+ const [ textDecoration, setTextDecoration ] = useStyleControl< string | null >( 'text-decoration' );
14
13
 
15
14
  const formats = [ fontStyle, ...( textDecoration || '' ).split( ' ' ) ];
16
15
 
17
16
  return (
18
- <ControlContainer>
19
- <ControlLabel>{ __( 'Style', 'elementor' ) }</ControlLabel>
20
- <ToggleButtonGroup value={ formats }>
21
- <ToggleButton
22
- value="italic"
23
- onChange={ ( v ) => setFontStyle( fontStyle === v ? null : v ) }
24
- aria-label="italic"
25
- sx={ { marginLeft: 'auto' } }
26
- >
27
- <ItalicIcon fontSize={ buttonSize } />
28
- </ToggleButton>
29
- <ShorthandControl
30
- value="line-through"
31
- currentValues={ textDecoration || '' }
32
- updateValues={ setTextDecoration }
33
- aria-label="line-through"
34
- >
35
- <StrikethroughIcon fontSize={ buttonSize } />
36
- </ShorthandControl>
37
- <ShorthandControl
38
- value="underline"
39
- currentValues={ textDecoration || '' }
40
- updateValues={ setTextDecoration }
41
- aria-label="underline"
42
- >
43
- <UnderlineIcon fontSize={ buttonSize } />
44
- </ShorthandControl>
45
- </ToggleButtonGroup>
46
- </ControlContainer>
17
+ <Grid container spacing={ 1 } alignItems="center">
18
+ <Grid item xs={ 6 }>
19
+ <ControlLabel>{ __( 'Style', 'elementor' ) }</ControlLabel>
20
+ </Grid>
21
+ <Grid item xs={ 6 }>
22
+ <ToggleButtonGroup value={ formats }>
23
+ <ToggleButton
24
+ value="italic"
25
+ onChange={ ( v ) => setFontStyle( fontStyle === v ? null : v ) }
26
+ aria-label="italic"
27
+ sx={ { marginLeft: 'auto' } }
28
+ >
29
+ <ItalicIcon fontSize={ buttonSize } />
30
+ </ToggleButton>
31
+ <ShorthandControl
32
+ value="line-through"
33
+ currentValues={ textDecoration || '' }
34
+ updateValues={ setTextDecoration }
35
+ aria-label="line-through"
36
+ >
37
+ <StrikethroughIcon fontSize={ buttonSize } />
38
+ </ShorthandControl>
39
+ <ShorthandControl
40
+ value="underline"
41
+ currentValues={ textDecoration || '' }
42
+ updateValues={ setTextDecoration }
43
+ aria-label="underline"
44
+ >
45
+ <UnderlineIcon fontSize={ buttonSize } />
46
+ </ShorthandControl>
47
+ </ToggleButtonGroup>
48
+ </Grid>
49
+ </Grid>
47
50
  );
48
51
  };
49
52
 
@@ -1,9 +1,9 @@
1
1
  import * as React from 'react';
2
- import { ControlContainer } from '../../../controls/components/control-container';
3
- import { StyleControl } from '../../../controls/style-control';
4
2
  import { __ } from '@wordpress/i18n';
5
- import { ToggleControl } from '../../../controls/control-types/toggle-control';
3
+ import { Grid } from '@elementor/ui';
6
4
  import { LetterCaseIcon, LetterCaseLowerIcon, LetterCaseUpperIcon } from '@elementor/icons';
5
+ import { StyleControl } from '../../../controls/style-control';
6
+ import { ToggleControl } from '../../../controls/control-types/toggle-control';
7
7
 
8
8
  const options = [
9
9
  { value: 'capitalize', label: __( 'Capitalize', 'elementor' ), icon: LetterCaseIcon },
@@ -11,13 +11,15 @@ const options = [
11
11
  { value: 'lowercase', label: __( 'Lowercase', 'elementor' ), icon: LetterCaseLowerIcon },
12
12
  ];
13
13
 
14
- export const TransformControl = () => {
15
- return (
16
- <ControlContainer>
17
- <StyleControl.Label>{ __( 'Transform', 'elementor' ) }</StyleControl.Label>
18
- <StyleControl bind={ 'text-transform' }>
14
+ export const TransformControl = () => (
15
+ <StyleControl bind={ 'text-transform' }>
16
+ <Grid container spacing={ 1 } alignItems="center">
17
+ <Grid item xs={ 6 }>
18
+ <StyleControl.Label>{ __( 'Transform', 'elementor' ) }</StyleControl.Label>
19
+ </Grid>
20
+ <Grid item xs={ 6 }>
19
21
  <ToggleControl options={ options } />
20
- </StyleControl>
21
- </ControlContainer>
22
- );
23
- };
22
+ </Grid>
23
+ </Grid>
24
+ </StyleControl>
25
+ );
@@ -11,6 +11,7 @@ import { WordSpacingControl } from './word-spacing-control';
11
11
  import { CollapsibleContent } from '../../collapsible-content';
12
12
  import { TransformControl } from './transform-control';
13
13
  import { TextAlignmentControl } from './text-alignment-control';
14
+ import { TextDirectionControl } from './text-direction-control';
14
15
 
15
16
  export const TypographySection = () => {
16
17
  return (
@@ -28,6 +29,7 @@ export const TypographySection = () => {
28
29
  <TextAlignmentControl />
29
30
  <TextStyleControl />
30
31
  <TransformControl />
32
+ <TextDirectionControl />
31
33
  </Stack>
32
34
  </CollapsibleContent>
33
35
  </Stack>
@@ -1,16 +1,20 @@
1
1
  import * as React from 'react';
2
+ import { __ } from '@wordpress/i18n';
3
+ import { Grid } from '@elementor/ui';
2
4
  import { StyleControl } from '../../../controls/style-control';
3
5
  import { SizeControl } from '../../../controls/control-types/size-control';
4
- import { ControlContainer } from '../../../controls/components/control-container';
5
- import { __ } from '@wordpress/i18n';
6
6
 
7
7
  export const WordSpacingControl = () => {
8
8
  return (
9
9
  <StyleControl bind="word-spacing">
10
- <ControlContainer>
11
- <StyleControl.Label>{ __( 'Word Spacing', 'elementor' ) }</StyleControl.Label>
12
- <SizeControl />
13
- </ControlContainer>
10
+ <Grid container spacing={ 1 } alignItems="center">
11
+ <Grid item xs={ 6 }>
12
+ <StyleControl.Label>{ __( 'Word Spacing', 'elementor' ) }</StyleControl.Label>
13
+ </Grid>
14
+ <Grid item xs={ 6 }>
15
+ <SizeControl />
16
+ </Grid>
17
+ </Grid>
14
18
  </StyleControl>
15
19
  );
16
20
  };
@@ -8,6 +8,7 @@ import { TypographySection } from './style-sections/typography-section/typograph
8
8
  import { PositionSection } from './style-sections/position-section/position-section';
9
9
  import { StyleDefinition } from '@elementor/editor-style';
10
10
  import { SpacingSection } from './style-sections/spacing-section/spacing-section';
11
+ import { EffectsSection } from './style-sections/effects-section/effects-section';
11
12
 
12
13
  const CLASSES_PROP_KEY = 'classes';
13
14
 
@@ -22,6 +23,7 @@ export const StyleTab = () => {
22
23
  <PositionSection />
23
24
  <TypographySection />
24
25
  <SpacingSection />
26
+ <EffectsSection />
25
27
  </Stack>
26
28
  </StyleContext>
27
29
  );
@@ -30,7 +32,9 @@ export const StyleTab = () => {
30
32
  function useClassesProp(): string {
31
33
  const { elementType } = useElementContext();
32
34
 
33
- const prop = Object.entries( elementType.propsSchema ).find( ( [ , { type } ] ) => type.key === CLASSES_PROP_KEY );
35
+ const prop = Object.entries( elementType.propsSchema ).find(
36
+ ( [ , propType ] ) => propType.kind === 'array' && propType.key === CLASSES_PROP_KEY
37
+ );
34
38
 
35
39
  if ( ! prop ) {
36
40
  throw new Error( 'Element does not have a classes prop' );
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+ import { styled, Box, BoxProps } from '@elementor/ui';
3
+ import { ControlLayout, ControlType, getLayoutByType } from '../controls-registry';
4
+
5
+ export const ControlTypeContainer = ( {
6
+ controlType,
7
+ children,
8
+ }: React.PropsWithChildren< { controlType: ControlType } > ) => {
9
+ const layout = getLayoutByType( controlType );
10
+
11
+ return <StyledContainer layout={ layout }>{ children }</StyledContainer>;
12
+ };
13
+
14
+ const StyledContainer = styled( Box, {
15
+ shouldForwardProp: ( prop: string ) => ! [ 'layout' ].includes( prop ),
16
+ } )< BoxProps & { layout: ControlLayout } >( ( { layout, theme } ) => ( {
17
+ display: 'grid',
18
+ gridGap: theme.spacing( 1 ),
19
+ ...getGridLayout( layout ),
20
+ } ) );
21
+
22
+ const getGridLayout = ( layout: ControlLayout ) => ( {
23
+ justifyContent: 'space-between',
24
+ gridTemplateColumns: {
25
+ full: '1fr',
26
+ 'two-columns': 'repeat(2, 1fr)',
27
+ }[ layout ],
28
+ } );
@@ -0,0 +1,197 @@
1
+ import * as React from 'react';
2
+ import { useId, useRef, useState } from 'react';
3
+ import { __ } from '@wordpress/i18n';
4
+ import { PlusIcon, XIcon, CopyIcon, EyeIcon, EyeOffIcon } from '@elementor/icons';
5
+ import {
6
+ Box,
7
+ Stack,
8
+ Popover,
9
+ IconButton,
10
+ bindTrigger,
11
+ bindPopover,
12
+ usePopupState,
13
+ UnstableTagProps,
14
+ UnstableTag,
15
+ Typography,
16
+ } from '@elementor/ui';
17
+
18
+ const SIZE = 'tiny';
19
+
20
+ type AnchorEl = HTMLElement | null;
21
+
22
+ type Item< T > = {
23
+ disabled?: boolean;
24
+ } & T;
25
+
26
+ export type RepeaterProps< T > = {
27
+ label: string;
28
+ values?: T[];
29
+ setValues: ( newValue: T[] ) => void;
30
+ itemSettings: {
31
+ initialValues: T;
32
+ Label: React.ComponentType< { value: T } >;
33
+ Icon: React.ComponentType< { value: T } >;
34
+ Content: React.ComponentType< {
35
+ value: T;
36
+ setValue: ( newValue: T ) => void;
37
+ anchorEl: AnchorEl;
38
+ } >;
39
+ };
40
+ };
41
+
42
+ export const Repeater = < T, >( {
43
+ label,
44
+ itemSettings,
45
+ values: repeaterValues = [],
46
+ setValues: setRepeaterValues,
47
+ }: RepeaterProps< Item< T > > ) => {
48
+ const addRepeaterItem = () => {
49
+ const newItem = structuredClone( itemSettings.initialValues );
50
+
51
+ setRepeaterValues( [ ...repeaterValues, newItem ] );
52
+ };
53
+
54
+ const duplicateRepeaterItem = ( index: number ) => {
55
+ setRepeaterValues( [
56
+ ...repeaterValues.slice( 0, index ),
57
+ structuredClone( repeaterValues[ index ] ),
58
+ ...repeaterValues.slice( index ),
59
+ ] );
60
+ };
61
+
62
+ const removeRepeaterItem = ( index: number ) => {
63
+ setRepeaterValues( repeaterValues.filter( ( _, i ) => i !== index ) );
64
+ };
65
+
66
+ const toggleDisableRepeaterItem = ( index: number ) => {
67
+ setRepeaterValues(
68
+ repeaterValues.map( ( value, i ) => {
69
+ if ( i === index ) {
70
+ const { disabled, ...rest } = value;
71
+
72
+ // If the items should not be disabled, remove the disabled property.
73
+ return { ...rest, ...( disabled ? {} : { disabled: true } ) } as Item< T >;
74
+ }
75
+
76
+ return value;
77
+ } )
78
+ );
79
+ };
80
+
81
+ return (
82
+ <Stack>
83
+ <Stack direction="row" justifyContent="space-between" sx={ { py: 0.5 } }>
84
+ <Typography component="label" variant="caption" color="text.secondary">
85
+ { label }
86
+ </Typography>
87
+ <IconButton size={ SIZE } onClick={ addRepeaterItem } aria-label={ __( 'Add item', 'elementor' ) }>
88
+ <PlusIcon fontSize={ SIZE } />
89
+ </IconButton>
90
+ </Stack>
91
+ <Stack gap={ 1 }>
92
+ { repeaterValues.map( ( value, index ) => (
93
+ <RepeaterItem
94
+ key={ index }
95
+ disabled={ value.disabled }
96
+ label={ <itemSettings.Label value={ value } /> }
97
+ startIcon={ <itemSettings.Icon value={ value } /> }
98
+ removeItem={ () => removeRepeaterItem( index ) }
99
+ duplicateItem={ () => duplicateRepeaterItem( index ) }
100
+ toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
101
+ >
102
+ { ( props ) => (
103
+ <itemSettings.Content
104
+ { ...props }
105
+ value={ value }
106
+ setValue={ ( newValue ) =>
107
+ setRepeaterValues(
108
+ repeaterValues.map( ( item, i ) => ( i === index ? newValue : item ) )
109
+ )
110
+ }
111
+ />
112
+ ) }
113
+ </RepeaterItem>
114
+ ) ) }
115
+ </Stack>
116
+ </Stack>
117
+ );
118
+ };
119
+
120
+ type RepeaterItemProps = {
121
+ label: React.ReactNode;
122
+ disabled?: boolean;
123
+ startIcon: UnstableTagProps[ 'startIcon' ];
124
+ removeItem: () => void;
125
+ duplicateItem: () => void;
126
+ toggleDisableItem: () => void;
127
+ children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
128
+ };
129
+
130
+ const RepeaterItem = ( {
131
+ label,
132
+ disabled,
133
+ startIcon,
134
+ children,
135
+ removeItem,
136
+ duplicateItem,
137
+ toggleDisableItem,
138
+ }: RepeaterItemProps ) => {
139
+ const popupId = useId();
140
+ const tagRef = useRef< HTMLElement >( null );
141
+ const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
142
+
143
+ const popoverState = usePopupState( { popupId, variant: 'popover' } );
144
+
145
+ const popoverProps = bindPopover( popoverState );
146
+
147
+ return (
148
+ <>
149
+ <UnstableTag
150
+ ref={ tagRef }
151
+ label={ label }
152
+ showActionsOnHover
153
+ variant="outlined"
154
+ aria-label={ __( 'Open item', 'elementor' ) }
155
+ { ...bindTrigger( popoverState ) }
156
+ startIcon={ startIcon }
157
+ actions={
158
+ <>
159
+ <IconButton
160
+ size={ SIZE }
161
+ onClick={ duplicateItem }
162
+ aria-label={ __( 'Duplicate item', 'elementor' ) }
163
+ >
164
+ <CopyIcon fontSize={ SIZE } />
165
+ </IconButton>
166
+ <IconButton
167
+ size={ SIZE }
168
+ onClick={ toggleDisableItem }
169
+ aria-label={
170
+ disabled ? __( 'Enable item', 'elementor' ) : __( 'Disable item', 'elementor' )
171
+ }
172
+ >
173
+ { disabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
174
+ </IconButton>
175
+ <IconButton
176
+ size={ SIZE }
177
+ onClick={ removeItem }
178
+ aria-label={ __( 'Remove item', 'elementor' ) }
179
+ >
180
+ <XIcon fontSize={ SIZE } />
181
+ </IconButton>
182
+ </>
183
+ }
184
+ />
185
+ <Popover
186
+ disablePortal
187
+ slotProps={ {
188
+ paper: { ref: setAnchorEl, sx: { width: tagRef.current?.getBoundingClientRect().width } },
189
+ } }
190
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
191
+ { ...popoverProps }
192
+ >
193
+ <Box p={ 2 }>{ children( { anchorEl } ) }</Box>
194
+ </Popover>
195
+ </>
196
+ );
197
+ };
@@ -0,0 +1,58 @@
1
+ import * as React from 'react';
2
+ import { bindPopover, bindToggle, IconButton, Popover, Stack, Tooltip, Typography, usePopupState } from '@elementor/ui';
3
+ import { ComponentType, ElementType as ReactElementType, useId } from 'react';
4
+ import { XIcon } from '@elementor/icons';
5
+
6
+ const SIZE = 'tiny';
7
+
8
+ export type PopoverActionProps = {
9
+ title: string;
10
+ visible?: boolean;
11
+ icon: ReactElementType;
12
+ popoverContent: ComponentType< { closePopover: () => void } >;
13
+ };
14
+
15
+ export default function PopoverAction( {
16
+ title,
17
+ visible = true,
18
+ icon: Icon,
19
+ popoverContent: PopoverContent,
20
+ }: PopoverActionProps ) {
21
+ const id = useId();
22
+ const popupState = usePopupState( {
23
+ variant: 'popover',
24
+ popupId: `elementor-popover-action-${ id }`,
25
+ } );
26
+
27
+ if ( ! visible ) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <>
33
+ <Tooltip placement="top" title={ title }>
34
+ <IconButton aria-label={ title } key={ id } size={ SIZE } { ...bindToggle( popupState ) }>
35
+ <Icon fontSize={ SIZE } />
36
+ </IconButton>
37
+ </Tooltip>
38
+ <Popover
39
+ disablePortal
40
+ disableScrollLock
41
+ anchorOrigin={ {
42
+ vertical: 'bottom',
43
+ horizontal: 'center',
44
+ } }
45
+ { ...bindPopover( popupState ) }
46
+ >
47
+ <Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
48
+ <Icon fontSize={ SIZE } sx={ { mr: 0.5 } } />
49
+ <Typography variant="subtitle2">{ title }</Typography>
50
+ <IconButton sx={ { ml: 'auto' } } size={ SIZE } onClick={ popupState.close }>
51
+ <XIcon fontSize={ SIZE } />
52
+ </IconButton>
53
+ </Stack>
54
+ <PopoverContent closePopover={ popupState.close } />
55
+ </Popover>
56
+ </>
57
+ );
58
+ }
@@ -0,0 +1,8 @@
1
+ import PopoverAction from './actions/popover-action';
2
+ import { createMenu } from '@elementor/menus';
3
+
4
+ export const controlActionsMenu = createMenu( {
5
+ components: {
6
+ PopoverAction,
7
+ },
8
+ } );
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ import { styled, UnstableFloatingActionBar } from '@elementor/ui';
3
+ import { PropsWithChildren } from 'react';
4
+ import { controlActionsMenu } from './control-actions-menu';
5
+
6
+ const { useMenuItems } = controlActionsMenu;
7
+
8
+ // CSS hack to hide empty floating bars.
9
+ const FloatingBar = styled( UnstableFloatingActionBar )`
10
+ & .MuiPaper-root:empty {
11
+ display: none;
12
+ }
13
+
14
+ // this is for a fix which would be added later on - to force the width externally
15
+ width: 100%;
16
+ & > :first-of-type {
17
+ width: 100%;
18
+ }
19
+ `;
20
+
21
+ export type ControlActionsProps = PropsWithChildren< {
22
+ fullWidth?: boolean;
23
+ } >;
24
+
25
+ export default function ControlActions( { fullWidth = false, children }: ControlActionsProps ) {
26
+ const items = useMenuItems().default;
27
+
28
+ if ( items.length === 0 ) {
29
+ return children;
30
+ }
31
+
32
+ return (
33
+ <FloatingBar
34
+ actions={ items.map( ( { MenuItem, id } ) => (
35
+ <MenuItem key={ id } />
36
+ ) ) }
37
+ // TODO - work on a general layouting solution instead
38
+ sx={ fullWidth ? { width: '100%' } : undefined }
39
+ >
40
+ { children }
41
+ </FloatingBar>
42
+ );
43
+ }
@@ -1,26 +1,34 @@
1
1
  import { PropValue } from '../types';
2
+ import { ComponentType } from 'react';
3
+ import { useControl } from './control-context';
2
4
 
3
5
  type ReplaceWhenParams = {
4
6
  value: PropValue;
5
7
  };
6
8
 
7
9
  type ControlReplacement = {
8
- component: React.ComponentType;
10
+ component: ComponentType;
9
11
  condition: ( { value }: ReplaceWhenParams ) => boolean;
10
12
  };
11
13
 
12
14
  let controlReplacement: ControlReplacement | undefined;
13
15
 
14
- export const replaceControl = ( { component, condition }: ControlReplacement ) => {
15
- controlReplacement = { component, condition };
16
- };
17
-
18
- export const getControlReplacement = ( { value }: ReplaceWhenParams ) => {
16
+ const getControlReplacement = ( { value }: ReplaceWhenParams ) => {
19
17
  let shouldReplace = false;
20
18
 
21
19
  try {
22
- shouldReplace = !! controlReplacement?.condition( { value } );
20
+ shouldReplace = !! controlReplacement?.condition( { value } ) && !! controlReplacement?.component;
23
21
  } catch {}
24
22
 
25
23
  return shouldReplace ? controlReplacement?.component : undefined;
26
24
  };
25
+
26
+ export function replaceControl( { component, condition }: ControlReplacement ) {
27
+ controlReplacement = { component, condition };
28
+ }
29
+
30
+ export function useControlReplacement() {
31
+ const { value } = useControl();
32
+
33
+ return getControlReplacement( { value } );
34
+ }