@elementor/editor-controls 1.0.0 → 1.2.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 (33) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/index.d.mts +78 -41
  3. package/dist/index.d.ts +78 -41
  4. package/dist/index.js +875 -617
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +713 -467
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +11 -11
  9. package/src/components/font-family-selector.tsx +50 -174
  10. package/src/components/popover-content.tsx +3 -11
  11. package/src/components/repeater.tsx +27 -11
  12. package/src/components/text-field-popover.tsx +3 -3
  13. package/src/controls/aspect-ratio-control.tsx +20 -2
  14. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +2 -2
  15. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-size.tsx +2 -2
  16. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +9 -4
  17. package/src/controls/box-shadow-repeater-control.tsx +2 -2
  18. package/src/controls/equal-unequal-sizes-control.tsx +3 -9
  19. package/src/controls/filter-repeater-control.tsx +186 -0
  20. package/src/controls/font-family-control/font-family-control.tsx +6 -2
  21. package/src/controls/gap-control.tsx +3 -3
  22. package/src/controls/image-control.tsx +22 -35
  23. package/src/controls/key-value-control.tsx +119 -0
  24. package/src/controls/link-control.tsx +3 -1
  25. package/src/controls/linked-dimensions-control.tsx +3 -3
  26. package/src/controls/number-control.tsx +3 -3
  27. package/src/controls/position-control.tsx +109 -0
  28. package/src/controls/repeatable-control.tsx +119 -0
  29. package/src/controls/size-control.tsx +11 -9
  30. package/src/controls/stroke-control.tsx +2 -2
  31. package/src/controls/svg-media-control.tsx +0 -2
  32. package/src/hooks/use-repeatable-control-context.ts +24 -0
  33. package/src/index.ts +6 -1
