Skip to content

Commit 15c4048

Browse files
committed
Add web component support
Web Components are awesome! They naturally solve the use case of "late", or "dynamic" insertion, without needing to rely on selecting elements, or manually calling the undocumented `tsminibar()` function. It just works! == npm run size == JS transerSize (compressed): * HEAD~1 : 2209 * HEAD : 2175 * After : 2248 (+39 B, +1.7%) CSS: * HEAD : 1861 * After : 1921 (+60 B, +3.2%) == npm test == ``` File | % Stmts | % Branch | % Funcs | % Lines | ----------------------|---------|----------|---------|---------| before | 94.28 | 85.71 | 90.47 | 95.78 | after | 95.41 | 84.72 | 95.23 | 95.95 | ```
1 parent 6b71be0 commit 15c4048

File tree

5 files changed

+101
-9
lines changed

5 files changed

+101
-9
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* **Accessible**, keyboard navigation, arrow keys, close on `Esc` or outside click
2020
* **Fast**, leverages preconnect (Resource Hints), LRU memory cache
2121
* **Easy to install**, fully declarative via HTML (zero-code setup!)
22+
* **Web Component** ready
2223

2324
## Getting started
2425

@@ -48,12 +49,30 @@ Distribution:
4849
npm i --save typesense-minibar
4950
```
5051
```js
52+
// CommonJS
5153
require('typesense-minibar');
54+
// ESM
5255
import 'typesense-minibar';
5356
```
5457

5558
## API
5659

60+
### Web Component
61+
62+
`<typesense-minibar><form>…</form></typesense-minibar>` is equivalent to `<form class="tsmb-form">`.
63+
64+
```html
65+
<typesense-minibar>
66+
<form role="search" action="https://duckduckgo.com"
67+
data-origin=""
68+
data-collection=""
69+
data-key="">
70+
<input type="search" name="q" placeholder="Search..." autocomplete="off">
71+
<input type="hidden" name="sites" value="example.org">
72+
</form>
73+
</typesense-minibar>
74+
```
75+
5776
### Configuration
5877

5978
* ***data-origin*** (Required): Base URL to your Typesense server.
@@ -168,6 +187,19 @@ Notes:
168187

169188
## FAQ: Troubleshooting
170189

190+
* Why is the form not interactive if I insert it later?
191+
192+
If you create or insert the element dynamically with JavaScript, it is recommended to write the form as a web component instead, like so:
193+
```html
194+
<typescript-minibar>
195+
<form ..>..</form>
196+
</typescript-minibar>
197+
```
198+
199+
Web components automatically activate the relevant JavaScript, no matter when they are inserted on the page.
200+
201+
By default, typescript-minibar.js also makes sure that any `<form class="tsmb-form">` elements on the page are hydrated and activated. This should catch any static element on the page (i.e. before "document ready", or the DOMContentLoaded event). This works internally by levering the fact that script execution is naturally deferred until the document is ready, via the `defer` and `type="module"` attributes on the `<script>` tag.
202+
171203
* How does this prevent JavaScript errors in older browsers? What about ES5?
172204

173205
If you load typesense-minibar.js standalone, make sure you have the `type="module"` attribute on the `<script>` tag. Scripts with this type are naturally ignored by older browsers. The element works fine **without JavaScript**, following the principles of [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement). This technique is analogous to "[cutting the mustard](https://responsivenews.tumblr.com/post/18948466399/cutting-the-mustard)".

demo/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@
2727
</form>
2828
</header>
2929
<main>
30+
<h2>Web Component</h2>
31+
<typesense-minibar>
32+
<form role="search" data-origin="https://typesense.jquery.com" data-collection="qunitjs_com" data-key="Zh8mMgohXECel9wjPwqT7lekLSG3OCgz" data-foot="true" action="https://duckduckgo.com">
33+
<input type="search" name="q" aria-label="Search" placeholder="Search..." autocomplete="off">
34+
<input type="hidden" name="sites" value="qunitjs.com">
35+
</form>
36+
</typesense-minibar>
37+
38+
<h2>Class</h2>
39+
<form role="search" class="tsmb-form" data-origin="https://typesense.jquery.com" data-collection="qunitjs_com" data-key="Zh8mMgohXECel9wjPwqT7lekLSG3OCgz" data-foot="true" action="https://duckduckgo.com">
40+
<input type="search" name="q" aria-label="Search" placeholder="Search..." autocomplete="off">
41+
<input type="hidden" name="sites" value="qunitjs.com">
42+
</form>
43+
3044
<h2>customize layout</h2>
3145
<form role="search" class="tsmb-form demo-custom-form" data-origin="https://typesense.jquery.com" data-collection="jquery_com" data-key="Zh8mMgohXECel9wjPwqT7lekLSG3OCgz" data-foot="true" data-group="true" action="https://duckduckgo.com">
3246
<input type="search" name="q" aria-label="Search" placeholder="Search..." autocomplete="off">

test/test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,4 +530,29 @@ QUnit.module('typesense-minibar', hooks => {
530530
await new Promise(resolve => setTimeout(resolve));
531531
assert.strictEqual(document.head.querySelectorAll('link[rel=preconnect]').length, 1, 'preconnect link');
532532
});
533+
534+
QUnit.test('Web Component [input, listbox, re-focus]', async assert => {
535+
const element = parseHTML('<typesense-minibar><form><input type="search"></form></typesense-minibar>');
536+
document.querySelector('#qunit-fixture').append(element);
537+
const form = element.querySelector('form');
538+
assert.equal(form.className, 'tsmb-form tsmb-form--slash', 'set form class');
539+
540+
const input = form.firstChild;
541+
const listbox = element.querySelector('[role=listbox]');
542+
543+
mockFetchResponse = API_RESP_FULL_MATCH_SOMETHING;
544+
input.value = 'something';
545+
await expectRender(form, () => {
546+
simulate(input, 'input');
547+
});
548+
assert.false(listbox.hidden, 'listbox not hidden');
549+
assert.equal(listbox.querySelector('mark').outerHTML, '<mark>something</mark>', 'snippet');
550+
551+
mockFetchResponse = null;
552+
simulate(document.body, 'click', { bubbles: true });
553+
assert.true(listbox.hidden, 'listbox hidden');
554+
555+
input.focus();
556+
assert.false(listbox.hidden, 'listbox re-opened');
557+
});
533558
});

typesense-minibar.css

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*! https://github.com/jquery/typesense-minibar 1.2.0 */
2-
.tsmb-form {
2+
.tsmb-form,
3+
typesense-minibar {
34
--tsmb-size-edge: 1px;
45
--tsmb-size-radius: 3px;
56
--tsmb-size-highlight: 2px;
@@ -23,15 +24,19 @@
2324
--tsmb-color-primary30: #390f39;
2425
--tsmb-color-primary50: #9c3493;
2526
--tsmb-color-primary90: #fbdbfb;
27+
}
2628

29+
.tsmb-form,
30+
typesense-minibar form {
2731
position: relative;
2832
width: 20rem;
2933
max-width: 100%;
3034
padding: 0;
3135
color: var(--tsmb-color-base30);
3236
}
3337

34-
.tsmb-form input[type=search] {
38+
.tsmb-form input[type=search],
39+
typesense-minibar input[type=search] {
3540
box-sizing: border-box;
3641
-webkit-appearance: none;
3742
-moz-appearance: none;
@@ -46,31 +51,40 @@
4651
line-height: var(--tsmb-size-input);
4752
}
4853

49-
.tsmb-form input[type=search]::placeholder {
54+
.tsmb-form input[type=search]::placeholder,
55+
typesense-minibar input[type=search]::placeholder {
5056
color: var(--tsmb-color-base50);
5157
opacity: 1;
5258
}
5359

54-
.tsmb-form:focus-within {
60+
.tsmb-form:focus-within,
61+
typesense-minibar form:focus-within {
5562
color: var(--tsmb-color-focus30);
5663
}
5764

58-
.tsmb-form:focus-within input[type=search] {
65+
.tsmb-form:focus-within input[type=search],
66+
typesense-minibar:focus-within input[type=search] {
5967
background: var(--tsmb-color-focus-background);
6068
}
6169

62-
.tsmb-form:focus-within input[type=search]::placeholder {
70+
.tsmb-form:focus-within input[type=search]::placeholder,
71+
typesense-minibar:focus-within input[type=search]::placeholder {
6372
color: var(--tsmb-color-focus50);
6473
}
6574

6675
.tsmb-form input[type=search]::-webkit-search-decoration,
6776
.tsmb-form input[type=search]::-webkit-search-cancel-button,
6877
.tsmb-form input[type=search]::-webkit-search-results-button,
69-
.tsmb-form input[type=search]::-webkit-search-results-decoration {
78+
.tsmb-form input[type=search]::-webkit-search-results-decoration,
79+
typesense-minibar input[type=search]::-webkit-search-decoration,
80+
typesense-minibar input[type=search]::-webkit-search-cancel-button,
81+
typesense-minibar input[type=search]::-webkit-search-results-button,
82+
typesense-minibar input[type=search]::-webkit-search-results-decoration {
7083
display: none;
7184
}
7285

73-
.tsmb-form::before {
86+
.tsmb-form::before,
87+
typesense-minibar form::before {
7488
content: '';
7589
background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='654 -372 1664 1664' width='20' height='20'><path d='M1806,332c0-123.3-43.8-228.8-131.5-316.5C1586.8-72.2,1481.3-116,1358-116s-228.8,43.8-316.5,131.5 C953.8,103.2,910,208.7,910,332s43.8,228.8,131.5,316.5C1129.2,736.2,1234.7,780,1358,780s228.8-43.8,316.5-131.5 C1762.2,560.8,1806,455.3,1806,332z M2318,1164c0,34.7-12.7,64.7-38,90s-55.3,38-90,38c-36,0-66-12.7-90-38l-343-342 c-119.3,82.7-252.3,124-399,124c-95.3,0-186.5-18.5-273.5-55.5s-162-87-225-150s-113-138-150-225S654,427.3,654,332 s18.5-186.5,55.5-273.5s87-162,150-225s138-113,225-150S1262.7-372,1358-372s186.5,18.5,273.5,55.5s162,87,225,150s113,138,150,225 S2062,236.7,2062,332c0,146.7-41.3,279.7-124,399l343,343C2305.7,1098.7,2318,1128.7,2318,1164z'/></svg>") 0 50% / contain no-repeat;
7690
position: absolute;

typesense-minibar.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ globalThis.tsminibar = function tsminibar (form) {
9292

9393
function connect () {
9494
document.addEventListener('click', onDocClick);
95+
form.classList.add('tsmb-form');
9596
if (form.dataset.slash !== 'false') {
9697
document.addEventListener('keydown', onDocSlash);
9798
form.classList.add('tsmb-form--slash');
@@ -164,4 +165,10 @@ globalThis.tsminibar = function tsminibar (form) {
164165

165166
return { form, connect, disconnect };
166167
};
167-
document.querySelectorAll('.tsmb-form[data-origin]').forEach(form => tsminibar(form));
168+
document.querySelectorAll('.tsmb-form[data-origin]').forEach(tsminibar);
169+
window.customElements.define('typesense-minibar', class extends HTMLElement {
170+
connectedCallback () {
171+
const form = this.querySelector('form');
172+
if (form) tsminibar(form);
173+
}
174+
});

0 commit comments

Comments
 (0)