@elementor/editor-editing-panel 1.0.0 → 1.1.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 (117) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/index.d.mts +10 -19
  3. package/dist/index.d.ts +10 -19
  4. package/dist/index.js +1283 -1751
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1305 -1762
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +12 -12
  9. package/src/components/add-or-remove-content.tsx +3 -3
  10. package/src/components/collapse-icon.tsx +12 -0
  11. package/src/components/collapsible-content.tsx +5 -14
  12. package/src/components/collapsible-field.tsx +5 -3
  13. package/src/components/css-class-selector-section.tsx +76 -0
  14. package/src/components/editing-panel-hooks.tsx +2 -0
  15. package/src/components/editing-panel-tabs.tsx +23 -13
  16. package/src/components/editing-panel.tsx +9 -6
  17. package/src/components/multi-combobox/index.ts +3 -0
  18. package/src/components/multi-combobox/multi-combobox.tsx +120 -0
  19. package/src/components/multi-combobox/types.ts +26 -0
  20. package/src/components/multi-combobox/use-combobox-actions.ts +62 -0
  21. package/src/components/section.tsx +37 -0
  22. package/src/components/sections-list.tsx +6 -0
  23. package/src/components/settings-tab.tsx +11 -11
  24. package/src/components/style-sections/background-section/background-color-field.tsx +4 -4
  25. package/src/components/style-sections/background-section/background-section.tsx +9 -7
  26. package/src/components/style-sections/border-section/border-color-field.tsx +4 -4
  27. package/src/components/style-sections/border-section/border-field.tsx +4 -3
  28. package/src/components/style-sections/border-section/border-radius-field.tsx +4 -3
  29. package/src/components/style-sections/border-section/border-section.tsx +7 -10
  30. package/src/components/style-sections/border-section/border-style-field.tsx +4 -4
  31. package/src/components/style-sections/border-section/border-width-field.tsx +4 -3
  32. package/src/components/style-sections/effects-section/effects-section.tsx +7 -10
  33. package/src/components/style-sections/layout-section/display-field.tsx +32 -0
  34. package/src/components/style-sections/layout-section/justify-content-field.tsx +82 -0
  35. package/src/components/style-sections/layout-section/layout-section.tsx +17 -0
  36. package/src/components/style-sections/layout-section/utils/rotate-flex-icon.ts +12 -0
  37. package/src/components/style-sections/position-section/dimensions-field.tsx +6 -6
  38. package/src/components/style-sections/position-section/position-field.tsx +4 -4
  39. package/src/components/style-sections/position-section/position-section.tsx +45 -15
  40. package/src/components/style-sections/position-section/z-index-field.tsx +4 -4
  41. package/src/components/style-sections/size-section/overflow-field.tsx +8 -8
  42. package/src/components/style-sections/size-section/size-section.tsx +33 -26
  43. package/src/components/style-sections/spacing-section/spacing-section.tsx +11 -13
  44. package/src/components/style-sections/typography-section/font-family-field.tsx +40 -0
  45. package/src/components/style-sections/typography-section/font-size-field.tsx +4 -4
  46. package/src/components/style-sections/typography-section/font-weight-field.tsx +4 -4
  47. package/src/components/style-sections/typography-section/letter-spacing-field.tsx +4 -4
  48. package/src/components/style-sections/typography-section/text-alignment-field.tsx +9 -9
  49. package/src/components/style-sections/typography-section/text-color-field.tsx +4 -4
  50. package/src/components/style-sections/typography-section/text-direction-field.tsx +7 -7
  51. package/src/components/style-sections/typography-section/text-stroke-field.tsx +3 -3
  52. package/src/components/style-sections/typography-section/text-style-field.tsx +5 -4
  53. package/src/components/style-sections/typography-section/transform-field.tsx +23 -9
  54. package/src/components/style-sections/typography-section/typography-section.tsx +26 -27
  55. package/src/components/style-sections/typography-section/word-spacing-field.tsx +4 -4
  56. package/src/components/style-tab.tsx +67 -31
  57. package/src/contexts/classes-prop-context.tsx +1 -1
  58. package/src/contexts/element-context.tsx +2 -2
  59. package/src/contexts/style-context.tsx +6 -5
  60. package/src/control-replacement.tsx +1 -1
  61. package/src/controls-actions.ts +3 -2
  62. package/src/controls-registry/control-type-container.tsx +3 -2
  63. package/src/controls-registry/control.tsx +2 -1
  64. package/src/controls-registry/controls-registry.tsx +8 -1
  65. package/src/controls-registry/settings-field.tsx +5 -4
  66. package/src/controls-registry/styles-field.tsx +3 -2
  67. package/src/dynamics/components/dynamic-selection-control.tsx +15 -14
  68. package/src/dynamics/components/dynamic-selection.tsx +9 -8
  69. package/src/dynamics/dynamic-control.tsx +4 -4
  70. package/src/dynamics/hooks/use-dynamic-tag.ts +3 -2
  71. package/src/dynamics/hooks/use-prop-dynamic-action.tsx +6 -5
  72. package/src/dynamics/hooks/use-prop-dynamic-tags.ts +3 -2
  73. package/src/dynamics/init.ts +5 -3
  74. package/src/dynamics/sync/get-elementor-config.ts +1 -1
  75. package/src/dynamics/types.ts +2 -2
  76. package/src/dynamics/utils.ts +3 -2
  77. package/src/hooks/use-close-editor-panel.ts +23 -0
  78. package/src/hooks/use-direction.ts +13 -0
  79. package/src/hooks/use-open-editor-panel.ts +4 -3
  80. package/src/hooks/use-prop-value-history.ts +45 -0
  81. package/src/hooks/use-style-prop-history.ts +75 -0
  82. package/src/hooks/use-styles-field.ts +25 -4
  83. package/src/index.ts +1 -1
  84. package/src/init.ts +5 -4
  85. package/src/panel.ts +1 -0
  86. package/src/popover-action.tsx +1 -1
  87. package/src/sync/enqueue-font.ts +7 -0
  88. package/src/sync/get-elementor-config.ts +7 -0
  89. package/src/sync/{should-use-v2-panel.ts → is-atomic-widget-selected.ts} +1 -1
  90. package/src/sync/types.ts +20 -21
  91. package/src/components/accordion-section.tsx +0 -26
  92. package/src/components/control-label.tsx +0 -10
  93. package/src/controls/bound-prop-context.tsx +0 -30
  94. package/src/controls/components/control-toggle-button-group.tsx +0 -68
  95. package/src/controls/components/repeater.tsx +0 -197
  96. package/src/controls/components/text-field-inner-selection.tsx +0 -75
  97. package/src/controls/control-actions/control-actions-context.tsx +0 -27
  98. package/src/controls/control-actions/control-actions-menu.ts +0 -7
  99. package/src/controls/control-actions/control-actions.tsx +0 -31
  100. package/src/controls/controls/box-shadow-repeater-control.tsx +0 -210
  101. package/src/controls/controls/color-control.tsx +0 -25
  102. package/src/controls/controls/equal-unequal-sizes-control.tsx +0 -196
  103. package/src/controls/controls/image-control.tsx +0 -58
  104. package/src/controls/controls/image-media-control.tsx +0 -64
  105. package/src/controls/controls/linked-dimensions-control.tsx +0 -139
  106. package/src/controls/controls/number-control.tsx +0 -29
  107. package/src/controls/controls/select-control.tsx +0 -30
  108. package/src/controls/controls/size-control.tsx +0 -71
  109. package/src/controls/controls/stroke-control.tsx +0 -105
  110. package/src/controls/controls/text-area-control.tsx +0 -31
  111. package/src/controls/controls/text-control.tsx +0 -17
  112. package/src/controls/controls/toggle-control.tsx +0 -26
  113. package/src/controls/create-control-replacement.tsx +0 -53
  114. package/src/controls/create-control.tsx +0 -40
  115. package/src/controls/hooks/use-sync-external-state.tsx +0 -51
  116. package/src/controls/index.ts +0 -24
  117. package/src/dynamics/hooks/use-prop-value-history.ts +0 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-editing-panel",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,18 +39,18 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "^0.16.0",
