From 97ef09177b878654f3ce481d5223d132fc766fc2 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Wed, 22 Jan 2025 03:03:00 +0000 Subject: [PATCH] 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. --- ...21-introduce-multi-select-module-picker.md | 40 +++++ .../2022-04-16-redesign-module-selector.md | 142 ++++++++++++++++++ docs/_posts/2022-04-17-qunit-2-18-2.md | 1 + docs/_sass/custom.scss | 35 ++++- 4 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 docs/_posts/2016-04-21-introduce-multi-select-module-picker.md create mode 100644 docs/_posts/2022-04-16-redesign-module-selector.md diff --git a/docs/_posts/2016-04-21-introduce-multi-select-module-picker.md b/docs/_posts/2016-04-21-introduce-multi-select-module-picker.md new file mode 100644 index 000000000..db2ce83da --- /dev/null +++ b/docs/_posts/2016-04-21-introduce-multi-select-module-picker.md @@ -0,0 +1,40 @@ +--- +layout: post +title: "Introduce multi-select module picker" +author: Maciej Lato, Richard Gibson +tags: +- feature +--- + +Introduce a multi-select module dropdown, to replace the module selector. + +## Features + +This replaces the module select dropdown with a dropdown that opens up into a multi-selector. + +* Multi-select window with checkboxes and scrolling. +* Search box for filtering by module or test name. +* "Apply" button to run selected tests or modules. +* "Reset" button to clear selection, returnig to implied default of "Select all". + +## Accessibility + +* Display current module section (comma-separated) in placeholder and tooltip text. +* Close on Escape keydown. + +
+
[QUnit 1.23.1 demo](https://codepen.io/Krinkle/full/QwLZWWe)
+ +
+ +
+
[QUnit 2.0.0 demo](https://codepen.io/Krinkle/full/mybzddj)
+ +
+ +## See also + +* [Update UI to allow multiple test/module selection · Issue #953](https://github.com/qunitjs/qunit/issues/953) +* [HTML Reporter: Add multi-select module dropdown · Pull Request #973](https://github.com/qunitjs/qunit/pull/973) +* [HTML Reporter: Improve toolbar styles & accessibility · Pull Request #989](https://github.com/qunitjs/qunit/pull/989) +* [QUnit 2.0.0 Release]({% post_url 2016-06-16-qunit-2-0-0 %}) diff --git a/docs/_posts/2022-04-16-redesign-module-selector.md b/docs/_posts/2022-04-16-redesign-module-selector.md new file mode 100644 index 000000000..b3f9c3593 --- /dev/null +++ b/docs/_posts/2022-04-16-redesign-module-selector.md @@ -0,0 +1,142 @@ +--- +layout: post +title: "Redesign module selector - Fast & Fuzzy" +author: krinkle +tags: +- feature + +--- + +The typeahead field for the module dropdown menu is now faster, fuzzier, and fully keyboard accessible! + +## Faster startup + +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. + +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. + +Whatever the cause may be, we've cut this cost by lazily rendering the menu when the module field is first focussed. + +
+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. +
After a few milliseconds in our moduleListHtml function, Chrome spends 5 seconds in an unknown task.
+
+ +* [q4000 Benchmark on QUnit 2.18.1](https://codepen.io/Krinkle/full/MYgqNpQ) +* [q4000 Benchmark on QUnit 2.18.2](https://codepen.io/Krinkle/full/gbYdVRr) + +## Instant typeahead + +
+
+
Before (QUnit 2.18.1)
+Before: While typing a word, the module list remains unchanged. One beat after you stop typing, the results appear at once. (GIF animation) +
+
+
After (QUnit 2.18.2)
+After: While typing a word, the module list updates in real-time on each keystroke. (GIF animation) +
+
+ +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. + +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. + +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. + +[Try it here: **q4000 Benchmark**](/resources/q4000.html){: target="_blank"} + +### What's the difference between a 0 ms debounce, and no debounce? + +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. + +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. + +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. + +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. + +## Accessibility + +The redesign includes various usability and accessibility improvements for the module dropdown menu. + +Highlights: + +* Currently selected choices are now hoisted to the top and always visible, even if not matched by the current filter. +* There is no longer a "dead" tab target between the action buttons and the first menu option (see below animation). +* The focus ring for the "Apply" button is no longer clipped on two sides by overflow (see below animation). +* More breathable design for options and buttons. Toolbar buttons have a solid outline and no longer lost in a sea of greyness. + +
+
+ +
Keyboard navigation (before)
+
+
+ +
Keyboard navigation (after)
+
+ +
+ +
Button and list design (before)
+
+
+ +
Button and list design (after)
+
+ +
+ +
Selection and cursor (before)
+
+
+ +
Selection and cursor (after)
+
+ +
+ +
Placeholder (before)
+
+
+ +
Placeholder (after)
+
+
+ +## Love The Fuzz + +In [fuzzysort.js](https://github.com/farzher/fuzzysort), each result is internally ranked on a range from several thousands points below zero (worst) upto 1.0 (perfect match). + +One of the Fuzzysort features is the "threshold" option, which omits results below a certain score. We previously had this to `-1000`, which sounds like it should let most results through. + +
+
+
Before
+Before: No results for 'support for pomise' +
+
+
After
+After: Various results even for 'suortprose eachwhit' +
+
+ +In practice, it corresponded to tolerating a few missing letters 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! + +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. + +In this redesign, I've disabled the "threshold", which achieves the desired effect. + +## See also + +* [HTML Reporter: Faster startup and improved usability of module filter · Issue #1664](https://github.com/qunitjs/qunit/issues/1664) +* [HTML Reporter: Faster and fuzzier module dropdown · Pull Request #1685](https://github.com/qunitjs/qunit/pull/1685) +* [QUnit 2.18.2 Release]({% post_url 2022-04-17-qunit-2-18-2 %}) +* [Blog: Introduce mult-select module picker]({% post_url 2016-04-21-introduce-multi-select-module-picker %}), April 2016. + +------- + +Footnotes: + +[^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. diff --git a/docs/_posts/2022-04-17-qunit-2-18-2.md b/docs/_posts/2022-04-17-qunit-2-18-2.md index 7928238cf..7ea83ac75 100644 --- a/docs/_posts/2022-04-17-qunit-2-18-2.md +++ b/docs/_posts/2022-04-17-qunit-2-18-2.md @@ -22,3 +22,4 @@ tags: ## See also * [Git tag: 2.18.2](https://github.com/qunitjs/qunit/releases/tag/2.18.2) +* [Blog: Redesign module selector]({% post_url 2022-04-16-redesign-module-selector %}) diff --git a/docs/_sass/custom.scss b/docs/_sass/custom.scss index d7c184317..989def164 100644 --- a/docs/_sass/custom.scss +++ b/docs/_sass/custom.scss @@ -46,16 +46,34 @@ h3 a { height: auto; } -.main figure { - text-align: center; +.content figure { margin: $size-spacing 0; + text-align: center; + font-size: $size-sm; +} +.content figcaption { + padding: 0.1em 0.4em 0.3em 0.3em; +} + +.content figure:has(figcaption:first-child) { + border: 1px solid $color-off-white; +} +.content figure figcaption:first-child { + padding: 0.3em 0.4em; + border-bottom: 1px solid $color-off-white; + background: $color-light; +} + +.content figure img { + // Avoid gap at the bottom due to line-height + vertical-align: middle; } @media (max-width: 768px) { // The negative margins are "pop out" // and must match the padding on `.main-columns` - .main figure, - .main pre.highlight { + .content figure, + .content pre.highlight { margin-left: (-$size-spacing); margin-right: (-$size-spacing); } @@ -73,12 +91,21 @@ h3 a { // "pop out" margin: $size-spacing (-$size-spacing); } + .figure-row:has(figcaption:first-child) { + align-items: stretch; + gap: $box-spacing 1px; + } .figure-row figure { + flex-grow: 1; margin: 0; width: calc(50% - ($box-spacing/2) - 1px); } } +.content a.footnote { + text-decoration: none; +} + /* Browser */ // Discourage selecting to copy/paste because this demonstrates