@elementor/editor-variables 0.18.0 → 3.32.0-21

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 (56) hide show
  1. package/CHANGELOG.md +0 -28
  2. package/dist/index.d.mts +19 -1
  3. package/dist/index.d.ts +19 -1
  4. package/dist/index.js +1282 -1026
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1262 -990
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +16 -14
  9. package/src/api.ts +18 -2
  10. package/src/components/fields/color-field.tsx +3 -3
  11. package/src/components/fields/font-field.tsx +21 -10
  12. package/src/components/fields/label-field.tsx +31 -5
  13. package/src/components/ui/edit-confirmation-dialog.tsx +75 -0
  14. package/src/components/ui/missing-variable-alert.tsx +39 -0
  15. package/src/components/ui/no-variables.tsx +59 -26
  16. package/src/components/ui/tags/missing-tag.tsx +25 -0
  17. package/src/components/ui/variable/assigned-variable.tsx +11 -14
  18. package/src/components/ui/variable/deleted-variable.tsx +102 -50
  19. package/src/components/ui/variable/missing-variable.tsx +44 -0
  20. package/src/components/{color-variable-creation.tsx → variable-creation.tsx} +51 -22
  21. package/src/components/variable-edit.tsx +221 -0
  22. package/src/components/variable-restore.tsx +117 -0
  23. package/src/components/variable-selection-popover.tsx +91 -92
  24. package/src/components/variables-manager/variables-manager-panel.tsx +115 -0
  25. package/src/components/{font-variables-selection.tsx → variables-selection.tsx} +38 -17
  26. package/src/context/variable-selection-popover.context.tsx +19 -0
  27. package/src/context/variable-type-context.tsx +23 -0
  28. package/src/controls/variable-control.tsx +26 -0
  29. package/src/hooks/use-initial-value.ts +22 -0
  30. package/src/hooks/use-permissions.ts +15 -0
  31. package/src/hooks/use-prop-variable-action.tsx +53 -0
  32. package/src/hooks/use-prop-variables.ts +2 -2
  33. package/src/index.ts +1 -0
  34. package/src/init.ts +33 -4
  35. package/src/register-variable-types.tsx +29 -0
  36. package/src/repeater-injections.ts +5 -1
  37. package/src/service.ts +2 -19
  38. package/src/transformers/inheritance-transformer.tsx +30 -0
  39. package/src/transformers/utils/resolve-css-variable.ts +24 -0
  40. package/src/transformers/variable-transformer.ts +3 -16
  41. package/src/utils/tracking.ts +39 -0
  42. package/src/utils/validations.ts +40 -6
  43. package/src/variables-registry/create-variable-type-registry.ts +77 -0
  44. package/src/variables-registry/variable-type-registry.ts +3 -0
  45. package/src/components/color-variable-edit.tsx +0 -157
  46. package/src/components/color-variables-selection.tsx +0 -128
  47. package/src/components/font-variable-creation.tsx +0 -106
  48. package/src/components/font-variable-edit.tsx +0 -157
  49. package/src/components/variable-selection-popover.context.ts +0 -7
  50. package/src/controls/color-variable-control.tsx +0 -39
  51. package/src/controls/font-variable-control.tsx +0 -37
  52. package/src/hooks/use-prop-color-variable-action.tsx +0 -25
  53. package/src/hooks/use-prop-font-variable-action.tsx +0 -25
  54. package/src/init-color-variables.ts +0 -27
  55. package/src/init-font-variables.ts +0 -24
  56. package/src/utils.ts +0 -20