43
- "@elementor/editor-elements": "^0.2.0",
44
- "@elementor/editor-props": "^0.2.0",
45
- "@elementor/editor-panels": "^0.9.0",
46
- "@elementor/editor-responsive": "^0.12.2",
47
- "@elementor/editor-styles": "^0.2.0",
48
- "@elementor/editor-v1-adapters": "^0.8.3",
49
- "@elementor/icons": "^1.18.1",
42
+ "@elementor/editor": "^0.17.0",
43
+ "@elementor/editor-controls": "^0.1.0",
44
+ "@elementor/editor-elements": "^0.3.0",
50
45
  "@elementor/menus": "^0.1.1",
51
- "@elementor/ui": "^1.21.7",
52
- "@elementor/utils": "^0.2.2",
53
- "@elementor/wp-media": "^0.2.1",
46
+ "@elementor/editor-props": "^0.3.0",
47
+ "@elementor/editor-panels": "^0.10.0",
48
+ "@elementor/editor-responsive": "^0.12.2",
49
+ "@elementor/editor-styles": "^0.2.1",
50
+ "@elementor/editor-v1-adapters": "^0.8.4",
51
+ "@elementor/icons": "^1.19.0",
52
+ "@elementor/ui": "^1.21.13",
53
+ "@elementor/utils": "^0.3.0",
54
54
  "@wordpress/i18n": "^4.45.0"
