@elementor/editor-editing-panel 4.3.0-1048 → 4.3.0-1049

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-editing-panel",
3
- "version": "4.3.0-1048",
3
+ "version": "4.3.0-1049",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,31 +39,31 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "4.3.0-1048",
43
- "@elementor/editor-canvas": "4.3.0-1048",
44
- "@elementor/editor-controls": "4.3.0-1048",
45
- "@elementor/editor-documents": "4.3.0-1048",
46
- "@elementor/editor-elements": "4.3.0-1048",
47
- "@elementor/editor-interactions": "4.3.0-1048",
48
- "@elementor/editor-notifications": "4.3.0-1048",
49
- "@elementor/editor-panels": "4.3.0-1048",
50
- "@elementor/editor-props": "4.3.0-1048",
51
- "@elementor/editor-responsive": "4.3.0-1048",
52
- "@elementor/editor-styles": "4.3.0-1048",
53
- "@elementor/editor-styles-repository": "4.3.0-1048",
54
- "@elementor/editor-ui": "4.3.0-1048",
55
- "@elementor/editor-v1-adapters": "4.3.0-1048",
56
- "@elementor/http-client": "4.3.0-1048",
42
+ "@elementor/editor": "4.3.0-1049",
43
+ "@elementor/editor-canvas": "4.3.0-1049",
44
+ "@elementor/editor-controls": "4.3.0-1049",
45
+ "@elementor/editor-documents": "4.3.0-1049",
46
+ "@elementor/editor-elements": "4.3.0-1049",
47
+ "@elementor/editor-interactions": "4.3.0-1049",
48
+ "@elementor/editor-notifications": "4.3.0-1049",
49
+ "@elementor/editor-panels": "4.3.0-1049",
50
+ "@elementor/editor-props": "4.3.0-1049",
51
+ "@elementor/editor-responsive": "4.3.0-1049",
52
+ "@elementor/editor-styles": "4.3.0-1049",
53
+ "@elementor/editor-styles-repository": "4.3.0-1049",
54
+ "@elementor/editor-ui": "4.3.0-1049",
55
+ "@elementor/editor-v1-adapters": "4.3.0-1049",
56
+ "@elementor/http-client": "4.3.0-1049",
57
57
  "@elementor/icons": "~1.75.1",
58
- "@elementor/editor-variables": "4.3.0-1048",
59
- "@elementor/locations": "4.3.0-1048",
60
- "@elementor/menus": "4.3.0-1048",
61
- "@elementor/query": "4.3.0-1048",
62
- "@elementor/schema": "4.3.0-1048",
63
- "@elementor/session": "4.3.0-1048",
58
+ "@elementor/editor-variables": "4.3.0-1049",
59
+ "@elementor/locations": "4.3.0-1049",
60
+ "@elementor/menus": "4.3.0-1049",
61
+ "@elementor/query": "4.3.0-1049",
62
+ "@elementor/schema": "4.3.0-1049",
63
+ "@elementor/session": "4.3.0-1049",
64
64
  "@elementor/ui": "1.37.5",
65
- "@elementor/utils": "4.3.0-1048",
66
- "@elementor/wp-media": "4.3.0-1048",
65
+ "@elementor/utils": "4.3.0-1049",
66
+ "@elementor/wp-media": "4.3.0-1049",
67
67
  "@wordpress/i18n": "^5.13.0"
68
68
  },
