@blackcube/aurelia-kit 1.0.0-rc1 → 1.0.0-rc10

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.
Files changed (59) hide show
  1. package/README.md +250 -16
  2. package/dist/index.es.js +1619 -590
  3. package/dist/index.es.js.map +1 -1
  4. package/dist/types/abstracts/components/abstract-dialog.d.ts +41 -0
  5. package/dist/types/abstracts/components/abstract-dialog.d.ts.map +1 -0
  6. package/dist/types/abstracts/components/abstract-dropdown.d.ts +117 -0
  7. package/dist/types/abstracts/components/abstract-dropdown.d.ts.map +1 -0
  8. package/dist/types/abstracts/components/abstract-pager.d.ts +51 -0
  9. package/dist/types/abstracts/components/abstract-pager.d.ts.map +1 -0
  10. package/dist/types/abstracts/components/abstract-toaster.d.ts +42 -0
  11. package/dist/types/abstracts/components/abstract-toaster.d.ts.map +1 -0
  12. package/dist/types/abstracts/components/index.d.ts +7 -0
  13. package/dist/types/abstracts/components/index.d.ts.map +1 -0
  14. package/dist/types/attributes/ajaxify-trigger.d.ts.map +1 -1
  15. package/dist/types/attributes/alert.d.ts.map +1 -1
  16. package/dist/types/attributes/badge.d.ts.map +1 -1
  17. package/dist/types/attributes/burger.d.ts.map +1 -1
  18. package/dist/types/attributes/dropdown.d.ts.map +1 -1
  19. package/dist/types/attributes/first-error.d.ts +19 -0
  20. package/dist/types/attributes/first-error.d.ts.map +1 -0
  21. package/dist/types/attributes/focus.d.ts +13 -0
  22. package/dist/types/attributes/focus.d.ts.map +1 -0
  23. package/dist/types/attributes/index.d.ts +2 -0
  24. package/dist/types/attributes/index.d.ts.map +1 -1
  25. package/dist/types/attributes/menu.d.ts +23 -1
  26. package/dist/types/attributes/menu.d.ts.map +1 -1
  27. package/dist/types/attributes/modal-trigger.d.ts +0 -1
  28. package/dist/types/attributes/modal-trigger.d.ts.map +1 -1
  29. package/dist/types/attributes/overlay.d.ts.map +1 -1
  30. package/dist/types/attributes/pager.d.ts.map +1 -1
  31. package/dist/types/attributes/password.d.ts.map +1 -1
  32. package/dist/types/attributes/popover-trigger.d.ts.map +1 -1
  33. package/dist/types/attributes/popover.d.ts.map +1 -1
  34. package/dist/types/attributes/profile.d.ts.map +1 -1
  35. package/dist/types/attributes/tabs.d.ts.map +1 -1
  36. package/dist/types/attributes/toast.d.ts +2 -2
  37. package/dist/types/attributes/toast.d.ts.map +1 -1
  38. package/dist/types/attributes/toaster-trigger.d.ts +3 -3
  39. package/dist/types/attributes/toaster-trigger.d.ts.map +1 -1
  40. package/dist/types/attributes/toaster.d.ts +2 -7
  41. package/dist/types/attributes/toaster.d.ts.map +1 -1
  42. package/dist/types/components/bleet-ajaxify.d.ts.map +1 -1
  43. package/dist/types/components/bleet-dialog.d.ts +15 -26
  44. package/dist/types/components/bleet-dialog.d.ts.map +1 -1
  45. package/dist/types/enums/event-aggregator.d.ts +14 -7
  46. package/dist/types/enums/event-aggregator.d.ts.map +1 -1
  47. package/dist/types/index.d.ts +7 -5
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/interfaces/dialog.d.ts +1 -2
  50. package/dist/types/interfaces/dialog.d.ts.map +1 -1
  51. package/dist/types/interfaces/event-aggregator.d.ts +12 -4
  52. package/dist/types/interfaces/event-aggregator.d.ts.map +1 -1
  53. package/dist/types/services/storage-service.d.ts.map +1 -1
  54. package/dist/types/services/transition-service.d.ts.map +1 -1
  55. package/dist/types/services/trap-focus-service.d.ts +17 -1
  56. package/dist/types/services/trap-focus-service.d.ts.map +1 -1
  57. package/dist/types/utils/toaster.d.ts +25 -0
  58. package/dist/types/utils/toaster.d.ts.map +1 -0
  59. package/package.json +1 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@ widgets, styled with Tailwind classes written in the PHP markup. The HTTP layer