@@ -0,0 +1,117 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { PopoverContent, useBoundProp } from '@elementor/editor-controls';
4
+ import { PopoverBody } from '@elementor/editor-editing-panel';
5
+ import { PopoverHeader } from '@elementor/editor-ui';
6
+ import { Button, CardActions, Divider, FormHelperText } from '@elementor/ui';
7
+ import { __ } from '@wordpress/i18n';
8
+
9
+ import { PopoverContentRefContextProvider } from '../context/variable-selection-popover.context';
10
+ import { useVariableType } from '../context/variable-type-context';
11
+ import { restoreVariable, useVariable } from '../hooks/use-prop-variables';
12
+ import { ERROR_MESSAGES, mapServerError } from '../utils/validations';
13
+ import { LabelField, useLabelError } from './fields/label-field';
14
+
15
+ const SIZE = 'tiny';
16
+
17
+ type Props = {
18
+ variableId: string;
19
+ onClose: () => void;
20
+ onSubmit?: () => void;
21
+ };
22
+
23
+ export const VariableRestore = ( { variableId, onClose, onSubmit }: Props ) => {
24
+ const { icon: VariableIcon, valueField: ValueField, variableType, propTypeUtil } = useVariableType();
25
+
26
+ const { setValue: notifyBoundPropChange } = useBoundProp( propTypeUtil );
27
+
28
+ const variable = useVariable( variableId );
29
+
30
+ if ( ! variable ) {
31
+ throw new Error( `Global ${ variableType } variable not found` );
32
+ }
33
+
34
+ const [ errorMessage, setErrorMessage ] = useState( '' );
35
+ const [ label, setLabel ] = useState( variable.label );
36
+ const [ value, setValue ] = useState( variable.value );
37
+
38
+ const { labelFieldError, setLabelFieldError } = useLabelError( {
39
+ value: variable.label,
40
+ message: ERROR_MESSAGES.DUPLICATED_LABEL,
41
+ } );
42
+
43
+ const handleRestore = () => {
44
+ restoreVariable( variableId, label, value )
45
+ .then( () => {
46
+ notifyBoundPropChange( variableId );
47
+ onSubmit?.();
48
+ } )
49
+ .catch( ( error ) => {
50
+ const mappedError = mapServerError( error );
51
+ if ( mappedError && 'label' === mappedError.field ) {
52
+ setLabel( '' );
53
+ setLabelFieldError( {
54
+ value: label,
55
+ message: mappedError.message,
56
+ } );
57
+ return;
58
+ }
59
+
60
+ setErrorMessage( ERROR_MESSAGES.UNEXPECTED_ERROR );
61
+ } );
62
+ };
63
+
64
+ const hasEmptyValues = () => {
65
+ return ! value.trim() || ! label.trim();
66
+ };
67
+
68
+ const noValueChanged = () => {
69
+ return value === variable.value && label === variable.label;
70
+ };
71
+
72
+ const hasErrors = () => {
73
+ return !! errorMessage;
74
+ };
75
+
76
+ const isSubmitDisabled = noValueChanged() || hasEmptyValues() || hasErrors();
77
+
78
+ return (
79
+ <PopoverContentRefContextProvider>
80
+ <PopoverBody height="auto">
81
+ <PopoverHeader
82
+ icon={ <VariableIcon fontSize={ SIZE } /> }
83
+ title={ __( 'Restore variable', 'elementor' ) }
84
+ onClose={ onClose }
85
+ />
86
+
87
+ <Divider />
88
+
89
+ <PopoverContent p={ 2 }>
90
+ <LabelField
91
+ value={ label }
92
+ error={ labelFieldError }
93
+ onChange={ ( newValue ) => {
94
+ setLabel( newValue );
95
+ setErrorMessage( '' );
96
+ } }
97
+ />
98
+ <ValueField
99
+ value={ value }
100
+ onChange={ ( newValue ) => {
101
+ setValue( newValue );
102
+ setErrorMessage( '' );
103
+ } }
104
+ />
105
+
106
+ { errorMessage && <FormHelperText error>{ errorMessage }</FormHelperText> }
107
+ </PopoverContent>
108
+
109
+ <CardActions sx={ { pt: 0.5, pb: 1 } }>
110
+ <Button size="small" variant="contained" disabled={ isSubmitDisabled } onClick={ handleRestore }>
111
+ { __( 'Restore', 'elementor' ) }
112
+ </Button>
113
+ </CardActions>
114
+ </PopoverBody>
115
+ </PopoverContentRefContextProvider>
116
+ );
117
+ };
@@ -1,17 +1,16 @@
1
1
  import * as React from 'react';
2
- import { useRef, useState } from 'react';
3
- import { Box } from '@elementor/ui';
2
+ import { useState } from 'react';
3
+ import type { PropTypeKey } from '@elementor/editor-props';
4
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
4
5
 
5
- import { colorVariablePropTypeUtil } from '../prop-types/color-variable-prop-type';
6
- import { fontVariablePropTypeUtil } from '../prop-types/font-variable-prop-type';
6
+ import { PopoverContentRefContextProvider } from '../context/variable-selection-popover.context';
7
+ import { VariableTypeProvider } from '../context/variable-type-context';
8
+ import { usePermissions } from '../hooks/use-permissions';
7
9
  import { type Variable } from '../types';