55
55
  },
56
56
  "peerDependencies": {
@@ -1,8 +1,8 @@
1
1
  import * as React from 'react';
2
- import { PropsWithChildren } from 'react';
2
+ import { type PropsWithChildren } from 'react';
3
+ import { ControlLabel } from '@elementor/editor-controls';
3
4
  import { MinusIcon, PlusIcon } from '@elementor/icons';
4
5
  import { Collapse, IconButton, Stack } from '@elementor/ui';
5
- import { ControlLabel } from './control-label';
6
6
 
7
7
  const SIZE = 'tiny';
8
8
 
@@ -34,7 +34,7 @@ export const AddOrRemoveContent = ( { isAdded, label, onAdd, onRemove, children
34
34
  </IconButton>
35
35
  ) }
36
36
  </Stack>
37
- <Collapse in={ isAdded }>
37
+ <Collapse in={ isAdded } unmountOnExit>
38
38
  <Stack gap={ 1.5 }>{ children }</Stack>
39
39
  </Collapse>
40
40
  </Stack>
@@ -0,0 +1,12 @@
1
+ import { ChevronDownIcon } from '@elementor/icons';
2
+ import { styled } from '@elementor/ui';
3
+
4
+ // TODO: Replace this with future Rotate component that will be implemented in elementor-ui
5
+ export const CollapseIcon = styled( ChevronDownIcon, {
6
+ shouldForwardProp: ( prop ) => prop !== 'open',
7
+ } )< { open: boolean } >( ( { theme, open } ) => ( {
8
+ transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
9
+ transition: theme.transitions.create( 'transform', {
10
+ duration: theme.transitions.duration.standard,
11
+ } ),
12
+ } ) );
@@ -1,9 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import { useState } from 'react';
3
- import { ChevronDownIcon } from '@elementor/icons';
4
- import { Button, Collapse, Stack, styled } from '@elementor/ui';
3
+ import { Button, Collapse, Stack } from '@elementor/ui';
5
4
  import { __ } from '@wordpress/i18n';
6
5
 
6
+ import { CollapseIcon } from './collapse-icon';
7
+
7
8
  type CollapsibleContentProps = React.PropsWithChildren< {
8
9
  defaultOpen?: boolean;
9
10
  } >;
@@ -23,23 +24,13 @@ export const CollapsibleContent = ( { children, defaultOpen = false }: Collapsib
23
24
  color="secondary"
24
25
  variant="outlined"
25
26
  onClick={ handleToggle }
26
- endIcon={ <ChevronIcon open={ open } /> }
27
+ endIcon={ <CollapseIcon open={ open } /> }
27
28
  >
28
29
  { open ? __( 'Show less', 'elementor' ) : __( 'Show more', 'elementor' ) }
29
30
  </Button>
30
- <Collapse in={ open } timeout="auto">
31
+ <Collapse in={ open } timeout="auto" unmountOnExit>
31
32
  { children }
32
33
  </Collapse>
33
34
  </Stack>
34
35
  );
35
36
  };
36
-
37
- // TODO: Replace this with future Rotate component that will be implemented in elementor-ui
38
- const ChevronIcon = styled( ChevronDownIcon, {
39
- shouldForwardProp: ( prop ) => prop !== 'open',
40
- } )< { open: boolean } >( ( { theme, open } ) => ( {
41
- transform: open ? 'rotate(180deg)' : 'rotate(0)',
42
- transition: theme.transitions.create( 'transform', {
43
- duration: theme.transitions.duration.standard,
44
- } ),
45
- } ) );
@@ -1,8 +1,8 @@
1
1
  import * as React from 'react';
2
2
  import { useState } from 'react';
3
- import { __ } from '@wordpress/i18n';
4
- import { Collapse, IconButton, Stack } from '@elementor/ui';
5
3
  import { MinusIcon, PlusIcon } from '@elementor/icons';
4
+ import { Collapse, IconButton, Stack } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
6
 