12
12
  npm install @blackcube/aurelia-kit @blackcube/aurelia-api
13
13
  ```
14
14
 
15
- Peer dependencies: `aurelia` `^2.0.0-rc.2`, `@blackcube/aurelia-api` `^1.0.0`.
15
+ Peer dependencies: `aurelia` `^2.0.0-rc.2`, `@blackcube/aurelia-api` `^1.0.0-rc1`.
16
16
 
17
17
  The package is ESM only (`dist/index.es.js`, built by Vite in library mode); the application bundler resolves it.
18
18
 
@@ -61,7 +61,35 @@ An element that must leave the layout when hidden also carries the HTML `hidden`
61
61
  (Tailwind's preflight turns it into `display: none`); the attribute toggles it, never a class.
62
62
 
63
63
  Attribute options use the Aurelia multi-binding syntax, as written by the PHP widgets:
64
- `bleet-toaster-trigger="color: success; title: Saved; content: Done; duration: 3000"`.
64
+ `bleet-toaster-trigger="level: success; title: Saved; content: Done; duration: 3000"`.
65
+
66
+ ## Abstract components
67
+
68
+ For a SPA, where the application renders its own markup, the kit gives the behavior of a component as an abstract
69
+ class, without markup: the application's element extends it and gives its template — its markup, its classes, its
70
+ texts, its icons. The abstract classes are not registered; the element that extends them is, by the application.
71
+
72
+ ```typescript
73
+ import { customElement } from 'aurelia';
74
+ import { AbstractDialog } from '@blackcube/aurelia-kit';
75
+
76
+ @customElement('app-drawer')
77
+ export class AppDrawer extends AbstractDialog {}
78
+ ```
79
+
80
+ - The bindables and the lifecycle of the abstract class are inherited: Aurelia reads the bindables along the prototype
81
+ chain. An element that overrides `attached()` or `detaching()` calls `super`.
82
+ - The template binds the members of the instance directly (`${title}`, `toast of toasts`) and gives the refs the names
83
+ the class reads (`ref="dialogElement"`).
84
+ - The state classes of the host (`data-<name>-show`, `data-<name>-hide`) and its attributes (`popover`) go on the root
85
+ `<template>` of the template: Aurelia carries them to the host.
86
+
87
+ | Class | Component |
88
+ |---|---|
89
+ | `AbstractDialog` | a dialog: modal, drawer — see [Dialogs](#dialogs) |
90
+ | `AbstractDropdown` | a dropdown: searchable, multiple — see [`AbstractDropdown`](#abstractdropdown-spa) |
91
+ | `AbstractPager` | the pagination of a list — see [`AbstractPager`](#abstractpager-spa) |
92
+ | `AbstractToaster` | the toaster — see [Toasts](#toasts) |
65
93
 
66
94
  ## Overlay
67
95
 
@@ -101,6 +129,7 @@ sends. Its template holds no markup of its own:
101
129
  <au-slot name="loader" if.bind="loading"><progress></progress></au-slot>
102
130
  <template else>
103
131
  <au-compose if.bind="contentView" template.bind="contentView"></au-compose>
132
+ <au-slot else></au-slot>
104
133
  </template>
105
134
  </dialog>
106
135
  ```
@@ -140,18 +169,22 @@ Behavior:
140
169
  - `content` is the whole HTML of the dialog (frame, header, body, footer), rendered by the server; it becomes the
141
170
  view of the `au-compose`, so the `bleet-*` attributes it holds work. A response without `content` keeps the
142
171
  current view.