8
- import { ColorVariableCreation } from './color-variable-creation';
9
- import { ColorVariableEdit } from './color-variable-edit';
10
- import { ColorVariablesSelection } from './color-variables-selection';
11
- import { FontVariableCreation } from './font-variable-creation';
12
- import { FontVariableEdit } from './font-variable-edit';
13
- import { FontVariablesSelection } from './font-variables-selection';
14
- import { PopoverContentRefContext } from './variable-selection-popover.context';
10
+ import { VariableCreation } from './variable-creation';
11
+ import { VariableEdit } from './variable-edit';
12
+ import { usePanelActions } from './variables-manager/variables-manager-panel';
13
+ import { VariablesSelection } from './variables-selection';
15
14
 
16
15
  const VIEW_LIST = 'list';
17
16
  const VIEW_ADD = 'add';
@@ -21,121 +20,121 @@ type View = typeof VIEW_LIST | typeof VIEW_ADD | typeof VIEW_EDIT;
21
20
 
22
21
  type Props = {
23
22
  closePopover: () => void;
24
- propTypeKey: string;
25
23
  selectedVariable?: Variable;
24
+ propTypeKey: PropTypeKey;
26
25
  };
27
26
 
28
27
  export const VariableSelectionPopover = ( { closePopover, propTypeKey, selectedVariable }: Props ) => {
29
28
  const [ currentView, setCurrentView ] = useState< View >( VIEW_LIST );
30
- const editIdRef = useRef< string >( '' );
31
- const anchorRef = useRef< HTMLDivElement >( null );
29
+ const [ editId, setEditId ] = useState< string >( '' );
30
+ const { open } = usePanelActions();
31
+ const onSettingsAvailable = isExperimentActive( 'e_variables_settings' )
32
+ ? () => {
33
+ open();
34
+ }
35
+ : undefined;
32
36
 
33
37
  return (
34
- <PopoverContentRefContext.Provider value={ anchorRef }>
35
- <Box ref={ anchorRef }>
36
- { renderStage( {
38
+ <VariableTypeProvider propTypeKey={ propTypeKey }>
39
+ <PopoverContentRefContextProvider>
40
+ { RenderView( {
37
41
  propTypeKey,
38
42
  currentView,
39
43
  selectedVariable,
40
- editIdRef,
44
+ editId,
45
+ setEditId,
41
46
  setCurrentView,
42
47
  closePopover,
48
+ onSettings: onSettingsAvailable,
43
49
  } ) }
44
- </Box>
45
- </PopoverContentRefContext.Provider>
50
+ </PopoverContentRefContextProvider>
51
+ </VariableTypeProvider>
46
52
  );
47
53
  };
48
54
 
49
- type StageProps = {
55
+ type ViewProps = {
50
56
  propTypeKey: string;
51
57
  currentView: View;
52
58
  selectedVariable?: Variable;
53
- editIdRef: React.MutableRefObject< string >;
59
+ editId: string;
60
+ setEditId: ( id: string ) => void;
54
61
  setCurrentView: ( stage: View ) => void;
55
62
  closePopover: () => void;
63
+ onSettings?: () => void;
56
64
  };
57
65
 
58
- function renderStage( props: StageProps ): React.ReactNode {
59
- const handleSubmitOnEdit = () => {
60
- if ( props?.selectedVariable?.key === props.editIdRef.current ) {
66
+ type Handlers = {
67
+ onClose: () => void;
68
+ onGoBack?: () => void;
69
+ onAdd?: () => void;
70
+ onEdit?: ( key: string ) => void;
71
+ onSettings?: () => void;
72
+ };
73
+
74
+ function RenderView( props: ViewProps ): React.ReactNode {
75
+ const userPermissions = usePermissions();
76
+
77
+ const handlers: Handlers = {
78
+ onClose: () => {
61
79
  props.closePopover();
62
- } else {
80
+ },
81
+ onGoBack: () => {
63
82
  props.setCurrentView( VIEW_LIST );
64
- }
83
+ },
65
84
  };
66
85
 
67
- if ( fontVariablePropTypeUtil.key === props.propTypeKey ) {
68
- if ( VIEW_LIST === props.currentView ) {
69
- return (
70
- <FontVariablesSelection
71
- closePopover={ props.closePopover }
72
- onAdd={ () => {
73
- props.setCurrentView( VIEW_ADD );
74
- } }
75
- onEdit={ ( key ) => {
76
- props.editIdRef.current = key;
77
- props.setCurrentView( VIEW_EDIT );
78
- } }
79
- />
80
- );
81
- }
86
+ if ( userPermissions.canAdd() ) {
87
+ handlers.onAdd = () => {
88
+ props.setCurrentView( VIEW_ADD );
89
+ };
90
+ }
82
91
 
83
- if ( VIEW_ADD === props.currentView ) {
84
- return (
85
- <FontVariableCreation
86
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
87
- onClose={ props.closePopover }
88
- />
89
- );
90
- }
92
+ if ( userPermissions.canEdit() ) {
93
+ handlers.onEdit = ( key: string ) => {
94
+ props.setEditId( key );
95
+ props.setCurrentView( VIEW_EDIT );
96
+ };
97
+ }
91
98
 
92
- if ( VIEW_EDIT === props.currentView ) {
93
- return (
94
- <FontVariableEdit
95
- editId={ props.editIdRef.current ?? '' }
96
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
97
- onClose={ props.closePopover }
98
- onSubmit={ handleSubmitOnEdit }
99
- />
100
- );
101
- }
99
+ if ( userPermissions.canManageSettings() && props.onSettings ) {
100
+ handlers.onSettings = () => {
101
+ props.onSettings?.();
102
+ props.closePopover();
103
+ };
102
104
  }
103
105
 
104
- if ( colorVariablePropTypeUtil.key === props.propTypeKey ) {
105
- if ( VIEW_LIST === props.currentView ) {
106
- return (
107
- <ColorVariablesSelection
108
- closePopover={ props.closePopover }
109
- onAdd={ () => {
110
- props.setCurrentView( VIEW_ADD );
111
- } }
112
- onEdit={ ( key ) => {
113
- props.editIdRef.current = key;
114
- props.setCurrentView( VIEW_EDIT );
115
- } }
116
- />
117
- );
106
+ const handleSubmitOnEdit = () => {
107
+ if ( props?.selectedVariable?.key === props.editId ) {
108
+ handlers.onClose();
109
+ } else {
110
+ handlers.onGoBack?.();
118
111
  }
112
+ };
119
113
 
120
- if ( VIEW_ADD === props.currentView ) {
121
- return (
122
- <ColorVariableCreation
123
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
124
- onClose={ props.closePopover }
125
- />
126
- );
127
- }
114
+ if ( VIEW_LIST === props.currentView ) {
115
+ return (
116
+ <VariablesSelection
117
+ closePopover={ handlers.onClose }
118
+ onAdd={ handlers.onAdd }
119
+ onEdit={ handlers.onEdit }
120
+ onSettings={ handlers.onSettings }
121
+ />
122
+ );
123
+ }
128
124
 
129
- if ( VIEW_EDIT === props.currentView ) {
130
- return (
131
- <ColorVariableEdit
132
- editId={ props.editIdRef.current ?? '' }
133
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
134
- onClose={ props.closePopover }
135
- onSubmit={ handleSubmitOnEdit }
136
- />
137
- );
138
- }
125
+ if ( VIEW_ADD === props.currentView ) {
126
+ return <VariableCreation onGoBack={ handlers.onGoBack } onClose={ handlers.onClose } />;
127
+ }
128
+
129
+ if ( VIEW_EDIT === props.currentView ) {
130
+ return (
131
+ <VariableEdit
132
+ editId={ props.editId }
133
+ onGoBack={ handlers.onGoBack }
134
+ onClose={ handlers.onClose }
135
+ onSubmit={ handleSubmitOnEdit }
136
+ />
137
+ );
139
138
  }
140
139
 
141
140
  return null;
@@ -0,0 +1,115 @@
1
+ import * as React from 'react';
2
+ import { useEffect } from 'react';
3
+ import {
4
+ __createPanel as createPanel,
5
+ Panel,
6
+ PanelBody,
7
+ PanelFooter,
8
+ PanelHeader,
9
+ PanelHeaderTitle,
10
+ } from '@elementor/editor-panels';
11
+ import { ThemeProvider } from '@elementor/editor-ui';
12
+ import { changeEditMode } from '@elementor/editor-v1-adapters';
13
+ import { FilterIcon, XIcon } from '@elementor/icons';
14
+ import { Alert, Box, Button, Divider, ErrorBoundary, IconButton, type IconButtonProps, Stack } from '@elementor/ui';
15
+ import { __ } from '@wordpress/i18n';
16
+
17
+ const id = 'variables-manager';
18
+
19
+ export const { panel, usePanelActions } = createPanel( {
20
+ id,
21
+ component: VariablesManagerPanel,
22
+ allowedEditModes: [ 'edit', id ],
23
+ onOpen: () => {
24
+ changeEditMode( id );
25
+ },
26
+ onClose: () => {
27
+ changeEditMode( 'edit' );
28
+ },
29
+ } );
30
+
31
+ export function VariablesManagerPanel() {
32
+ const { close: closePanel } = usePanelActions();
33
+ const isDirty = false;
34
+
35
+ usePreventUnload( isDirty );
36
+
37
+ return (
38
+ <ThemeProvider>
39
+ <ErrorBoundary fallback={ <ErrorBoundaryFallback /> }>
40
+ <Panel>
41
+ <PanelHeader>
42
+ <Stack p={ 1 } pl={ 2 } width="100%" direction="row" alignItems="center">
43
+ <Stack width="100%" direction="row" gap={ 1 }>
44
+ <PanelHeaderTitle sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
45
+ <FilterIcon fontSize="inherit" />
46
+ { __( 'Variables Manager', 'elementor' ) }
47
+ </PanelHeaderTitle>
48
+ </Stack>
49
+ <CloseButton
50
+ sx={ { marginLeft: 'auto' } }
51
+ onClose={ () => {
52
+ closePanel();
53
+ } }
54
+ />
55
+ </Stack>
56
+ </PanelHeader>
57
+ <PanelBody
58
+ sx={ {
59
+ display: 'flex',
60
+ flexDirection: 'column',
61
+ height: '100%',
62
+ } }
63
+ >
64
+ <Divider />
65
+ <Box
66
+ px={ 2 }
67
+ sx={ {
68
+ flexGrow: 1,
69
+ overflowY: 'auto',
70
+ } }
71
+ >
72
+ List
73
+ </Box>
74
+ </PanelBody>
75
+
76
+ <PanelFooter>
77
+ <Button fullWidth size="small" color="global" variant="contained" disabled={ ! isDirty }>
78
+ { __( 'Save changes', 'elementor' ) }
79
+ </Button>
80
+ </PanelFooter>
81
+ </Panel>
82
+ </ErrorBoundary>
83
+ </ThemeProvider>
84
+ );
85
+ }
86
+
87
+ const CloseButton = ( { onClose, ...props }: IconButtonProps & { onClose: () => void } ) => (
88
+ <IconButton size="small" color="secondary" onClick={ onClose } aria-label="Close" { ...props }>
89
+ <XIcon fontSize="small" />
90
+ </IconButton>
91
+ );
92
+
93
+ const ErrorBoundaryFallback = () => (
94
+ <Box role="alert" sx={ { minHeight: '100%', p: 2 } }>
95
+ <Alert severity="error" sx={ { mb: 2, maxWidth: 400, textAlign: 'center' } }>
96
+ <strong>{ __( 'Something went wrong', 'elementor' ) }</strong>
97
+ </Alert>
98
+ </Box>
99
+ );
100
+
101
+ const usePreventUnload = ( isDirty: boolean ) => {
102
+ useEffect( () => {
103
+ const handleBeforeUnload = ( event: BeforeUnloadEvent ) => {
104
+ if ( isDirty ) {
105
+ event.preventDefault();
106
+ }
107
+ };
108
+
109
+ window.addEventListener( 'beforeunload', handleBeforeUnload );
110
+
111
+ return () => {
112
+ window.removeEventListener( 'beforeunload', handleBeforeUnload );
113
+ };
114
+ }, [ isDirty ] );
115
+ };
@@ -3,13 +3,14 @@ import { useState } from 'react';
3
3
  import { useBoundProp } from '@elementor/editor-controls';
4
4
  import { PopoverBody } from '@elementor/editor-editing-panel';
5
5
  import { PopoverHeader, PopoverMenuList, PopoverSearch, type VirtualizedItem } from '@elementor/editor-ui';
6
- import { ColorFilterIcon, PlusIcon, SettingsIcon, TextIcon } from '@elementor/icons';
6
+ import { ColorFilterIcon, PlusIcon, SettingsIcon } from '@elementor/icons';
7
7
  import { Divider, IconButton } from '@elementor/ui';
8
- import { __ } from '@wordpress/i18n';
8
+ import { __, sprintf } from '@wordpress/i18n';
9
9
 
10
+ import { useVariableType } from '../context/variable-type-context';
10
11
  import { useFilteredVariables } from '../hooks/use-prop-variables';
11
- import { fontVariablePropTypeUtil } from '../prop-types/font-variable-prop-type';
12
12
  import { type ExtendedVirtualizedItem } from '../types';
13
+ import { trackVariableEvent } from '../utils/tracking';
13
14
  import { MenuItemContent } from './ui/menu-item-content';
14
15
  import { NoSearchResults } from './ui/no-search-results';
15
16
  import { NoVariables } from './ui/no-variables';
@@ -24,26 +25,42 @@ type Props = {
24
25
  onSettings?: () => void;
25
26
  };
26
27
 
27
- export const FontVariablesSelection = ( { closePopover, onAdd, onEdit, onSettings }: Props ) => {
28
- const { value: variable, setValue: setVariable } = useBoundProp( fontVariablePropTypeUtil );
28
+ export const VariablesSelection = ( { closePopover, onAdd, onEdit, onSettings }: Props ) => {
29
+ const { icon: VariableIcon, startIcon, variableType, propTypeUtil } = useVariableType();
30
+
31
+ const { value: variable, setValue: setVariable, path } = useBoundProp( propTypeUtil );
29
32
  const [ searchValue, setSearchValue ] = useState( '' );
30
33
 
31
34
  const {
32
35
  list: variables,
33
36
  hasMatches: hasSearchResults,
34
37
  isSourceNotEmpty: hasVariables,
35
- } = useFilteredVariables( searchValue, fontVariablePropTypeUtil.key );
38
+ } = useFilteredVariables( searchValue, propTypeUtil.key );
36
39
 
37
40
  const handleSetVariable = ( key: string ) => {
38
41
  setVariable( key );
42
+ trackVariableEvent( {
43
+ varType: variableType,
44
+ controlPath: path.join( '.' ),
45
+ action: 'connect',
46
+ } );
39
47
  closePopover();
40
48
  };
41
49
 
50
+ const onAddAndTrack = () => {
51
+ onAdd?.();
52
+ trackVariableEvent( {
53
+ varType: variableType,
54
+ controlPath: path.join( '.' ),
55
+ action: 'add',
56
+ } );
57
+ };
58
+
42
59
  const actions = [];
43
60
 
44
61
  if ( onAdd ) {
45
62
  actions.push(
46
- <IconButton key="add" size={ SIZE } onClick={ onAdd }>
63
+ <IconButton key="add" size={ SIZE } onClick={ onAddAndTrack }>
47
64
  <PlusIcon fontSize={ SIZE } />
48
65
  </IconButton>
49
66
  );
@@ -57,13 +74,15 @@ export const FontVariablesSelection = ( { closePopover, onAdd, onEdit, onSetting
57
74
  );
58
75
  }
59
76
 
77
+ const StartIcon = startIcon || ( () => <VariableIcon fontSize={ SIZE } /> );
78
+
60
79
  const items: ExtendedVirtualizedItem[] = variables.map( ( { value, label, key } ) => ( {
61
80
  type: 'item' as const,
62
81
  value: key,
63
82
  label,
64
- icon: <TextIcon fontSize={ SIZE } />,
83
+ icon: <StartIcon value={ value } />,
65
84
  secondaryText: value,
66
- onEdit: () => onEdit?.( key ),
85
+ onEdit: onEdit ? () => onEdit?.( key ) : undefined,
67
86
  } ) );
68
87
 
69
88
  const handleSearch = ( search: string ) => {
@@ -74,12 +93,18 @@ export const FontVariablesSelection = ( { closePopover, onAdd, onEdit, onSetting
74
93
  setSearchValue( '' );
75
94
  };
76
95
 
96
+ const noVariableTitle = sprintf(
97
+ /* translators: %s: Variable Type. */
98
+ __( 'Create your first %s variable', 'elementor' ),
99
+ variableType
100
+ );
101
+
77
102
  return (
78
103
  <PopoverBody>
79
104
  <PopoverHeader
80
105
  title={ __( 'Variables', 'elementor' ) }
81
- onClose={ closePopover }
82
106
  icon={ <ColorFilterIcon fontSize={ SIZE } /> }
107
+ onClose={ closePopover }
83
108
  actions={ actions }
84
109
  />
85
110
 
@@ -99,7 +124,7 @@ export const FontVariablesSelection = ( { closePopover, onAdd, onEdit, onSetting
99
124
  onSelect={ handleSetVariable }
100
125
  onClose={ () => {} }
101
126
  selectedValue={ variable }
102
- data-testid="font-variables-list"
127
+ data-testid={ `${ variableType }-variables-list` }
103
128
  menuListTemplate={ VariablesStyledMenuList }
104
129
  menuItemContentTemplate={ ( item: VirtualizedItem< 'item', string > ) => (
105
130
  <MenuItemContent item={ item } />
@@ -111,16 +136,12 @@ export const FontVariablesSelection = ( { closePopover, onAdd, onEdit, onSetting
111
136
  <NoSearchResults
112
137
  searchValue={ searchValue }
113
138
  onClear={ handleClearSearch }
114
- icon={ <TextIcon fontSize="large" /> }
139
+ icon={ <VariableIcon fontSize="large" /> }
115
140
  />
116
141
  ) }
117
142
 
118
143
  { ! hasVariables && (
119
- <NoVariables
120
- title={ __( 'Create your first font variable', 'elementor' ) }
121
- icon={ <TextIcon fontSize="large" /> }
122
- onAdd={ onAdd }
123
- />
144
+ <NoVariables title={ noVariableTitle } icon={ <VariableIcon fontSize="large" /> } onAdd={ onAdd } />
124
145
  ) }
125
146
  </PopoverBody>
126
147
  );
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { createContext, type PropsWithChildren, type RefObject, useContext, useState } from 'react';
3
+ import { Box } from '@elementor/ui';
4
+
5
+ const PopoverContentRefContext = createContext< RefObject< HTMLDivElement > | null >( null );
6
+
7
+ export const PopoverContentRefContextProvider = ( { children }: PropsWithChildren ) => {
8
+ const [ anchorRef, setAnchorRef ] = useState< RefObject< HTMLDivElement > | null >( null );
9
+
10
+ return (
11
+ <PopoverContentRefContext.Provider value={ anchorRef }>
12
+ <Box ref={ setAnchorRef }>{ children }</Box>
13
+ </PopoverContentRefContext.Provider>
14
+ );
15
+ };
16
+
17
+ export const usePopoverContentRef = () => {
18
+ return useContext( PopoverContentRefContext );
19
+ };
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import { createContext, type PropsWithChildren, useContext } from 'react';
3
+ import { type PropTypeKey } from '@elementor/editor-props';
4
+
5
+ import { getVariableType } from '../variables-registry/variable-type-registry';
6
+
7
+ type Props = PropsWithChildren< { propTypeKey: PropTypeKey } >;
8
+
9
+ const VariableTypeContext = createContext< PropTypeKey | null >( null );
10
+
11
+ export function VariableTypeProvider( { children, propTypeKey }: Props ) {
12
+ return <VariableTypeContext.Provider value={ propTypeKey }>{ children }</VariableTypeContext.Provider>;
13
+ }
14
+
15
+ export function useVariableType() {
16
+ const context = useContext( VariableTypeContext );
17
+
18
+ if ( context === null ) {
19
+ throw new Error( 'useVariableType must be used within a VariableTypeProvider' );
20
+ }
21
+
22
+ return getVariableType( context );
23
+ }
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ import { useBoundProp } from '@elementor/editor-controls';
3
+ import { type TransformablePropValue } from '@elementor/editor-props';
4
+
5
+ import { AssignedVariable } from '../components/ui/variable/assigned-variable';
6
+ import { DeletedVariable } from '../components/ui/variable/deleted-variable';
7
+ import { MissingVariable } from '../components/ui/variable/missing-variable';
8
+ import { useVariable } from '../hooks/use-prop-variables';
9
+
10
+ export const VariableControl = () => {
11
+ const boundProp = useBoundProp().value as TransformablePropValue< string, string >;
12
+
13
+ const assignedVariable = useVariable( boundProp?.value );
14
+
15
+ if ( ! assignedVariable ) {
16
+ return <MissingVariable />;
17
+ }
18
+
19
+ const { $$type: propTypeKey } = boundProp;
20
+
21
+ if ( assignedVariable?.deleted ) {
22
+ return <DeletedVariable variable={ assignedVariable } propTypeKey={ propTypeKey } />;
23
+ }
24
+
25
+ return <AssignedVariable variable={ assignedVariable } propTypeKey={ propTypeKey } />;
26
+ };