7
7
  type CollapsibleFieldProps = React.PropsWithChildren< {
8
8
  label: React.ReactNode;
@@ -28,7 +28,9 @@ export const CollapsibleField = ( { label, children, defaultOpen = false }: Coll
28
28
  { open ? <MinusIcon fontSize="tiny" /> : <PlusIcon fontSize="tiny" /> }
29
29
  </IconButton>
30
30
  </Stack>
31
- <Collapse in={ open }>{ children }</Collapse>
31
+ <Collapse in={ open } unmountOnExit>
32
+ { children }
33
+ </Collapse>
32
34
  </Stack>
33
35
  );
34
36
  };
@@ -0,0 +1,76 @@
1
+ import * as React from 'react';
2
+ import { useElementSetting, useElementStyles } from '@elementor/editor-elements';
3
+ import { type ClassesPropValue } from '@elementor/editor-props';
4
+ import { Chip, Stack, Typography } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { useClassesProp } from '../contexts/classes-prop-context';
8
+ import { useElement } from '../contexts/element-context';
9
+ import { useStyle } from '../contexts/style-context';
10
+ import { MultiCombobox, type Option } from './multi-combobox';
11
+
12
+ const ID = 'elementor-css-class-selector';
13
+ const TAGS_LIMIT = 8;
14
+
15
+ export function CssClassSelectorSection() {
16
+ const options = useOptions();
17
+
18
+ const { id: activeId, setId: setActiveId } = useStyle();
19
+ const appliedIds = useAppliedClassesIds();
20
+
21
+ const applied = options.filter( ( option ) => appliedIds.includes( option.value ) );
22
+ const active = options.find( ( option ) => option.value === activeId ) || null;
23
+
24
+ return (
25
+ <Stack gap={ 1 } p={ 2 }>
26
+ <Typography component="label" variant="caption" htmlFor={ ID }>
27
+ { __( 'CSS Classes', 'elementor' ) }
28
+ </Typography>
29
+ <MultiCombobox
30
+ id={ ID }
31
+ size="tiny"
32
+ options={ options }
33
+ selected={ applied }
34
+ limitTags={ TAGS_LIMIT }
35
+ optionsLabel={ __( 'Global CSS Classes', 'elementor' ) }
36
+ renderTags={ ( tagValue, getTagProps ) =>
37
+ tagValue.map( ( option, index ) => {
38
+ const chipProps = getTagProps( { index } );
39
+
40
+ return (
41
+ <Chip
42
+ { ...chipProps }
43
+ key={ chipProps.key }
44
+ size="small"
45
+ label={ option.label }
46
+ variant={ option.value === active?.value ? 'filled' : 'standard' }
47
+ color={ option.color ?? 'default' }
48
+ onClick={ () => setActiveId( option.value ) }
49
+ onDelete={ null }
50
+ />
51
+ );
52
+ } )
53
+ }
54
+ />
55
+ </Stack>
56
+ );
57
+ }
58
+
59
+ function useAppliedClassesIds() {
60
+ const { element } = useElement();
61
+ const currentClassesProp = useClassesProp();
62
+
63
+ return useElementSetting< ClassesPropValue >( element.id, currentClassesProp )?.value || [];
64
+ }
65
+
66
+ function useOptions() {
67
+ const { element } = useElement();
68
+
69
+ const styleDefs = useElementStyles( element.id );
70
+
71
+ return Object.values( styleDefs ).map< Option >( ( styleDef ) => ( {
72
+ label: styleDef.label,
73
+ value: styleDef.id,
74
+ color: 'primary',
75
+ } ) );
76
+ }
@@ -1,7 +1,9 @@
1
+ import { useCloseEditorPanel } from '../hooks/use-close-editor-panel';
1
2
  import { useOpenEditorPanel } from '../hooks/use-open-editor-panel';
2
3
 
3
4
  export const EditingPanelHooks = () => {
4
5
  useOpenEditorPanel();
6
+ useCloseEditorPanel();
5
7
 
6
8
  return null;
7
9
  };
@@ -1,26 +1,36 @@
1
- import { Stack, Tabs, Tab, TabPanel, useTabs } from '@elementor/ui';
2
1
  import * as React from 'react';
2
+ import { Fragment } from 'react';
3
+ import { Divider, Stack, Tab, TabPanel, Tabs, useTabs } from '@elementor/ui';
3
4
  import { __ } from '@wordpress/i18n';
5
+
6
+ import { useElement } from '../contexts/element-context';
4
7
  import { SettingsTab } from './settings-tab';
5
8
  import { StyleTab } from './style-tab';
