@elementor/editor-editing-panel 1.38.1 → 1.40.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 (30) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/index.js +1038 -754
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +1014 -725
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +10 -9
  7. package/src/components/creatable-autocomplete/autocomplete-option-internal-properties.ts +3 -6
  8. package/src/components/creatable-autocomplete/creatable-autocomplete.tsx +25 -2
  9. package/src/components/creatable-autocomplete/types.ts +13 -4
  10. package/src/components/creatable-autocomplete/use-autocomplete-change.ts +59 -46
  11. package/src/components/creatable-autocomplete/use-create-option.ts +4 -4
  12. package/src/components/css-classes/css-class-context.tsx +30 -0
  13. package/src/components/css-classes/css-class-item.tsx +8 -19
  14. package/src/components/css-classes/css-class-menu.tsx +78 -78
  15. package/src/components/css-classes/css-class-selector.tsx +46 -32
  16. package/src/components/css-classes/use-apply-and-unapply-class.ts +178 -0
  17. package/src/components/editing-panel-tabs.tsx +7 -1
  18. package/src/components/settings-tab.tsx +14 -1
  19. package/src/components/style-indicator.tsx +1 -1
  20. package/src/components/style-sections/size-section/object-fit-field.tsx +1 -1
  21. package/src/components/style-sections/size-section/object-position-field.tsx +1 -1
  22. package/src/components/style-sections/size-section/size-section.tsx +13 -5
  23. package/src/components/style-sections/typography-section/typography-section.tsx +1 -1
  24. package/src/components/style-tab.tsx +82 -24
  25. package/src/controls-registry/controls-registry.tsx +2 -0
  26. package/src/hooks/use-active-style-def-id.ts +5 -2
  27. package/src/hooks/use-default-panel-settings.ts +33 -0
  28. package/src/hooks/use-state-by-element.ts +2 -1
  29. package/src/sync/experiments-flags.ts +4 -0
  30. package/src/hooks/use-unapply-class.ts +0 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-editing-panel",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,24 +39,25 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "0.19.3",
43
- "@elementor/editor-canvas": "0.22.0",
44
- "@elementor/editor-controls": "0.31.0",
45
- "@elementor/editor-current-user": "0.4.0",
42
+ "@elementor/editor": "0.19.4",
43
+ "@elementor/editor-canvas": "0.22.1",
44
+ "@elementor/editor-controls": "0.33.0",
45
+ "@elementor/editor-current-user": "0.5.0",
46
+ "@elementor/editor-documents": "0.13.6",
46
47
  "@elementor/editor-elements": "0.8.4",
47
- "@elementor/editor-panels": "0.15.3",
48
+ "@elementor/editor-panels": "0.15.4",
48
49
  "@elementor/editor-props": "0.12.1",
49
50
  "@elementor/editor-responsive": "0.13.5",
50
51
  "@elementor/editor-styles": "0.6.8",
51
- "@elementor/editor-styles-repository": "0.9.0",
52
- "@elementor/editor-ui": "0.8.2",
52
+ "@elementor/editor-styles-repository": "0.10.0",
53
+ "@elementor/editor-ui": "0.9.0",
53
54
  "@elementor/editor-v1-adapters": "0.12.0",
54
55
  "@elementor/icons": "1.40.1",
55
56
  "@elementor/locations": "0.8.0",
56
57
  "@elementor/menus": "0.1.5",
57
58
  "@elementor/schema": "0.1.2",
58
59
  "@elementor/session": "0.1.0",
59
- "@elementor/ui": "1.34.2",
60
+ "@elementor/ui": "1.34.5",
60
61
  "@elementor/utils": "0.4.0",
61
62
  "@wordpress/i18n": "^5.13.0"
62
63
  },
@@ -12,10 +12,7 @@ export function addGroupToOptions< TOption extends Option >(
12
12
  } );
13
13
  }
14
14
 
