@@ -40,8 +40,38 @@ const enum AccordionState {
4040export class Accordion implements ComponentInterface {
4141 private accordionGroupEl ?: HTMLIonAccordionGroupElement | null ;
4242 private updateListener = ( ev : CustomEvent < AccordionGroupChangeEventDetail > ) => {
43- const initialUpdate = ev . detail ?. initial ?? false ;
44- this . updateState ( initialUpdate ) ;
43+ /**
44+ * Determine if this update will cause an actual state change.
45+ * We only want to mark as "interacted" if the state is changing.
46+ */
47+ const accordionGroup = this . accordionGroupEl ;
48+ if ( accordionGroup ) {
49+ const value = accordionGroup . value ;
50+ const accordionValue = this . value ;
51+ const shouldExpand = Array . isArray ( value ) ? value . includes ( accordionValue ) : value === accordionValue ;
52+ const isExpanded = this . state === AccordionState . Expanded || this . state === AccordionState . Expanding ;
53+ const stateWillChange = shouldExpand !== isExpanded ;
54+
55+ /**
56+ * Only mark as interacted if:
57+ * 1. This is not the first update we've received with a defined value
58+ * 2. The state is actually changing (prevents redundant updates from enabling animations)
59+ */
60+ if ( this . hasReceivedFirstUpdate && stateWillChange ) {
61+ this . hasInteracted = true ;
62+ }
63+
64+ /**
65+ * Only count this as the first update if the group value is defined.
66+ * This prevents the initial undefined value from the group's componentDidLoad
67+ * from being treated as the first real update.
68+ */
69+ if ( value !== undefined ) {
70+ this . hasReceivedFirstUpdate = true ;
71+ }
72+ }
73+
74+ this . updateState ( ) ;
4575 } ;
4676 private contentEl : HTMLDivElement | undefined ;
4777 private contentElWrapper : HTMLDivElement | undefined ;
@@ -55,12 +85,24 @@ export class Accordion implements ComponentInterface {
5585 @State ( ) isNext = false ;
5686 @State ( ) isPrevious = false ;
5787 /**
58- * Tracks whether the component has completed its initial render .
59- * Animations are disabled until after the first render completes .
60- * This prevents the accordion from animating when it starts
61- * expanded or collapsed on initial load.
88+ * Tracks whether a user-initiated interaction has occurred .
89+ * Animations are disabled until the first interaction happens .
90+ * This prevents the accordion from animating when it's programmatically
91+ * set to an expanded or collapsed state on initial load.
6292 */
63- @State ( ) hasRendered = false ;
93+ @State ( ) hasInteracted = false ;
94+
95+ /**
96+ * Tracks if this accordion has ever been expanded.
97+ * Used to prevent the first expansion from animating.
98+ */
99+ private hasEverBeenExpanded = false ;
100+
101+ /**
102+ * Tracks if this accordion has received its first update from the group.
103+ * Used to distinguish initial programmatic sets from user interactions.
104+ */
105+ private hasReceivedFirstUpdate = false ;
64106
65107 /**
66108 * The value of the accordion. Defaults to an autogenerated
@@ -99,7 +141,7 @@ export class Accordion implements ComponentInterface {
99141 connectedCallback ( ) {
100142 const accordionGroupEl = ( this . accordionGroupEl = this . el ?. closest ( 'ion-accordion-group' ) ) ;
101143 if ( accordionGroupEl ) {
102- this . updateState ( true ) ;
144+ this . updateState ( ) ;
103145 addEventListener ( accordionGroupEl , 'ionValueChange' , this . updateListener ) ;
104146 }
105147 }
@@ -130,18 +172,6 @@ export class Accordion implements ComponentInterface {
130172 } ) ;
131173 }
132174
133- componentDidRender ( ) {
134- /**
135- * After the first render completes, mark that we've rendered.
136- * Setting this state property triggers a re-render, at which point
137- * animations will be enabled. This ensures animations are disabled
138- * only for the initial render, avoiding unwanted animations on load.
139- */
140- if ( ! this . hasRendered ) {
141- this . hasRendered = true ;
142- }
143- }
144-
145175 private setItemDefaults = ( ) => {
146176 const ionItem = this . getSlottedHeaderIonItem ( ) ;
147177 if ( ! ionItem ) {
@@ -235,10 +265,16 @@ export class Accordion implements ComponentInterface {
235265 ionItem . appendChild ( iconEl ) ;
236266 } ;
237267
238- private expandAccordion = ( initialUpdate = false ) => {
268+ private expandAccordion = ( ) => {
239269 const { contentEl, contentElWrapper } = this ;
240- if ( initialUpdate || contentEl === undefined || contentElWrapper === undefined ) {
270+
271+ /**
272+ * If the content elements aren't available yet, just set the state.
273+ * This happens on initial render before the DOM is ready.
274+ */
275+ if ( contentEl === undefined || contentElWrapper === undefined ) {
241276 this . state = AccordionState . Expanded ;
277+ this . hasEverBeenExpanded = true ;
242278 return ;
243279 }
244280
@@ -250,6 +286,12 @@ export class Accordion implements ComponentInterface {
250286 cancelAnimationFrame ( this . currentRaf ) ;
251287 }
252288
289+ /**
290+ * Mark that this accordion has been expanded at least once.
291+ * This allows subsequent expansions to animate.
292+ */
293+ this . hasEverBeenExpanded = true ;
294+
253295 if ( this . shouldAnimate ( ) ) {
254296 raf ( ( ) => {
255297 this . state = AccordionState . Expanding ;
@@ -270,9 +312,14 @@ export class Accordion implements ComponentInterface {
270312 }
271313 } ;
272314
273- private collapseAccordion = ( initialUpdate = false ) => {
315+ private collapseAccordion = ( ) => {
274316 const { contentEl } = this ;
275- if ( initialUpdate || contentEl === undefined ) {
317+
318+ /**
319+ * If the content element isn't available yet, just set the state.
320+ * This happens on initial render before the DOM is ready.
321+ */
322+ if ( contentEl === undefined ) {
276323 this . state = AccordionState . Collapsed ;
277324 return ;
278325 }
@@ -315,11 +362,15 @@ export class Accordion implements ComponentInterface {
315362 */
316363 private shouldAnimate = ( ) => {
317364 /**
318- * Don't animate until after the first render cycle completes .
365+ * Don't animate until after the first user interaction .
319366 * This prevents animations on initial load when accordions
320- * start in an expanded or collapsed state.
367+ * start in an expanded or collapsed state programmatically.
368+ *
369+ * Additionally, don't animate the very first expansion even if
370+ * hasInteracted is true. This handles edge cases like React StrictMode
371+ * where effects run twice and might incorrectly mark as interacted.
321372 */
322- if ( ! this . hasRendered ) {
373+ if ( ! this . hasInteracted || ! this . hasEverBeenExpanded ) {
323374 return false ;
324375 }
325376
@@ -344,7 +395,7 @@ export class Accordion implements ComponentInterface {
344395 return true ;
345396 } ;
346397
347- private updateState = async ( initialUpdate = false ) => {
398+ private updateState = async ( ) => {
348399 const accordionGroup = this . accordionGroupEl ;
349400 const accordionValue = this . value ;
350401
@@ -357,10 +408,10 @@ export class Accordion implements ComponentInterface {
357408 const shouldExpand = Array . isArray ( value ) ? value . includes ( accordionValue ) : value === accordionValue ;
358409
359410 if ( shouldExpand ) {
360- this . expandAccordion ( initialUpdate ) ;
411+ this . expandAccordion ( ) ;
361412 this . isNext = this . isPrevious = false ;
362413 } else {
363- this . collapseAccordion ( initialUpdate ) ;
414+ this . collapseAccordion ( ) ;
364415
365416 /**
366417 * When using popout or inset,
@@ -418,6 +469,12 @@ export class Accordion implements ComponentInterface {
418469
419470 if ( disabled || readonly ) return ;
420471
472+ /**
473+ * Mark that the user has interacted with the accordion.
474+ * This enables animations for all future state changes.
475+ */
476+ this . hasInteracted = true ;
477+
421478 if ( accordionGroupEl ) {
422479 /**
423480 * Because the accordion group may or may
0 commit comments