From a3cb4a6903d3b3a103c6096f77ec81c6eaccd082 Mon Sep 17 00:00:00 2001 From: Sage Abdullah Date: Wed, 24 Jan 2024 17:09:55 +0000 Subject: [PATCH] Fix filters from being removed when searching Tippy unmounts its popper element from the DOM on hide, which means that the filter fields get detached from the form. As a result, when performing a search (which means the filters popup is closed), any filters that have been applied will be lost. The issue does not occur the other way around (filtering after search) because the filter fields will still be in the DOM (as the popup is open while you're applying the filters). Unfortunately, Tippy does not offer a built-in option to keep the popper mounted in the DOM when it's hidden. As a workaround, use Tippy's hooks to re-append the popper to the DOM. - onCreate: ensure the popper element is mounted even when the tippy was just created (normally it's only mounted when shown). This is useful to keep the filters when searching after a full-page refresh (e.g. after navigating to the next page in pagination). - onHidden: ensure the popper is still mounted, to solve the main issue - onShow: remove the hidden attribute and let tippy move the popper element as necessary (though usually this isn't necessary, because we remount it in the same position (the controller's element). The 'hidden' attribute shouldn't cause any issues here as Tippy doesn't use it. Without it, the solution still works because Tippy uses CSS to make the element invisible. However, the complete CSS doesn't get applied until the tippy is shown at least once. This means on initial load with the onCreate hook, the tippy will take up the space while being invisible. Using the attribute fixes it (and display:none probably would too). This behaviour can be enabled using the newly-added DropdownController value 'keepMounted'. --- client/src/controllers/DropdownController.ts | 34 ++++++++++++++++--- .../shared/dropdown/dropdown.html | 11 +++++- .../wagtailadmin/shared/headers/_filters.html | 2 +- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/client/src/controllers/DropdownController.ts b/client/src/controllers/DropdownController.ts index e1533bfb9869..b80229dda1ce 100644 --- a/client/src/controllers/DropdownController.ts +++ b/client/src/controllers/DropdownController.ts @@ -95,20 +95,22 @@ export class DropdownController extends Controller { static targets = ['toggle', 'content']; static values = { hideOnClick: { default: false, type: Boolean }, + keepMounted: { default: false, type: Boolean }, offset: Array, theme: { default: 'dropdown' as TippyTheme, type: String }, }; // Hide on click *inside* the dropdown. Differs from tippy's hideOnClick // option for outside clicks that defaults to true and we don't yet expose it. - declare hideOnClickValue: boolean; - declare offsetValue: [number, number]; + declare readonly hideOnClickValue: boolean; + declare readonly keepMountedValue: boolean; + declare readonly offsetValue: [number, number]; + declare readonly hasOffsetValue: boolean; + declare readonly themeValue: TippyTheme; declare readonly contentTarget: HTMLDivElement; declare readonly hasContentTarget: boolean; - declare readonly hasOffsetValue: boolean; declare readonly toggleTarget: HTMLButtonElement; - declare readonly themeValue: TippyTheme; tippy?: Instance; @@ -144,10 +146,22 @@ export class DropdownController extends Controller { }); } - const onShow = () => { + const onCreate = (instance: Instance) => { + if (this.keepMountedValue) { + const { popper } = instance; + this.element.append(popper); + popper.hidden = true; + } + }; + + const onShow = (instance: Instance) => { if (hoverTooltipInstance) { hoverTooltipInstance.disable(); } + if (this.keepMountedValue) { + const { popper } = instance; + popper.hidden = false; + } }; const onShown = () => { @@ -161,6 +175,14 @@ export class DropdownController extends Controller { } }; + const onHidden = (instance: Instance) => { + if (this.keepMountedValue) { + const { popper } = instance; + this.element.append(popper); + popper.hidden = true; + } + }; + return { ...(this.hasContentTarget ? { content: this.contentTarget as Content } @@ -171,9 +193,11 @@ export class DropdownController extends Controller { ...(this.hasOffsetValue && { offset: this.offsetValue }), getReferenceClientRect: () => this.reference.getBoundingClientRect(), theme: this.themeValue, + onCreate, onShow, onShown, onHide, + onHidden, }; } diff --git a/wagtail/admin/templates/wagtailadmin/shared/dropdown/dropdown.html b/wagtail/admin/templates/wagtailadmin/shared/dropdown/dropdown.html index 15e77cd16379..d68bc38f2525 100644 --- a/wagtail/admin/templates/wagtailadmin/shared/dropdown/dropdown.html +++ b/wagtail/admin/templates/wagtailadmin/shared/dropdown/dropdown.html @@ -14,12 +14,21 @@ - `toggle_classname` (string?) - additional toggle classes - `toggle_tooltip_offset` (string?) - Tooltip offset prop - `hide_on_click` (boolean?) - Whether or not the tooltip should hide when clicking inside + - `keep_mounted` (boolean?) - Whether or not the tooltip should keep its DOM node mounted when hidden - `children` - Dropdown contents (`a` and `button` elements only) {% endcomment %} {% fragment as class %}{% classnames 'w-dropdown' classname %}{% if theme %} w-dropdown--{{ theme }}{% endif %}{% endfragment %} -
+