Posted on Sunday, September 27, 2026 Revisiting Interactive Report Control Breaks Category APEX Tips and Tricks In 2022 I wrote about adding expand and collapse controls to Interactive Report control breaks with a Dynamic Action and a small amount of JavaScript. Interactive Grid already had similar functionality, while Interactive Report did not, so the solution simply added a button to each control break header and toggled the rows belonging to that group. A few years later I revisited the code with Oracle APEX 26.1 and decided that, rather than publishing another updated JavaScript snippet, it made more sense to turn the idea into a reusable Dynamic Action plug-in.The original post is still a useful example of how Interactive Report control breaks can be manipulated on the client side: Collapse Interactive Report Control BreakThis post looks at what changed when turning that small solution into a proper plug-in.The Original ApproachThe original implementation was intentionally simple.A Dynamic Action was attached to the Interactive Report After Refresh event, and the JavaScript it executed searched for control break headers using:th.a-IRR-header--groupFor each control break header, the code added a button. Clicking the button found the following report rows up to the next control break header and toggled their visibility.Conceptually, the important part was only this:this$ .closest("tr") .nextUntil(":has(" + lCtrBreak + ")") .toggle();That remains the basic idea in the new implementation.The old code also needed to run after every Interactive Report refresh because APEX replaces the generated report markup when the report is refreshed.Why Revisit It?For a single page, the original snippet is perfectly reasonable.Once the same behavior is needed in several places, however, more questions appear:Should groups initially be expanded or collapsed?What happens after the report refreshes?Should a user's choices be remembered?How should multiple reports on the same page be handled?Can application developers expand or collapse all groups programmatically?How should the generated controls expose their state to assistive technology?How can another Dynamic Action react when a control break changes?One question on the original post was also how to have the groups initially collapsed rather than expanded. The original solution could do that with a small code change, but configuration is a better fit once the behavior becomes reusable.Those were good reasons to move the implementation out of Dynamic Action JavaScript and into a plug-in.IR Control Break TogglerThe result is IR Control Break Toggler, a Dynamic Action plug-in. The basic page setup is still very similar to the original post.Create a Dynamic Action on the Interactive Report:Event: After RefreshSelection Type: RegionRegion: <Your Interactive Report> Then add a True Action:Action: IR Control Break Toggler [Plug-In]Fire on Initialization: Yes There is no JavaScript to copy into the Dynamic Action anymore.The plug-in receives the report that triggered the Dynamic Action and initializes the control break buttons for that region.Remembering Control Break StateOne of the larger differences from the original implementation is that the state of individual groups can now optionally be remembered.The plug-in can use APEX scoped browser storage for either the current browser session or persistent browser storage.This means a user can collapse a particular group, refresh the Interactive Report, and have that group remain collapsed.The state is kept separate for each application, page, and report region.I also wanted changing the plug-in configuration to behave predictably. For that reason, the configured initial state is stored as metadata with the remembered state. If Initially Expanded is later changed, previously remembered group states are discarded and the new configuration takes effect.One detail that turned out to matter was group identity. APEX-generated control break header IDs can be reused when the report is paginated, so they cannot safely be used as persistent state keys. The plug-in therefore uses a separate key for remembered group state instead of relying on the DOM IDs of the currently rendered report.Button Position and DirectionThe original example always placed the button before the control break header text.The plug-in can now place it at either the logical Start or End of the control break header.Using logical positions instead of left and right also makes the behavior work with right-to-left layouts.There is another small detail here: the default expand chevron changes direction depending on where the button is positioned.For example, in a left-to-right layout:START → fa-chevron-rightEND → fa-chevron-leftThe icon therefore points toward the content rather than always pointing in the same physical direction.An explicitly configured Expand Icon still takes precedence.AccessibilityThe first version already used a native button and maintained aria-expanded and an accessible label.For the plug-in I extended this by adding aria-controls.Each toggle button references the report rows it controls:<button type="button" aria-expanded="true" aria-controls="row_1 row_2" aria-label="Collapse"> ... </button>Interactive Report data rows do not always have IDs suitable for this relationship, so the plug-in assigns IDs where needed while preserving any existing IDs.The icon itself remains decorative with:aria-hidden="true"A Small Public APIOne limitation of page-level Dynamic Action JavaScript is that it becomes awkward to control from elsewhere in the application.The plug-in exposes a small API:fi_jaris_plugin.ir.controlBreakToggler.expandAll(region); fi_jaris_plugin.ir.controlBreakToggler.collapseAll(region); fi_jaris_plugin.ir.controlBreakToggler.resetState(region);Each method intentionally controls one Interactive Report region.With the Button Trigger Action introduced in APEX 26.1, this works particularly well because the selected region can be passed directly to the API.For example, a Collapse All button can use:Triggered Action: Execute JavaScript CodeSelection Type: RegionRegion: <Your Interactive Report>Code:fi_jaris_plugin.ir.controlBreakToggler.collapseAll( this.affectedElements );There is no need to hard-code the report Static ID into the JavaScript.An Expand All button uses the same setup, but with this JavaScript code:fi_jaris_plugin.ir.controlBreakToggler.expandAll( this.affectedElements ); Reacting to ChangesThe plug-in also exposes a Dynamic Action event:Control Break Change [IR Control Break Toggler]This means another Dynamic Action can react when a user expands or collapses a group, without manually wiring up a JavaScript event listener.The same event is raised when the public API changes the state.Internally the JavaScript event is:ircontrolbreakchangeand its event data includes the report region, affected control break header IDs, resulting state, and the source of the change.For bulk operations such as Expand All or Collapse All, one event is raised after the operation if one or more groups changed state, rather than one event for every control break header.The Interesting Part Hasn't ChangedDespite the extra functionality, the central technique is still the one from the 2022 post. Find the control break header:th.a-IRR-header--groupand consider the rows after its <tr> up to the next control break header to be the rows belonging to that group.Everything else in the plug-in is mostly about making that behavior reusable:configuration,refresh handling,state management,accessibility,application integration,and avoiding duplicate initialization.That is usually how small APEX utilities evolve. The first version proves that the idea works; the reusable version is mostly about dealing cleanly with everything around that idea.Source CodeThe plug-in source, installable APEX export, and documentation are available on GitHub:https://github.com/jariolaine/apex-ir-control-break-togglerThe repository also contains the unminified JavaScript, CSS, render procedure, and plug-in export, so the implementation can be inspected or modified without having to extract files from the APEX plug-in.See a working example on oracleapex.com.