Skip to content

Commit db716b1

Browse files
committed
Docs: Retropost "Introduce multi-select" (2016) and "Redesign module selector" (2022)
Preserving here for improved discovery and easy reference. * The first, quickly composed of verbatim quotes from the 2016 issues/PRs. * The second, rewritten based on two linked issue tracker comments by me that I wrote effectively as a blog post already, which I did for this exact purpose.
1 parent 65d0e6f commit db716b1

4 files changed

+214
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
layout: post
3+
title: "Introduce multi-select module picker"
4+
author: Maciej Lato, Richard Gibson
5+
tags:
6+
- feature
7+
---
8+
9+
Introduce a multi-select module dropdown, to replace the module selector.
10+
11+
## Features
12+
13+
This replaces the module select dropdown with a dropdown that opens up into a multi-selector.
14+
15+
* Multi-select window with checkboxes and scrolling.
16+
* Search box for filtering by module or test name.
17+
* "Apply" button to run selected tests or modules.
18+
* "Reset" button to clear selection, returnig to implied default of "Select all".
19+
20+
## Accessibility
21+
22+
* Display current module section (comma-separated) in placeholder and tooltip text.
23+
* Close on Escape keydown.
24+
25+
<figure>
26+
<figcaption markdown="span">[QUnit 1.23.1 demo](https://codepen.io/Krinkle/full/QwLZWWe)</figcaption>
27+
<img alt="" width="614" src="https://github.com/user-attachments/assets/d2377b8e-2e1e-4d2f-b0e8-d455cc59bd78">
28+
</figure>
29+
30+
<figure>
31+
<figcaption markdown="span">[QUnit 2.0.0 demo](https://codepen.io/Krinkle/full/mybzddj)</figcaption>
32+
<img alt="" width="740" src="https://github.com/user-attachments/assets/fcfd3fb2-3b43-4177-a8cf-b89f0b1eea88">
33+
</figure>
34+
35+
## See also
36+
37+
* [Update UI to allow multiple test/module selection · Issue #953](https://github.com/qunitjs/qunit/issues/953)
38+
* [HTML Reporter: Add multi-select module dropdown · Pull Request #973](https://github.com/qunitjs/qunit/pull/973)
39+
* [HTML Reporter: Improve toolbar styles & accessibility · Pull Request #989](https://github.com/qunitjs/qunit/pull/989)
40+
* [QUnit 2.0.0 Release]({% post_url 2016-06-16-qunit-2-0-0 %})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
layout: post
3+
title: "Redesign module selector - Fast & Fuzzy"
4+
author: krinkle
5+
tags:
6+
- feature
7+
8+
---
9+
10+
The typeahead field for the module dropdown menu is now faster, fuzzier, and fully keyboard accessible!
11+
12+
## Faster startup
13+
14+
Matthew Beale [noticed](https://github.com/qunitjs/qunit/issues/1664) that on test suites with 800 modules, test startup was sometimes delayed by ~5 seconds in Chrome 95.
15+
16+
In previous QUnit versions, we eagerly rendered the dropdown menu with options for all module names. (The menu is only shown on focus.) The JavaScript code for this only takes about 5-10ms (0.005 seconds), even on very large projects. But, every so often there is an unexplained slow task right after this function. This performance issue did not affect Firefox and Safari.
17+
18+
Whatever the cause may be, we've cut this cost by lazily rendering the menu when the module field is first focussed.
19+
20+
<figure>
21+
<img alt="Chrome DevTools shows 3ms spent in native parseHTML, as part of the moduleListHtml() function. The next task is an unexplained grey box, 5 seconds wide. Its type is unknown, and has no children or parent associations." src="https://user-images.githubusercontent.com/8752/140397773-5780d375-c731-4111-b703-bb76815c63ea.png">
22+
<figcaption>After a few milliseconds in our moduleListHtml function, Chrome spends 5 seconds in an unknown task.</figcaption>
23+
</figure>
24+
25+
* [q4000 Benchmark on QUnit 2.18.1](https://codepen.io/Krinkle/full/MYgqNpQ)
26+
* [q4000 Benchmark on QUnit 2.18.2](https://codepen.io/Krinkle/full/gbYdVRr)
27+
28+
## Instant typeahead
29+
30+
<div class="figure-row">
31+
<figure>
32+
<figcaption>Before (QUnit 2.18.1)</figcaption>
33+
<img alt="Before: While typing a word, the module list remains unchanged. One beat after you stop typing, the results appear at once. (GIF animation)" src="https://user-images.githubusercontent.com/156867/162635920-a167c006-0130-4a42-a903-0a6d70e464d9.gif">
34+
</figure>
35+
<figure>
36+
<figcaption>After (QUnit 2.18.2)</figcaption>
37+
<img alt="After: While typing a word, the module list updates in real-time on each keystroke. (GIF animation)" src="https://user-images.githubusercontent.com/156867/162635924-ddaacfc5-4817-46ea-81a8-024048954668.gif">
38+
</figure>
39+
</div>
40+
41+
We previously applied a 200 ms input debounce on filtering the dropdown menu. It seems a lot people type _just_ fast enough for the menu to sit idle until you stop typing. This provides a subpar user experience, because the you won't know if what you typed will find what you're looking for, until you stop typing.
42+
43+
The module selector in QUnit is powered by the super fast [fuzzysort.js](https://github.com/farzher/fuzzysort) library. Fuzzysearch actually takes only ~1ms, so it should be able to keep up in real-time, even for projects with hundreds of QUnit modules defined. I considered removing this debounce entirely, but that risks causing a different kind of lag instead.
44+
45+
The new design lowers the debounce to a delay of 0 ms.[^1] The module selector now feels smooth as butter, with an instant response on every key stroke.
46+
47+
[Try it here: **q4000 Benchmark**](/resources/q4000.html){: target="_blank"}
48+
49+
### What's the difference between a 0 ms debounce, and no debounce?
50+
51+
Consider what happens if you type faster than the browser rendering can keep up with. For example, rendering may take longer in some cases. The event handler will be running and, meanwhile, another character is typed.
52+
53+
Without a debounce, these keyboard events will pile up. Each one will be honoured and diligently played back-to-back and in order. Each event callback will _begin_ its rendering long after other keystrokes were already sent. It will feel akin to acting on a remote server over bad WiFi, with an ever-growing backlog of unprocessed input events.
54+
55+
With 0 ms debounce, we queue up at most 1 render callback, and that "next" render will always be for the then-current value of the input field. Another way to look at it: It is as if, whenever we finish a rendering, we will cancel all-except-the-last pending callbacks.
56+
57+
For the common case where rendering is quick enough to keep up with keystrokes, both ways improve what we had before. Both ways will render results immediately on every keystroke, with no delay. Check [Debounce demo on CodePen](https://codepen.io/Krinkle/pen/wvpXxwM?editors=0010) to experience the difference.
58+
59+
## Accessibility
60+
61+
The redesign includes various usability and accessibility improvements for the module dropdown menu.
62+
63+
Highlights:
64+
65+
* Currently selected choices are now hoisted to the top and always visible, even if not matched by the current filter.
66+
* There is no longer a "dead" tab target between the action buttons and the first menu option (see below animation).
67+
* The focus ring for the "Apply" button is no longer clipped on two sides by overflow (see below animation).
68+
* More breathable design for options and buttons. Toolbar buttons have a solid outline and no longer lost in a sea of greyness.
69+
70+
<div class="figure-row">
71+
<figure>
72+
<img alt="" width="530" src="https://user-images.githubusercontent.com/156867/163740098-26e9bfde-cdac-4035-99b9-19146b2d63e4.gif">
73+
<figcaption>Keyboard navigation (before)</figcaption>
74+
</figure>
75+
<figure>
76+
<img alt="" width="530" src="https://user-images.githubusercontent.com/156867/163740122-e1af8c35-35c8-4596-a367-1f7d2f904e75.gif">
77+
<figcaption>Keyboard navigation (after)</figcaption>
78+
</figure>
79+
80+
<figure>
81+
<img alt="" width="748" src="https://user-images.githubusercontent.com/156867/163740197-abcf8f9a-f440-4a02-ac3e-d345b38268c3.png">
82+
<figcaption>Button and list design (before)</figcaption>
83+
</figure>
84+
<figure>
85+
<img alt="" width="748" src="https://user-images.githubusercontent.com/156867/163740193-df44e637-dab4-4915-aec4-dd02905914ec.png">
86+
<figcaption>Button and list design (after)</figcaption>
87+
</figure>
88+
89+
<figure>
90+
<img alt="" width="455" src="https://user-images.githubusercontent.com/156867/163740288-d0e45308-d714-43a9-bf9c-1c198b5e55ba.png">
91+
<figcaption>Selection and cursor (before)</figcaption>
92+
</figure>
93+
<figure>
94+
<img alt="" width="458" src="https://user-images.githubusercontent.com/156867/163740299-9fb2b8ee-02ab-4dc3-993b-0d70daca8005.png">
95+
<figcaption>Selection and cursor (after)</figcaption>
96+
</figure>
97+
98+
<figure>
99+
<img alt="" width="467" src="https://user-images.githubusercontent.com/156867/163740326-4254ef7b-0ced-424a-a2ae-2f209a65f0b8.png">
100+
<figcaption>Placeholder (before)</figcaption>
101+
</figure>
102+
<figure>
103+
<img alt="" width="467" src="https://user-images.githubusercontent.com/156867/163740324-dd5f9b8c-913f-493c-be0d-a58b9a9641f2.png">
104+
<figcaption>Placeholder (after)</figcaption>
105+
</figure>
106+
</div>
107+
108+
## Love The Fuzz
109+
110+
The module selector in QUnit is powered by the super fast [fuzzysort.js](https://github.com/farzher/fuzzysort) library. Each result is internally ranked between several thousands points below zero (worst) upto 1.0 (perfect match).
111+
112+
One of the Fuzzysort features is the "threshold" option, which omits results with a lower score. We previously had this to `-1000`, which sounds like it would let most results through.
113+
114+
<div class="figure-row">
115+
<figure>
116+
<figcaption>Before</figcaption>
117+
<img alt="Before: No results for 'support for pomise'" src="https://user-images.githubusercontent.com/156867/162635139-3f3bd458-e322-4479-b5a2-cab8cff22751.png" width="479">
118+
</figure>
119+
<figure>
120+
<figcaption>After</figcaption>
121+
<img alt="After: Various results even for 'suortprose eachwhit'" src="https://user-images.githubusercontent.com/156867/162635233-70105acc-114e-4e9b-a9b3-d7d7ffcf68e0.png" width="470">
122+
</figure>
123+
</div>
124+
125+
In practice, it corresponded to tolerating some missing letters only in the first word. For example, `suort for promise` did find `support for promise`. But, `support for pomise` already yielded zero results, despite only missing one letter!
126+
127+
This is counter-intuitive and contrary to how fuzzy search works in text editors. For example, in Sublime Text, all files of which you have typed a subset of the name, are included. It is only when you type a character that isn't in an entry's name, that it is removed from the options.
128+
129+
In this redesign, I've disabled the "threshold", which achieves the desired effect.
130+
131+
## See also
132+
133+
* [HTML Reporter: Faster startup and improved usability of module filter · Issue #1664](https://github.com/qunitjs/qunit/issues/1664)
134+
* [HTML Reporter: Faster and fuzzier module dropdown · Pull Request #1685](https://github.com/qunitjs/qunit/pull/1685)
135+
* [QUnit 2.18.2 Release]({% post_url 2022-04-17-qunit-2-18-2 %})
136+
* [Blog: Introduce mult-select module picker]({% post_url 2016-04-21-introduce-multi-select-module-picker %}), April 2016.
137+
138+
-------
139+
140+
Footnotes:
141+
142+
[^1]: Note that timers from [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) have a minimum delay of 4ms in practice, which is close enough to zero.

docs/_posts/2022-04-17-qunit-2-18-2.md

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ tags:
2222
## See also
2323

2424
* [Git tag: 2.18.2](https://github.com/qunitjs/qunit/releases/tag/2.18.2)
25+
* [Blog: Redesign module selector]({% post_url 2022-04-16-redesign-module-selector %})

docs/_sass/custom.scss

+31-4
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,34 @@ h3 a {
4646
height: auto;
4747
}
4848

49-
.main figure {
50-
text-align: center;
49+
.content figure {
5150
margin: $size-spacing 0;
51+
text-align: center;
52+
font-size: $size-sm;
53+
}
54+
.content figcaption {
55+
padding: 0.1em 0.4em 0.3em 0.3em;
56+
}
57+
58+
.content figure:has(figcaption:first-child) {
59+
border: 1px solid $color-off-white;
60+
}
61+
.content figure figcaption:first-child {
62+
padding: 0.3em 0.4em;
63+
border-bottom: 1px solid $color-off-white;
64+
background: $color-light;
65+
}
66+
67+
.content figure img {
68+
// Avoid gap at the bottom due to line-height
69+
vertical-align: middle;
5270
}
5371

5472
@media (max-width: 768px) {
5573
// The negative margins are "pop out"
5674
// and must match the padding on `.main-columns`
57-
.main figure,
58-
.main pre.highlight {
75+
.content figure,
76+
.content pre.highlight {
5977
margin-left: (-$size-spacing);
6078
margin-right: (-$size-spacing);
6179
}
@@ -73,12 +91,21 @@ h3 a {
7391
// "pop out"
7492
margin: $size-spacing (-$size-spacing);
7593
}
94+
.figure-row:has(figcaption:first-child) {
95+
align-items: stretch;
96+
gap: $box-spacing 1px;
97+
}
7698
.figure-row figure {
99+
flex-grow: 1;
77100
margin: 0;
78101
width: calc(50% - ($box-spacing/2) - 1px);
79102
}
80103
}
81104

105+
.content a.footnote {
106+
text-decoration: none;
107+
}
108+
82109
/* Browser */
83110

84111
// Discourage selecting to copy/paste because this demonstrates

0 commit comments

Comments
 (0)