6
9
 
7
10
  type TabValue = 'settings' | 'style';
8
11
 
9
12
  export const EditingPanelTabs = () => {
13
+ const { element } = useElement();
14
+
10
15
  const { getTabProps, getTabPanelProps, getTabsProps } = useTabs< TabValue >( 'settings' );
11
16
 
12
17
  return (
13
- <Stack direction="column" sx={ { width: '100%' } }>
14
- <Tabs variant="fullWidth" indicatorColor="secondary" textColor="inherit" { ...getTabsProps() }>
15
- <Tab label={ __( 'General', 'elementor' ) } { ...getTabProps( 'settings' ) } />
16
- <Tab label={ __( 'Style', 'elementor' ) } { ...getTabProps( 'style' ) } />
17
- </Tabs>
18
- <TabPanel { ...getTabPanelProps( 'settings' ) } disablePadding>
19
- <SettingsTab />
20
- </TabPanel>
21
- <TabPanel { ...getTabPanelProps( 'style' ) } disablePadding>
22
- <StyleTab />
23
- </TabPanel>
24
- </Stack>
18
+ // When switching between elements, the local states should be reset. We are using key to rerender the tabs.
19
+ // Reference: https://react.dev/learn/preserving-and-resetting-state#resetting-a-form-with-a-key
20
+ <Fragment key={ element.id }>
21
+ <Stack direction="column" sx={ { width: '100%' } }>
22
+ <Tabs variant="fullWidth" indicatorColor="secondary" textColor="inherit" { ...getTabsProps() }>
23
+ <Tab label={ __( 'General', 'elementor' ) } { ...getTabProps( 'settings' ) } />
24
+ <Tab label={ __( 'Style', 'elementor' ) } { ...getTabProps( 'style' ) } />
25
+ </Tabs>
26
+ <Divider />
27
+ <TabPanel { ...getTabPanelProps( 'settings' ) } disablePadding>
28
+ <SettingsTab />
29
+ </TabPanel>
30
+ <TabPanel { ...getTabPanelProps( 'style' ) } disablePadding>
31
+ <StyleTab />
32
+ </TabPanel>
33
+ </Stack>
34
+ </Fragment>
25
35
  );
26
36
  };
@@ -1,14 +1,17 @@
1
1
  import * as React from 'react';
2
- import { __ } from '@wordpress/i18n';
2
+ import { ControlActionsProvider, ControlReplacementProvider } from '@elementor/editor-controls';
3
3
  import { useSelectedElement } from '@elementor/editor-elements';