69
69
  "peerDependencies": {
@@ -0,0 +1,141 @@
1
+ import * as React from 'react';
2
+ import { ControlFormLabel, Repeater, type RepeaterItem, type SetRepeaterValuesMeta } from '@elementor/editor-controls';
3
+ import { updateElementEditorSettings, useElementChildren, useElementEditorSettings } from '@elementor/editor-elements';
4
+ import { booleanPropTypeUtil, type CreateOptions } from '@elementor/editor-props';
5
+ import { Stack, TextField } from '@elementor/ui';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { useElement } from '../../../contexts/element-context';
9
+ import { ACCORDION_ELEMENT_TYPE, ACCORDION_ITEM_ELEMENT_TYPE, type AccordionItem, useActions } from './use-actions';
10
+ import { useShowIconWriteThrough } from './use-show-icon-write-through';
11
+
12
+ export const AccordionItemsControl = ( { label }: { label: string } ) => {
13
+ const { element, settings } = useElement();
14
+ const { addItem, duplicateItem, moveItem, removeItem } = useActions();
15
+
16
+ // Items are direct children of the root, so the root itself is the parent to read them from —
17
+ // there is no menu / content-area indirection as in Tabs.
18
+ const { [ ACCORDION_ITEM_ELEMENT_TYPE ]: items } = useElementChildren(
19
+ element.id,
20
+ { [ ACCORDION_ELEMENT_TYPE ]: ACCORDION_ITEM_ELEMENT_TYPE },
21
+ { includeSelfAsParent: true }
22
+ );
23
+
24
+ // The root's `show_icon` (rendered elsewhere in this same "Content" section via the standard
25
+ // `Switch_Control` -> `SettingsField` pipeline). Read here, alongside the repeater, purely to
26
+ // drive the write-through to every head - see `useShowIconWriteThrough` for why this can't live
27
+ // inside the generic switch control itself.
28
+ const showIcon = booleanPropTypeUtil.extract( settings.show_icon ) ?? true;
29
+
30
+ useShowIconWriteThrough( element.id, showIcon );
31
+
32
+ const repeaterValues: RepeaterItem< AccordionItem >[] = items.map( ( item, index ) => {
33
+ return {
34
+ id: item.id,
35
+ title: item.editorSettings?.title,
36
+ index,
37
+ };
38
+ } );
39
+
40
+ const setValue = (
41
+ _newValues: RepeaterItem< AccordionItem >[],
42
+ _options: CreateOptions,
43
+ meta?: SetRepeaterValuesMeta< RepeaterItem< AccordionItem > >
44
+ ) => {
45
+ if ( meta?.action?.type === 'add' ) {
46
+ return addItem( {
47
+ accordionId: element.id,
48
+ // The new item's number comes from the titles already in use, not from the item count -
49
+ // see `getNextItemNumber`.
50
+ existingTitles: repeaterValues.map( ( { title } ) => title ),
51
+ items: meta.action.payload,
52
+ showIcon,
53
+ } );
54
+ }
55
+
56
+ if ( meta?.action?.type === 'remove' ) {
57
+ return removeItem( { items: meta.action.payload } );
58
+ }
59
+
60
+ if ( meta?.action?.type === 'duplicate' ) {
61
+ return duplicateItem( { items: meta.action.payload } );
62
+ }
63
+
64
+ if ( meta?.action?.type === 'reorder' ) {
65
+ const { from, to } = meta.action.payload;
66
+
67
+ return moveItem( {
68
+ accordionId: element.id,
69
+ movedElementId: items[ from ].id,
70
+ toIndex: to,
71
+ } );
72
+ }
73
+ };
74
+
75
+ return (
76
+ <Repeater
77
+ showToggle={ false }
78
+ values={ repeaterValues }
79
+ setValues={ setValue }
80
+ showRemove={ repeaterValues.length > 1 }
81
+ label={ label }
82
+ // Accordion items are element children, not a prop on the root, so this repeater has no
83
+ // prop of its own to bind to - and the default adornments include the component
84
+ // "expose property" indicator, which reads whatever prop key is in context. Wrapping the
85
+ // repeater in a `SettingsField` just to give that indicator a context (the way Tabs does
86
+ // with `default-active-tab`) made exposing "Accordion Items" write the overridable wrapper
87
+ // into `default_state`, so the Default State toggle - the real owner of that key - turned
88
+ // exposed at the same time. Nothing here is exposable, hence no adornments at all.
89
+ adornment={ () => null }
90
+ itemSettings={ {
91
+ getId: ( { item } ) => item.id,
92
+ initialValues: { id: '', title: __( 'Accordion Item', 'elementor' ) },
93
+ Label: ItemLabel,
94
+ Content: ItemContent,
95
+ Icon: () => null,
96
+ } }
97
+ />
98
+ );
99
+ };
100
+
101
+ const ItemLabel = ( { value }: { value: AccordionItem } ) => {
102
+ return (
103
+ <Stack sx={ { minHeight: 20 } } direction="row" alignItems="center" gap={ 1.5 }>
104
+ <span>{ value?.title }</span>
105
+ </Stack>
106
+ );
107
+ };
108
+
109
+ const ItemContent = ( { value }: { value: AccordionItem } ) => {
110
+ if ( ! value.id ) {
111
+ return null;
112
+ }
113
+
114
+ return (
115
+ <Stack p={ 2 } gap={ 1.5 }>
116
+ <ItemNameControl elementId={ value.id } />
117
+ </Stack>
118
+ );
119
+ };
120
+
121
+ const ItemNameControl = ( { elementId }: { elementId: string } ) => {
122
+ const editorSettings = useElementEditorSettings( elementId );
123
+
124
+ const label = editorSettings?.title ?? '';
125
+
126
+ return (
127
+ <Stack gap={ 1 }>
128
+ <ControlFormLabel>{ __( 'Name', 'elementor' ) }</ControlFormLabel>
129
+ <TextField
130
+ size="tiny"
131
+ value={ label }
132
+ onChange={ ( { target }: React.ChangeEvent< HTMLInputElement > ) => {
133
+ updateElementEditorSettings( {
134
+ elementId,
135
+ settings: { title: target.value },
136
+ } );
137
+ } }
138
+ />
139
+ </Stack>
140
+ );
141
+ };
@@ -0,0 +1,235 @@
1
+ import { type ItemsActionPayload } from '@elementor/editor-controls';
2
+ import {
3
+ type CreateElementParams,
4
+ createElements,
5
+ duplicateElements,
6
+ generateElementId,
7
+ getContainer,
8
+ moveElements,
9
+ removeElements,
10
+ type V1ElementData,
11
+ } from '@elementor/editor-elements';
12
+ import { booleanPropTypeUtil, escapedHtmlPropTypeUtil } from '@elementor/editor-props';
13
+ import { __, sprintf } from '@wordpress/i18n';
14
+
15
+ export type AccordionItem = {
16
+ id: string;
17
+ title?: string;
18
+ };
19
+
20
+ export const ACCORDION_ELEMENT_TYPE = 'e-accordion';
21
+ export const ACCORDION_ITEM_ELEMENT_TYPE = 'e-accordion-item';
22
+
23
+ const ACCORDION_ITEM_HEADER_ELEMENT_TYPE = 'e-accordion-item-header';
24
+ const ACCORDION_ITEM_TITLE_ELEMENT_TYPE = 'e-accordion-item-title';
25
+ const ACCORDION_ITEM_ICON_ELEMENT_TYPE = 'e-accordion-item-icon';
26
+ const ACCORDION_ITEM_CONTENT_ELEMENT_TYPE = 'e-accordion-item-content';
27
+ const PARAGRAPH_WIDGET_TYPE = 'e-paragraph';
28
+
29
+ const getItemTitle = ( position: number ) =>
30
+ /* translators: %d: Accordion item position. */
31
+ sprintf( __( 'Accordion Item %d', 'elementor' ), position );
32
+
33
+ const TRAILING_NUMBER = /(\d+)\s*$/;
34
+
35
+ // Picks the number for a newly added item from the titles that exist right now, never from how many
36
+ // items there are.
37
+ //
38
+ // The Repeater's add action reports `index = items.length`, i.e. the current visible count. Deriving
39
+ // the number from that collides after a delete: remove "Accordion Item 1" from the default pair and
40
+ // the count drops to 1, so the next add would be numbered 2 — a duplicate of the surviving
41
+ // "Accordion Item 2". The number is not just a Structure-panel label, it is baked into the item's
42
+ // rendered paragraph, so the canvas would show two identical headers.
43
+ //
44
+ // Taking `max( trailing number of each existing title ) + 1` keeps the surviving item in view when
45
+ // the number is chosen, which is what fixes that case. The final loop then guarantees the property
46
+ // we actually care about — the new title differs from every title currently present — even when a
47
+ // user has renamed items by hand. It cannot know about numbers used by items that were already
48
+ // deleted, so a delete-the-highest-then-add sequence can still reuse a number; that reuse is never a
49
+ // visible duplicate, which is the failure that matters.
50
+ const getNextItemNumber = ( existingTitles: ( string | undefined )[] ) => {
51
+ const taken = new Set( existingTitles.filter( ( title ): title is string => Boolean( title ) ) );
52
+
53
+ const highest = existingTitles.reduce( ( max: number, title ) => {
54
+ const [ , trailingNumber ] = title?.match( TRAILING_NUMBER ) ?? [];
55
+ const parsed = trailingNumber ? Number( trailingNumber ) : 0;
56
+
57
+ return Number.isFinite( parsed ) && parsed > max ? parsed : max;
58
+ }, 0 );
59
+
60
+ let next = highest + 1;
61
+
62
+ while ( taken.has( getItemTitle( next ) ) ) {
63
+ next += 1;
64
+ }
65
+
66
+ return next;
67
+ };
68
+
69
+ // Builds one `e-accordion-item` subtree, mirroring `Atomic_Accordion::build_default_item()`.
70
+ //
71
+ // Each level of an atomic element's default children is hydrated independently
72
+ // (`AtomicElementBaseModel.onElementCreate()`), so the title slot's own defaults cannot know which
73
+ // item they belong to and would render an unnumbered "Accordion Item". Spelling the
74
+ // item -> header -> title -> paragraph chain out here is what lets the numbered text reach the
75
+ // rendered paragraph, exactly as the default two-item tree does. The icon and content branches need
76
+ // no per-index content, so they keep hydrating their own defaults.
77
+ //
78
+ // `V1ElementData` is the plain-object form of a model tree: nested `elements` are objects that
79
+ // Backbone turns into models on `initialize`, and ids have to be generated here because only the
80
+ // outermost model gets one from `document/elements/create`.
81
+ const buildItemModel = ( position: number, showIcon: boolean ): V1ElementData => {
82
+ const numberedTitle = getItemTitle( position );
83
+
84
+ return {
85
+ elType: ACCORDION_ITEM_ELEMENT_TYPE,
86
+ id: generateElementId(),
87
+ editor_settings: { title: numberedTitle, initial_position: position },
88
+ elements: [
89
+ {
90
+ elType: ACCORDION_ITEM_HEADER_ELEMENT_TYPE,
91
+ id: generateElementId(),
92
+ editor_settings: { title: __( 'Header', 'elementor' ) },
93
+ // Seeded from the root's *current* `show_icon` value, not the schema default: if a user
94
+ // has already turned Show Icon off, a newly added item must start with its icon hidden
95
+ // too, not re-show one just because the item itself is brand new. See the comment on
96
+ // the mirrored prop in `Atomic_Accordion_Item_Header` for why this duplication exists.
97
+ settings: { show_icon: booleanPropTypeUtil.create( showIcon ) },
98
+ elements: [
99
+ {
100
+ elType: ACCORDION_ITEM_TITLE_ELEMENT_TYPE,
101
+ id: generateElementId(),
102
+ editor_settings: { title: __( 'Title', 'elementor' ) },
103
+ elements: [
104
+ {
105
+ elType: 'widget',
106
+ widgetType: PARAGRAPH_WIDGET_TYPE,
107
+ id: generateElementId(),
108
+ // A leaf still needs an explicit empty `elements`: `ElementModel.initialize()` only
109
+ // turns the attribute into a collection when it is defined, and v1 code walking a
110
+ // subtree (`deselectRecursive()` on delete) calls `.forEach()` on it unguarded.
111
+ elements: [],
112
+ settings: {
113
+ paragraph: escapedHtmlPropTypeUtil.create( numberedTitle ),
114
+ tag: { $$type: 'string', value: 'span' },
115
+ },
116
+ },
117
+ ],
118
+ },
119
+ {
120
+ elType: ACCORDION_ITEM_ICON_ELEMENT_TYPE,
121
+ id: generateElementId(),
122
+ editor_settings: { title: __( 'Icon', 'elementor' ) },
123
+ elements: [],
124
+ hydrateDefaultChildren: true,
125
+ },
126
+ ],
127
+ },
128
+ {
129
+ elType: ACCORDION_ITEM_CONTENT_ELEMENT_TYPE,
130
+ id: generateElementId(),
131
+ editor_settings: { title: __( 'Content', 'elementor' ) },
132
+ elements: [],
133
+ hydrateDefaultChildren: true,
134
+ },
135
+ ],
136
+ };
137
+ };
138
+
139
+ export const useActions = () => {
140
+ const addItem = ( {
141
+ accordionId,
142
+ existingTitles,
143
+ items,
144
+ showIcon,
145
+ }: {
146
+ accordionId: string;
147
+ existingTitles: ( string | undefined )[];
148
+ items: ItemsActionPayload< AccordionItem >;
149
+ // The root's *current* `show_icon` value, so the new item's header starts in sync with it
150
+ // instead of the schema default - see the comment on `buildItemModel`.
151
+ showIcon: boolean;
152
+ } ) => {
153
+ const accordion = getContainer( accordionId );
154
+
155
+ if ( ! accordion ) {
156
+ throw new Error( 'Accordion container not found' );
157
+ }
158
+
159
+ // Grows as we go, so adding several items in one action can't hand out the same number twice.
160
+ const titles = [ ...existingTitles ];
161
+
162
+ items.forEach( () => {
163
+ const position = getNextItemNumber( titles );
164
+
165
+ createElements( {
166
+ title: __( 'Accordion', 'elementor' ),
167
+ elements: [
168
+ {
169
+ container: accordion,
170
+ // `buildItemModel()` returns `V1ElementData`, whose `elements` field is plain nested
171
+ // data (`V1ElementData[]`) - correct for a tree we're constructing to send as a
172
+ // creation payload. `CreateElementParams['model']` is derived from
173
+ // `V1ElementModelProps`, whose `elements` field is typed as `V1Model<...>[]` - live
174
+ // Backbone-model wrappers with `get`/`set`/`toJSON`, the shape for reading an
175
+ // *existing* element out of the document, not for describing one to create. A single
176
+ // cast is enough (the two types overlap enough for TS to allow it directly); no
177
+ // `unknown` escape hatch needed.
178
+ model: buildItemModel( position, showIcon ) as CreateElementParams[ 'model' ],
179
+ },
180
+ ],
181
+ } );
182
+
183
+ titles.push( getItemTitle( position ) );
184
+ } );
185
+ };
186
+
187
+ const removeItem = ( { items }: { items: ItemsActionPayload< AccordionItem > } ) => {
188
+ removeElements( {
189
+ title: __( 'Accordion', 'elementor' ),
190
+ elementIds: items.map( ( { item } ) => item.id ),
191
+ } );
192
+ };
193
+
194
+ const duplicateItem = ( { items }: { items: ItemsActionPayload< AccordionItem > } ) => {
195
+ duplicateElements( {
196
+ title: __( 'Duplicate Accordion Item', 'elementor' ),
197
+ elementIds: items.map( ( { item } ) => item.id ),
198
+ } );
199
+ };
200
+
201
+ const moveItem = ( {
202
+ accordionId,
203
+ movedElementId,
204
+ toIndex,
205
+ }: {
206
+ accordionId: string;
207
+ movedElementId: string;
208
+ toIndex: number;
209
+ } ) => {
210
+ const accordion = getContainer( accordionId );
211
+ const movedElement = getContainer( movedElementId );
212
+
213
+ if ( ! accordion || ! movedElement ) {
214
+ throw new Error( 'Accordion item or container not found' );
215
+ }
216
+
217
+ moveElements( {
218
+ title: __( 'Reorder Accordion Items', 'elementor' ),
219
+ moves: [
220
+ {
221
+ element: movedElement,
222
+ targetContainer: accordion,
223
+ options: { at: toIndex },
224
+ },
225
+ ],
226
+ } );
227
+ };
228
+
229
+ return {
230
+ addItem,
231
+ removeItem,
232
+ duplicateItem,
233
+ moveItem,
234
+ };
235
+ };
@@ -0,0 +1,135 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { getContainer, getElementSettings, updateElementSettings, type V1Element } from '@elementor/editor-elements';
3
+ import { booleanPropTypeUtil } from '@elementor/editor-props';
4
+ import { undoable } from '@elementor/editor-v1-adapters';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { HISTORY_DEBOUNCE_WAIT } from '../../../hooks/use-styles-fields';
8
+ import { ACCORDION_ITEM_ELEMENT_TYPE } from './use-actions';
9
+
10
+ const ACCORDION_ITEM_HEADER_ELEMENT_TYPE = 'e-accordion-item-header';
11
+
12
+ type CascadeShowIconPayload = {
13
+ accordionId: string;
14
+ showIcon: boolean;
15
+ };
16
+
17
+ // A header's `show_icon` is either an explicit boolean prop value (`booleanPropTypeUtil.create`'s
18
+ // return shape) or `null`/absent when it was never set - see `undo()` below for why both must
19
+ // round-trip correctly.
20
+ type CascadeShowIconResult = {
21
+ previous: Record< string, ReturnType< typeof booleanPropTypeUtil.create > | null | undefined >;
22
+ };
23
+
24
+ // `show_icon` is global on the root (`Atomic_Accordion::define_props_schema()`); every
25
+ // `e-accordion-item-header` carries a *mirrored* `show_icon` prop of its own purely because the
26
+ // children-dependencies reconciler evaluates a rule against the declaring element's own settings
27
+ // and can only attach/detach that element's own direct children (see the comment on both props in
28
+ // PHP). This function is the write-through: it pushes the root's current value onto every header's
29
+ // mirrored prop, in one undo-able history transaction, so a single undo restores every header at
30
+ // once. It does not touch the icon child itself - each header's own `Child_Dependency` (bound to that
31
+ // header's settings `change` event) reacts to the prop flip and attaches/detaches/restashes the icon
32
+ // on its own, exactly as it would if the header's `show_icon` had been changed directly.
33
+ export const cascadeShowIconToHeaders = undoable< CascadeShowIconPayload, CascadeShowIconResult, undefined >(
34
+ {
35
+ do: ( { accordionId, showIcon } ) => {
36
+ const headerIds = getAccordionHeaderIds( accordionId );
37
+
38
+ const previous = Object.fromEntries(
39
+ headerIds.map(
40
+ ( headerId ) =>
41
+ [
42
+ headerId,
43
+ getElementSettings< ReturnType< typeof booleanPropTypeUtil.create > >( headerId, [
44
+ 'show_icon',
45
+ ] ).show_icon,
46
+ ] as const
47
+ )
48
+ );
49
+
50
+ headerIds.forEach( ( headerId ) => {
51
+ updateElementSettings( {
52
+ id: headerId,
53
+ props: { show_icon: booleanPropTypeUtil.create( showIcon ) },
54
+ withHistory: false,
55
+ } );
56
+ } );
57
+
58
+ return { previous };
59
+ },
60
+ undo: ( _payload, { previous } ) => {
61
+ // Write back whatever `previousValue` actually was, including `null`/`undefined` (a header
62
+ // whose `show_icon` was never explicitly set before the cascade). Skipping on falsy here
63
+ // used to only ever skip that unset case - explicit `false` is a *prop object*
64
+ // (`{ $$type: 'boolean', value: false }`), never itself falsy - so the guard silently ate
65
+ // the common case (the default two-item tree's headers start with `show_icon` unset) instead
66
+ // of the one it looked like it was guarding against. An explicit `null` evaluates the same
67
+ // as "unset" in the `Child_Dependency` reconciler (`extractValue()` -> `?.value` is
68
+ // `undefined` either way), so this restores the icon exactly as it was.
69
+ Object.entries( previous ).forEach( ( [ headerId, previousValue ] ) => {
70
+ updateElementSettings( {
71
+ id: headerId,
72
+ props: { show_icon: previousValue ?? null },
73
+ withHistory: false,
74
+ } );
75
+ } );
76
+ },
77
+ },
78
+ {
79
+ title: __( 'Accordion', 'elementor' ),
80
+ subtitle: __( 'Show Icon', 'elementor' ),
81
+ // `undoable()`'s debounce only delays when the *history entry* is pushed onto the undo stack -
82
+ // the settings write itself (`do()`) still runs synchronously on every call. The root's own
83
+ // `show_icon` change goes through `SettingsField` -> `useUndoableUpdateElementProp`
84
+ // (`settings-field.tsx`), which debounces its history push by `HISTORY_DEBOUNCE_WAIT` (800ms).
85
+ // Without matching that here, this cascade's history entry (pushed immediately) would land on
86
+ // the undo stack *before* the root's (pushed 800ms later), so the first Undo after a toggle
87
+ // would revert the root switch alone while every header stayed on its new value - the switch and
88
+ // the icons would visibly disagree until a second Undo. Matching the debounce window fixes the
89
+ // stack order to be deterministic (root's own entry, then this one) regardless of how quickly a
90
+ // user hits Undo. It does not make the two changes one atomic transaction - see the comment on
91
+ // `useShowIconWriteThrough` for why that would require forking the shared `Switch_Control`.
92
+ debounce: { wait: HISTORY_DEBOUNCE_WAIT },
93
+ }
94
+ );
95
+
96
+ function getAccordionHeaderIds( accordionId: string ): string[] {
97
+ const accordion = getContainer( accordionId );
98
+
99
+ const itemContainers = ( accordion?.children ?? [] ).filter(
100
+ ( child ) => child.model.get( 'elType' ) === ACCORDION_ITEM_ELEMENT_TYPE
101
+ );
102
+
103
+ const headerContainers = itemContainers
104
+ .map(
105
+ ( item ) =>
106
+ item.children?.find( ( child ) => child.model.get( 'elType' ) === ACCORDION_ITEM_HEADER_ELEMENT_TYPE )
107
+ )
108
+ .filter( ( header ): header is V1Element => Boolean( header ) );
109
+
110
+ return headerContainers.map( ( header ) => header.id );
111
+ }
112
+
113
+ // Watches the root's *current* `show_icon` value (read reactively from the panel's element
114
+ // settings) and cascades it to every header on change. Skips the very first render for a given
115
+ // element so mounting the panel on an already-toggled-off accordion doesn't re-fire a no-op
116
+ // cascade. Deliberately lives alongside the repeater control rather than inside the generic
117
+ // `Switch_Control` -> `SettingsField` pipeline: that pipeline is shared by every atomic element's
118
+ // switch controls, so it is not a safe place to special-case one element's cross-element write-
119
+ // through.
120
+ export function useShowIconWriteThrough( accordionId: string, showIcon: boolean ): void {
121
+ const previousRef = useRef< { accordionId: string; showIcon: boolean } | null >( null );
122
+
123
+ useEffect( () => {
124
+ const previous = previousRef.current;
125
+ previousRef.current = { accordionId, showIcon };
126
+
127
+ // No baseline yet, or the accordion identity changed (e.g. a different element got selected
128
+ // into the same mounted control) - just record the new baseline, never cascade.
129
+ if ( ! previous || previous.accordionId !== accordionId || previous.showIcon === showIcon ) {
130
+ return;
131
+ }
132
+
133
+ cascadeShowIconToHeaders( { accordionId, showIcon } );
134
+ }, [ accordionId, showIcon ] );
135
+ }
@@ -1,10 +1,12 @@
1
1
  import { type ControlComponent } from '@elementor/editor-controls';
2
2
 
3
3
  import { type ControlRegistry, controlsRegistry } from '../controls-registry';
4
+ import { AccordionItemsControl } from './accordion-items-control/accordion-items-control';
4
5
  import { TabsControl } from './tabs-control/tabs-control';
5
6
 
6
7
  const controlTypes = {
7
8
  tabs: { component: TabsControl as ControlComponent, layout: 'full' },
9
+ 'accordion-items': { component: AccordionItemsControl as ControlComponent, layout: 'full' },
8
10
  } as const satisfies ControlRegistry;
9
11
 
10
12
  export const registerElementControls = () => {