@@ -5,7 +5,6 @@ import { chevronDown } from 'ionicons/icons';
55
66import { config } from '../../global/config' ;
77import { getIonMode } from '../../global/ionic-global' ;
8- import type { AccordionGroupChangeEventDetail } from '../accordion-group/accordion-group-interface' ;
98
109const enum AccordionState {
1110 Collapsed = 1 << 0 ,
@@ -39,9 +38,39 @@ const enum AccordionState {
3938} )
4039export class Accordion implements ComponentInterface {
4140 private accordionGroupEl ?: HTMLIonAccordionGroupElement | null ;
42- private updateListener = ( ev : CustomEvent < AccordionGroupChangeEventDetail > ) => {
43- const initialUpdate = ev . detail ?. initial ?? false ;
44- this . updateState ( initialUpdate ) ;
41+ private updateListener = ( ) => {
42+ /**
43+ * Determine if this update will cause an actual state change.
44+ * We only want to mark as "interacted" if the state is changing.
45+ */
46+ const accordionGroup = this . accordionGroupEl ;
47+ if ( accordionGroup ) {
48+ const value = accordionGroup . value ;
49+ const accordionValue = this . value ;
50+ const shouldExpand = Array . isArray ( value ) ? value . includes ( accordionValue ) : value === accordionValue ;
51+ const isExpanded = this . state === AccordionState . Expanded || this . state === AccordionState . Expanding ;
52+ const stateWillChange = shouldExpand !== isExpanded ;
53+
54+ /**
55+ * Only mark as interacted if:
56+ * 1. This is not the first update we've received with a defined value
57+ * 2. The state is actually changing (prevents redundant updates from enabling animations)
58+ */
59+ if ( this . hasReceivedFirstUpdate && stateWillChange ) {
60+ this . hasInteracted = true ;
61+ }
62+
63+ /**
64+ * Only count this as the first update if the group value is defined.
65+ * This prevents the initial undefined value from the group's componentDidLoad
66+ * from being treated as the first real update.
67+ */
68+ if ( value !== undefined ) {
69+ this . hasReceivedFirstUpdate = true ;
70+ }
71+ }
72+
73+ this . updateState ( ) ;
4574 } ;
4675 private contentEl : HTMLDivElement | undefined ;
4776 private contentElWrapper : HTMLDivElement | undefined ;
@@ -55,12 +84,24 @@ export class Accordion implements ComponentInterface {
5584 @State ( ) isNext = false ;
5685 @State ( ) isPrevious = false ;
5786 /**
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.
87+ * Tracks whether a user-initiated interaction has occurred .
88+ * Animations are disabled until the first interaction happens .
89+ * This prevents the accordion from animating when it's programmatically
90+ * set to an expanded or collapsed state on initial load.
6291 */
63- @State ( ) hasRendered = false ;
92+ @State ( ) hasInteracted = false ;
93+
94+ /**
95+ * Tracks if this accordion has ever been expanded.
96+ * Used to prevent the first expansion from animating.
97+ */
98+ private hasEverBeenExpanded = false ;
99+
100+ /**
101+ * Tracks if this accordion has received its first update from the group.
102+ * Used to distinguish initial programmatic sets from user interactions.
103+ */
104+ private hasReceivedFirstUpdate = false ;
64105
65106 /**
66107 * The value of the accordion. Defaults to an autogenerated
@@ -99,7 +140,7 @@ export class Accordion implements ComponentInterface {
99140 connectedCallback ( ) {
100141 const accordionGroupEl = ( this . accordionGroupEl = this . el ?. closest ( 'ion-accordion-group' ) ) ;
101142 if ( accordionGroupEl ) {
102- this . updateState ( true ) ;
143+ this . updateState ( ) ;
103144 addEventListener ( accordionGroupEl , 'ionValueChange' , this . updateListener ) ;
104145 }
105146 }
@@ -130,18 +171,6 @@ export class Accordion implements ComponentInterface {
130171 } ) ;
131172 }
132173
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-
145174 private setItemDefaults = ( ) => {
146175 const ionItem = this . getSlottedHeaderIonItem ( ) ;
147176 if ( ! ionItem ) {
@@ -235,10 +264,16 @@ export class Accordion implements ComponentInterface {
235264 ionItem . appendChild ( iconEl ) ;
236265 } ;
237266
238- private expandAccordion = ( initialUpdate = false ) => {
267+ private expandAccordion = ( ) => {
239268 const { contentEl, contentElWrapper } = this ;
240- if ( initialUpdate || contentEl === undefined || contentElWrapper === undefined ) {
269+
270+ /**
271+ * If the content elements aren't available yet, just set the state.
272+ * This happens on initial render before the DOM is ready.
273+ */
274+ if ( contentEl === undefined || contentElWrapper === undefined ) {
241275 this . state = AccordionState . Expanded ;
276+ this . hasEverBeenExpanded = true ;
242277 return ;
243278 }
244279
@@ -250,6 +285,12 @@ export class Accordion implements ComponentInterface {
250285 cancelAnimationFrame ( this . currentRaf ) ;
251286 }
252287
288+ /**
289+ * Mark that this accordion has been expanded at least once.
290+ * This allows subsequent expansions to animate.
291+ */
292+ this . hasEverBeenExpanded = true ;
293+
253294 if ( this . shouldAnimate ( ) ) {
254295 raf ( ( ) => {
255296 this . state = AccordionState . Expanding ;
@@ -270,9 +311,14 @@ export class Accordion implements ComponentInterface {
270311 }
271312 } ;
272313
273- private collapseAccordion = ( initialUpdate = false ) => {
314+ private collapseAccordion = ( ) => {
274315 const { contentEl } = this ;
275- if ( initialUpdate || contentEl === undefined ) {
316+
317+ /**
318+ * If the content element isn't available yet, just set the state.
319+ * This happens on initial render before the DOM is ready.
320+ */
321+ if ( contentEl === undefined ) {
276322 this . state = AccordionState . Collapsed ;
277323 return ;
278324 }
@@ -315,11 +361,15 @@ export class Accordion implements ComponentInterface {
315361 */
316362 private shouldAnimate = ( ) => {
317363 /**
318- * Don't animate until after the first render cycle completes .
364+ * Don't animate until after the first user interaction .
319365 * This prevents animations on initial load when accordions
320- * start in an expanded or collapsed state.
366+ * start in an expanded or collapsed state programmatically.
367+ *
368+ * Additionally, don't animate the very first expansion even if
369+ * hasInteracted is true. This handles edge cases like React StrictMode
370+ * where effects run twice and might incorrectly mark as interacted.
321371 */
322- if ( ! this . hasRendered ) {
372+ if ( ! this . hasInteracted || ! this . hasEverBeenExpanded ) {
323373 return false ;
324374 }
325375
@@ -344,7 +394,7 @@ export class Accordion implements ComponentInterface {
344394 return true ;
345395 } ;
346396
347- private updateState = async ( initialUpdate = false ) => {
397+ private updateState = async ( ) => {
348398 const accordionGroup = this . accordionGroupEl ;
349399 const accordionValue = this . value ;
350400
@@ -357,10 +407,10 @@ export class Accordion implements ComponentInterface {
357407 const shouldExpand = Array . isArray ( value ) ? value . includes ( accordionValue ) : value === accordionValue ;
358408
359409 if ( shouldExpand ) {
360- this . expandAccordion ( initialUpdate ) ;
410+ this . expandAccordion ( ) ;
361411 this . isNext = this . isPrevious = false ;
362412 } else {
363- this . collapseAccordion ( initialUpdate ) ;
413+ this . collapseAccordion ( ) ;
364414
365415 /**
366416 * When using popout or inset,
@@ -418,6 +468,12 @@ export class Accordion implements ComponentInterface {
418468
419469 if ( disabled || readonly ) return ;
420470
471+ /**
472+ * Mark that the user has interacted with the accordion.
473+ * This enables animations for all future state changes.
474+ */
475+ this . hasInteracted = true ;
476+
421477 if ( accordionGroupEl ) {
422478 /**
423479 * Because the accordion group may or may
0 commit comments