172
+ - `Open` (or `Toggle` when closed) with a `content` and no `url`: that HTML is shown as the server's would be, without
173
+ request (the same view, its `bleet-*` attributes, its forms posted by the element) — a page that already holds the
174
+ HTML (a confirmation, a `<template>` of the page).
143
175
  - A `<form>` submitted inside the dialog is posted by the element (multipart, method from `_method` or the form,
144
176
  POST by default, with the clicked submit button `name` / `value`); its `IDialogResponse` is applied the same way.
145
177
  - A `<form>` marked `data-dialog="native"` is not intercepted: the browser submits it (download, plain post).
146
178
  - A click on an element inside `[data-dialog="close"]`, the Escape key, or a click on the overlay closes the dialog
147
179
  with the hide transition; closing empties the view. A `cancel` coming from an `<input type="file">` (file chooser
148
180
  dismissed) does not close it. An `Open` received while the dialog is closing cancels the closing.
181
+ - Escape while a menu of the dialog is open (`[aria-expanded="true"]`: `bleet-dropdown`, `bleet-profile`) closes
182
+ that menu, not the dialog.
149
183
  - Statuses `Opening`, `Opened`, `Closing`, `Closed` (with the `id`) on `Channels.ModalStatus`.
150
184
 
151
185
  ```typescript
152
186
  interface IDialogResponse {
153
187
  content?: string; // the whole HTML of the dialog
154
- color?: UiColor; // not used by the element: the server HTML carries its own colors
155
188
  icon?: string | null; // not used by the element: the server HTML carries its own icon
156
189
  action?: DialogAction; // Close closes the dialog, RefreshAndClose closes it and reloads the page
157
190
  toast?: IToast; // published on Channels.Toaster
@@ -161,7 +194,45 @@ interface IDialogResponse {
161
194
  }
162
195
  ```
163
196
 
