Bulma HTML5 Dialog
Saturday 31 December 2022

Grunewald, Berlin

I’m using Bulma.io for a while now. it’s a beautiful CSS framework. My latest project Xlog uses Bulma. This project includes a tools dialog, using Bulma Modal. That solution needed a lot of javascript behavior that should be possible with an HTML5 dialog tag. The following post describes how I simplified the solution and reduced the Javascript involved.

Current solution

Copy pasta from Bulma Modal documentation got me something running that looks like this.

The code that powers it is a piece of HTML styles with Bulma classes

 1<button class="button js-modal-trigger" data-target="modal-js">
 2  Tools: Ctrl+K
 3</button>
 4
 5<div id="modal-js" class="modal" tabindex="0">
 6  <div class="modal-background"></div>
 7
 8  <div class="modal-content">
 9    <div class="box">
10      <aside class="menu">
11        // Tools HTML here
12      </aside>
13    </div>
14  </div>
15
16  <button class="modal-close is-large" aria-label="close"></button>
17</div>

And some javascript that show/hides the dialog

 1 (function() {
 2     // Functions to open and close a modal
 3     function openModal($el) {
 4         $el.classList.add('is-active');
 5     }
 6
 7     function closeModal($el) {
 8         $el.classList.remove('is-active');
 9     }
10
11     function closeAllModals() {
12         (document.querySelectorAll('.modal') || []).forEach(function($modal) {
13             closeModal($modal);
14         });
15     }
16
17     // Add a click event on buttons to open a specific modal
18     (document.querySelectorAll('.js-modal-trigger') || []).forEach(function($trigger) {
19         const modal = $trigger.dataset.target;
20         const $target = document.getElementById(modal);
21
22         $trigger.addEventListener('click', function() {
23             openModal($target);
24         });
25     });
26
27     // Add a click event on various child elements to close the parent modal
28     (document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(function($close) {
29         const $target = $close.closest('.modal');
30
31         $close.addEventListener('click', function() {
32             closeModal($target);
33         });
34     });
35
36     // Add a keyboard event to close all modals
37     document.addEventListener('keydown', function(event) {
38         const e = event || window.event;
39         const kCharCode = "K".charCodeAt();
40
41         if (e.keyCode === 27) { // Escape key
42             closeAllModals();
43             return;
44         }
45
46         if ( e.keyCode === kCharCode && ( e.metaKey || e.ctrlKey ) ) {
47             e.preventDefault();
48             const $target = document.getElementById('modal-js');
49             openModal($target);
50             $target.focus();
51         }
52
53     });
54 })();

I don’t know about you but this solution looks too complicated. there are lot of Javascript at play here that we probably don’t need it.

I tried to have an input to filter the tools at the top of the dialog and have it focused when the dialog appears. but that required more javascript. I know HTML5 has autofocus attribute that works with dialog. When the dialog is shown it will autofocus the input by default. so apparently I should turn around and use dialog tag and autofocus attribute.

Use HTML5 dialog

HTML5 has a dialog tag with some default behavior. So instead of using a Div tag I replaced it with dialog.

That means I can show and hide the dialog with .showModal and .close methods instead adding/removing is-active class.

When I did that the dialog didn’t appear. so turns out Bulma modal class hides the dialog. The browser already does that. so no need to use Bulma modal class.

Use form to hide dialog

To hide a dialog HTML offers another synergy with form tag. adding a form tag with method="dialog" and a button to submit it will hide it’s parent dialog.

 1<dialog id="tools-modal">
 2  <div class="modal-content">
 3    <div class="box">
 4      <aside class="menu">
 5        // TOOLS HTML HERE
 6      </aside>
 7    </div>
 8  </div>
 9
10  <form method="dialog">
11    <button class="modal-close is-large" aria-label="close"></button>
12  </form>
13</dialog>

That changes the dialog look

Autofocus search input

I noticed that focus is set on the first item in the list. but this list will be longer so I need to have a search input at the top that gets the focus automatically and filter the following list.

Adding the following before the aside tag does the trick.

1      <div class="field">
2        <div class="control">
3           <input class="input" type="search" placeholder="Search..." autofocus/>
4        </div>
5      </div>

Showing the dialog focuses the input instead of the link:

Cleaning up JS

There is so many in the original JS code to allow for multiple dialogs and hide the dialog when clicking different parts of the page when the dialog is open. I have one dialog here so lets clean it up.

 1 const tools = document.getElementById('tools-modal');
 2
 3 document.addEventListener('keydown', function(e) {
 4     const kCharCode = "K".charCodeAt();
 5
 6     if (e.keyCode === 27) { // Escape key
 7         tools.close();
 8         return;
 9     }
10
11     if ( e.keyCode === kCharCode && ( e.metaKey || e.ctrlKey ) ) {
12         e.preventDefault();
13         tools.showModal();
14     }
15 });

The button that shows the dialog was wired with the openModal function dynamically. there is also no need for that we can call window.openToolsModal in onclick attribute

1<button class="button" onclick="tools.showModal()">Tools: Ctrl+K</button>

Searching tools list

I need to filter the list of items that shows under the input whenever the user writes a keyword. tools items are in li tags. so I created a function that takes a keyword and hide all li tags in the dialog that doesn’t include this keyword.

 1 function filterToolsList(v) {
 2     const keyword = v.toLowerCase();
 3     const lis = document.querySelectorAll("#tools-modal li");
 4
 5     for(var i=0; i < lis.length; i++){
 6         const li = lis[i];
 7         if( li.textContent.toLowerCase().includes(keyword) ) {
 8             li.classList.remove("is-hidden");
 9         } else {
10             li.classList.add("is-hidden");
11         }
12     }
13 }

Then called it onkeyup of the search input.

1<input class="input" type="search" placeholder="Search..." onkeyup="filterToolsList(this.value)" autofocus/>

And to save myself some code I passed the input value directly as I don’t need the keyup event nor the whole input element. I just needed the value of the input in this case so I passed exactly what I need.

In action

Full code

The full implementation can be found on xlog repository on github. I tried to remove irrelevant HTML attributes and surrounding code in this post for simplification.

Backlinks