@woylie/doggo 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,11 +21,21 @@ The package can be installed by adding `doggo` to your list of dependencies in
21
21
  ```elixir
22
22
  def deps do
23
23
  [
24
- {:doggo, "~> 0.16.1"}
24
+ {:doggo, "~> 0.17.0"}
25
25
  ]
26
26
  end
27
27
  ```
28
28
 
29
+ Enable diagnostic checks at render time in `config/dev.exs` and
30
+ `config/test.exs` :
31
+
32
+ ```elixir
33
+ config :doggo, :diagnostics, true
34
+ ```
35
+
36
+ These checks raise errors if components are not wired up correctly, for example
37
+ if an accessible name is not set. If disabled, the checks are not compiled.
38
+
29
39
  ### Compatibility
30
40
 
31
41
  This package is tested against the Elixir and OTP versions that are still
@@ -172,6 +182,7 @@ import {
172
182
  MenuButton,
173
183
  SplitPane,
174
184
  Tabs,
185
+ Toggletip,
175
186
  Toolbar,
176
187
  Tooltip,
177
188
  Tree,
@@ -186,6 +197,7 @@ const hooks = {
186
197
  "Doggo.MenuButton": MenuButton,
187
198
  "Doggo.SplitPane": SplitPane,
188
199
  "Doggo.Tabs": Tabs,
200
+ "Doggo.Toggletip": Toggletip,
189
201
  "Doggo.Toolbar": Toolbar,
190
202
  "Doggo.Tooltip": Tooltip,
191
203
  "Doggo.Tree": Tree,
@@ -215,6 +227,7 @@ import {
215
227
  MenuButton,
216
228
  SplitPane,
217
229
  Tabs,
230
+ Toggletip,
218
231
  Toolbar,
219
232
  Tooltip,
220
233
  Tree,
@@ -231,6 +244,7 @@ import {
231
244
  "Doggo.MenuButton": MenuButton,
232
245
  "Doggo.SplitPane": SplitPane,
233
246
  "Doggo.Tabs": Tabs,
247
+ "Doggo.Toggletip": Toggletip,
234
248
  "Doggo.Toolbar": Toolbar,
235
249
  "Doggo.Tooltip": Tooltip,
236
250
  "Doggo.Tree": Tree,
@@ -328,6 +342,35 @@ The `data-invalid` attribute is set on the field wrapper whenever the field has
328
342
  errors. The `:empty` selector does not work here, because the rendered list
329
343
  contains whitespace.
330
344
 
345
+ ### Text direction
346
+
347
+ Some components render text you pass in an attribute next to text of their
348
+ own, such as a switch label beside its state text. These elements have
349
+ `dir="auto"`. The browser takes their direction from the text itself, and
350
+ right-to-left text in a left-to-right page does not reorder the text around
351
+ it.
352
+
353
+ Text you pass in a slot is not automatically isolated. If it can hold text in
354
+ another direction, wrap it in `<bdi>`.
355
+
356
+ ```heex
357
+ <.steps current_step={1}>
358
+ <:step><bdi>{@first_step_name}</bdi></:step>
359
+ <:step><bdi>{@second_step_name}</bdi></:step>
360
+ </.steps>
361
+ ```
362
+
363
+ The `field` label holds your label text next to the required or optional
364
+ mark, without an element of its own. If your labels can be in a different
365
+ direction than the page, set `unicode-bidi: plaintext` on the label. The
366
+ browser then takes the label's direction from its content.
367
+
368
+ ```css
369
+ .field label {
370
+ unicode-bidi: plaintext;
371
+ }
372
+ ```
373
+
331
374
  ## Design decisions
332
375
 
333
376
  - Favor semantic HTML elements over CSS classes for structure and clarity.
@@ -338,6 +381,8 @@ contains whitespace.
338
381
  attributes.
339
382
  - The library is designed without default styles and does not prefer any
340
383
  particular CSS framework.
384
+ - Don't render a container if it doesn't have any children, for example a
385
+ navigation without links.
341
386
 
342
387
  ## Demo app
343
388
 
@@ -400,6 +445,7 @@ component at any level:
400
445
 
401
446
  - Adding an attribute.
402
447
  - Adding a class.
448
+ - Changing the whitespace between elements.
403
449
  - Removing an attribute that normally no CSS styles are attached to.
404
450
 
405
451
  These changes **are** breaking:
@@ -413,6 +459,8 @@ These changes **are** breaking:
413
459
  - Reordering elements.
414
460
  - Renaming a JavaScript hook.
415
461
 
462
+ The output format of `mix dog.safelist` only changes in a major release.
463
+
416
464
  ## Feedback
417
465
 
418
466
  If you encounter any issues with a component, have suggestions for improvements,
@@ -9,7 +9,7 @@ const hasClosedBy = () => "closedBy" in HTMLDialogElement.prototype;
9
9
  export function initDialog(dialog, { execJS = () => {} } = {}) {
10
10
  let invoke;
11
11
 
12
- const dismissable = () => dialog.getAttribute("closedby") === "any";
12
+ const closedBy = () => dialog.getAttribute("closedby");
13
13
 
14
14
  // Dispatched by `Doggo.show_modal/2`.
15
15
  dialog.addEventListener("doggo:open", () => {
@@ -47,13 +47,13 @@ export function initDialog(dialog, { execJS = () => {} } = {}) {
47
47
  // `closedby="none"` has to hold Escape as well, which is the one part of
48
48
  // the attribute that native `<dialog>` does not give us anyway.
49
49
  dialog.addEventListener("cancel", (e) => {
50
- if (!dismissable()) e.preventDefault();
50
+ if (closedBy() === "none") e.preventDefault();
51
51
  });
52
52
 
53
53
  // A click on the backdrop reaches the dialog itself, never a child, so
54
54
  // the target is the test for being outside.
55
55
  dialog.addEventListener("click", (e) => {
56
- if (dismissable() && e.target === dialog) dialog.close();
56
+ if (closedBy() === "any" && e.target === dialog) dialog.close();
57
57
  });
58
58
  }
59
59
 
@@ -0,0 +1,82 @@
1
+ // A long announcement cannot be interrupted, so the text is cut. The region is
2
+ // emptied afterwards, since the open panel holds the same text.
3
+ const maxLength = 250;
4
+ const clearAfter = 1000;
5
+
6
+ const announcement = (panel) => {
7
+ const text = panel.textContent.replace(/\s+/g, " ").trim();
8
+ return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
9
+ };
10
+
11
+ // Remove the positioning once CSS anchor positioning is widely baseline
12
+ // available; the example CSS places the panel where it is supported.
13
+ const anchored = () => window.CSS?.supports?.("position-area: block-end");
14
+
15
+ export function initToggletip(toggletip) {
16
+ const button = toggletip.querySelector(":scope > button");
17
+ const panel = toggletip.querySelector(":scope > [popover]");
18
+ const status = toggletip.querySelector(':scope > [role="status"]');
19
+ let timer;
20
+
21
+ const place = () => {
22
+ const rect = button.getBoundingClientRect();
23
+ const rtl = getComputedStyle(toggletip).direction === "rtl";
24
+ const start = rtl ? window.innerWidth - rect.right : rect.left;
25
+
26
+ const style = {
27
+ inset: "auto",
28
+ margin: "0",
29
+ "margin-block-start": "var(--toggletip-offset)",
30
+ "inset-block-start": `${rect.bottom}px`,
31
+ "inset-inline-start": `${start}px`,
32
+ };
33
+
34
+ for (const [name, value] of Object.entries(style)) {
35
+ panel.style.setProperty(name, value);
36
+ }
37
+ };
38
+
39
+ const position = () => {
40
+ if (!anchored() && panel.matches(":popover-open")) place();
41
+ };
42
+
43
+ const beforeToggle = (e) => {
44
+ if (!anchored() && e.newState === "open") place();
45
+ };
46
+
47
+ const announce = (e) => {
48
+ clearTimeout(timer);
49
+ status.textContent = e.newState === "open" ? announcement(panel) : "";
50
+ timer = setTimeout(() => (status.textContent = ""), clearAfter);
51
+ };
52
+
53
+ panel.addEventListener("beforetoggle", beforeToggle);
54
+ panel.addEventListener("toggle", announce);
55
+ window.addEventListener("scroll", position, true);
56
+ window.addEventListener("resize", position);
57
+
58
+ return {
59
+ update: position,
60
+ destroy() {
61
+ clearTimeout(timer);
62
+ panel.removeEventListener("beforetoggle", beforeToggle);
63
+ panel.removeEventListener("toggle", announce);
64
+ window.removeEventListener("scroll", position, true);
65
+ window.removeEventListener("resize", position);
66
+ },
67
+ };
68
+ }
69
+
70
+ export default {
71
+ mounted() {
72
+ this.instance = initToggletip(this.el);
73
+ },
74
+
75
+ updated() {
76
+ this.instance.update();
77
+ },
78
+
79
+ destroyed() {
80
+ this.instance.destroy();
81
+ },
82
+ };
@@ -8,4 +8,5 @@ export { default as SplitPane, initSplitPane } from "./hooks/split_pane.js";
8
8
  export { default as Tabs, initTabs } from "./hooks/tabs.js";
9
9
  export { default as Tree, initTree } from "./hooks/tree.js";
10
10
  export { default as Toolbar, initToolbar } from "./hooks/toolbar.js";
11
+ export { default as Toggletip, initToggletip } from "./hooks/toggletip.js";
11
12
  export { default as Tooltip, initTooltip } from "./hooks/tooltip.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woylie/doggo",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "JavaScript hooks for the Doggo component library",
5
5
  "keywords": [
6
6
  "phoenix",
@@ -29,6 +29,7 @@
29
29
  "./combobox": "./assets/js/hooks/combobox.js",
30
30
  "./tabs": "./assets/js/hooks/tabs.js",
31
31
  "./toolbar": "./assets/js/hooks/toolbar.js",
32
+ "./toggletip": "./assets/js/hooks/toggletip.js",
32
33
  "./tooltip": "./assets/js/hooks/tooltip.js",
33
34
  "./menu": "./assets/js/hooks/menu.js",
34
35
  "./menu_button": "./assets/js/hooks/menu_button.js",