@natachah/vanilla-frontend 0.1.22 → 0.2.1

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.
@@ -31,7 +31,7 @@
31
31
  <rect width="100" height="10" x="0" y="80" rx="0"></rect>
32
32
  </svg>
33
33
  </button>
34
- <div id="drawer" class="drawer" tabindex="0" hidden>
34
+ <div id="drawer" class="drawer" hidden>
35
35
  My awsome drawer !
36
36
  </div>
37
37
  </doc-code>
@@ -63,6 +63,8 @@
63
63
 
64
64
  <blockquote class="note">The <code>#backdrop</code> must be a child of the <code>&lt;body&gt;</code> ! <br> And the default CSS breakpoint to view the backdrop is 960px</blockquote>
65
65
 
66
+ <blockquote class="warning"> You should add a toggle button inside the drawer to avoid some Focus Trap !</blockquote>
67
+
66
68
  <h2>Javascript</h2>
67
69
  <p>This component is mostly in javascript, to use it you must import the javascript file and create a new Drawer object.</p>
68
70
  <p>You can have a <b>Backdrop</b> if you want to make it more like a drawer opening on the front of the content, you juste need a <code>#backdrop</code> somewhere on your page.</p>
@@ -106,7 +108,7 @@
106
108
  <doc-code data-type="js">
107
109
  import Drawer from '@natachah/vanilla-frontend/js/utilities/_drawer.js'
108
110
  const drawer = document.getElementById('drawer')
109
- if (drawer) new Drawer(drawer, { breakpoint : 960, cookkie: '_drawer-cookie' })
111
+ if (drawer) new Drawer(drawer, { breakpoint : 960, cookie: '_drawer-cookie' })
110
112
  </doc-code>
111
113
 
112
114
  <blockquote>
@@ -26,10 +26,8 @@
26
26
 
27
27
  <p>You can create a basic grid via the <code>.grid</code> class.</p>
28
28
 
29
- <p>This method is perfect for <b>fixed</b> and/or <b>complex</b> layout.</p>
30
-
31
29
  <doc-demo>
32
- <div class="grid">
30
+ <div class="grid" style="--grid-columns:8">
33
31
  <div>1</div>
34
32
  <div>2</div>
35
33
  <div>3</div>
@@ -46,29 +44,20 @@
46
44
  </doc-demo>
47
45
 
48
46
  <doc-code>
49
- <div class="grid">
47
+ <div class="grid" style="--grid-columns:8">
50
48
  <div>Col 1</div>
51
49
  <div>Col 2</div>
52
50
  <div>Col 3</div>
53
- <div>Col 4</div>
54
- <div>Col 5</div>
55
- <div>Col 6</div>
56
- <div>Col 7</div>
57
- <div>Col 8</div>
58
- <div>Col 9</div>
59
- <div>Col 10</div>
60
- <div>Col 11</div>
61
- <div>Col 12</div>
51
+ <div>...</div>
62
52
  </div>
63
53
  </doc-code>
64
54
 
65
55
  <h2 id="flex-grid">Flex grid</h2>
66
56
 
67
57
  <p>You can create a flexible grid via the <code>.flex-grid</code> class.</p>
68
- <p>This method is more for <b>flexible</b> layout with a random number of items to display.</p>
69
58
 
70
59
  <doc-demo>
71
- <div class="flex-grid">
60
+ <div class="flex-grid" style="--grid-columns:8; --grid-grow:0">
72
61
  <div>1</div>
73
62
  <div>2</div>
74
63
  <div>3</div>
@@ -85,7 +74,39 @@
85
74
  </doc-demo>
86
75
 
87
76
  <doc-code>
88
- <div class="flex-grid">
77
+ <div class="flex-grid" style="--grid-columns:8; --grid-grow:0">
78
+ <div>1</div>
79
+ <div>2</div>
80
+ <div>3</div>
81
+ <div>...</div>
82
+ </div>
83
+ </doc-code>
84
+
85
+ <h2>Customization</h2>
86
+
87
+ <p>Both of them can be customize by changing the CSS custom properties.</p>
88
+
89
+ <doc-code data-type="scss">
90
+ --grid-columns: 12;
91
+ --grid-gap-inline: 1rem;
92
+ --grid-gap-block: 1rem;
93
+ --grid-min-column-size: 0%;
94
+ --grid-min-columns: auto-fit;
95
+ </doc-code>
96
+
97
+ <p>But <code>.flex-grid</code> as also the property <code>--grid-grow</code> !</p>
98
+
99
+ <h3>Responsive</h3>
100
+
101
+ <p>There is multiple way to make them responsive, but a simple one is to change the CSS custom properties as:</p>
102
+
103
+ <doc-code data-type="scss">
104
+ --grid-columns: 6; // The maximum number of columns
105
+ --grid-min-column-size: 120px; // The minimum width of a column
106
+ </doc-code>
107
+
108
+ <doc-demo>
109
+ <div class="grid" style="--grid-columns: 6;--grid-min-column-size: 120px;">
89
110
  <div>1</div>
