mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 16:42:18 +00:00
* fix(ds): keep DS::Menu/Popover panels anchored across Turbo morphs The app refreshes pages via Turbo morph (`turbo_refreshes_with method: :morph`), and same-page account actions (disable, exclude, set-default, etc.) trigger one. Two bugs in the shared floating-ui controllers surface as a result: - The panel's `position: fixed` only ever existed as a JS-applied inline style. Idiomorph resets every menu/popover's `style` attribute to match the server-rendered markup (which has none), silently stripping `position: fixed` from every panel on the page. The next dropdown/ popover opened before floating-ui's async recompute lands briefly renders in normal flex flow, shoving its own trigger sideways and making computePosition anchor to that phantom position instead of the real button. Fix: make `position: fixed` part of the static markup so it can never be stripped. - `this.show` was a plain instance property. Because the morph preserves the Stimulus controller in place (stable-id turbo frame), `this.show` doesn't reset when idiomorph re-closes the content element, so it can desync from the DOM and swallow the next click. Fix: derive `show` from the content element's own class instead of tracking it separately. Reproduced and verified against the actual Turbo/Stimulus/floating-ui pipeline in an isolated harness before and after the fix. * test(ds): add regression coverage for menu/popover reopen-after-morph Simulates what idiomorph does to an open panel on a same-page Turbo morph — resets the content element's class back to the always-hidden server-rendered markup and strips the JS-applied inline style, without going through toggle()/close(). Verified against the pre-fix controllers that this fails without the DOM-derived `show` getter.
66 lines
2.3 KiB
Ruby
66 lines
2.3 KiB
Ruby
require "application_system_test_case"
|
|
|
|
# Regression coverage for the bug fixed alongside DS::Menu / DS::Popover's
|
|
# `fixed`-in-static-class change: a Turbo morph re-renders an open panel's
|
|
# content element back to its server-rendered (closed) state without going
|
|
# through the controller's own open/close methods. `show` used to be a plain
|
|
# instance property that a morph couldn't touch, so the next click toggled
|
|
# off an already-closed panel — swallowing the click. Deriving `show` from
|
|
# the content element's own `hidden` class (rather than tracked state) means
|
|
# it can't desync from what the DOM actually shows.
|
|
class DsOverlayMorphTest < ApplicationSystemTestCase
|
|
setup do
|
|
sign_in @user = users(:family_admin)
|
|
end
|
|
|
|
test "DS::Menu reopens correctly after a morph resets it to hidden" do
|
|
visit tags_url
|
|
|
|
trigger = find("[aria-haspopup='menu']", match: :first)
|
|
content_id = trigger["aria-controls"]
|
|
|
|
trigger.click
|
|
assert_selector "##{content_id}", visible: true
|
|
assert_equal "true", trigger["aria-expanded"]
|
|
|
|
simulate_morph_reset(content_id)
|
|
|
|
trigger.click
|
|
assert_selector "##{content_id}", visible: true
|
|
assert_equal "true", trigger["aria-expanded"]
|
|
end
|
|
|
|
test "DS::Popover reopens correctly after a morph resets it to hidden" do
|
|
visit root_url
|
|
|
|
within_testid "user-menu" do
|
|
trigger = find("[aria-haspopup='dialog']")
|
|
content_id = trigger["aria-controls"]
|
|
|
|
trigger.click
|
|
assert_selector "##{content_id}", visible: true
|
|
assert_equal "true", trigger["aria-expanded"]
|
|
|
|
simulate_morph_reset(content_id)
|
|
|
|
trigger.click
|
|
assert_selector "##{content_id}", visible: true
|
|
assert_equal "true", trigger["aria-expanded"]
|
|
end
|
|
end
|
|
|
|
private
|
|
# Mimics what idiomorph does to an open panel on a same-page Turbo morph:
|
|
# the content element's class list is reconciled back to the always-hidden
|
|
# server-rendered markup, and any inline style the controller applied
|
|
# (position coordinates) is stripped — all without calling the Stimulus
|
|
# controller's toggle()/close().
|
|
def simulate_morph_reset(content_id)
|
|
page.execute_script(<<~JS, content_id)
|
|
const el = document.getElementById(arguments[0]);
|
|
el.classList.add("hidden");
|
|
el.removeAttribute("style");
|
|
JS
|
|
end
|
|
end
|