Skip to content

Commit f374c23

Browse files
pierry01claude
andcommitted
Fix accordion clipping content that grows after opening
revealContent froze the panel at the height measured on open, so content that appears after opening (lazy-loaded iframes/images, expanding rows) was clipped. Release the height to `auto` once the open animation finishes, and collapse from the current rendered height since Motion can't interpolate from `auto`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 58317ff commit f374c23

3 files changed

Lines changed: 33 additions & 19 deletions

File tree

‎docs/app/javascript/controllers/ruby_ui/accordion_controller.js‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,35 @@ export default class extends Controller {
7979
duration: this.animationDurationValue,
8080
easing: this.animationEasingValue,
8181
},
82-
);
82+
)
83+
.finished.then(() => {
84+
if (content.dataset.state === "open") content.style.height = "auto";
85+
})
86+
.catch(() => {});
8387
}
8488

8589
// Hide the accordion content with animation
8690
hideContent() {
8791
const content = this.contentTarget;
8892
content.dataset.state = "closed";
8993

94+
const currentHeight = content.getBoundingClientRect().height;
9095
animate(
9196
content,
92-
{ height: 0 },
97+
{ height: [`${currentHeight}px`, "0px"] },
9398
{
9499
duration: this.animationDurationValue,
95100
easing: this.animationEasingValue,
96101
},
97-
).finished.then(() => {
98-
// After animation completes, truly hide the element so it is removed
99-
// from layout and form focus — prevents trapped validation errors
100-
if (content.dataset.state === "closed") {
101-
content.setAttribute("hidden", "");
102-
}
103-
});
102+
)
103+
.finished.then(() => {
104+
// After animation completes, truly hide the element so it is removed
105+
// from layout and form focus — prevents trapped validation errors
106+
if (content.dataset.state === "closed") {
107+
content.setAttribute("hidden", "");
108+
}
109+
})
110+
.catch(() => {});
104111
}
105112

106113
// Rotate the accordion icon 180deg using animate function

‎gem/lib/ruby_ui/accordion/accordion_controller.js‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,35 @@ export default class extends Controller {
7979
duration: this.animationDurationValue,
8080
easing: this.animationEasingValue,
8181
},
82-
);
82+
)
83+
.finished.then(() => {
84+
if (content.dataset.state === "open") content.style.height = "auto";
85+
})
86+
.catch(() => {});
8387
}
8488

8589
// Hide the accordion content with animation
8690
hideContent() {
8791
const content = this.contentTarget;
8892
content.dataset.state = "closed";
8993

94+
const currentHeight = content.getBoundingClientRect().height;
9095
animate(
9196
content,
92-
{ height: 0 },
97+
{ height: [`${currentHeight}px`, "0px"] },
9398
{
9499
duration: this.animationDurationValue,
95100
easing: this.animationEasingValue,
96101
},
97-
).finished.then(() => {
98-
// After animation completes, truly hide the element so it is removed
99-
// from layout and form focus — prevents trapped validation errors
100-
if (content.dataset.state === "closed") {
101-
content.setAttribute("hidden", "");
102-
}
103-
});
102+
)
103+
.finished.then(() => {
104+
// After animation completes, truly hide the element so it is removed
105+
// from layout and form focus — prevents trapped validation errors
106+
if (content.dataset.state === "closed") {
107+
content.setAttribute("hidden", "");
108+
}
109+
})
110+
.catch(() => {});
104111
}
105112

106113
// Rotate the accordion icon 180deg using animate function