@@ -0,0 +1,186 @@
1
+ import * as React from 'react';
2
+ import { useRef } from 'react';
3
+ import {
4
+ blurFilterPropTypeUtil,
5
+ brightnessFilterPropTypeUtil,
6
+ type FilterItemPropValue,
7
+ filterPropTypeUtil,
8
+ type PropKey,
9
+ type PropTypeUtil,
10
+ type SizePropValue,
11
+ } from '@elementor/editor-props';
12
+ import { MenuListItem } from '@elementor/editor-ui';
13
+ import { Box, Grid, Select, type SelectChangeEvent } from '@elementor/ui';
14
+ import { __ } from '@wordpress/i18n';
15
+
16
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
17
+ import { ControlLabel } from '../components/control-label';
18
+ import { PopoverContent } from '../components/popover-content';
19
+ import { PopoverGridContainer } from '../components/popover-grid-container';
20
+ import { Repeater } from '../components/repeater';
21
+ import { createControl } from '../create-control';
22
+ import { defaultUnits } from '../utils/size-control';
23
+ import { SizeControl } from './size-control';
24
+
25
+ type FilterType = FilterItemPropValue[ '$$type' ];
26
+ type FilterValue = FilterItemPropValue[ 'value' ];
27
+
28
+ const DEFAULT_FILTER_KEY: FilterType = 'blur';
29
+
30
+ type FilterItemConfig = {
31
+ defaultValue: FilterValue;
32
+ name: string;
33
+ valueName: string;
34
+ propType: PropTypeUtil< FilterValue, FilterValue >;
35
+ units?: Exclude< SizePropValue[ 'value' ][ 'unit' ], 'custom' | 'auto' >[];
36
+ };
37
+
38
+ const filterConfig: Record< FilterType, FilterItemConfig > = {
39
+ blur: {
40
+ defaultValue: { $$type: 'radius', radius: { $$type: 'size', value: { size: 0, unit: 'px' } } },
41
+ name: __( 'Blur', 'elementor' ),
42
+ valueName: __( 'Radius', 'elementor' ),
43
+ propType: blurFilterPropTypeUtil,
44
+ units: defaultUnits.filter( ( unit ) => unit !== '%' ),
45
+ },
46
+ brightness: {
47
+ defaultValue: { $$type: 'amount', amount: { $$type: 'size', value: { size: 100, unit: '%' } } },
48
+ name: __( 'Brightness', 'elementor' ),
49
+ valueName: __( 'Amount', 'elementor' ),
50
+ propType: brightnessFilterPropTypeUtil,
51
+ units: [ '%' ],
52
+ },
53
+ };
54
+
55
+ const filterKeys = Object.keys( filterConfig ) as FilterType[];
56
+
57
+ const singleSizeFilterNames = filterKeys.filter( ( name ) => {
58
+ const filter = filterConfig[ name as FilterType ].defaultValue;
59
+
60
+ return filter[ filter.$$type ].$$type === 'size';
61
+ } ) as FilterType[];
62
+
63
+ export const FilterRepeaterControl = createControl( () => {
64
+ const { propType, value: filterValues, setValue, disabled } = useBoundProp( filterPropTypeUtil );
65
+
66
+ return (
67
+ <PropProvider propType={ propType } value={ filterValues } setValue={ setValue }>
68
+ <Repeater
69
+ openOnAdd
70
+ disabled={ disabled }
71
+ values={ filterValues ?? [] }
72
+ setValues={ setValue }
73
+ label={ __( 'Filter', 'elementor' ) }
74
+ itemSettings={ {
75
+ Icon: ItemIcon,
76
+ Label: ItemLabel,
77
+ Content: ItemContent,
78
+ initialValues: {
79
+ $$type: DEFAULT_FILTER_KEY,
80
+ value: filterConfig[ DEFAULT_FILTER_KEY ].defaultValue,
81
+ } as FilterItemPropValue,
82
+ } }
83
+ />
84
+ </PropProvider>
85
+ );
86
+ } );
87
+
88
+ const ItemIcon = () => <></>;
89
+
90
+ const ItemLabel = ( props: { value: FilterItemPropValue } ) => {
91
+ const { $$type } = props.value;
92
+
93
+ return singleSizeFilterNames.includes( $$type ) && <SingleSizeItemLabel value={ props.value } />;
94
+ };
95
+
96
+ const SingleSizeItemLabel = ( { value }: { value: FilterItemPropValue } ) => {
97
+ const { $$type, value: sizeValue } = value;
98
+ const { $$type: key } = filterConfig[ $$type ].defaultValue;
99
+ const defaultUnit = filterConfig[ $$type ].defaultValue[ key ].value.unit;
100
+ const { unit, size } = sizeValue[ key ]?.value ?? { unit: defaultUnit, size: 0 };
101
+
102
+ const label = (
103
+ <Box component="span" style={ { textTransform: 'capitalize' } }>
104
+ { value.$$type }:
105
+ </Box>
106
+ );
107
+
108
+ return (
109
+ <Box component="span">
110
+ { label }
111
+ { unit !== 'custom' ? ` ${ size ?? 0 }${ unit ?? defaultUnit }` : size }
112
+ </Box>
113
+ );
114
+ };
115
+
116
+ const ItemContent = ( { bind }: { bind: PropKey } ) => {
117
+ const { value: filterValues, setValue } = useBoundProp( filterPropTypeUtil );
118
+ const itemIndex = parseInt( bind, 10 );
119
+ const item = filterValues?.[ itemIndex ];
120
+
121
+ const handleChange = ( e: SelectChangeEvent< string > ) => {
122
+ const newFilterValues = [ ...filterValues ];
123
+ const filterType = e.target.value as FilterType;
124
+
125
+ newFilterValues[ itemIndex ] = {
126
+ $$type: filterType,
127
+ value: filterConfig[ filterType ].defaultValue,
128
+ } as FilterItemPropValue;
129
+
130
+ setValue( newFilterValues );
131
+ };
132
+
133
+ return (
134
+ <PropKeyProvider bind={ bind }>
135
+ <PopoverContent p={ 1.5 }>
136
+ <PopoverGridContainer>
137
+ <Grid item xs={ 6 }>
138
+ <ControlLabel>{ __( 'Filter', 'elementor' ) }</ControlLabel>
139
+ </Grid>
140
+ <Grid item xs={ 6 }>
141
+ <Select
142
+ sx={ { overflow: 'hidden' } }
143
+ size="tiny"
144
+ value={ item?.$$type ?? DEFAULT_FILTER_KEY }
145
+ onChange={ handleChange }
146
+ fullWidth
147
+ >
148
+ { filterKeys.map( ( filterKey ) => (
149
+ <MenuListItem key={ filterKey } value={ filterKey }>
150
+ { filterConfig[ filterKey ].name }
151
+ </MenuListItem>
152
+ ) ) }
153
+ </Select>
154
+ </Grid>
155
+ </PopoverGridContainer>
156
+ <Content filterType={ item?.$$type } />
157
+ </PopoverContent>
158
+ </PropKeyProvider>
159
+ );
160
+ };
161
+
162
+ const Content = ( { filterType }: { filterType: FilterType } ) => {
163
+ return singleSizeFilterNames.includes( filterType ) && <SingleSizeItemContent filterType={ filterType } />;
164
+ };
165
+
166
+ const SingleSizeItemContent = ( { filterType }: { filterType: FilterType } ) => {
167
+ const { propType, valueName, defaultValue, units } = filterConfig[ filterType ];
168
+ const { $$type } = defaultValue;
169
+ const context = useBoundProp( propType );
170
+ const rowRef = useRef< HTMLDivElement >( null );
171
+
172
+ return (
173
+ <PropProvider { ...context }>
174
+ <PropKeyProvider bind={ $$type }>
175
+ <PopoverGridContainer ref={ rowRef }>
176
+ <Grid item xs={ 6 }>
177
+ <ControlLabel>{ valueName }</ControlLabel>
178
+ </Grid>
179
+ <Grid item xs={ 6 }>
180
+ <SizeControl anchorRef={ rowRef } units={ units } />
181
+ </Grid>
182
+ </PopoverGridContainer>
183
+ </PropKeyProvider>
184
+ </PropProvider>
185
+ );
186
+ };
@@ -15,11 +15,12 @@ export type FontCategory = {
15
15
 
16
16
  type FontFamilyControlProps = {
17
17
  fontFamilies: FontCategory[];
18
+ sectionWidth: number;
18
19
  };
19
20
 
20
21
  const SIZE = 'tiny';
21
22
 
22
- export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyControlProps ) => {
23
+ export const FontFamilyControl = createControl( ( { fontFamilies, sectionWidth }: FontFamilyControlProps ) => {
23
24
  const { value: fontFamily, setValue: setFontFamily, disabled } = useBoundProp( stringPropTypeUtil );
24
25
 
25
26
  const popoverState = usePopupState( { variant: 'popover' } );
@@ -39,7 +40,9 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
39
40
  <Popover
40
41
  disablePortal
41
42
  disableScrollLock
42
- anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
43
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'right' } }
44
+ transformOrigin={ { vertical: 'top', horizontal: 'right' } }
45
+ sx={ { my: 1.5 } }
43
46
  { ...bindPopover( popoverState ) }
44
47
  >
45
48
  <FontFamilySelector
@@ -47,6 +50,7 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
47
50
  fontFamily={ fontFamily }
48
51
  onFontFamilyChange={ setFontFamily }
49
52
  onClose={ popoverState.close }
53
+ sectionWidth={ sectionWidth }
50
54
  />
51
55
  </Popover>
52
56
  </>
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type MutableRefObject, useRef } from 'react';
2
+ import { type RefObject, useRef } from 'react';
3
3
  import { layoutDirectionPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
4
4
  import { DetachIcon, LinkIcon } from '@elementor/icons';
5
5
  import { Grid, Stack, ToggleButton, Tooltip } from '@elementor/ui';
@@ -19,7 +19,7 @@ export const GapControl = createControl( ( { label }: { label: string } ) => {
19
19
  disabled: directionDisabled,
20
20
  } = useBoundProp( layoutDirectionPropTypeUtil );
21
21
 
22
- const stackRef: MutableRefObject< HTMLElement | undefined > = useRef();
22
+ const stackRef = useRef< HTMLDivElement >( null );
23
23
 
24
24
  const { value: sizeValue, setValue: setSizeValue, disabled: sizeDisabled } = useBoundProp( sizePropTypeUtil );
25
25
 
@@ -96,7 +96,7 @@ const Control = ( {
96
96
  }: {
97
97
  bind: PropKey;
98
98
  isLinked: boolean;
99
- anchorRef: MutableRefObject< HTMLElement | undefined >;
99
+ anchorRef: RefObject< HTMLDivElement >;
100
100
  } ) => {
101
101
  if ( isLinked ) {
102
102
  return <SizeControl anchorRef={ anchorRef } />;
@@ -1,11 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import { imagePropTypeUtil } from '@elementor/editor-props';
3
- import { Grid, Stack } from '@elementor/ui';
3
+ import { Stack } from '@elementor/ui';
4
4
  import { type MediaType } from '@elementor/wp-media';
5
- import { __ } from '@wordpress/i18n';
6
5
 
7
6
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
8
- import { ControlFormLabel } from '../components/control-form-label';
9
7
  import { createControl } from '../create-control';
10
8
  import { useUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
11
9
  import { ImageMediaControl } from './image-media-control';
@@ -13,40 +11,29 @@ import { SelectControl } from './select-control';
13
11
 
14
12
  type ImageControlProps = {
15
13
  sizes: { label: string; value: string }[];
16
- resolutionLabel?: string;
17
14
  showMode?: 'all' | 'media' | 'sizes';
18
15
  };
19
16
 
20
- export const ImageControl = createControl(
21
- ( { sizes, resolutionLabel = __( 'Image resolution', 'elementor' ), showMode = 'all' }: ImageControlProps ) => {
22
- const propContext = useBoundProp( imagePropTypeUtil );
17
+ export const ImageControl = createControl( ( { sizes, showMode = 'all' }: ImageControlProps ) => {
18
+ const propContext = useBoundProp( imagePropTypeUtil );
23
19
 
24
- const { data: allowSvgUpload } = useUnfilteredFilesUpload();
25
- const mediaTypes: MediaType[] = allowSvgUpload ? [ 'image', 'svg' ] : [ 'image' ];
20
+ const { data: allowSvgUpload } = useUnfilteredFilesUpload();
21
+ const mediaTypes: MediaType[] = allowSvgUpload ? [ 'image', 'svg' ] : [ 'image' ];
26
22
 
27
- return (
28
- <PropProvider { ...propContext }>
29
- <Stack gap={ 1.5 }>
30
- { [ 'all', 'media' ].includes( showMode ) ? (
31
- <PropKeyProvider bind={ 'src' }>
32
- <ControlFormLabel>{ __( 'Image', 'elementor' ) }</ControlFormLabel>
33
- <ImageMediaControl mediaTypes={ mediaTypes } />
34
- </PropKeyProvider>
35
- ) : null }
36
- { [ 'all', 'sizes' ].includes( showMode ) ? (
37
- <PropKeyProvider bind={ 'size' }>
38
- <Grid container gap={ 1.5 } alignItems="center" flexWrap="nowrap">
39
- <Grid item xs={ 6 }>
40
- <ControlFormLabel>{ resolutionLabel }</ControlFormLabel>
41
- </Grid>
42
- <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
43
- <SelectControl options={ sizes } />
44
- </Grid>
45
- </Grid>
46
- </PropKeyProvider>
47
- ) : null }
48
- </Stack>
49
- </PropProvider>
50
- );
51
- }
52
- );
23
+ return (
24
+ <PropProvider { ...propContext }>
25
+ <Stack gap={ 1.5 }>
26
+ { [ 'all', 'media' ].includes( showMode ) ? (
27
+ <PropKeyProvider bind={ 'src' }>
28
+ <ImageMediaControl mediaTypes={ mediaTypes } />
29
+ </PropKeyProvider>
30
+ ) : null }
31
+ { [ 'all', 'sizes' ].includes( showMode ) ? (
32
+ <PropKeyProvider bind={ 'size' }>
33
+ <SelectControl options={ sizes } />
34
+ </PropKeyProvider>
35
+ ) : null }
36
+ </Stack>
37
+ </PropProvider>
38
+ );
39
+ } );
@@ -0,0 +1,119 @@
1
+ import * as React from 'react';
2
+ import { type ChangeEvent, useMemo, useState } from 'react';
3
+ import { keyValuePropTypeUtil } from '@elementor/editor-props';
4
+ import { FormHelperText, FormLabel, Grid, TextField } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { useBoundProp } from '../bound-prop-context';
8
+ import ControlActions from '../control-actions/control-actions';
9
+ import { createControl } from '../create-control';
10
+
11
+ type FieldType = 'key' | 'value';
12
+
13
+ type KeyValueControlProps = {
14
+ keyName?: string;
15
+ valueName?: string;
16
+ regexKey?: string;
17
+ regexValue?: string;
18
+ validationErrorMessage?: string;
19
+ };
20
+
21
+ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {} ) => {
22
+ const { value, setValue } = useBoundProp( keyValuePropTypeUtil );
23
+ const [ keyError, setKeyError ] = useState< string | null >( null );
24
+ const [ valueError, setValueError ] = useState< string | null >( null );
25
+
26
+ const [ sessionState, setSessionState ] = useState( {
27
+ key: value?.key?.value || '',
28
+ value: value?.value?.value || '',
29
+ } );
30
+
31
+ const keyLabel = props.keyName || __( 'Key', 'elementor' );
32
+ const valueLabel = props.valueName || __( 'Value', 'elementor' );
33
+
34
+ const [ keyRegex, valueRegex, errMsg ] = useMemo< [ RegExp | undefined, RegExp | undefined, string ] >(
35
+ () => [
36
+ props.regexKey ? new RegExp( props.regexKey ) : undefined,
37
+ props.regexValue ? new RegExp( props.regexValue ) : undefined,
38
+ props.validationErrorMessage || __( 'Invalid Format', 'elementor' ),
39
+ ],
40
+ [ props.regexKey, props.regexValue, props.validationErrorMessage ]
41
+ );
42
+
43
+ const validate = ( newValue: string, fieldType: string ): boolean => {
44
+ if ( fieldType === 'key' && keyRegex ) {
45
+ const isValid = keyRegex.test( newValue );
46
+ setKeyError( isValid ? null : errMsg );
47
+ return isValid;
48
+ } else if ( fieldType === 'value' && valueRegex ) {
49
+ const isValid = valueRegex.test( newValue );
50
+ setValueError( isValid ? null : errMsg );
51
+ return isValid;
52
+ }
53
+ return true;
54
+ };
55
+
56
+ const handleChange = ( event: ChangeEvent< HTMLInputElement >, fieldType: FieldType ) => {
57
+ const newValue = event.target.value;
58
+
59
+ setSessionState( ( prev ) => ( {
60
+ ...prev,
61
+ [ fieldType ]: newValue,
62
+ } ) );
63
+
64
+ if ( validate( newValue, fieldType ) ) {
65
+ setValue( {
66
+ ...value,
67
+ [ fieldType ]: {
68
+ value: newValue,
69
+ $$type: 'string',
70
+ },
71
+ } );
72
+ } else {
73
+ setValue( {
74
+ ...value,
75
+ [ fieldType ]: {
76
+ value: '',
77
+ $$type: 'string',
78
+ },
79
+ } );
80
+ }
81
+ };
82
+
83
+ const isKeyInvalid = keyError !== null;
84
+ const isValueInvalid = valueError !== null;
85
+
86
+ return (
87
+ <ControlActions>
88
+ <Grid container gap={ 1.5 }>
89
+ <Grid item xs={ 12 }>
90
+ <FormLabel size="tiny">{ keyLabel }</FormLabel>
91
+ <TextField
92
+ // eslint-disable-next-line jsx-a11y/no-autofocus
93
+ autoFocus
94
+ sx={ { pt: 1 } }
95
+ size="tiny"
96
+ fullWidth
97
+ value={ sessionState.key }
98
+ onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'key' ) }
99
+ error={ isKeyInvalid }
100
+ />
101
+ { isKeyInvalid && <FormHelperText error>{ keyError }</FormHelperText> }
102
+ </Grid>
103
+ <Grid item xs={ 12 }>
104
+ <FormLabel size="tiny">{ valueLabel }</FormLabel>
105
+ <TextField
106
+ sx={ { pt: 1 } }
107
+ size="tiny"
108
+ fullWidth
109
+ value={ sessionState.value }
110
+ onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'value' ) }
111
+ disabled={ isKeyInvalid }
112
+ error={ isValueInvalid }
113
+ />
114
+ { isValueInvalid && <FormHelperText error>{ valueError }</FormHelperText> }
115
+ </Grid>
116
+ </Grid>
117
+ </ControlActions>
118
+ );
119
+ } );
@@ -38,6 +38,7 @@ type Props = ControlProps< {
38
38
  allowCustomValues?: boolean;
39
39
  minInputLength?: number;
40
40
  placeholder?: string;
41
+ label?: string;
41
42
  } >;
