Skip to content

Commit b476118

Browse files
Fix slider tooltip and switch label overlap
1 parent a01638c commit b476118

4 files changed

Lines changed: 138 additions & 4 deletions

File tree

assets/css/responsive-filters.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@
6868
display: none;
6969
}
7070

71+
/* RangeSlider tooltips are absolutely positioned and therefore do not add to
72+
the slider's layout height. Reserve a row for bottom tooltips before laying
73+
out the missing-value switch so its label cannot overlap them. */
74+
.range-filter__slider-with-switch {
75+
padding-bottom: 2.75rem;
76+
}
77+
78+
/* The ISP control has two stacked sliders and does not use build_range_filter.
79+
Give both bottom tooltip rows space so they clear the next heading/switch. */
80+
.isp-speed-filter__range {
81+
padding-bottom: 2.75rem;
82+
}
83+
7184
@media (max-width: 1099.98px) {
7285
.listing-page-layout {
7386
display: block;

pages/component_factories.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,17 @@ def build_range_filter(
7979
if marks is not None:
8080
slider_kwargs["marks"] = marks
8181

82-
body_children = [dcc.RangeSlider(**slider_kwargs)]
82+
slider = dcc.RangeSlider(**slider_kwargs)
83+
has_missing_switch = bool(
84+
include_missing_switch_id and include_missing_switch_label
85+
)
86+
body_children = [
87+
html.Div(slider, className="range-filter__slider-with-switch")
88+
if has_missing_switch
89+
else slider
90+
]
8391

84-
if include_missing_switch_id and include_missing_switch_label:
92+
if has_missing_switch:
8593
body_children.append(
8694
dmc.Switch(
8795
id=include_missing_switch_id,
@@ -96,7 +104,7 @@ def build_range_filter(
96104
return html.Div(
97105
[
98106
html.Div(list(header_children or [])),
99-
html.Div(body_children, id=dynamic_id),
107+
html.Div(body_children, id=dynamic_id, className="range-filter__controls"),
100108
],
101109
style=container_style,
102110
id=component_id,
@@ -132,7 +140,7 @@ def build_isp_speed_components(max_download: float, max_upload: float) -> html.D
132140
},
133141
),
134142
],
135-
style={"marginBottom": "15px"},
143+
className="isp-speed-filter__range",
136144
),
137145
html.Div(
138146
[
@@ -150,6 +158,7 @@ def build_isp_speed_components(max_download: float, max_upload: float) -> html.D
150158
},
151159
),
152160
],
161+
className="isp-speed-filter__range",
153162
),
154163
dmc.Switch(
155164
id="isp_speed_missing_switch",
@@ -161,6 +170,7 @@ def build_isp_speed_components(max_download: float, max_upload: float) -> html.D
161170
),
162171
],
163172
id="isp_speed_div",
173+
className="isp-speed-filter",
164174
)
165175

166176