‎mcp/data/registry.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
{
1717
"path": "accordion_controller.js",
18-
"content": "import { Controller } from \"@hotwired/stimulus\";\nimport { animate } from \"motion\";\n\n// Connects to data-controller=\"ruby-ui--accordion\"\nexport default class extends Controller {\n static targets = [\"icon\", \"content\"];\n static values = {\n open: {\n type: Boolean,\n default: false,\n },\n animationDuration: {\n type: Number,\n default: 0.15, // Default animation duration (in seconds)\n },\n animationEasing: {\n type: String,\n default: \"ease-in-out\", // Default animation easing\n },\n rotateIcon: {\n type: Number,\n default: 180, // Default icon rotation (in degrees)\n },\n };\n\n connect() {\n // Set the initial state of the accordion\n let originalAnimationDuration = this.animationDurationValue;\n this.animationDurationValue = 0;\n this.openValue ? this.open() : this.close();\n this.animationDurationValue = originalAnimationDuration;\n }\n\n // Toggle the 'open' value\n toggle() {\n this.openValue = !this.openValue;\n }\n\n // Handle changes in the 'open' value\n openValueChanged(isOpen, wasOpen) {\n if (isOpen) {\n this.open();\n } else {\n this.close();\n }\n }\n\n // Open the accordion content\n open() {\n if (this.hasContentTarget) {\n this.revealContent();\n this.hasIconTarget && this.rotateIcon();\n this.openValue = true;\n }\n }\n\n // Close the accordion content\n close() {\n if (this.hasContentTarget) {\n this.hideContent();\n this.hasIconTarget && this.rotateIcon();\n this.openValue = false;\n }\n }\n\n // Reveal the accordion content with animation\n revealContent() {\n const content = this.contentTarget;\n\n // Remove hidden so the element participates in layout before measuring\n content.removeAttribute(\"hidden\");\n content.dataset.state = \"open\";\n\n const contentHeight = content.scrollHeight;\n animate(\n content,\n { height: `${contentHeight}px` },\n {\n duration: this.animationDurationValue,\n easing: this.animationEasingValue,\n },\n );\n }\n\n // Hide the accordion content with animation\n hideContent() {\n const content = this.contentTarget;\n content.dataset.state = \"closed\";\n\n animate(\n content,\n { height: 0 },\n {\n duration: this.animationDurationValue,\n easing: this.animationEasingValue,\n },\n ).finished.then(() => {\n // After animation completes, truly hide the element so it is removed\n // from layout and form focus — prevents trapped validation errors\n if (content.dataset.state === \"closed\") {\n content.setAttribute(\"hidden\", \"\");\n }\n });\n }\n\n // Rotate the accordion icon 180deg using animate function\n rotateIcon() {\n animate(this.iconTarget, {\n rotate: `${this.openValue ? this.rotateIconValue : 0}deg`,\n });\n }\n}\n"
18+
"content": "import { Controller } from \"@hotwired/stimulus\";\nimport { animate } from \"motion\";\n\n// Connects to data-controller=\"ruby-ui--accordion\"\nexport default class extends Controller {\n static targets = [\"icon\", \"content\"];\n static values = {\n open: {\n type: Boolean,\n default: false,\n },\n animationDuration: {\n type: Number,\n default: 0.15, // Default animation duration (in seconds)\n },\n animationEasing: {\n type: String,\n default: \"ease-in-out\", // Default animation easing\n },\n rotateIcon: {\n type: Number,\n default: 180, // Default icon rotation (in degrees)\n },\n };\n\n connect() {\n // Set the initial state of the accordion\n let originalAnimationDuration = this.animationDurationValue;\n this.animationDurationValue = 0;\n this.openValue ? this.open() : this.close();\n this.animationDurationValue = originalAnimationDuration;\n }\n\n // Toggle the 'open' value\n toggle() {\n this.openValue = !this.openValue;\n }\n\n // Handle changes in the 'open' value\n openValueChanged(isOpen, wasOpen) {\n if (isOpen) {\n this.open();\n } else {\n this.close();\n }\n }\n\n // Open the accordion content\n open() {\n if (this.hasContentTarget) {\n this.revealContent();\n this.hasIconTarget && this.rotateIcon();\n this.openValue = true;\n }\n }\n\n // Close the accordion content\n close() {\n if (this.hasContentTarget) {\n this.hideContent();\n this.hasIconTarget && this.rotateIcon();\n this.openValue = false;\n }\n }\n\n // Reveal the accordion content with animation\n revealContent() {\n const content = this.contentTarget;\n\n // Remove hidden so the element participates in layout before measuring\n content.removeAttribute(\"hidden\");\n content.dataset.state = \"open\";\n\n const contentHeight = content.scrollHeight;\n animate(\n content,\n { height: `${contentHeight}px` },\n {\n duration: this.animationDurationValue,\n easing: this.animationEasingValue,\n },\n )\n .finished.then(() => {\n if (content.dataset.state === \"open\") content.style.height = \"auto\";\n })\n .catch(() => {});\n }\n\n // Hide the accordion content with animation\n hideContent() {\n const content = this.contentTarget;\n content.dataset.state = \"closed\";\n\n const currentHeight = content.getBoundingClientRect().height;\n animate(\n content,\n { height: [`${currentHeight}px`, \"0px\"] },\n {\n duration: this.animationDurationValue,\n easing: this.animationEasingValue,\n },\n )\n .finished.then(() => {\n // After animation completes, truly hide the element so it is removed\n // from layout and form focus — prevents trapped validation errors\n if (content.dataset.state === \"closed\") {\n content.setAttribute(\"hidden\", \"\");\n }\n })\n .catch(() => {});\n }\n\n // Rotate the accordion icon 180deg using animate function\n rotateIcon() {\n animate(this.iconTarget, {\n rotate: `${this.openValue ? this.rotateIconValue : 0}deg`,\n });\n }\n}\n"
1919
},
2020
{
2121
"path": "accordion_default_content.rb",

0 commit comments

Comments
 (0)