90
111
  <div>2</div>
91
112
  <div>3</div>
@@ -94,23 +115,13 @@
94
115
  <div>6</div>
95
116
  <div>7</div>
96
117
  <div>8</div>
97
- <div>9</div>
98
- <div>10</div>
99
- <div>11</div>
100
- <div>12</div>
101
118
  </div>
102
- </doc-code>
103
-
104
- <h2>Responsive</h2>
105
-
106
- <p>Both of them can be responsive by changing the CSS custom property <code>--grid-columns</code> inside a <code>@media</code> or a <code>@container</code>.</p>
107
-
108
- <h3>Auto-fill</h3>
119
+ </doc-demo>
109
120
 
110
- <p>With <code>.grid</code> you can add an auto layout with a maximum column size by changing the CSS custom properties <code>--grid-columns:auto-fill</code> and <code>--grid-column-size</code> and .</p>
121
+ <p>Same thing for the Flex grid:</p>
111
122
 
112
123
  <doc-demo>
113
- <div class="grid" style="--grid-columns: auto-fill; --grid-column-size: 270px">
124
+ <div class="flex-grid" style="--grid-columns: 6;--grid-min-column-size: 120px;">
114
125
  <div>1</div>
115
126
  <div>2</div>
116
127
  <div>3</div>
@@ -119,10 +130,6 @@
119
130
  <div>6</div>
120
131
  <div>7</div>
121
132
  <div>8</div>
122
- <div>9</div>
123
- <div>10</div>
124
- <div>11</div>
125
- <div>12</div>
126
133
  </div>
127
134
  </doc-demo>
128
135
 
@@ -130,10 +137,10 @@
130
137
 
131
138
  <h3>Offset</h3>
132
139
 
133
- <p>To make a column ossfet with <code>.grid</code>, change the properties <code>grid-column-start</code> and <code>grid-column-end</code> of the column.</p>
140
+ <p>To make a column ossfet with <code>.grid</code>, change the properties <code>--grid-min-columns</code> to a fixed number, and add the properties <code>grid-column-start</code> and <code>grid-column-end</code> into the column.</p>
134
141
 
135
142
  <doc-demo>
136
- <div class="grid" style="--grid-columns:4">
143
+ <div class="grid" style="--grid-min-columns:4">
137
144
  <div>Grid 1</div>
138
145
  <div>Grid 2</div>
139
146
  <div style="grid-column-start: 4;grid-column-end: 5;">Offset</div>
@@ -171,47 +178,6 @@
171
178
  <div id="flexGridDemoWider">Wider</div>
172
179
  </div>
173
180
  </doc-demo>
174
-
175
- <h3>Auto</h3>
176
-
177
- <p>Both grids support auto column but are triggered by different CSS custom properties.</p>
178
-
179
- <p>For <code>.grid</code> use the property <code>--grid-columns:auto-fit</code>.</p>
180
-
181
- <doc-demo>
182
- <div class="grid" style="--grid-columns:auto-fit">
183
- <div>Grid 1</div>
184
- <div>Grid 2</div>
185
- <div>Grid 3</div>
186
- <div>Grid 4</div>
187
- </div>
188
- </doc-demo>
189
-
190
- <p>And for the <code>.flex-grid</code> use the property <code>--grid-grow:1</code>.</p>
191
-
192
- <doc-demo>
193
- <div class="flex-grid" style="--grid-grow:1">
194
- <div>Flex 1</div>
195
- <div>Flex 2</div>
196
- <div>Flex 3</div>
197
- <div>Flex 4</div>
198
- </div>
199
- </doc-demo>
200
-
201
- <h2>Customization</h2>
202
-
203
- <p>Both grid can be customize via CSS custom property.</p>
204
-
205
- <doc-code data-type="css">
206
- --grid-gap-block
207
- --grid-gap-inline
208
- --grid-justify
209
- --grid-align
210
- --grid-columns
211
- --grid-grow /* only for .flex-grid */
212
- --grid-column-size /* only for .grid */
213
- </doc-code>
214
-
215
181
  </doc-layout>
216
182
  <script type="module" src="/main.js"></script>
217
183
  </body>