164
- PHP side, `AureliaCommunication::dialogContent($content, $color)` builds the `content` / `color` part of it.
197
+ PHP side, `AureliaCommunication::dialogContent($content)` builds the `content` part of it: the look of the dialog is
198
+ in its HTML.
199
+
200
+ **In a SPA**, the frame is the application's: its element extends `AbstractDialog` (see
201
+ [Abstract components](#abstract-components)), its template holds the `<dialog ref="dialogElement">`, and the dialog is
202
+ opened without `url`:
203
+
204
+ ```typescript
205
+ @customElement('app-drawer')
206
+ export class AppDrawer extends AbstractDialog {}
207
+ ```
208
+
209
+ ```html
210
+ <template data-dialog-show="translate-x-0" data-dialog-hide="translate-x-full">
211
+ <dialog ref="dialogElement" class="… translate-x-full" aria-label.bind="title || null">
212
+ <header …>${title} <button type="button" data-dialog="close">…</button></header>
213
+ <au-slot></au-slot>
214
+ </dialog>
215
+ </template>
216
+ ```
217
+
218
+ ```typescript
219
+ resolve(IEventAggregator).publish(Channels.Modal, <IModal>{ action: ModalAction.Open, id: 'drawer', title: 'Edit user' });
220
+ ```
221
+
222
+ | `AbstractDialog` | Meaning |
223
+ |---|---|
224
+ | `id` (bindable) | the id of the dialog on `Channels.Modal` |
225
+ | `title` | the one of the opening, kept until the dialog is closed |
226
+ | `dialogElement` | the `<dialog>` of the template (`ref`) |
227
+ | `show(data)`, `close()`, `clear()` | protected, to extend: an opening, a closing, what to clear once closed |
228
+
229
+ - `Open` (or `Toggle` when closed) opens the dialog; Escape, any `[data-dialog="close"]`, `Close` and `Toggle` close it
230
+ through the hide transition — a menu open inside (`aria-expanded`) is closed first.
231
+ - `Open` on a dialog already open only renews its `title`: the page shown in it changed (a creation turned into its
232
+ edition), the dialog stays.
233
+ - What the slot holds belongs to the page that projects it, its forms included.
234
+ - `ModalStatus.Closed` with the `id` tells the page the dialog is closed (its route can be left).
235
+ - `<bleet-dialog>` itself extends `AbstractDialog`, with the server HTML on top of it.
165
236
 
166
237
  ## Ajaxify
167
238
 
@@ -216,7 +287,7 @@ Markup it expects:
216
287
  <div bleet-toaster popover="manual"
217
288
  class="fixed inset-auto top-4 right-0 z-70 m-0 p-0 pr-4 border-0 bg-transparent overflow-visible flex flex-col gap-4 pointer-events-none">
218
289
 
219
- <!-- one template per UiColor: primary, secondary, success, danger, warning, info, accent -->
290
+ <!-- one template per UiLevel: info, success, warning, danger -->
220
291
  <template data-toaster="success">
221
292
  <div class="… translate-x-full opacity-0 transition-all duration-500"
222
293
  data-toaster-show="translate-x-0 opacity-100"
@@ -242,24 +313,24 @@ box of a popover (`inset`, `margin`, `padding`, `border`, `background`, `overflo
242
313
 
243
314
  Behavior:
244
315
 
245
- - `ToasterAction.Add`: clones `template[data-toaster=<color>]`, puts the icon of
316
+ - `ToasterAction.Add`: clones `template[data-toaster=<level>]`, puts the icon of
246
317
  `template[data-toaster=icon-<icon>]` in the `icon` slot, fills `title` and `content`, appends the card to the
247
318
  container, then swaps its hide classes for its show classes. With `duration` > 0 the toast closes itself after
248
319
  `duration` ms; `0` keeps it until it is closed.
249
320
  - `ToasterAction.Remove` (or a click on `close`): swaps the show classes for the hide classes, waits for the end
250
321
  of the transition, removes the card.
251
- - A toast whose `id` is already displayed is ignored. A color without template is logged and ignored.
322
+ - A toast whose `id` is already displayed is ignored. A level without template is logged and ignored.
252
323
 
253
324
  Publishing a toast from TypeScript:
254
325
 
255
326
  ```typescript
256
327
  import { resolve, IEventAggregator } from 'aurelia';
257
- import { Channels, IToaster, ToasterAction, UiColor, UiToastIcon } from '@blackcube/aurelia-kit';
328
+ import { Channels, IToaster, ToasterAction, UiLevel, UiToastIcon } from '@blackcube/aurelia-kit';
258
329
 
259
330
  resolve(IEventAggregator).publish(Channels.Toaster, <IToaster>{
260
331
  action: ToasterAction.Add,
261
332
  toast: {
262
- color: UiColor.Success,
333
+ level: UiLevel.Success,
263
334
  icon: UiToastIcon.Success,
264
335
  title: 'Saved',
265
336
  content: 'The page has been saved.',
@@ -276,7 +347,7 @@ interface IToaster {
276
347
 
277
348
  interface IToast {
278
349
  id?: string; // generated when absent
279
- color: UiColor; // default Info
350
+ level: UiLevel; // default Info
280
351
  icon?: UiToastIcon; // default Info
281
352
  title?: string;
282
353
  content: string; // HTML
@@ -290,20 +361,57 @@ Publishes one toast when the page loads (after `DOMContentLoaded`). Rendered by
290
361
  as a hidden element:
291
362
 
292
363
  ```html
293
- <div hidden bleet-toast="color: success; icon: check-circle; title: Saved; content: Done; duration: 3000"></div>
364
+ <div hidden bleet-toast="level: success; icon: check-circle; title: Saved; content: Done; duration: 3000"></div>
294
365
  ```
295
366
 
296
- Options: `id`, `color`, `icon`, `title`, `content`, `duration` (same meaning as `IToast`).
367
+ Options: `id`, `level`, `icon`, `title`, `content`, `duration` (same meaning as `IToast`).
297
368
 
298
369
  ### `bleet-toaster-trigger`
299
370
 
300
371
  Publishes one toast on click. Rendered by `Bleet::toast()->…->trigger()` as attributes of another element:
301
372
 
302
373
  ```html
303
- <button bleet-toaster-trigger="color: success; content: Saved!">Save</button>
374
+ <button bleet-toaster-trigger="level: success; content: Saved!">Save</button>
375
+ ```
376
+
377
+ Options: `id`, `level`, `icon`, `title`, `content`, `duration`.
378
+
379
+ ### `AbstractToaster` (SPA)
380
+
381
+ The toaster of a SPA, where the application renders the markup: the same channel, the same payload and the same state
382
+ classes as the `bleet-toaster` attribute, each toast rendered by the template of the application's element, which
383
+ extends `AbstractToaster` (see [Abstract components](#abstract-components)). The host is the stack.
384
+
385
+ ```typescript
386
+ @customElement('app-toaster')
387
+ export class AppToaster extends AbstractToaster {}
304
388
  ```
305
389
 
306
- Options: `id`, `color`, `icon`, `title`, `content`, `duration`.
390
+ ```html
391
+ <template popover="manual" class="fixed inset-auto top-4 right-0 … flex flex-col gap-4 pointer-events-none">
392
+ <div repeat.for="toast of toasts" data-toaster-id.bind="toast.id"
393
+ class="… translate-x-full opacity-0 transition-all duration-500"
394
+ data-toaster-show="translate-x-0 opacity-100"
395
+ data-toaster-hide="translate-x-full opacity-0">
396
+ … ${toast.title} … <div innerhtml.bind="toast.content"></div> …
397
+ <button type="button" click.trigger="onClickClose($event, toast.id)">…</button>
398
+ </div>
399
+ </template>
400
+ ```
401
+
402
+ | `AbstractToaster` | Meaning |
403
+ |---|---|
404
+ | `toasts` | the toasts shown: `id`, `level`, `icon`, `title`, `content`, `duration`, with the defaults of the attribute |
405
+ | `onClickClose(event, id)` | closes the toast (its button) |
406
+
407
+ Behavior:
408
+
409
+ - The root of each rendered toast carries `data-toaster-id="${toast.id}"`: once rendered, its hide classes are
410
+ swapped for its show classes; its button, `ToasterAction.Remove` or the end of its `duration` swap them back, then the
411
+ toast leaves the list once the transition ends.
412
+ - With `popover="manual"` on the host, it is shown again on each toast and hidden once the last one is gone, as the
413
+ attribute's container.
414
+ - The texts, the icons and the classes are the application's: its i18n, its icon component, its theme.
307
415
 
308
416
  ## Alerts
309
417
 
@@ -390,7 +498,13 @@ Behavior:
390
498
 
391
499
  - Listens on `Channels.Menu`: `MenuAction.Open` applies the show classes and opens the overlay, `MenuAction.Close`
392
500
  applies the hide classes and closes the overlay, `MenuAction.Toggle` does one or the other. `aria-hidden` follows.
393
- - A click on `[data-menu=close]` closes the menu; the menu closes too when the overlay closes.
501
+ - A click on `[data-menu=close]` closes the menu; the menu closes too when the overlay closes, on `Escape` and on a
502
+ click on a link of the menu (the link is followed).
503
+ - Open, the focus is trapped in the menu (`ITrapFocusService`), on `[data-menu=close]` first; once closed, it goes back
504
+ to what opened the menu (the burger).
505
+ - The page follows: `<body>` takes the classes of the menu's `data-menu-page-show` while it is open and of its
506
+ `data-menu-page-hide` once closed — `data-menu-page-show="overflow-hidden"` keeps the page from scrolling under it.
507
+ Without these attributes, the page is left as is.
394
508
  - Publishes `MenuStatus.Opening`, `Opened`, `Closing`, `Closed` on `Channels.MenuStatus`.
395
509
  - `[data-menu=toggle-button-ID]` shows or hides `[data-menu=toggle-list-ID]` (`hidden` attribute), sets the
396
510
  data-menu-show / data-menu-hide classes of its `svg[data-menu=icon]` and its `aria-expanded`. Accordion: opening a
@@ -459,6 +573,43 @@ Behavior:
459
573
  select to `N`.
460
574
  - Changing the select clicks the matching button.
461
575
 
576
+ ### `AbstractPager` (SPA)
577
+
578
+ The pagination of a list in a SPA — bleet's `Pager` widget with buttons instead of links: the application's element
579
+ extends `AbstractPager` (see [Abstract components](#abstract-components)) and renders the window of numbers around the
580
+ current page. The pagination is `@blackcube/aurelia-api`'s (`IPagination`), given as it is by the page; a page chosen is
581
+ announced on the host (`PagerEvent.Change`, `bleet-page`, its `detail` an `IPagerDetail`): the page writes it in its URL
582
+ and reloads its list, whose new pagination comes back.
583
+
584
+ ```typescript
585
+ @customElement('app-pager')
586
+ export class AppPager extends AbstractPager {}
587
+ ```
588
+
589
+ ```html
590
+ <template>
591
+ <nav if.bind="pagination.totalPages > 1">
592
+ <button if.bind="page > 1" type="button" click.trigger="onClickPage($event, page - 1)">…</button>
593
+ <button repeat.for="number of numbers" type="button" aria-current.bind="number === page ? 'page' : null"
594
+ click.trigger="onClickPage($event, number)">${number}</button>
595
+ <button if.bind="page < pagination.totalPages" type="button" click.trigger="onClickPage($event, page + 1)">…</button>
596
+ </nav>
597
+ </template>
598
+ ```
599
+
600
+ ```html
601
+ <app-pager pagination.bind="pagination" bleet-page.trigger="onChangePage($event)"></app-pager>
602
+ ```
603
+
604
+ | `AbstractPager` | Meaning |
605
+ |---|---|
606
+ | `pagination` | `IPagination`: `limit`, `currentPage`, `totalPages`, `totalCount` |
607
+ | `maxButtonCount` | how many numbers at most (10) |
608
+ | `page`, `numbers`, `begin`, `end` | the page shown, the window of numbers around it, the range of the items shown |
609
+ | `selectedPage` | two-way with a dropdown of the pages (a narrow screen): a choice in it goes to that page |
610
+ | `onClickPage(event, number)` | a number, previous, next |
611
+ | `refreshed()` | protected, to extend: what the element adds once the pages are computed again |
612
+
462
613
  ## Forms
463
614
 
464
615
  ### `bleet-password`
@@ -538,6 +689,88 @@ Behavior:
538
689
  is selected, the `count` template text beyond.
539
690
  - The panel does not open while the native select is disabled.
540
691
 
692
+ ### `AbstractDropdown` (SPA)
693
+
694
+ The dropdown of a SPA, driven by its data rather than by a native select: the application's element extends
695
+ `AbstractDropdown` (see [Abstract components](#abstract-components)) and renders its rows; the values are typed — an
696
+ id, an enum — and bound both ways.
697
+
698
+ ```typescript
699
+ @customElement('app-select')
700
+ export class AppSelect<T> extends AbstractDropdown<T> {}
701
+ ```
702
+
703
+ ```html
704
+ <template>
705
+ <button ref="button" type="button" aria-expanded.bind="open ? 'true' : 'false'" click.trigger="onClickToggle($event)">
706
+ ${selection.length > 0 ? selection[0].label : placeholder}
707
+ </button>
708
+ <div ref="panel" hidden.bind="open === false" role="listbox">
709
+ <input ref="search" value.bind="query">
710
+ <template repeat.for="group of groups">
711
+ <div if.bind="group.label">${group.label}</div>
712
+ <button repeat.for="row of group.rows" type="button" role="option" aria-selected.bind="row.selected ? 'true' : 'false'"
713
+ disabled.bind="row.disabled" click.trigger="onClickItem($event, row)">${row.label}</button>
714
+ </template>
715
+ <div if.bind="empty">…</div>
716
+ </div>
717
+ </template>
718
+ ```
719
+
720
+ ```html
721
+ <app-select options.bind="schools" value.two-way="model.schoolId" placeholder="Choose"></app-select>
722
+ <app-select multiple.bind="true" options.bind="roles" values.two-way="model.roles"></app-select>
723
+ ```
724
+
725
+ | `AbstractDropdown` | Meaning |
726
+ |---|---|
727
+ | `options` | `IDropdownOption[]`: `value`, `label`, `group`, `disabled` |
728
+ | `value`, `values` | two-way: the choice, or the choices with `multiple` |
729
+ | `multiple`, `disabled`, `placeholder` | bindables |
730
+ | `open`, `query`, `empty` | the panel is open; the text searched (the search field binds it); nothing matches |
731
+ | `groups`, `selection` | the rows matching the query by group (`IDropdownRow`: `value`, `label`, `disabled`, `selected`); the rows chosen |
732
+ | `button`, `panel`, `search` | the button, the panel and the optional search field of the template (`ref`) |
733
+ | `onClickToggle(event)`, `onClickItem(event, row)`, `onClickRemove(event, row)` | the button, a row, the remove control of a tag |
734
+
735
+ - A single choice sets `value` and closes the panel; with a `placeholder`, a first row sets it back to `null`. A
736
+ multiple one toggles in `values`, the panel staying open.
737
+ - The rows are built once per set of options; a choice changes their state in place, the rendered rows stay. A list
738
+ computed again with the same content (a new array, the same options) is not a new set: the rows stay too, and so
739
+ does the one that has the focus.
740
+ - Open, the focus is trapped in the panel (`ITrapFocusService`): Tab loops, the arrows move, Escape and a click outside
741
+ close it. It opens on the search field, else on its selection, scrolled into view.
742
+ - A remove control inside the button is not a `<button>`: the HTML parser closes a button at the next one.
743
+
744
+ ### `bleet-focus`
745
+
746
+ An element that takes the focus as it appears — a field or a message shown later (a condition of the page, a fragment
747
+ loaded by ajaxify):
748
+
749
+ ```html
750
+ <input bleet-focus …>
751
+ ```
752
+
753
+ When attached, the element is focused on the next frame; the browser brings it into view.
754
+
755
+ ### `bleet-first-error`
756
+
757
+ A form whose first field in error takes the focus when the page says « refused » (a SPA validating its form before
758
+ posting it):
759
+
760
+ ```html
761
+ <form bleet-first-error="account" submit.trigger="onSubmit($event)" novalidate>
762
+ <input aria-invalid.bind="…" …>
763
+ </form>
764
+ ```
765
+
766
+ `Channels.Form` carries `IForm` (`{ action: FormAction.FirstError, id?: string }`): the form whose `id` is given,
767
+ every form without id, focuses its first `[aria-invalid=true]` on the next frame — once the invalid marks of the fields
768
+ are rendered. A form without a field in error does nothing.
769
+
770
+ ```typescript
771
+ resolve(IEventAggregator).publish(Channels.Form, <IForm>{ action: FormAction.FirstError, id: 'account' });
772
+ ```
773
+
541
774
  ## Upload channel
542
775
 
543
776
  `Channels.Upload` carries `IUpload` (`{ action: UploadAction.Reset, id?: string }`) for the upload bridges
@@ -550,7 +783,8 @@ resolve(IEventAggregator).publish(Channels.Upload, <IUpload>{ action: UploadActi
550
783
  ## Exports
551
784
 
552
785
  Attributes: `BleetToasterCustomAttribute`, `BleetToastCustomAttribute`, `BleetToasterTriggerCustomAttribute`,
553
- and the attributes of the other widgets. Enums: `Channels`, `ToasterAction`, `ToasterStatus`, `UiColor`,
786
+ and the attributes of the other widgets. Abstract components: `AbstractDialog`, `AbstractDropdown`, `AbstractPager` (its `PagerEvent`, `IPagerDetail`),
787
+ `AbstractToaster`. Enums: `Channels`, `ToasterAction`, `ToasterStatus`, `UiLevel`,
554
788
  `UiToastIcon`, `UiIcon`, and the actions and statuses of the other channels. Interfaces: `IToaster`, `IToast`,
555
789
  and the payloads of the other channels. Services: `IStorageService`, `ITransitionService`, `ITrapFocusService`.
556
790