42
43
 
43
44
  type LinkSessionValue = {
@@ -66,6 +67,7 @@ export const LinkControl = createControl( ( props: Props ) => {
66
67
  placeholder,
67
68
  minInputLength = 2,
68
69
  context: { elementId },
70
+ label = __( 'Link', 'elementor' ),
69
71
  } = props || {};
70
72
 
71
73
  const [ linkInLinkRestriction, setLinkInLinkRestriction ] = useState( getLinkInLinkRestriction( elementId ) );
@@ -163,7 +165,7 @@ export const LinkControl = createControl( ( props: Props ) => {
163
165
  marginInlineEnd: -0.75,
164
166
  } }
165
167
  >
166
- <ControlFormLabel>{ __( 'Link', 'elementor' ) }</ControlFormLabel>
168
+ <ControlFormLabel>{ label }</ControlFormLabel>
167
169
  <ConditionalInfoTip isVisible={ ! isActive } linkInLinkRestriction={ linkInLinkRestriction }>
168
170
  <ToggleIconControl
169
171
  disabled={ shouldDisableAddingLink }
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type MutableRefObject, useRef } from 'react';
2
+ import { type RefObject, useRef } from 'react';
3
3
  import { dimensionsPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
4
4
  import { isExperimentActive } from '@elementor/editor-v1-adapters';
5
5
  import { DetachIcon, LinkIcon, SideBottomIcon, SideLeftIcon, SideRightIcon, SideTopIcon } from '@elementor/icons';
@@ -24,7 +24,7 @@ export const LinkedDimensionsControl = createControl(
24
24
  extendedOptions?: ExtendedOption[];
25
25
  } ) => {
26
26
  const { value: sizeValue, setValue: setSizeValue, disabled: sizeDisabled } = useBoundProp( sizePropTypeUtil );
27
- const gridRowRefs: MutableRefObject< HTMLElement | undefined >[] = [ useRef(), useRef() ];
27
+ const gridRowRefs: RefObject< HTMLDivElement >[] = [ useRef( null ), useRef( null ) ];
28
28
 
29
29
  const {
30
30
  value: dimensionsValue,
@@ -127,7 +127,7 @@ const Control = ( {
127
127
  startIcon: React.ReactNode;
128
128
  isLinked: boolean;
129
129
  extendedOptions?: ExtendedOption[];
130
- anchorRef: MutableRefObject< HTMLElement | undefined >;
130
+ anchorRef: RefObject< HTMLDivElement >;
131
131
  } ) => {
132
132
  if ( isLinked ) {
133
133
  return <SizeControl startIcon={ startIcon } extendedOptions={ extendedOptions } anchorRef={ anchorRef } />;
@@ -13,7 +13,7 @@ const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
13
13
 
14
14
  export const NumberControl = createControl(
15
15
  ( {
16
- placeholder,
16
+ placeholder: labelPlaceholder,
17
17
  max = Number.MAX_VALUE,
18
18
  min = -Number.MAX_VALUE,
19
19
  step = 1,
@@ -25,7 +25,7 @@ export const NumberControl = createControl(
25
25
  step?: number;
26
26
  shouldForceInt?: boolean;
27
27
  } ) => {
28
- const { value, setValue, disabled } = useBoundProp( numberPropTypeUtil );
28
+ const { value, setValue, placeholder, disabled } = useBoundProp( numberPropTypeUtil );
29
29
 
30
30
  const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
31
31
  const eventValue: string = event.target.value;
@@ -50,7 +50,7 @@ export const NumberControl = createControl(
50
50
  disabled={ disabled }
51
51
  value={ isEmptyOrNaN( value ) ? '' : value }
52
52
  onChange={ handleChange }
53
- placeholder={ placeholder }
53
+ placeholder={ labelPlaceholder ?? ( placeholder ? String( placeholder ) : '' ) }
54
54
  inputProps={ { step } }
55
55
  onKeyDown={ ( event: KeyboardEvent ) => {
56
56
  if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
@@ -0,0 +1,109 @@
1
+ import * as React from 'react';
2
+ import { useMemo } from 'react';
3
+ import { positionPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
4
+ import { MenuListItem } from '@elementor/editor-ui';
5
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
6
+ import { LetterXIcon, LetterYIcon } from '@elementor/icons';
7
+ import { Grid, Select, type SelectChangeEvent } from '@elementor/ui';
8
+ import { __ } from '@wordpress/i18n';
9
+
10
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
11
+ import { ControlFormLabel } from '../components/control-form-label';
12
+ import { SizeControl } from './size-control';
13
+
14
+ type Positions =
15
+ | 'center center'
16
+ | 'center left'
17
+ | 'center right'
18
+ | 'top center'
19
+ | 'top left'
20
+ | 'top right'
21
+ | 'bottom center'
22
+ | 'bottom left'
23
+ | 'bottom right'
24
+ | 'custom';
25
+
26
+ const positionOptions = [
27
+ { label: __( 'Center center', 'elementor' ), value: 'center center' },
28
+ { label: __( 'Center left', 'elementor' ), value: 'center left' },
29
+ { label: __( 'Center right', 'elementor' ), value: 'center right' },
30
+ { label: __( 'Top center', 'elementor' ), value: 'top center' },
31
+ { label: __( 'Top left', 'elementor' ), value: 'top left' },
32
+ { label: __( 'Top right', 'elementor' ), value: 'top right' },
33
+ { label: __( 'Bottom center', 'elementor' ), value: 'bottom center' },
34
+ { label: __( 'Bottom left', 'elementor' ), value: 'bottom left' },
35
+ { label: __( 'Bottom right', 'elementor' ), value: 'bottom right' },
36
+ ];
37
+
38
+ export const PositionControl = () => {
39
+ const positionContext = useBoundProp( positionPropTypeUtil );
40
+ const stringPropContext = useBoundProp( stringPropTypeUtil );
41
+
42
+ const isVersion331Active = isExperimentActive( 'e_v_3_31' );
43
+ const isCustom = !! positionContext.value && isVersion331Active;
44
+
45
+ const availablePositionOptions = useMemo( () => {
46
+ const options = [ ...positionOptions ];
47
+
48
+ if ( isVersion331Active ) {
49
+ options.push( { label: __( 'Custom', 'elementor' ), value: 'custom' } );
50
+ }
51
+
52
+ return options;
53
+ }, [ isVersion331Active ] );
54
+
55
+ const handlePositionChange = ( event: SelectChangeEvent< Positions > ) => {
56
+ const value = event.target.value || null;
57
+
58
+ if ( value === 'custom' && isVersion331Active ) {
59
+ positionContext.setValue( { x: null, y: null } );
60
+ } else {
61
+ stringPropContext.setValue( value );
62
+ }
63
+ };
64
+
65
+ return (
66
+ <Grid container spacing={ 1.5 }>
67
+ <Grid item xs={ 12 }>
68
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
69
+ <Grid item xs={ 6 }>
70
+ <ControlFormLabel>{ __( 'Object position', 'elementor' ) }</ControlFormLabel>
71
+ </Grid>
72
+ <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
73
+ <Select
74
+ size="tiny"
75
+ disabled={ stringPropContext.disabled }
76
+ value={ ( positionContext.value ? 'custom' : stringPropContext.value ) ?? '' }
77
+ onChange={ handlePositionChange }
78
+ fullWidth
79
+ >
80
+ { availablePositionOptions.map( ( { label, value } ) => (
81
+ <MenuListItem key={ value } value={ value ?? '' }>
82
+ { label }
83
+ </MenuListItem>
84
+ ) ) }
85
+ </Select>
86
+ </Grid>
87
+ </Grid>
88
+ </Grid>
89
+ { isCustom && (
90
+ <PropProvider { ...positionContext }>
91
+ <Grid item xs={ 12 }>
92
+ <Grid container spacing={ 1.5 }>
93
+ <Grid item xs={ 6 }>
94
+ <PropKeyProvider bind={ 'x' }>
95
+ <SizeControl startIcon={ <LetterXIcon fontSize={ 'tiny' } /> } />
96
+ </PropKeyProvider>
97
+ </Grid>
98
+ <Grid item xs={ 6 }>
99
+ <PropKeyProvider bind={ 'y' }>
100
+ <SizeControl startIcon={ <LetterYIcon fontSize={ 'tiny' } /> } />
101
+ </PropKeyProvider>
102
+ </Grid>
103
+ </Grid>
104
+ </Grid>
105
+ </PropProvider>
106
+ ) }
107
+ </Grid>
108
+ );
109
+ };