15
- export function removeOptionsInternalKeys< TOption extends Option >( options: InternalOption< TOption >[] ): TOption[] {
16
- return options.map( ( option ) => {
17
- const { _group, _action, ...rest } = option;
18
-
19
- return rest as unknown as TOption;
20
- } );
15
+ export function removeInternalKeys< TOption extends Option >( option: InternalOption< TOption > ): TOption {
16
+ const { _group, _action, ...rest } = option;
17
+ return rest as unknown as TOption;
21
18
  }
@@ -19,6 +19,8 @@ import { useInputState, useOpenState } from './use-autocomplete-states';
19
19
  import { useCreateOption } from './use-create-option';
20
20
  import { useFilterOptions } from './use-filter-options';
21
21
 
22
+ const MIN_INPUT_LENGTH = 2;
23
+
22
24
  export const CreatableAutocomplete = React.forwardRef( CreatableAutocompleteInner ) as <
23
25
  TOption extends SafeOptionConstraint,
24
26
  >(
@@ -36,6 +38,7 @@ function CreatableAutocompleteInner< TOption extends SafeOptionConstraint >(
36
38
  placeholder,
37
39
  onCreate,
38
40
  validate,
41
+ renderEmptyState,
39
42
  ...props
40
43
  }: CreatableAutocompleteProps< TOption >,
41
44
  ref: React.ForwardedRef< HTMLElement >
@@ -56,8 +59,13 @@ function CreatableAutocompleteInner< TOption extends SafeOptionConstraint >(
56
59
  setInputValue,
57
60
  closeDropdown,
58
61
  } );
62
+
59
63
  const filterOptions = useFilterOptions( { options, selected, onCreate, entityName } );
60
64
 
65
+ const isCreatable = Boolean( onCreate );
66
+
67
+ const freeSolo = isCreatable || inputValue.length < MIN_INPUT_LENGTH || undefined;
68
+
61
69
  return (
62
70
  <Autocomplete< InternalOption< TOption >, true, true, true >
63
71
  renderTags={ ( tagValue, getTagProps ) => {
@@ -72,7 +80,8 @@ function CreatableAutocompleteInner< TOption extends SafeOptionConstraint >(
72
80
  } }
73
81
  { ...( props as AutocompleteProps< InternalOption< TOption >, true, true, true > ) }
74
82
  ref={ ref }
75
- freeSolo
83
+ freeSolo={ freeSolo }
84
+ forcePopupIcon={ false }
76
85
  multiple
77
86
  clearOnBlur
78
87
  selectOnFocus
@@ -98,8 +107,8 @@ function CreatableAutocompleteInner< TOption extends SafeOptionConstraint >(
98
107
  return (
99
108
  <TextField
100
109
  { ...params }
101
- placeholder={ placeholder }
102
110
  error={ Boolean( error ) }
111
+ placeholder={ placeholder }
103
112
  { ...inputHandlers }
104
113
  sx={ ( theme: Theme ) => ( {
105
114
  '.MuiAutocomplete-inputRoot.MuiInputBase-adornedStart': {
@@ -134,6 +143,20 @@ function CreatableAutocompleteInner< TOption extends SafeOptionConstraint >(
134
143
  </li>
135
144
  );
136
145
  } }
146
+ noOptionsText={ renderEmptyState?.( {
147
+ searchValue: inputValue,
148
+ onClear: () => {
149
+ setInputValue( '' );
150
+ closeDropdown();
151
+ },
152
+ } ) }
153
+ isOptionEqualToValue={ ( option, value ) => {
154
+ if ( typeof option === 'string' ) {
155
+ return option === value;
156
+ }
157
+
158
+ return option.value === value.value;
159
+ } }
137
160
  />
138
161
  );
139
162
  }
@@ -1,4 +1,4 @@
1
- import { type AutocompleteProps } from '@elementor/ui';
1
+ import { type AutocompleteChangeReason, type AutocompleteProps } from '@elementor/ui';
2
2
 
3
3
  export type Option = {
4
4
  label: string;
@@ -21,6 +21,14 @@ export type SafeOptionConstraint = Option & {
21
21
  export type ValidationResult = { isValid: true; errorMessage: null } | { isValid: false; errorMessage: string };
22
22
  export type ValidationEvent = 'inputChange' | 'create';
23
23
 
24
+ export type OnSelect< TOption extends Option > = (
25
+ value: TOption[],
26
+ reason: AutocompleteChangeReason,
27
+ option: TOption
28
+ ) => void;
29
+ type OnCreate = ( value: string ) => unknown;
30
+ type Validate = ( value: string, event: ValidationEvent ) => ValidationResult;
31
+
24
32
  export type CreatableAutocompleteProps< TOption extends SafeOptionConstraint > = Omit<
25
33
  AutocompleteProps< TOption, true, true, true >,
26
34
  'renderInput' | 'onSelect' | 'options'
@@ -32,7 +40,8 @@ export type CreatableAutocompleteProps< TOption extends SafeOptionConstraint > =
32
40
  singular: string;
33
41
  plural: string;
34
42
  };
35
- onSelect?: ( value: TOption[] ) => void;
36
- onCreate?: ( value: string ) => unknown;
37
- validate?: ( value: string, event: ValidationEvent ) => ValidationResult;
43
+ renderEmptyState?: ( props: { searchValue: string; onClear: () => void } ) => React.ReactNode;
44
+ onSelect?: OnSelect< TOption >;
45
+ onCreate?: OnCreate;
46
+ validate?: Validate;
38
47
  };
@@ -1,75 +1,88 @@
1
- import { removeOptionsInternalKeys } from './autocomplete-option-internal-properties';
2
- import { type InternalOption, type Option } from './types';
1
+ import { type AutocompleteChangeDetails, type AutocompleteChangeReason } from '@elementor/ui';
2
+
3
+ import { removeInternalKeys } from './autocomplete-option-internal-properties';
4
+ import { type InternalOption, type OnSelect, type Option } from './types';
3
5
 
4
6
  export function useAutocompleteChange< TOption extends Option >( params: {
5
7
  options: InternalOption< TOption >[];
6
- onSelect?: ( value: TOption[] ) => void;
7
- createOption: ( value: string ) => Promise< unknown >;
8
+ onSelect?: OnSelect< TOption >;
9
+ createOption: ( ( value: string ) => Promise< unknown > ) | null;
8
10
  setInputValue: ( value: string ) => void;
9
11
  closeDropdown: () => void;
10
12
  } ) {
11
13
  const { options, onSelect, createOption, setInputValue, closeDropdown } = params;
12
14
 
15
+ if ( ! onSelect && ! createOption ) {
16
+ return;
17
+ }
18
+
13
19
  const handleChange = async (
14
20
  _: React.SyntheticEvent,
15
21
  selectedOrInputValue: Array< InternalOption< TOption > | string >,
16
- reason: string
22
+ reason: AutocompleteChangeReason,
23
+ details?: AutocompleteChangeDetails< InternalOption< TOption > | string >
17
24
  ) => {
18
- // Separate options and new input value
25
+ const changedOption = details?.option;
26
+ if ( ! changedOption || ( typeof changedOption === 'object' && changedOption.fixed ) ) {
27
+ // If `changedOption` is nullish it means no option was selected, created or removed.
28
+ // The reason is either "blur" which we don't support (can't be "clear" since we disabled it).
29
+ // If the option is fixed, it can't be selected, created or removed.
30
+ return;
31
+ }
32
+
19
33
  const selectedOptions = selectedOrInputValue.filter( ( option ) => typeof option !== 'string' );
20
34
 
21
- const newInputValue = selectedOrInputValue.reduce( ( acc: string | null, option ): string | null => {
22
- if ( typeof option === 'string' ) {
23
- return option;
24
- } else if ( option._action === 'create' ) {
25
- return option.value;
26
- }
27
- return acc;
28
- }, null );
35
+ switch ( reason ) {
36
+ case 'removeOption':
37
+ const removedOption = changedOption as InternalOption< TOption >;
38
+ updateSelectedOptions( selectedOptions, 'removeOption', removedOption );
39
+ break;
29
40
 
30
- const inputValueMatchesExistingOption =
31
- newInputValue && options.find( ( option ) => option.label === newInputValue );
41
+ // User clicked an option. It's either an existing option, or "Create <new option>".
42
+ case 'selectOption': {
43
+ const selectedOption = changedOption as InternalOption< TOption >;
32
44
 
33
- // Handle creation of new option
34
- if (
35
- newInputValue &&
36
- shouldCreateNewOption( reason, selectedOptions, newInputValue, Boolean( inputValueMatchesExistingOption ) )
37
- ) {
38
- return createOption( newInputValue );
39
- }
45
+ if ( selectedOption._action === 'create' ) {
46
+ const newOption = selectedOption.value as string;
47
+ return createOption?.( newOption );
48
+ }
49
+
50
+ updateSelectedOptions( selectedOptions, 'selectOption', selectedOption );
51
+ break;
52
+ }
53
+
54
+ // User pressed "Enter" after typing input. The input is either matching existing option or a new option to create.
55
+ case 'createOption': {
56
+ const inputValue = changedOption as string;
40
57
 
41
- // Handle selection of existing option
42
- if ( reason === 'createOption' && inputValueMatchesExistingOption ) {
43
- selectedOptions.push( inputValueMatchesExistingOption );
58
+ const matchingOption = options.find(
59
+ ( option ) => option.label.toLocaleLowerCase() === inputValue.toLocaleLowerCase()
60
+ );
61
+ if ( matchingOption ) {
62
+ selectedOptions.push( matchingOption );
63
+ updateSelectedOptions( selectedOptions, 'selectOption', matchingOption );
64
+ } else {
65
+ return createOption?.( inputValue );
66
+ }
67
+ break;
68
+ }
44
69
  }
45
70
 
46
- updateSelectedOptions( selectedOptions );
47
71
  setInputValue( '' );
48
72
  closeDropdown();
49
73
  };
50
74
 
51
75
  return handleChange;
52
76
 
53
- function shouldCreateNewOption(
54
- reason: string,
77
+ function updateSelectedOptions(
55
78
  selectedOptions: InternalOption< TOption >[],
56
- newInputValue: string | undefined,
57
- inputValueMatchesExistingOption: boolean
79
+ reason: AutocompleteChangeReason,
80
+ changedOption: InternalOption< TOption >
58
81
  ) {
59
- const createOptionWasClicked =
60
- reason === 'selectOption' && selectedOptions.some( ( option ) => option._action === 'create' );
61
-
62
- const enterWasPressed =
63
- reason === 'createOption' && ! options.some( ( option ) => option.label === newInputValue );
64
- const createOptionWasDisplayed = ! inputValueMatchesExistingOption;
65
-
66
- return createOptionWasClicked || ( enterWasPressed && createOptionWasDisplayed );
67
- }
68
-
69
- function updateSelectedOptions( selectedOptions: InternalOption< TOption >[] ) {
70
- const fixedOptions = options.filter( ( option ) => !! option.fixed );
71
- const updatedOptions = [ ...fixedOptions, ...selectedOptions.filter( ( option ) => ! option.fixed ) ];
72
-
73
- onSelect?.( removeOptionsInternalKeys( updatedOptions ) );
82
+ onSelect?.(
83
+ selectedOptions.map( ( option ) => removeInternalKeys( option ) ),
84
+ reason,
85
+ removeInternalKeys( changedOption )
86
+ );
74
87
  }
75
88
  }
@@ -13,11 +13,11 @@ export function useCreateOption( params: {
13
13
 
14
14
  const [ loading, setLoading ] = useState( false );
15
15
 
16
- const createOption = async ( value: string ) => {
17
- if ( ! onCreate ) {
18
- return;
19
- }
16
+ if ( ! onCreate ) {
17
+ return { createOption: null, loading: false };
18
+ }
20
19
 
20
+ const createOption = async ( value: string ) => {
21
21
  setLoading( true );
22
22
 
23
23
  if ( validate ) {
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ type CssClassContextType = {
5
+ id: string | null;
6
+ provider: string | null;
7
+ label: string;
8
+ isActive: boolean;
9
+ onClickActive: ( id: string | null ) => void;
10
+ handleRename: () => void;
11
+ setError?: ( error: string | null ) => void;
12
+ };
13
+
14
+ const CssClassContext = createContext< CssClassContextType | null >( null );
15
+
16
+ export const useCssClass = () => {
17
+ const context = useContext( CssClassContext );
18
+ if ( ! context ) {
19
+ throw new Error( 'useCssClass must be used within a CssClassProvider' );
20
+ }
21
+ return context;
22
+ };
23
+
24
+ type CssClassProviderProps = CssClassContextType & {
25
+ children: React.ReactNode;
26
+ };
27
+
28
+ export function CssClassProvider( { children, ...contextValue }: CssClassProviderProps ) {
29
+ return <CssClassContext.Provider value={ contextValue }>{ children }</CssClassContext.Provider>;
30
+ }
@@ -17,6 +17,7 @@ import {
17
17
  import { __ } from '@wordpress/i18n';
18
18
 
19
19
  import { useStyle } from '../../contexts/style-context';
20
+ import { CssClassProvider } from './css-class-context';
20
21
  import { CssClassMenu } from './css-class-menu';
21
22
 
22
23
  type CssClassItemProps = {
@@ -35,18 +36,10 @@ type CssClassItemProps = {
35
36
 
36
37
  const CHIP_SIZE = 'tiny';
37
38
 
38
- export function CssClassItem( {
39
- id,
40
- provider,
41
- label,
42
- isActive,
43
- color: colorProp,
44
- icon,
45
- chipProps,
46
- onClickActive,
47
- renameLabel,
48
- setError,
49
- }: CssClassItemProps ) {
39
+ export function CssClassItem( props: CssClassItemProps ) {
40
+ const { chipProps, icon, color: colorProp, ...classProps } = props;
41
+ const { id, provider, label, isActive, onClickActive, renameLabel, setError } = classProps;
42
+
50
43
  const { meta, setMetaState } = useStyle();
51
44
  const popupState = usePopupState( { variant: 'popover' } );
52
45
  const [ chipRef, setChipRef ] = useState< HTMLElement | null >( null );
@@ -145,13 +138,9 @@ export function CssClassItem( {
145
138
  />
146
139
  ) }
147
140
  </UnstableChipGroup>
148
- <CssClassMenu
149
- styleId={ id }
150
- popupState={ popupState }
151
- provider={ provider }
152
- handleRename={ openEditMode }
153
- anchorEl={ chipRef }
154
- />
141
+ <CssClassProvider { ...classProps } handleRename={ openEditMode }>
142
+ <CssClassMenu popupState={ popupState } anchorEl={ chipRef } />
143
+ </CssClassProvider>
155
144
  </>
156
145
  );
157
146
  }
@@ -1,29 +1,39 @@
1
1
  import * as React from 'react';
2
2
  import { type StyleDefinitionState } from '@elementor/editor-styles';
3
- import { isElementsStylesProvider, stylesRepository } from '@elementor/editor-styles-repository';
4
- import { MenuListItem } from '@elementor/editor-ui';
3
+ import {
4
+ isElementsStylesProvider,
5
+ stylesRepository,
6
+ useUserStylesCapability,
7
+ } from '@elementor/editor-styles-repository';
8
+ import { MenuItemInfotip, MenuListItem } from '@elementor/editor-ui';
5
9
  import { bindMenu, Divider, Menu, MenuSubheader, type PopupState, Stack } from '@elementor/ui';
6
10
  import { __ } from '@wordpress/i18n';
7
11
 
8
12
  import { useStyle } from '../../contexts/style-context';
9
- import { useUnapplyClass } from '../../hooks/use-unapply-class';
10
13
  import { type StyleDefinitionStateWithNormal } from '../../styles-inheritance/types';
11
- import { StyleIndicator, type StyleIndicatorVariant } from '../style-indicator';
14
+ import { StyleIndicator } from '../style-indicator';
15
+ import { useCssClass } from './css-class-context';
16
+ import { useUnapplyClass } from './use-apply-and-unapply-class';
12
17
 
13
- const STATES: NonNullable< StyleDefinitionState >[] = [ 'hover', 'focus', 'active' ];
18
+ type State = {
19
+ key: StyleDefinitionStateWithNormal;
20
+ value: StyleDefinitionState | null;
21
+ };
22
+
23
+ const STATES: State[] = [
24
+ { key: 'normal', value: null },
25
+ { key: 'hover', value: 'hover' },
26
+ { key: 'focus', value: 'focus' },
27
+ { key: 'active', value: 'active' },
28
+ ];
14
29
 
15
30
  type CssClassMenuProps = {
16
- styleId: string | null;
17
- provider: string | null;
18
31
  popupState: PopupState;
19
- handleRename: () => void;
20
32
  anchorEl: HTMLElement | null;
21
33
  };
22
34
 
23
- export function CssClassMenu( { styleId, provider, popupState, handleRename, anchorEl }: CssClassMenuProps ) {
24
- const styledStates = useStyledStates( styleId );
25
-
26
- const indicatorVariant = ! provider || isElementsStylesProvider( provider ) ? 'local' : 'global';
35
+ export function CssClassMenu( { popupState, anchorEl }: CssClassMenuProps ) {
36
+ const { provider } = useCssClass();
27
37
 
28
38
  const handleKeyDown = ( e: React.KeyboardEvent< HTMLElement > ) => {
29
39
  e.stopPropagation();
@@ -47,35 +57,18 @@ export function CssClassMenu( { styleId, provider, popupState, handleRename, anc
47
57
  disableAutoFocusItem
48
58
  >
49
59
  { /* It has to be an array since MUI menu doesn't accept a Fragment as a child, and wrapping the items with an HTML element disrupts keyboard navigation */ }
50
- { getMenuItemsByProvider( { provider, styleId, handleRename, closeMenu: popupState.close } ) }
60
+ { getMenuItemsByProvider( { provider, closeMenu: popupState.close } ) }
51
61
  <MenuSubheader sx={ { typography: 'caption', color: 'text.secondary', pb: 0.5, pt: 1 } }>
52
62
  { __( 'States', 'elementor' ) }
53
63
  </MenuSubheader>
54
- <StateMenuItem
55
- key="normal"
56
- state={ null }
57
- styleId={ styleId }
58
- closeMenu={ popupState.close }
59
- isStyled={ styledStates.normal }
60
- indicatorVariant={ indicatorVariant }
61
- />
62
64
  { STATES.map( ( state ) => {
63
- return (
64
- <StateMenuItem
65
- key={ state }
66
- state={ state }
67
- styleId={ styleId }
68
- closeMenu={ popupState.close }
69
- isStyled={ styledStates[ state ] }
70
- indicatorVariant={ indicatorVariant }
71
- />
72
- );
65
+ return <StateMenuItem key={ state.key } state={ state.value } closeMenu={ popupState.close } />;
73
66
  } ) }
74
67
  </Menu>
75
68
  );
76
69
  }
77
70
 
78
- function useStyledStates( styleId: string | null ): Partial< Record< StyleDefinitionStateWithNormal, true > > {
71
+ function useModifiedStates( styleId: string | null ): Partial< Record< StyleDefinitionStateWithNormal, true > > {
79
72
  const { meta } = useStyle();
80
73
 
81
74
  const styleDef = stylesRepository.all().find( ( style ) => style.id === styleId );
@@ -87,18 +80,8 @@ function useStyledStates( styleId: string | null ): Partial< Record< StyleDefini
87
80
  );
88
81
  }
89
82
 
90
- function getMenuItemsByProvider( {
91
- provider,
92
- styleId,
93
- handleRename,
94
- closeMenu,
95
- }: {
96
- provider: string | null;
97
- styleId: string | null;
98
- handleRename: () => void;
99
- closeMenu: () => void;
100
- } ) {
101
- if ( ! styleId || ! provider ) {
83
+ function getMenuItemsByProvider( { provider, closeMenu }: { provider: string | null; closeMenu: () => void } ) {
84
+ if ( ! provider ) {
102
85
  return [];
103
86
  }
104
87
 
@@ -108,8 +91,8 @@ function getMenuItemsByProvider( {
108
91
  const [ canUpdate, canDelete ] = [ providerActions?.update, providerActions?.delete ];
109
92
 
110
93
  const actions = [
111
- canUpdate && <RenameClassMenuItem key="rename-class" handleRename={ handleRename } closeMenu={ closeMenu } />,
112
- canDelete && <UnapplyClassMenuItem key="unapply-class" styleId={ styleId } closeMenu={ closeMenu } />,
94
+ canUpdate && <RenameClassMenuItem key="rename-class" closeMenu={ closeMenu } />,
95
+ canDelete && <UnapplyClassMenuItem key="unapply-class" closeMenu={ closeMenu } />,
113
96
  ].filter( Boolean );
114
97
 
115
98
  if ( actions.length ) {
@@ -129,23 +112,23 @@ function getMenuItemsByProvider( {
129
112
 
130
113
  type StateMenuItemProps = {
131
114
  state: StyleDefinitionState;
132
- styleId: string | null;
133
115
  closeMenu: () => void;
134
- isStyled?: boolean;
135
- indicatorVariant: StyleIndicatorVariant;
136
116
  };
137
117
 
138
- function StateMenuItem( {
139
- state,
140
- styleId,
141
- closeMenu,
142
- isStyled = false,
143
- indicatorVariant,
144
- ...props
145
- }: StateMenuItemProps ) {
118
+ function StateMenuItem( { state, closeMenu, ...props }: StateMenuItemProps ) {
119
+ const { id: styleId, provider } = useCssClass();
146
120
  const { id: activeId, setId: setActiveId, setMetaState: setActiveMetaState, meta } = useStyle();
147
121
  const { state: activeState } = meta;
122
+ const { userCan } = useUserStylesCapability();
123
+
124
+ const modifiedStates = useModifiedStates( styleId );
148
125
 
126
+ const isUpdateAllowed = userCan( provider ?? '' ).updateProps;
127
+
128
+ const indicatorVariant = ! provider || isElementsStylesProvider( provider ) ? 'local' : 'global';
129
+
130
+ const isStyled = modifiedStates[ state ?? 'normal' ] ?? false;
131
+ const disabled = isUpdateAllowed ? false : ! isStyled;
149
132
  const isActive = styleId === activeId;
150
133
  const isSelected = state === activeState && isActive;
151
134
 
@@ -153,6 +136,7 @@ function StateMenuItem( {
153
136
  <MenuListItem
154
137
  { ...props }
155
138
  selected={ isSelected }
139
+ disabled={ disabled }
156
140
  sx={ { textTransform: 'capitalize' } }
157
141
  onClick={ () => {
158
142
  if ( ! isActive ) {
@@ -164,49 +148,65 @@ function StateMenuItem( {
164
148
  closeMenu();
165
149
  } }
166
150
  >
167
- <Stack gap={ 0.75 } direction="row" alignItems="center">
168
- { isStyled && (
169
- <StyleIndicator aria-label={ __( 'Has style', 'elementor' ) } variant={ indicatorVariant } />
170
- ) }
171
- { state ?? 'normal' }
172
- </Stack>
151
+ <MenuItemInfotip
152
+ showInfoTip={ disabled }
153
+ content={ __( 'With your role as an editor, you can only use existing states.', 'elementor' ) }
154
+ >
155
+ <Stack gap={ 0.75 } direction="row" alignItems="center">
156
+ { isStyled && (
157
+ <StyleIndicator aria-label={ __( 'Has style', 'elementor' ) } variant={ indicatorVariant } />
158
+ ) }
159
+ { state ?? 'normal' }
160
+ </Stack>
161
+ </MenuItemInfotip>
173
162
  </MenuListItem>
174
163
  );
175
164
  }
176
165
 
177
- function UnapplyClassMenuItem( { styleId, closeMenu, ...props }: { styleId: string; closeMenu: () => void } ) {
178
- const unapplyClass = useUnapplyClass( styleId );
166
+ function UnapplyClassMenuItem( { closeMenu, ...props }: { closeMenu: () => void } ) {
167
+ const { id: classId, label: classLabel } = useCssClass();
168
+ const unapplyClass = useUnapplyClass();
179
169
 
180
- return (
170
+ return classId ? (
181
171
  <MenuListItem
182
172
  { ...props }
183
173
  onClick={ () => {
184
- unapplyClass();
174
+ unapplyClass( { classId, classLabel } );
185
175
  closeMenu();
186
176
  } }
187
177
  >
188
178
  { __( 'Remove', 'elementor' ) }
189
179
  </MenuListItem>
190
- );
180
+ ) : null;
191
181
  }
192
182
 
193
- function RenameClassMenuItem( {
194
- handleRename,
195
- closeMenu,
196
- ...props
197
- }: {
198
- handleRename: () => void;
199
- closeMenu: () => void;
200
- } ) {
183
+ function RenameClassMenuItem( { closeMenu }: { closeMenu: () => void } ) {
184
+ const { handleRename, provider } = useCssClass();
185
+ const { userCan } = useUserStylesCapability();
186
+
187
+ if ( ! provider ) {
188
+ return null;
189
+ }
190
+
191
+ const isAllowed = userCan( provider ).update;
192
+
201
193
  return (
202
194
  <MenuListItem
203
- { ...props }
195
+ disabled={ ! isAllowed }
204
196
  onClick={ () => {
205
197
  closeMenu();
206
198
  handleRename();
207
199
  } }
208
200
  >
209
- { __( 'Rename', 'elementor' ) }
201
+ <MenuItemInfotip
202
+ showInfoTip={ ! isAllowed }
203
+ content={ __(
204
+ 'With your role as an editor, you can use existing classes but can’t modify them.',
205
+ 'elementor'
206
+ ) }
207
+ >
208
+ { __( 'Rename', 'elementor' ) }
209
+ </MenuItemInfotip>
210
210
  </MenuListItem>
211
211
  );
212
212
  }