@@ -0,0 +1,89 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Documentations: Javascript > Trap</title>
8
+ </head>
9
+
10
+ <body data-preload>
11
+ <doc-layout>
12
+
13
+ <h1>Trap</h1>
14
+ <p>The trap component make you able to create a focus trap on an element.</p>
15
+
16
+ <h2>Javascript</h2>
17
+ <p>This component is only in javascript, to use it you must import the javascript file and create a new Trap object.</p>
18
+ <p>You can pass the element and an array of exclusions when initiate the component.</p>
19
+
20
+ <doc-code data-type="js">
21
+ import Trap from "@natachah/vanilla-frontend/js/utilities/_trap"
22
+ new Trap(document.getElementById('drawer'), ['#backdrop'])
23
+ </doc-code>
24
+
25
+ <h3>Methods</h3>
26
+ <table>
27
+ <thead>
28
+ <tr>
29
+ <th>Method</th>
30
+ <th>Description</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ <tr>
35
+ <td data-label="Method">
36
+ <p>focusOnFirst()</p>
37
+ </td>
38
+ <td data-label="Description">
39
+ <p>This will focus on the first focusable element</p>
40
+ </td>
41
+ </tr>
42
+ <tr>
43
+ <td data-label="Method">
44
+ <p>activate()</p>
45
+ </td>
46
+ <td data-label="Description">
47
+ <p>This method will activate the trap, it will also set <code>inert</code> and <code>aria-hidden</code> attributes on other HTMLElements</p>
48
+ </td>
49
+ </tr>
50
+ <tr>
51
+ <td data-label="Method">
52
+ <p>desactivate()</p>
53
+ </td>
54
+ <td data-label="Description">
55
+ <p>This method will desactivate the trap and remove related attributes</p>
56
+ </td>
57
+ </tr>
58
+ <tr>
59
+ <td data-label="Method">
60
+ <p>handleKeydown(e)</p>
61
+ </td>
62
+ <td data-label="Description">
63
+ <p>This method will keep the <kbd>TAB</kbd> focus inside the element</p>
64
+ </td>
65
+ </tr>
66
+ <tr>
67
+ <td data-label="Method">
68
+ <p>getFocusableElements()</p>
69
+ </td>
70
+ <td data-label="Description">
71
+ <p>This method will return an array of focusable items inside the element</p>
72
+ </td>
73
+ </tr>
74
+ <tr>
75
+ <td data-label="Method">
76
+ <p>getSiblingsOutside()</p>
77
+ </td>
78
+ <td data-label="Description">
79
+ <p>This method will return an array of HTMLElements outside of the element</p>
80
+ </td>
81
+ </tr>
82
+ </tbody>
83
+ </table>
84
+
85
+ </doc-layout>
86
+ <script type="module" src="/main.js"></script>
87
+ </body>
88
+
89
+ </html>
@@ -28,69 +28,70 @@
28
28
  <doc-code data-type="css">
29
29
  :root {
30
30
 
31
- // Typography
32
- --font-size: 16px;
33
- --line-height: 1.5;
34
- --font-family: Arial;
35
- --font-weight: normal;
36
-
37
- --font-size-h1: 2.25em; // 36px
38
- --font-size-h2: 2.00em; // 32px
39
- --font-size-h3: 1.75em; // 28px
40
- --font-size-h4: 1.50em; // 24px
41
- --font-size-h5: 1.25em; // 20px
42
- --font-size-h6: 1.125em; // 18px
43
- --font-size-large: 1.125em; // 18px
44
- --font-size-small: .875em; // 14px
45
-
46
- // Anchor
47
- --decoration: none;
48
-
49
- // Layouts
50
- --padding-inline: .75em;
51
- --padding-block: .5em;
52
-
53
- // Border
54
- --border-size: 1px;
55
- --border-style: solid;
56
- --border-radius: .25rem;
57
-
58
- // Outline (:focus)
59
- --outline-size: 3px;
60
- --outline-style: solid;
61
- --outline-offset: 0;
62
- --outline-opacity: 50%;
63
-
64
- // Hover (color-mix)
65
- --hover-color: black;
66
- --hover-percent: 5%;
67
-
68
- // Active (color-mix)
69
- --active-color: black;
70
- --active-percent: 10%;
71
-
72
- // Disabled
73
- --disabled-opacity: 50%;
74
-
75
- // Colors
76
- --color-body: white;
77
- --color-font: black;
78
- --color-primary: #B790E5;
79
- --color-error: #DC3030;
80
- --color-success: #008A00;
81
- --color-warning: #FFA500;
82
-
83
- // Contrasts
84
- --color-warning-contrast: black;
85
-
86
- // Icons
87
- --icon-date: url('data:image/svg+xml,...');
88
- --icon-time: url('data:image/svg+xml,...');
89
- --icon-file: url('data:image/svg+xml,...');
90
- --icon-select: url('data:image/svg+xml,...');
91
- --icon-radio: url('data:image/svg+xml,...');
92
- --icon-check: url('data:image/svg+xml,...');
93
- --icon-switch: var(--icon-radio);
31
+ // Typography
32
+ --font-size: 16px;
33
+ --line-height: 1.5;
34
+ --font-family: Arial;
35
+ --font-weight: normal;
36
+
37
+ --font-size-h1: 2.25em; // 36px
38
+ --font-size-h2: 2.00em; // 32px
39
+ --font-size-h3: 1.75em; // 28px
40
+ --font-size-h4: 1.50em; // 24px
41
+ --font-size-h5: 1.25em; // 20px
42
+ --font-size-h6: 1.125em; // 18px
43
+ --font-size-large: 1.125em; // 18px
44
+ --font-size-small: .875em; // 14px
45
+
46
+ // Anchor
47
+ --decoration: none;
48
+
49
+ // Layouts
50
+ --padding-inline: .75em;
51
+ --padding-block: .5em;
52
+
53
+ // Border
54
+ --border-size: 1px;
55
+ --border-style: solid;
56
+ --border-radius: .25rem;
57
+
58
+ // Outline (:focus)
59
+ --outline-size: 3px;
60
+ --outline-style: solid;
61
+ --outline-offset: 0;
62
+ --outline-opacity: 50%;
63
+
64
+ // Hover (color-mix)
65
+ --hover-color: black;
66
+ --hover-percent: 5%;
67
+
68
+ // Active (color-mix)
69
+ --active-color: black;
70
+ --active-percent: 10%;
71
+
72
+ // Disabled
73
+ --disabled-opacity: 50%;
74
+
75
+ // Colors
76
+ --color-body: white;
77
+ --color-font: black;
78
+ --color-primary: #B790E5;
79
+ --color-error: #DC3030;
80
+ --color-success: #008A00;
81
+ --color-warning: #FFA500;
82
+
83
+ // Contrasts
84
+ --color-warning-contrast: black;
85
+
86
+ // Icons
87
+ --icon-external: url('data:image/svg+xml,...');
88
+ --icon-date: url('data:image/svg+xml,...');
89
+ --icon-time: url('data:image/svg+xml,...');
90
+ --icon-file: url('data:image/svg+xml,...');
91
+ --icon-select: url('data:image/svg+xml,...');
92
+ --icon-radio: url('data:image/svg+xml,...');
93
+ --icon-check: url('data:image/svg+xml,...');
94
+ --icon-switch: var(--icon-radio);
94
95
 
95
96
  }