4
4
  import { Panel, PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
5
- import { EditingPanelTabs } from './editing-panel-tabs';
6
- import { ControlActionsProvider, ControlReplacementProvider } from '../controls';
7
- import { getControlReplacement } from '../control-replacement';
8
5
  import { ErrorBoundary } from '@elementor/ui';
9
- import { EditorPanelErrorFallback } from './editing-panel-error-fallback';
6
+ import { __ } from '@wordpress/i18n';
7
+
10
8
  import { ElementProvider } from '../contexts/element-context';
11
- import { useMenuItems } from '../controls-actions';
9
+ import { getControlReplacement } from '../control-replacement';
10
+ import { controlActionsMenu } from '../controls-actions';
11
+ import { EditorPanelErrorFallback } from './editing-panel-error-fallback';
12
+ import { EditingPanelTabs } from './editing-panel-tabs';
13
+
14
+ const { useMenuItems } = controlActionsMenu;
12
15
 
13
16
  export const EditingPanel = () => {
14
17
  const { element, elementType } = useSelectedElement();
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './multi-combobox';
3
+ export * from './use-combobox-actions';
@@ -0,0 +1,120 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Autocomplete,
4
+ type AutocompleteProps,
5
+ type AutocompleteRenderGroupParams,
6
+ Box,
7
+ Chip,
8
+ styled,
9
+ TextField,
10
+ } from '@elementor/ui';
11
+ import { type FilterOptionsState } from '@mui/base';
12
+
13
+ import { type ActionOption, type Actions, type Option } from './types';
14
+ import { useComboboxActions } from './use-combobox-actions';
15
+
16
+ type Props = Omit< AutocompleteProps< Option, true, true, true >, 'renderInput' | 'getLimitTagsText' > & {
17
+ actions?: Actions;
18
+ selected: Option[];
19
+ options: Option[];
20
+ optionsLabel?: string;
21
+ onApply?: ( value: Option[] ) => void;
22
+ onCreate?: ( value: string ) => void;
23
+ };
24
+
25
+ export const MultiCombobox = ( {
26
+ actions = {},
27
+ selected,
28
+ options,
29
+ optionsLabel,
30
+ onApply,
31
+ onCreate,
32
+ ...props
33
+ }: Props ) => {
34
+ const { action: actionProps, option: optionProps } = useComboboxActions( selected, actions, optionsLabel, onApply );
35
+
36
+ const handleSelectOption = ( values: Array< ActionOption | Option | string > ) => {
37
+ const action = values.find( ( value ) => actionProps.is( value as ActionOption ) );
38
+
39
+ if ( action ) {
40
+ return actionProps.onChange( action as ActionOption );
41
+ }
42
+
43
+ return optionProps.onChange( values as Option[] );
44
+ };
45
+
46
+ const handleCreateOption = ( values: Array< ActionOption | Option | string > ) => {
47
+ const value = values.find( ( option ) => typeof option === 'string' );
48
+
49
+ onCreate?.( value as string );
50
+ };
51
+
52
+ return (
53
+ <Autocomplete
54
+ { ...props }
55
+ freeSolo
56
+ multiple
57
+ clearOnBlur
58
+ selectOnFocus
59
+ disableClearable
60
+ handleHomeEndKeys
61
+ value={ selected }
62
+ options={ options }
63
+ renderGroup={ renderGroup }
64
+ renderInput={ ( params ) => <TextField { ...params } /> }
65
+ getLimitTagsText={ ( more ) => <Chip size="tiny" variant="standard" label={ `+${ more }` } clickable /> }
66
+ onChange={ ( _, values, reason ) => {
67
+ if ( reason === 'selectOption' ) {
68
+ return handleSelectOption( values );
69
+ }
70
+
71
+ if ( reason === 'createOption' ) {
72
+ return handleCreateOption( values );
73
+ }
74
+
75
+ onApply?.( values as ActionOption[] );
76
+ } }
77
+ getOptionLabel={ ( option ) => {
78
+ if ( optionProps.is( option as ActionOption ) ) {
79
+ return optionProps.getLabel( option as Option );
80
+ }
81
+
82
+ return actionProps.getLabel( option as ActionOption ) ?? '';
83
+ } }
84
+ filterOptions={ ( optionList: Option[], params: FilterOptionsState< ActionOption | Option > ) => {
85
+ const filteredoptions = optionProps.getFilteredOptions( optionList, params );
86
+
87
+ const actionOptions = actionProps.getFilteredActions( optionList, params );
88
+
89
+ return [ ...actionOptions, ...filteredoptions ];
90
+ } }
91
+ groupBy={ ( option ) =>
92
+ ( optionProps.is( option ) ? optionProps.groupBy() : actionProps.groupBy( option ) ) ?? ''
93
+ }
94
+ />
95
+ );
96
+ };
97
+
98
+ export const renderGroup = ( params: AutocompleteRenderGroupParams ) => (
99
+ <Group key={ params.key }>
100
+ <GroupHeader>{ params.group }</GroupHeader>
101
+ <GroupItems>{ params.children }</GroupItems>
102
+ </Group>
103
+ );
104
+
105
+ const Group = styled( 'li' )`
106
+ &:not( :last-of-type ) {
107
+ border-bottom: 1px solid ${ ( { theme } ) => theme.palette.divider };
108
+ }
109
+ `;
110
+
111
+ const GroupHeader = styled( Box )( ( { theme } ) => ( {
112
+ position: 'sticky',
113
+ top: '-8px',
114
+ padding: theme.spacing( 1, 2 ),
115
+ color: theme.palette.text.tertiary,
116
+ } ) );
117
+
118
+ const GroupItems = styled( 'ul' )`
119
+ padding: 0;
120
+ `;
@@ -0,0 +1,26 @@
1
+ export type Option = {
2
+ label: string;
3
+ value: string;
4
+ color?: 'primary' | 'global';
5
+ };
6
+
7
+ export type Action = {
8
+ getLabel: ( inputValue: string ) => string;
9
+ apply: ( value: string ) => void;
10
+ condition: ( options: Option[], inputValue: string ) => boolean;
11
+ };
12
+
13
+ export type ActionsGroup = {
14
+ label: string;
15
+ actions: Action[];
16
+ };
17
+
18
+ export type ActionOption = Option & {
19
+ action: {
20
+ groupLabel: string;
21
+ apply: Action[ 'apply' ];
22
+ getLabel: Action[ 'getLabel' ];
23
+ };
24
+ };
25
+
26
+ export type Actions = Record< string, ActionsGroup >;
@@ -0,0 +1,62 @@
1
+ import { createFilterOptions } from '@elementor/ui';
2
+ import { type FilterOptionsState } from '@mui/base';
3
+
4
+ import { type Action, type ActionOption, type Actions, type Option } from './types';
5
+
6
+ export const useComboboxActions = (
7
+ applied: Option[],
8
+ actions: Actions,
9
+ optionsLabel?: string,
10
+ onApply?: ( value: Option[] ) => void
11
+ ) => ( {
12
+ action: {
13
+ is: ( opt: ActionOption ): opt is ActionOption => !! opt.action,
14
+ getLabel: ( option: ActionOption ) => option.action.getLabel( option.label ),
15
+ groupBy: ( option: ActionOption ) => option.action.groupLabel,
16
+ onChange: ( { action, label }: ActionOption ) => action?.apply( label ),
17
+ getFilteredActions: ( optionList: Option[], params: FilterOptionsState< ActionOption > ) => {
18
+ const actionGroups = Object.values( actions );
19
+
20
+ return actionGroups.reduce< Option[] >( ( groups, group ) => {
21
+ const actionOptions = group.actions.reduce< Option[] >( ( groupActions, action ) => {
22
+ const shouldShowAction = action.condition( optionList, params.inputValue );
23
+
24
+ if ( shouldShowAction ) {
25
+ const actionOption = createActionOption( group.label, action, params.inputValue );
26
+ groupActions.unshift( actionOption );
27
+ }
28
+
29
+ return groupActions;
30
+ }, [] );
31
+
32
+ return [ ...groups, ...actionOptions ];
33
+ }, [] );
34
+ },
35
+ },
36
+ option: {
37
+ is: ( opt: ActionOption | Option ): opt is Option => ! ( 'action' in opt ),
38
+ getLabel: ( option: Option ) => option.label,
39
+ groupBy: () => optionsLabel ?? '',
40
+ onChange: ( optionValues: Option[] ) => onApply?.( optionValues ),
41
+ getFilteredOptions: ( optionList: Option[], params: FilterOptionsState< Option > ) => {
42
+ const appliedValues = applied.map( ( option ) => option.value );
43
+
44
+ const optionsWithoutApplied = optionList.filter( ( option ) => ! appliedValues.includes( option.value ) );
45
+
46
+ return filter( optionsWithoutApplied, params );
47
+ },
48
+ },
49
+ } );
50
+
51
+ // Helper functions.
52
+ const filter = createFilterOptions< Option >();
53
+
54
+ const createActionOption = ( groupLabel: string, action: Action, inputValue: string ): ActionOption => ( {
55
+ value: '',
56
+ label: inputValue,
57
+ action: {
58
+ groupLabel,
59
+ apply: action.apply,
60
+ getLabel: action.getLabel,
61
+ },
62
+ } );
@@ -0,0 +1,37 @@
1
+ import * as React from 'react';
2
+ import { type PropsWithChildren, useId, useState } from 'react';
3
+ import { Collapse, Divider, ListItemButton, ListItemText, Stack } from '@elementor/ui';
4
+
5
+ import { CollapseIcon } from './collapse-icon';
6
+
7
+ type Props = PropsWithChildren< {
8
+ title: string;
9
+ defaultExpanded?: boolean;
10
+ } >;
11
+
12
+ export function Section( { title, children, defaultExpanded = false }: Props ) {
13
+ const [ isOpen, setIsOpen ] = useState( !! defaultExpanded );
14
+
15
+ const id = useId();
16
+ const labelId = `label-${ id }`;
17
+ const contentId = `content-${ id }`;
18
+
19
+ return (
20
+ <>
21
+ <ListItemButton
22
+ id={ labelId }
23
+ aria-controls={ contentId }
24
+ onClick={ () => setIsOpen( ( prev ) => ! prev ) }
25
+ >
26
+ <ListItemText secondary={ title } />
27
+ <CollapseIcon open={ isOpen } color="secondary" />
28
+ </ListItemButton>
29
+ <Collapse id={ contentId } aria-labelledby={ labelId } in={ isOpen } timeout="auto" unmountOnExit>
30
+ <Stack gap={ 2.5 } p={ 2 }>
31
+ { children }
32
+ </Stack>
33
+ </Collapse>
34
+ <Divider />
35
+ </>
36
+ );
37
+ }
@@ -0,0 +1,6 @@
1
+ import * as React from 'react';
2
+ import { List, type ListProps } from '@elementor/ui';
3
+
4
+ export function SectionsList( props: ListProps ) {
5
+ return <List disablePadding component="div" { ...props } />;
6
+ }
@@ -1,19 +1,20 @@
1
1
  import * as React from 'react';
2
- import { Stack } from '@elementor/ui';
2
+ import { ControlLabel } from '@elementor/editor-controls';
3
3
  import { type Control } from '@elementor/editor-elements';
4
- import { SettingsField } from '../controls-registry/settings-field';
5
- import { AccordionSection } from './accordion-section';
4
+
5
+ import { useElement } from '../contexts/element-context';
6
6
  import { Control as BaseControl } from '../controls-registry/control';
7
- import { ControlType, getControlByType } from '../controls-registry/controls-registry';
8
7
  import { ControlTypeContainer } from '../controls-registry/control-type-container';
9
- import { ControlLabel } from './control-label';
10
- import { useElement } from '../contexts/element-context';
8
+ import { type ControlType, getControlByType } from '../controls-registry/controls-registry';
9
+ import { SettingsField } from '../controls-registry/settings-field';
10
+ import { Section } from './section';
11
+ import { SectionsList } from './sections-list';
11
12
 
12
13
  export const SettingsTab = () => {
13
14
  const { elementType } = useElement();
14
15
 
15
16
  return (
16
- <Stack>
17
+ <SectionsList>
17
18
  { elementType.controls.map( ( { type, value }, index ) => {
18
19
  if ( type === 'control' ) {
19
20
  return <Control key={ value.bind } control={ value } />;
@@ -21,7 +22,7 @@ export const SettingsTab = () => {
21
22
 
22
23
  if ( type === 'section' ) {
23
24
  return (
24
- <AccordionSection key={ type + '.' + index } title={ value.label } defaultExpanded>
25
+ <Section title={ value.label } key={ type + '.' + index } defaultExpanded={ true }>
25
26
  { value.items?.map( ( item ) => {
26
27
  if ( item.type === 'control' ) {
27
28
  return <Control key={ item.value.bind } control={ item.value } />;
@@ -30,17 +31,16 @@ export const SettingsTab = () => {
30
31
  // TODO: Handle 2nd level sections
31
32
  return null;
32
33
  } ) }
33
- </AccordionSection>
34
+ </Section>
34
35
  );
35
36
  }
36
37
 
37
38
  return null;
38
39
  } ) }
39
- </Stack>
40
+ </SectionsList>
40
41
  );
41
42
  };
42
43
 
43
- // TODO: Create control wrapper by type for different layouts.
44
44
  const Control = ( { control }: { control: Control[ 'value' ] } ) => {
45
45
  if ( ! getControlByType( control.type as ControlType ) ) {
46
46
  return null;
@@ -1,14 +1,14 @@
1
1
  import * as React from 'react';
2
- import { __ } from '@wordpress/i18n';
2
+ import { ColorControl, ControlLabel } from '@elementor/editor-controls';
3
3
  import { Grid } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
4
6
  import { StylesField } from '../../../controls-registry/styles-field';
5
- import { ControlLabel } from '../../control-label';
6
- import { ColorControl } from '../../../controls';
7
7
 
8
8
  export const BackgroundColorField = () => {
9
9
  return (
10
10
  <StylesField bind="background-color">
11
- <Grid container spacing={ 1 } alignItems="center">
11
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
12
12
  <Grid item xs={ 6 }>
13
13
  <ControlLabel>{ __( 'Color', 'elementor' ) }</ControlLabel>
14
14
  </Grid>
@@ -1,15 +1,17 @@
1
1
  import * as React from 'react';
2
- import { __ } from '@wordpress/i18n';
2
+ import { BackgroundOverlayRepeaterControl } from '@elementor/editor-controls';
3
3
  import { Stack } from '@elementor/ui';
4
- import { AccordionSection } from '../../accordion-section';
4
+
5
+ import { StylesField } from '../../../controls-registry/styles-field';
5
6
  import { BackgroundColorField } from './background-color-field';
6
7
 
7
8
  export const BackgroundSection = () => {
8
9
  return (
9
- <AccordionSection title={ __( 'Background', 'elementor' ) }>
10
- <Stack gap={ 1.5 }>
11
- <BackgroundColorField />
12
- </Stack>
13
- </AccordionSection>
10
+ <Stack gap={ 1.5 }>
11
+ <StylesField bind="background-image">
12
+ <BackgroundOverlayRepeaterControl />
13
+ </StylesField>
14
+ <BackgroundColorField />
15
+ </Stack>
14
16
  );
15
17
  };