tests/e2e/responsive_filters.spec.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,79 @@ test("tablet uses a right drawer and desktop keeps a persistent sidebar", async
162162

163163
expect(errors).toEqual([]);
164164
});
165+
166+
test("range slider tooltips stay clear of missing-value switches", async ({ page }) => {
167+
await page.setViewportSize({ width: 440, height: 900 });
168+
await page.goto(`${BASE_URL}/buy`, { waitUntil: "domcontentloaded" });
169+
await waitForFilterState(page, "buy");
170+
await page.locator("#buy-filter-open-button").click();
171+
await page.getByRole("button", { name: "Lot Size", exact: true }).click();
172+
173+
const slider = page.locator("#lot_size_div_buy .range-filter__slider-with-switch");
174+
const missingSwitch = page.locator("#lot_size_missing_switch");
175+
await expect(slider).toBeVisible();
176+
await expect(missingSwitch).toBeVisible();
177+
178+
const geometry = await page.evaluate(() => {
179+
const tooltipBottom = Math.max(
180+
...Array.from(
181+
document.querySelectorAll(
182+
"#lot_size_div_buy .range-filter__slider-with-switch .rc-slider-tooltip",
183+
),
184+
(tooltip) => tooltip.getBoundingClientRect().bottom,
185+
),
186+
);
187+
const switchTop = document
188+
.getElementById("lot_size_missing_switch")
189+
.closest(".mantine-Switch-root")
190+
.getBoundingClientRect().top;
191+
return { tooltipBottom, switchTop };
192+
});
193+
194+
expect(geometry.switchTop).toBeGreaterThanOrEqual(geometry.tooltipBottom + 8);
195+
});
196+
197+
test("ISP slider tooltips stay clear of the next control", async ({ page }) => {
198+
await page.setViewportSize({ width: 440, height: 900 });
199+
await page.goto(BASE_URL, { waitUntil: "domcontentloaded" });
200+
await waitForFilterState(page, "lease");
201+
await page.locator("#lease-filter-open-button").click();
202+
await page
203+
.getByRole("button", { name: "Internet Service Provider (ISP) Speed", exact: true })
204+
.click();
205+
206+
const ispFilter = page.locator("#isp_speed_div");
207+
await expect(ispFilter).toBeVisible();
208+
209+
const geometry = await page.evaluate(() => {
210+
const ranges = Array.from(
211+
document.querySelectorAll("#isp_speed_div .isp-speed-filter__range"),
212+
);
213+
const tooltipBottom = (range) => Math.max(
214+
...Array.from(
215+
range.querySelectorAll(".rc-slider-tooltip"),
216+
(tooltip) => tooltip.getBoundingClientRect().bottom,
217+
),
218+
);
219+
const uploadHeadingTop = ranges[1]
220+
.querySelector("h6")
221+
.getBoundingClientRect().top;
222+
const switchTop = document
223+
.getElementById("isp_speed_missing_switch")
224+
.closest(".mantine-Switch-root")
225+
.getBoundingClientRect().top;
226+
return {
227+
downloadTooltipBottom: tooltipBottom(ranges[0]),
228+
uploadHeadingTop,
229+
uploadTooltipBottom: tooltipBottom(ranges[1]),
230+
switchTop,
231+
};
232+
});
233+
234+
expect(geometry.uploadHeadingTop).toBeGreaterThanOrEqual(
235+
geometry.downloadTooltipBottom + 8,
236+
);
237+
expect(geometry.switchTop).toBeGreaterThanOrEqual(
238+
geometry.uploadTooltipBottom + 8,
239+
);
240+
});

tests/test_component_smoke.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from dash import dcc, html
44

55
from pages.component_factories import (
6+
build_isp_speed_components,
67
build_location_filter_components,
8+
build_range_filter,
79
build_subtype_filter,
810
build_title_card,
911
)
@@ -64,6 +66,39 @@ def test_subtype_filter_defaults_to_include_all_state(self) -> None:
6466
self.assertIsInstance(dropdown, dcc.Dropdown)
6567
self.assertEqual(dropdown.value, [])
6668

69+
def test_range_filter_reserves_tooltip_space_before_switch(self) -> None:
70+
component = build_range_filter(
71+
slider_id="test-slider",
72+
min_value=0,
73+
max_value=100,
74+
value=[0, 100],
75+
component_id="test-range-filter",
76+
dynamic_id="test-range-filter-controls",
77+
include_missing_switch_id="test-missing-switch",
78+
include_missing_switch_label="Include unknown values",
79+
)
80+
81+
controls = component.children[1]
82+
slider_wrapper, missing_switch = controls.children
83+
84+
self.assertEqual(controls.className, "range-filter__controls")
85+
self.assertEqual(
86+
slider_wrapper.className,
87+
"range-filter__slider-with-switch",
88+
)
89+
self.assertIsInstance(slider_wrapper.children, dcc.RangeSlider)
90+
self.assertEqual(missing_switch.id, "test-missing-switch")
91+
92+
def test_isp_speed_filter_reserves_space_below_both_sliders(self) -> None:
93+
component = build_isp_speed_components(10_000, 10_000)
94+
95+
download_range, upload_range, missing_switch = component.children
96+
97+
self.assertEqual(component.className, "isp-speed-filter")
98+
self.assertEqual(download_range.className, "isp-speed-filter__range")
99+
self.assertEqual(upload_range.className, "isp-speed-filter__range")
100+
self.assertEqual(missing_switch.id, "isp_speed_missing_switch")
101+
67102
def test_title_card_links_to_mcp_setup_page(self) -> None:
68103
title_card = build_title_card(
69104
title="WhereToLive.LA",

0 commit comments

Comments
 (0)