96
97
  </doc-code>
@@ -102,20 +103,20 @@
102
103
  // This is the light theme (or if there is none)
103
104
  html[data-theme=light],
104
105
  html:not([data-theme]) {
105
- --color-body: white;
106
- --color-font: black;
106
+ --color-body: white;
107
+ --color-font: black;
107
108
  }
108
109
 
109
110
  // This is for the dark theme
110
111
  html[data-theme=dark] {
111
- --color-body: black;
112
- --color-font: white;
112
+ --color-body: black;
113
+ --color-font: white;
113
114
  }
114
115
 
115
116
  // This is for the dark theme
116
117
  html[data-theme=my-custom-theme] {
117
- --color-body: white;
118
- --color-font: orange;
118
+ --color-body: white;
119
+ --color-font: orange;
119
120
  }
120
121
  </doc-code>
121
122
 
@@ -137,10 +138,10 @@
137
138
 
138
139
  <doc-code data-type="scss">
139
140
  @use '@natachah/vanilla-frontend/scss/abstracts/_options' with (
140
- $colors: (
141
- primary,
142
- danger
143
- )
141
+ $colors: (
142
+ primary,
143
+ danger
144
+ )
144
145
  );
145
146
  @use "@natachah/vanilla-frontend/scss/base";
146
147
  </doc-code>
@@ -18,7 +18,7 @@ class DocLayout extends HTMLElement {
18
18
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pin-angle" viewBox="0 0 16 16">
19
19
  <path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146m.122 2.112v-.002zm0-.002v.002a.5.5 0 0 1-.122.51L6.293 6.878a.5.5 0 0 1-.511.12H5.78l-.014-.004a5 5 0 0 0-.288-.076 5 5 0 0 0-.765-.116c-.422-.028-.836.008-1.175.15l5.51 5.509c.141-.34.177-.753.149-1.175a5 5 0 0 0-.192-1.054l-.004-.013v-.001a.5.5 0 0 1 .12-.512l3.536-3.535a.5.5 0 0 1 .532-.115l.096.022c.087.017.208.034.344.034q.172.002.343-.04L9.927 2.028q-.042.172-.04.343a1.8 1.8 0 0 0 .062.46z"/>
20
20
  </svg>
21
- 0.1.22
21
+ 0.2.1
22
22
  </span>
23
23
  </li>
24
24
  <li>
@@ -36,7 +36,7 @@ class DocLayout extends HTMLElement {
36
36
  </ul>
37
37
  </nav>
38
38
  </header>
39
- <aside id="sidebar" class="drawer" hidden>
39
+ <div id="sidebar" class="drawer" hidden>
40
40
  <header>
41
41
  <a href="/index.html">
42
42
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-rocket" viewBox="0 0 16 16">
@@ -91,14 +91,18 @@ class DocLayout extends HTMLElement {
91
91
  <li><a href="/pages/javascript/sortable.html">Sortable</a></li>
92
92
  <li><a href="/pages/javascript/tabpanel.html">Tabpanel</a></li>
93
93
  <li><a href="/pages/javascript/toggle.html">Toggle</a></li>
94
+ <li><a href="/pages/javascript/trap.html">Trap</a></li>
94
95
  <li><a href="/pages/javascript/tree.html">Tree</a></li>
95
96
  </ul>
96
97
  <div id="copyright">
97
98
  Released under the MIT License.<br>
98
99
  Copyright © 2024-present <a href="https://natachaherth.ch">Natacha Herth</a>
99
100
  </div>
101
+ <button aria-expanded="false" aria-pressed="false" aria-controls="sidebar" aria-label="Close the sidebar navigation">
102
+ Close
103
+ </button>
100
104
  </nav>
101
- </aside>
105
+ </div>
102
106
  <main>
103
107
  ${this.innerHTML}
104
108
  </main>
@@ -40,7 +40,7 @@ doc-layout {
40
40
  overflow-y: auto;
41
41
  }
42
42
 
43
- aside {
43
+ #sidebar {
44
44
 
45
45
  --drawer-display: grid;
46
46
  --drawer-transform: translatex(-100%);
@@ -75,7 +75,7 @@ doc-layout {
75
75
  }
76
76
 
77
77
  @media (min-width: 960px) {
78
- &:has(aside:not([hidden])) {
78
+ &:has(#sidebar:not([hidden])) {
79
79
  grid-template-columns: 320px 1fr;
80
80
  }
81
81
  }
@@ -47,7 +47,7 @@ doc-layout {
47
47
  border-bottom: 1px solid var(--doc-border-color);
48
48
  }
49
49
 
50
- > aside {
50
+ > #sidebar {
51
51
 
52
52
  background-color: var(--color-body, white);
53
53
  border-right: 1px solid var(--doc-border-color);
package/js/_drawer.js CHANGED
@@ -13,6 +13,7 @@
13
13
  import BaseComponent from './utilities/_base-component'
14
14
  import ErrorMessage from "./utilities/_error"
15
15
  import Cookie from "./utilities/_cookie"
16
+ import Trap from "./utilities/_trap"
16
17
 
17
18
  export default class Drawer extends BaseComponent {
18
19
 
@@ -45,13 +46,12 @@ export default class Drawer extends BaseComponent {
45
46
 
46
47
  this._isOpen = !this._element.hidden
47
48
 
48
- this._focus = this._element.getAttribute('tabindex') === '0' ? this._element : this._element.querySelector('[tabindex="0"]') ?? this._element.querySelector('button, a, input')
49
-
50
49
  this._cookie = this._options.cookie ? new Cookie(this._options.cookie) : null
51
50
 
51
+ this._trap = new Trap(this._element, ['#backdrop'])
52
+
52
53
  // Init the event listener
53
54
  this.#init()
54
-
55
55
  }
56
56
 
57
57
  /**
@@ -64,29 +64,15 @@ export default class Drawer extends BaseComponent {
64
64
  // Set the cookie by default
65
65
  if (this._cookie && !this._cookie.has('drawer-is-open')) this._cookie.set({ ...this._cookie.value, 'drawer-is-open': this._isOpen })
66
66
 
67
- // Define the default state on bigger screen
68
- const shouldBeOpen = this._cookie ? this._cookie.get('drawer-is-open') : true
69
-
70
- // Window is bigger than breakpoint
71
- // -> Show/Hide the drawer by the cookie
72
- if (window.innerWidth > this._options.breakpoint) this.toggle(shouldBeOpen)
73
-
74
- // Window is smaller than breakpoint
75
- // -> Hide the drawer
76
- if (window.innerWidth <= this._options.breakpoint) this.toggle(false)
67
+ // Define the default status of the drawer by breakpoint
68
+ this.defineDrawerByBreakpoint()
77
69
 
78
70
  // On window resize
79
71
  // -> Toggle the drawer
80
72
  window.onresize = () => {
81
73
  clearTimeout(this._timeout)
82
74
  this._timeout = setTimeout(() => {
83
-
84
- // Bigger than breakpoint
85
- if (window.innerWidth > this._options.breakpoint && (!this._isOpen && shouldBeOpen)) this.toggle(true)
86
-
87
- // Smaller than breakpoint -> Invisible
88
- if (window.innerWidth <= this._options.breakpoint && this._isOpen) this.toggle(false)
89
-
75
+ this.defineDrawerByBreakpoint()
90
76
  }, 250)
91
77
  }
92
78
 
@@ -98,6 +84,34 @@ export default class Drawer extends BaseComponent {
98
84
 
99
85
  }
100
86
 
87
+ defineDrawerByBreakpoint() {
88
+
89
+ let drawerStatus
90
+
91
+ // On mobil, the drawer become a Dialog and it is hidden per default
92
+ if (window.innerWidth <= this._options.breakpoint) {
93
+ // Add Dialog role
94
+ this._element.setAttribute('role', 'dialog')
95
+ this._element.setAttribute('aria-modal', true)
96
+
97
+ // The drawer should be closed
98
+ drawerStatus = false
99
+ }
100
+
101
+ // On breakpoint, the drawer become by default "this._options.default"
102
+ if (window.innerWidth > this._options.breakpoint) {
103
+ // Remove Dialog role
104
+ this._element.removeAttribute('role')
105
+ this._element.removeAttribute('aria-modal')
106
+
107
+ // The drawer should be cookie or open
108
+ drawerStatus = this._cookie ? this._cookie.get('drawer-is-open') : true
109
+ }
110
+
111
+ if (drawerStatus != this._isOpen) this.toggle(drawerStatus)
112
+
113
+ }
114
+
101
115
  /**
102
116
  * Toggle the drawer
103
117
  *
@@ -122,15 +136,15 @@ export default class Drawer extends BaseComponent {
122
136
  this._cookie.set({ ...this._cookie.value, 'drawer-is-open': this._isOpen })
123
137
  }
124
138
 
125
- // Add the focus if open
139
+ // Add the Trap focus
126
140
  // * Need to wait the transition before make it focused
127
- if (this._focus && this._isOpen) {
128
- this._element.addEventListener("transitionend", () => {
129
- this._focus.focus()
130
- }, { once: true })
131
- }
141
+ this._element.addEventListener('transitionend', () => {
142
+ if (this._element.getAttribute('role') === 'dialog') {
143
+ this._isOpen ? this._trap.activate() : this._trap.desactivate()
144
+ } else if (this._isOpen) {
145
+ this._trap.focusOnFirst()
146
+ }
147
+ }, { once: true })
132
148
 
133
149
  }
134
-
135
-
136
150
  }
@@ -44,7 +44,6 @@ describe('Structure of the class', () => {
44
44
  expect(fakeDrawer._buttons).toStrictEqual(document.querySelectorAll('button[aria-controls="drawer"]'))
45
45
  expect(fakeDrawer._backdrop).toStrictEqual(document.getElementById('backdrop'))
46
46
  expect(fakeDrawer._isOpen).toBeTruthy()
47
- expect(fakeDrawer._focus).toStrictEqual(document.getElementById('focus'))
48
47
  })
49
48
 
50
49
  })
@@ -0,0 +1,119 @@
1
+ /**
2
+ * ------------------------------------------------------------------
3
+ * TEST for the /utilities/_trap.js
4
+ * ------------------------------------------------------------------
5
+ * The test will take care of:
6
+ * - Has all the public method
7
+ * - Constructor: Passing the correct parameters
8
+ * - FocusOnFirst(): Passing the correct parameters
9
+ * - Activate(): Save the new cookie value
10
+ * - Desactivate(): Passing the correct parameters
11
+ * - HandleKeydown(): Verify the existance of a key in the cookie value
12
+ * - GetFocusableElements(): Passing the correct parameters
13
+ */
14
+
15
+ import { describe, test, expect, beforeAll, vi } from "vitest"
16
+ import { fireEvent } from "@testing-library/dom"
17
+ import Trap from "../utilities/_trap"
18
+ import ErrorMessage from "../utilities/_error"
19
+
20
+ let fakeTrap, fakeBtn, fakeLink, fakeDisabledLink, fakeInput
21
+
22
+ /**
23
+ * Before all tests
24
+ *
25
+ */
26
+
27
+ beforeAll(() => {
28
+
29
+ document.body.innerHTML =
30
+ '<div id="trap">' +
31
+ '<button id="button"></button>' +
32
+ '<a id="link" href="#"></a>' +
33
+ '<a id="linkDisabled"></a>' +
34
+ '<input id="input">' +
35
+ '</div>' +
36
+ '<div id="backdrop"></div>' +
37
+ '<div id="content"></div>'
38
+
39
+ fakeTrap = new Trap(document.getElementById('trap'), ['#backdrop'])
40
+ fakeBtn = document.getElementById('button')
41
+ fakeLink = document.getElementById('link')
42
+ fakeDisabledLink = document.getElementById('linkDisabled')
43
+ fakeInput = document.getElementById('input')
44
+
45
+ for (const el of [fakeBtn, fakeLink, fakeDisabledLink, fakeInput]) {
46
+ Object.defineProperty(el, 'offsetWidth', { configurable: true, value: 100 })
47
+ Object.defineProperty(el, 'offsetHeight', { configurable: true, value: 20 })
48
+ }
49
+
50
+ })
51
+
52
+ describe('Structure of the class', () => {
53
+
54
+ test('Has all the public method', () => {
55
+ expect(fakeTrap.focusOnFirst).toBeTypeOf('function')
56
+ expect(fakeTrap.activate).toBeTypeOf('function')
57
+ expect(fakeTrap.desactivate).toBeTypeOf('function')
58
+ expect(fakeTrap.handleKeydown).toBeTypeOf('function')
59
+ expect(fakeTrap.getFocusableElements).toBeTypeOf('function')
60
+ expect(fakeTrap.getSiblingsOutside).toBeTypeOf('function')
61
+ })
62
+
63
+ test('Constructor: Passing the correct parameters', () => {
64
+ expect(() => new Trap()).toThrowError(ErrorMessage.instanceOf('el', 'HTMLElement'))
65
+ expect(() => new Trap(document.getElementById('trap'), 'error')).toThrowError(ErrorMessage.typeOf('exclusions', 'object'))
66
+ })
67
+
68
+ })
69
+
70
+ describe('FocusOnFirst()', () => {
71
+
72
+ test('Focus on first element', () => {
73
+
74
+ expect(fakeTrap._items).toStrictEqual([])
75
+ expect(fakeTrap._first).toStrictEqual(null)
76
+
77
+ fakeTrap.focusOnFirst()
78
+
79
+ expect(fakeTrap._items).toStrictEqual(fakeTrap.getFocusableElements())
80
+ expect(fakeTrap._first).toStrictEqual(fakeBtn)
81
+ expect(document.activeElement).toBe(fakeTrap._first)
82
+
83
+ })
84
+
85
+ })
86
+
87
+ describe('Toggle the trap', () => {
88
+
89
+ test('Activate the trap', () => {
90
+ const eventSpy = vi.spyOn(fakeTrap._element, 'addEventListener')
91
+
92
+ fakeTrap.activate()
93
+
94
+ expect(fakeTrap._items).toStrictEqual(fakeTrap.getFocusableElements())
95
+ expect(fakeTrap._items).not.toContain(fakeDisabledLink)
96
+ expect(fakeTrap._first).toStrictEqual(document.getElementById('button'))
97
+ expect(fakeTrap._last).toStrictEqual(document.getElementById('input'))
98
+ expect(fakeTrap._inerts).toStrictEqual(fakeTrap.getSiblingsOutside())
99
+ expect(fakeTrap._inerts).not.toContain(document.getElementById('backdrop'))
100
+ expect(document.activeElement).toBe(fakeTrap._first)
101
+ expect(eventSpy).toHaveBeenCalledWith('keydown', fakeTrap.handleKeydown)
102
+ expect(document.getElementById('backdrop').hasAttribute('inert')).toBeFalsy()
103
+ expect(document.getElementById('backdrop').hasAttribute('aria-hidden')).toBeFalsy()
104
+ expect(document.getElementById('content').hasAttribute('inert')).toBeTruthy()
105
+ expect(document.getElementById('content').hasAttribute('aria-hidden')).toBeTruthy()
106
+
107
+ })
108
+
109
+ test('Desactivate the trap', () => {
110
+ const eventSpy = vi.spyOn(fakeTrap._element, 'removeEventListener')
111
+ fakeTrap.desactivate()
112
+ expect(eventSpy).toHaveBeenCalledWith('keydown', fakeTrap.handleKeydown)
113
+ expect(document.getElementById('backdrop').hasAttribute('inert')).toBeFalsy()
114
+ expect(document.getElementById('backdrop').hasAttribute('aria-hidden')).toBeFalsy()
115
+ expect(document.getElementById('content').hasAttribute('inert')).toBeFalsy()
116
+ expect(document.getElementById('content').hasAttribute('aria-hidden')).toBeFalsy()
117
+ })
118
+
119
+ })
@@ -0,0 +1,155 @@
1
+ /**
2
+ * ------------------------------------------------------------------
3
+ * Focus Trap
4
+ * ------------------------------------------------------------------
5
+ * This class create a focus trap on an element as a drawer or a dialog.
6
+ *
7
+ * @author Natacha Herth
8
+ * @copyright Natacha Herth, design & web development
9
+ */
10
+
11
+ import ErrorMessage from "./_error"
12
+
13
+ export default class Trap {
14
+
15
+ /**
16
+ * Creates an instance
17
+ *
18
+ * @constructor
19
+ */
20
+ constructor(el, exclusions = null) {
21
+
22
+ // Check for errors
23
+ if (!(el instanceof HTMLElement)) throw new Error(ErrorMessage.instanceOf('el', 'HTMLElement'))
24
+ if (exclusions && typeof exclusions !== 'object') throw new Error(ErrorMessage.typeOf('exclusions', 'object'))
25
+
26
+ // Define elements
27
+ this._element = el
28
+ this._items = []
29
+ this._first = null
30
+ this._last = null
31
+ this._inerts = []
32
+ this._exclusions = exclusions ?? []
33
+ this.handleKeydown = this.handleKeydown.bind(this)
34
+
35
+ }
36
+
37
+ /**
38
+ * Focus on first element
39
+ *
40
+ */
41
+ focusOnFirst() {
42
+ // Define the focusable items
43
+ this._items = this.getFocusableElements()
44
+ if (this._items.length === 0) return
45
+
46
+ // Define the first and last item
47
+ this._first = this._element.querySelector('[tabindex="0"]') ?? this._items[0]
48
+
49
+ // Focus the first element by default
50
+ this._first.focus()
51
+ }
52
+
53
+ /**
54
+ * Activate the trap
55
+ *
56
+ */
57
+ activate() {
58
+ // Define the focusable items
59
+ this._items = this.getFocusableElements()
60
+ if (this._items.length === 0) return
61
+
62
+ // Define the first and last item
63
+ this._first = this._element.querySelector('[tabindex="0"]') ?? this._items[0]
64
+ this._last = this._items[this._items.length - 1]
65
+
66
+ // Define event listener
67
+ this._element.addEventListener('keydown', this.handleKeydown)
68
+
69
+ // Focus the first element by default
70
+ this._first.focus()
71
+
72
+ // Make the rest of the content inert for screen reader
73
+ this._inerts = this.getSiblingsOutside()
74
+ this._inerts.forEach(el => {
75
+ el.setAttribute('aria-hidden', 'true')
76
+ el.setAttribute('inert', true)
77
+ })
78
+
79
+ }
80
+
81
+ /**
82
+ * Desactivate the trap
83
+ *
84
+ */
85
+ desactivate() {
86
+ this._element.removeEventListener('keydown', this.handleKeydown)
87
+
88
+ this._inerts.forEach(el => {
89
+ el.removeAttribute('aria-hidden')
90
+ el.removeAttribute('inert')
91
+ })
92
+ }
93
+
94
+ /**
95
+ * Handle event on Tab
96
+ *
97
+ * @param {event} e - Event
98
+ */
99
+ handleKeydown(e) {
100
+
101
+ // Check for the Tab event only
102
+ if (e.key !== 'Tab') return
103
+
104
+ const isShift = e.shiftKey
105
+
106
+ if (isShift && document.activeElement === this._first) {
107
+ e.preventDefault()
108
+ this._last.focus()
109
+ } else if (!isShift && document.activeElement === this._last) {
110
+ e.preventDefault()
111
+ this._first.focus()
112
+ }
113
+
114
+ }
115
+
116
+ /**
117
+ * Get all focusable elements
118
+ *
119
+ */
120
+ getFocusableElements() {
121
+ return Array.from(
122
+ this._element.querySelectorAll('a[href], area[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]),textarea:not([disabled]), button:not([disabled]), iframe, object, embed,[contenteditable], [tabindex]:not([tabindex="-1"])')
123
+ ).filter(el => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement)
124
+ }
125
+
126
+ /**
127
+ * Find elements outside of the element
128
+ * Generated by ChatGPT
129
+ *
130
+ * @returns {HTMLElement[]}
131
+ */
132
+ getSiblingsOutside() {
133
+ const hiddenEls = new Set()
134
+ let current = this._element
135
+
136
+ while (current && current !== document.body) {
137
+ const parent = current.parentElement
138
+ if (!parent) break
139
+
140
+ const siblings = Array.from(parent.children).filter(el =>
141
+ el !== current &&
142
+ !current.contains(el) &&
143
+ !this._exclusions.some(sel => el.matches(sel))
144
+ )
145
+
146
+ siblings.forEach(el => hiddenEls.add(el))
147
+
148
+ current = parent
149
+ }
150
+
151
+ return Array.from(hiddenEls)
152
+
153
+ }
154
+
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natachah/vanilla-frontend",
3
- "version": "0.1.22",
3
+ "version": "0.2.1",
4
4
  "description": "A vanilla frontend framework",
5
5
  "keywords": [
6
6
  "html5",
@@ -120,16 +120,16 @@ body {
120
120
  overflow-x: hidden;
121
121
  margin-right: calc(100% - 100vw);
122
122
 
123
- // Avoid scroll when a dialog is opened
124
- &[inert] {
125
- overflow: hidden;
126
- pointer-events: none;
127
- touch-action: none;
128
- }
129
-
130
123
  // Data attribute to prevent animation on first load DOM
131
124
  &[data-preload] * {
132
125
  transition-duration: 0s !important;
133
126
  }
134
127
 
128
+ }
129
+
130
+ // Avoid scroll when a dialog is opened
131
+ [inert] {
132
+ overflow: hidden;
133
+ pointer-events: none;
134
+ touch-action: none;
135
135
  }
@@ -11,6 +11,9 @@
11
11
  ///
12
12
  ////
13
13
 
14
+ $grid-column-max-calc: calc((100% - var(--grid-gap-inline, 1rem) * (var(--grid-columns, 12) - 1)) / var(--grid-columns, 12));
15
+ $grid-column-min-calc: min(max($grid-column-max-calc, var(--grid-min-column-size, 0%)), 100%);
16
+
14
17
  .grid,
15
18
  .flex-grid {
16
19
  gap: var(--grid-gap-block, 1rem) var(--grid-gap-inline, 1rem);
@@ -20,7 +23,7 @@
20
23
 
21
24
  .grid {
22
25
  display: grid;
23
- grid-template-columns: repeat(var(--grid-columns, 12), minmax(min(var(--grid-column-size, 0%), 100%), 1fr));
26
+ grid-template-columns: repeat(var(--grid-min-columns, auto-fit), minmax($grid-column-min-calc, 1fr));
24
27
  }
25
28
 
26
29
  .flex-grid {
@@ -28,9 +31,7 @@
28
31
  flex-wrap: wrap;
29
32
 
30
33
  > * {
31
- $column: var(--grid-columns, 12);
32
- flex-basis: calc((1 * 100% - (($column - 1) * var(--grid-gap-inline, 1rem))) / $column);
33
- flex-grow: var(--grid-grow, 0);
34
+ flex-basis: $grid-column-min-calc;
35
+ flex-grow: var(--grid-grow, 1);
34
36
  }
35
-
36
37
  }