@mk-kit/ui 0.40.0 → 0.42.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, ElementRef, Directive, input, booleanAttribute, numberAttribute, contentChildren, computed, signal, ChangeDetectionStrategy, Component, output, effect, model, contentChild, TemplateRef } from '@angular/core';
2
+ import { Injectable, inject, ElementRef, Directive, input, booleanAttribute, numberAttribute, contentChildren, computed, signal, effect, ChangeDetectionStrategy, Component, output, model, contentChild, TemplateRef } from '@angular/core';
3
3
  import { MkLiveAnnouncer, MK_I18N, mkUniqueId } from '@mk-kit/ui/core';
4
4
  import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
5
5
 
@@ -102,6 +102,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
102
102
  args: [{ providedIn: 'root' }]
103
103
  }] });
104
104
 
105
+ /** Elements that take keyboard focus natively (no `tabindex` needed). */
106
+ const NATIVELY_FOCUSABLE = /^(BUTTON|INPUT|SELECT|TEXTAREA)$/;
105
107
  /**
106
108
  * Optional grip that restricts where a pointer drag of the enclosing
107
109
  * `[mkDrag]` may begin. Place it on the element the user should press to drag;
@@ -112,16 +114,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
112
114
  * Its look (grab cursor, muted colour, `touch-action: none`) ships as the
113
115
  * global `.mk-drag-handle` class in the theme stylesheet.
114
116
  *
117
+ * **Decorative grip** — a non-focusable element (`<span>`, `<mk-icon>`): the
118
+ * item itself stays the keyboard target (`role="button"`, focusable), so the
119
+ * grip should be `aria-hidden`:
120
+ *
115
121
  * ```html
116
122
  * <div mkDrag [mkDragData]="row">
117
123
  * <span mkDragHandle aria-hidden="true">⠿</span>
118
124
  * {{ row.name }}
119
125
  * </div>
120
126
  * ```
127
+ *
128
+ * **Focusable grip** — a `<button>` (or any element with `tabindex`): the
129
+ * handle becomes the keyboard target instead. The item is then a plain
130
+ * container (no role, not focusable), so rows may hold inputs, links and
131
+ * other buttons without nesting interactive controls, and `<li>` items keep
132
+ * valid list semantics. Give it an accessible name:
133
+ *
134
+ * ```html
135
+ * <li mkDrag [mkDragData]="row">
136
+ * <button type="button" mkDragHandle [attr.aria-label]="'Reorder ' + row.name">⠿</button>
137
+ * <input mkInput [(ngModel)]="row.name" />
138
+ * </li>
139
+ * ```
121
140
  */
122
141
  class MkDragHandle {
123
142
  /** The handle's host element. */
124
143
  element = inject(ElementRef).nativeElement;
144
+ /**
145
+ * Whether the handle can take keyboard focus itself — a native control
146
+ * (`<button>`, …), a link with `href`, or any element with a `tabindex`.
147
+ * A focusable handle carries the keyboard drag for its `[mkDrag]`.
148
+ */
149
+ isFocusable() {
150
+ const el = this.element;
151
+ return (NATIVELY_FOCUSABLE.test(el.tagName) ||
152
+ (el.tagName === 'A' && el.hasAttribute('href')) ||
153
+ el.hasAttribute('tabindex'));
154
+ }
125
155
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDragHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
126
156
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: MkDragHandle, isStandalone: true, selector: "[mkDragHandle]", host: { classAttribute: "mk-drag-handle" }, exportAs: ["mkDragHandle"], ngImport: i0 });
127
157
  }
@@ -149,13 +179,25 @@ const TOUCH_SLOP = 10;
149
179
  const SETTLE_MS = 180;
150
180
  /**
151
181
  * Makes an item inside a `[mkDropList]` draggable — by pointer (mouse / touch /
152
- * pen) **and** by keyboard (WCAG 2.1.1). The item is focusable, exposes
153
- * `role="button"` + `aria-roledescription="Draggable item"`, and every move is
154
- * announced via {@link MkLiveAnnouncer}.
182
+ * pen) **and** by keyboard (WCAG 2.1.1). Every move is announced via
183
+ * {@link MkLiveAnnouncer}. Which element carries the keyboard interaction
184
+ * depends on the handle:
185
+ *
186
+ * - **No handle, or a decorative one** (`<span mkDragHandle aria-hidden>`):
187
+ * the item itself is focusable and exposes `aria-roledescription="Draggable
188
+ * item"` with `role="button"` — or `role="option"` when it is an `<li>` of a
189
+ * `<ul mkDropList>`, which then becomes a labelled `listbox` (an `<li>` may
190
+ * not take the `button` role).
191
+ * - **A focusable handle** (`<button mkDragHandle aria-label="…">`, or any
192
+ * handle with `tabindex`): the handle is the keyboard target and receives
193
+ * the `aria-roledescription` / `aria-pressed` / `aria-grabbed` state; the
194
+ * item stays a plain container with no role and no `tabindex`, so it can hold
195
+ * inputs, links and buttons of its own (no nested interactive controls) and
196
+ * `<li>` items keep their list semantics.
155
197
  *
156
- * Keyboard: focus an item and press **Space/Enter** to pick it up, **Arrow**
157
- * keys to move it (crossing into connected lists at the ends / across the
158
- * perpendicular axis), **Space/Enter** to drop, **Escape** to cancel.
198
+ * Keyboard: focus the item (or its handle) and press **Space/Enter** to pick
199
+ * it up, **Arrow** keys to move it (crossing into connected lists at the ends /
200
+ * across the perpendicular axis), **Space/Enter** to drop, **Escape** to cancel.
159
201
  *
160
202
  * Touch: a swipe scrolls the page as usual — the drag only arms after a
161
203
  * long-press ({@link mkDragTouchDelay}, default 300 ms). While armed the item
@@ -168,9 +210,9 @@ const SETTLE_MS = 180;
168
210
  * synchronously on release so drops land exactly where the pointer ended.
169
211
  *
170
212
  * ```html
171
- * <li mkDrag [mkDragData]="row" [mkDragDisabled]="row.locked">
213
+ * <div mkDrag [mkDragData]="row" [mkDragDisabled]="row.locked">
172
214
  * <span mkDragHandle aria-hidden="true">⠿</span> {{ row.title }}
173
- * </li>
215
+ * </div>
174
216
  * ```
175
217
  *
176
218
  * @typeParam T item data type.
@@ -206,6 +248,28 @@ class MkDrag {
206
248
  */
207
249
  ownHandles = computed(() => this.handles().filter((h) => h.element.closest('[mkDrag]') === this.element), /* @ts-ignore */
208
250
  ...(ngDevMode ? [{ debugName: "ownHandles" }] : /* istanbul ignore next */ []));
251
+ /**
252
+ * The handle that carries the keyboard drag — the first of this item's
253
+ * handles that is focusable on its own (a `<button mkDragHandle>`, say).
254
+ * `null` when the item itself is the keyboard target.
255
+ */
256
+ keyboardHandle = computed(() => this.ownHandles().find((h) => h.isFocusable()) ?? null, /* @ts-ignore */
257
+ ...(ngDevMode ? [{ debugName: "keyboardHandle" }] : /* istanbul ignore next */ []));
258
+ /**
259
+ * The role the item itself exposes: `null` when a focusable handle carries
260
+ * the interaction; `option` inside a list that resolved to a `listbox`
261
+ * (`<ul mkDropList>` / `<li mkDrag>`); `button` otherwise.
262
+ */
263
+ itemRole = computed(() => {
264
+ if (this.keyboardHandle())
265
+ return null;
266
+ return this.home?.role() === 'listbox' ? 'option' : 'button';
267
+ }, /* @ts-ignore */
268
+ ...(ngDevMode ? [{ debugName: "itemRole" }] : /* istanbul ignore next */ []));
269
+ /** The element keyboard events act on: the focusable handle, else the item. */
270
+ keyboardTarget() {
271
+ return this.keyboardHandle()?.element ?? this.element;
272
+ }
209
273
  /** True while a pointer drag is in progress. */
210
274
  dragging = signal(false, /* @ts-ignore */
211
275
  ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
@@ -236,6 +300,29 @@ class MkDrag {
236
300
  originLeft = 0;
237
301
  originTop = 0;
238
302
  preview = null;
303
+ constructor() {
304
+ // Mirror the button state onto a focusable handle. Host bindings cannot
305
+ // reach a projected element, so the attributes are written directly; the
306
+ // effect re-runs whenever the handle or the lift/drag state changes.
307
+ effect(() => {
308
+ const handle = this.keyboardHandle();
309
+ if (!handle)
310
+ return;
311
+ const el = handle.element;
312
+ if (el.tagName !== 'BUTTON')
313
+ el.setAttribute('role', 'button');
314
+ el.setAttribute('aria-roledescription', 'Draggable item');
315
+ el.setAttribute('aria-grabbed', String(this.dragging() || this.lifted()));
316
+ this.toggleAttr(el, 'aria-pressed', this.lifted() ? 'true' : null);
317
+ this.toggleAttr(el, 'aria-disabled', this.disabled() ? 'true' : null);
318
+ });
319
+ }
320
+ toggleAttr(el, name, value) {
321
+ if (value === null)
322
+ el.removeAttribute(name);
323
+ else
324
+ el.setAttribute(name, value);
325
+ }
239
326
  moveHandler = (e) => this.onPointerMove(e);
240
327
  upHandler = (e) => this.onPointerUp(e);
241
328
  cancelHandler = () => this.finishPointer(true);
@@ -564,9 +651,10 @@ class MkDrag {
564
651
  onKeyDown(event) {
565
652
  const e = event;
566
653
  const key = e.key;
567
- // Keys act on the focused item only — a nested item's keydown bubbles up
568
- // through outer items, which must not pick themselves up.
569
- if (e.target !== this.element)
654
+ // Keys act on the focused item (or its focusable handle) only — a nested
655
+ // item's keydown bubbles up through outer items, which must not pick
656
+ // themselves up, and keys typed into a row's inputs are not drag keys.
657
+ if (e.target !== this.keyboardTarget())
570
658
  return;
571
659
  if (!this.lifted()) {
572
660
  if ((key === ' ' || key === 'Enter') && !this.disabled() && this.home && !this.dragging()) {
@@ -607,8 +695,12 @@ class MkDrag {
607
695
  break;
608
696
  }
609
697
  }
610
- onBlur() {
698
+ onFocusOut(event) {
611
699
  // Losing focus mid-lift cancels the keyboard drag to avoid a stuck state.
700
+ // `focusout` bubbles, so only the keyboard target's own blur counts — a
701
+ // nested control losing focus must not cancel the outer item's lift.
702
+ if (event.target !== this.keyboardTarget())
703
+ return;
612
704
  if (this.lifted())
613
705
  this.cancelKeyboard();
614
706
  }
@@ -903,19 +995,22 @@ class MkDrag {
903
995
  .matches ?? false);
904
996
  }
905
997
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDrag, deps: [], target: i0.ɵɵFactoryTarget.Component });
906
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", type: MkDrag, isStandalone: true, selector: "[mkDrag]", inputs: { mkDragData: { classPropertyName: "mkDragData", publicName: "mkDragData", isSignal: true, isRequired: false, transformFunction: null }, mkDragDisabled: { classPropertyName: "mkDragDisabled", publicName: "mkDragDisabled", isSignal: true, isRequired: false, transformFunction: null }, mkDragTouchDelay: { classPropertyName: "mkDragTouchDelay", publicName: "mkDragTouchDelay", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "button", "aria-roledescription": "Draggable item", "draggable": "false" }, listeners: { "pointerdown": "onPointerDown($event)", "keydown": "onKeyDown($event)", "blur": "onBlur()" }, properties: { "attr.tabindex": "disabled() ? -1 : 0", "attr.aria-disabled": "disabled() || null", "attr.aria-pressed": "lifted() || null", "attr.aria-grabbed": "dragging() || lifted()", "class.mk-drag--disabled": "disabled()", "class.mk-drag--dragging": "dragging()", "class.mk-drag--lifted": "lifted()", "class.mk-drag--armed": "armed()", "class.mk-drag--has-handle": "ownHandles().length > 0", "class.mk-drag--horizontal": "inHorizontalList()" }, classAttribute: "mk-drag" }, queries: [{ propertyName: "handles", predicate: MkDragHandle, descendants: true, isSignal: true }], exportAs: ["mkDrag"], ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{position:relative;cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none}:host(.mk-drag--horizontal){touch-action:manipulation}:host(.mk-drag--has-handle){cursor:default;touch-action:auto}:host(.mk-drag--dragging){cursor:grabbing}:host(.mk-drag--armed){cursor:grabbing;border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-md)}:host(.mk-drag--lifted){outline:var(--mk-border-width-strong) solid var(--mk-primary);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-md);background-color:var(--mk-surface-2);box-shadow:var(--mk-shadow-lg);z-index:var(--mk-z-sticky)}:host(.mk-drag--disabled){cursor:not-allowed;opacity:.55;touch-action:auto}:host(:focus-visible){outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
998
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", type: MkDrag, isStandalone: true, selector: "[mkDrag]", inputs: { mkDragData: { classPropertyName: "mkDragData", publicName: "mkDragData", isSignal: true, isRequired: false, transformFunction: null }, mkDragDisabled: { classPropertyName: "mkDragDisabled", publicName: "mkDragDisabled", isSignal: true, isRequired: false, transformFunction: null }, mkDragTouchDelay: { classPropertyName: "mkDragTouchDelay", publicName: "mkDragTouchDelay", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "draggable": "false" }, listeners: { "pointerdown": "onPointerDown($event)", "keydown": "onKeyDown($event)", "focusout": "onFocusOut($event)" }, properties: { "attr.role": "itemRole()", "attr.aria-roledescription": "itemRole() ? 'Draggable item' : null", "attr.tabindex": "itemRole() ? (disabled() ? -1 : 0) : null", "attr.aria-disabled": "itemRole() ? disabled() || null : null", "attr.aria-pressed": "itemRole() === 'button' ? lifted() || null : null", "attr.aria-selected": "itemRole() === 'option' ? lifted() : null", "attr.aria-grabbed": "itemRole() ? dragging() || lifted() : null", "class.mk-drag--disabled": "disabled()", "class.mk-drag--dragging": "dragging()", "class.mk-drag--lifted": "lifted()", "class.mk-drag--armed": "armed()", "class.mk-drag--has-handle": "ownHandles().length > 0", "class.mk-drag--horizontal": "inHorizontalList()" }, classAttribute: "mk-drag" }, queries: [{ propertyName: "handles", predicate: MkDragHandle, descendants: true, isSignal: true }], exportAs: ["mkDrag"], ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{position:relative;cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none}:host(.mk-drag--horizontal){touch-action:manipulation}:host(.mk-drag--has-handle){cursor:default;touch-action:auto}:host(.mk-drag--dragging){cursor:grabbing}:host(.mk-drag--armed){cursor:grabbing;border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-md)}:host(.mk-drag--lifted){outline:var(--mk-border-width-strong) solid var(--mk-primary);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-md);background-color:var(--mk-surface-2);box-shadow:var(--mk-shadow-lg);z-index:var(--mk-z-sticky)}:host(.mk-drag--disabled){cursor:not-allowed;opacity:.55;touch-action:auto}:host(:focus-visible){outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
907
999
  }
908
1000
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDrag, decorators: [{
909
1001
  type: Component,
910
1002
  args: [{ selector: '[mkDrag]', exportAs: 'mkDrag', changeDetection: ChangeDetectionStrategy.OnPush, host: {
911
1003
  class: 'mk-drag',
912
- role: 'button',
913
- 'aria-roledescription': 'Draggable item',
914
1004
  draggable: 'false',
915
- '[attr.tabindex]': 'disabled() ? -1 : 0',
916
- '[attr.aria-disabled]': 'disabled() || null',
917
- '[attr.aria-pressed]': 'lifted() || null',
918
- '[attr.aria-grabbed]': 'dragging() || lifted()',
1005
+ // Widget semantics live on the item only while no focusable handle takes
1006
+ // them over (see `keyboardHandle`); otherwise the item is a plain container.
1007
+ '[attr.role]': 'itemRole()',
1008
+ '[attr.aria-roledescription]': "itemRole() ? 'Draggable item' : null",
1009
+ '[attr.tabindex]': 'itemRole() ? (disabled() ? -1 : 0) : null',
1010
+ '[attr.aria-disabled]': 'itemRole() ? disabled() || null : null',
1011
+ '[attr.aria-pressed]': "itemRole() === 'button' ? lifted() || null : null",
1012
+ '[attr.aria-selected]': "itemRole() === 'option' ? lifted() : null",
1013
+ '[attr.aria-grabbed]': 'itemRole() ? dragging() || lifted() : null',
919
1014
  '[class.mk-drag--disabled]': 'disabled()',
920
1015
  '[class.mk-drag--dragging]': 'dragging()',
921
1016
  '[class.mk-drag--lifted]': 'lifted()',
@@ -924,12 +1019,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
924
1019
  '[class.mk-drag--horizontal]': 'inHorizontalList()',
925
1020
  '(pointerdown)': 'onPointerDown($event)',
926
1021
  '(keydown)': 'onKeyDown($event)',
927
- '(blur)': 'onBlur()',
1022
+ '(focusout)': 'onFocusOut($event)',
928
1023
  }, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{position:relative;cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none}:host(.mk-drag--horizontal){touch-action:manipulation}:host(.mk-drag--has-handle){cursor:default;touch-action:auto}:host(.mk-drag--dragging){cursor:grabbing}:host(.mk-drag--armed){cursor:grabbing;border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-md)}:host(.mk-drag--lifted){outline:var(--mk-border-width-strong) solid var(--mk-primary);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-md);background-color:var(--mk-surface-2);box-shadow:var(--mk-shadow-lg);z-index:var(--mk-z-sticky)}:host(.mk-drag--disabled){cursor:not-allowed;opacity:.55;touch-action:auto}:host(:focus-visible){outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
929
- }], propDecorators: { mkDragData: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragData", required: false }] }], mkDragDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragDisabled", required: false }] }], mkDragTouchDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragTouchDelay", required: false }] }], handles: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => MkDragHandle), { ...{ descendants: true }, isSignal: true }] }] } });
1024
+ }], ctorParameters: () => [], propDecorators: { mkDragData: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragData", required: false }] }], mkDragDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragDisabled", required: false }] }], mkDragTouchDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragTouchDelay", required: false }] }], handles: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => MkDragHandle), { ...{ descendants: true }, isSignal: true }] }] } });
930
1025
 
931
1026
  /* eslint-disable @typescript-eslint/no-explicit-any -- item-type params use
932
1027
  `any` to accept drags of any data type without generic-variance friction. */
1028
+ /**
1029
+ * Roles on which `aria-orientation` is permitted (WAI-ARIA 1.2). On any other
1030
+ * role the attribute is invalid, so the list only exposes it for these.
1031
+ */
1032
+ const ORIENTATION_ROLES = new Set([
1033
+ 'listbox',
1034
+ 'menu',
1035
+ 'radiogroup',
1036
+ 'scrollbar',
1037
+ 'select',
1038
+ 'separator',
1039
+ 'slider',
1040
+ 'tablist',
1041
+ 'toolbar',
1042
+ 'tree',
1043
+ 'treegrid',
1044
+ ]);
933
1045
  /**
934
1046
  * A drop container for reorderable `[mkDrag]` items.
935
1047
  *
@@ -941,14 +1053,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
941
1053
  * The array bound to `mkDropListData` is **not** mutated for you — handle
942
1054
  * `mkDropListDropped` and call {@link mkMoveItemInArray} / {@link mkTransferArrayItem}.
943
1055
  *
1056
+ * Semantics follow the host element and its items, so the tree is always
1057
+ * valid ARIA:
1058
+ *
1059
+ * - any host other than `<ul>`/`<ol>` is a `role="group"` (named by
1060
+ * `mkDropListLabel`) of `role="button"` items;
1061
+ * - a `<ul>`/`<ol>` whose `<li mkDrag>` items all carry a *focusable*
1062
+ * `[mkDragHandle]` stays a plain list — the handles are the controls;
1063
+ * - a `<ul>`/`<ol>` whose items are themselves the keyboard targets becomes a
1064
+ * `listbox` of `option`s (an `<li>` may not be a `button`); give it a
1065
+ * `mkDropListLabel`, listboxes need a name.
1066
+ *
1067
+ * A `role` you set in the template is kept, and `aria-orientation` is only
1068
+ * exposed on roles that allow it (`listbox`, `toolbar`, `tree`, …) — the
1069
+ * keyboard model handles both axes regardless.
1070
+ *
944
1071
  * ```html
945
- * <ul mkDropList [mkDropListData]="todo()"
946
- * mkDropListId="todo" [mkDropListConnectedTo]="['done']"
947
- * (mkDropListDropped)="drop($event)">
1072
+ * <div mkDropList [mkDropListData]="todo()" mkDropListLabel="To do"
1073
+ * mkDropListId="todo" [mkDropListConnectedTo]="['done']"
1074
+ * (mkDropListDropped)="drop($event)">
948
1075
  * @for (t of todo(); track t.id) {
949
- * <li mkDrag [mkDragData]="t">{{ t.title }}</li>
1076
+ * <div mkDrag [mkDragData]="t">{{ t.title }}</div>
950
1077
  * }
951
- * </ul>
1078
+ * </div>
952
1079
  * ```
953
1080
  *
954
1081
  * @typeParam T item data type.
@@ -988,6 +1115,31 @@ class MkDropList {
988
1115
  /** Announceable name: the label when set, otherwise the resolved id. */
989
1116
  label = computed(() => this.mkDropListLabel() || this.id(), /* @ts-ignore */
990
1117
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1118
+ /** A `role` written in the template — always kept. */
1119
+ explicitRole = this.element.getAttribute('role');
1120
+ isNativeList = /^(UL|OL)$/.test(this.element.tagName);
1121
+ /**
1122
+ * The role the host exposes. One set in the template wins. A `<ul>`/`<ol>`
1123
+ * keeps its implicit `list` role (`null` — nothing is written) while every
1124
+ * item hands the keyboard drag to a focusable handle, and becomes a
1125
+ * `listbox` (its items `option`s) otherwise. Any other element is a `group`.
1126
+ */
1127
+ role = computed(() => {
1128
+ if (this.explicitRole)
1129
+ return this.explicitRole;
1130
+ if (!this.isNativeList)
1131
+ return 'group';
1132
+ return this.drags().every((d) => d.keyboardHandle()) ? null : 'listbox';
1133
+ }, /* @ts-ignore */
1134
+ ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
1135
+ /** Whether `aria-orientation` is valid on the effective role. */
1136
+ orientationAllowed = computed(() => ORIENTATION_ROLES.has(this.role() ?? ''), /* @ts-ignore */
1137
+ ...(ngDevMode ? [{ debugName: "orientationAllowed" }] : /* istanbul ignore next */ []));
1138
+ /** A static `aria-label` written in the template, kept when no label input is set. */
1139
+ staticAriaLabel = this.element.getAttribute('aria-label');
1140
+ /** Accessible name of the list: `mkDropListLabel`, else the template's own. */
1141
+ ariaLabel = computed(() => this.mkDropListLabel() || this.staticAriaLabel || null, /* @ts-ignore */
1142
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
991
1143
  /** Connected-list ids, normalised to a plain array. */
992
1144
  connectedTo = computed(() => this.mkDropListConnectedTo() ?? [], /* @ts-ignore */
993
1145
  ...(ngDevMode ? [{ debugName: "connectedTo" }] : /* istanbul ignore next */ []));
@@ -1027,13 +1179,15 @@ class MkDropList {
1027
1179
  this.mkDropListDropped.emit(event);
1028
1180
  }
1029
1181
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDropList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1030
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", type: MkDropList, isStandalone: true, selector: "[mkDropList]", inputs: { mkDropListData: { classPropertyName: "mkDropListData", publicName: "mkDropListData", isSignal: true, isRequired: false, transformFunction: null }, mkDropListId: { classPropertyName: "mkDropListId", publicName: "mkDropListId", isSignal: true, isRequired: false, transformFunction: null }, mkDropListConnectedTo: { classPropertyName: "mkDropListConnectedTo", publicName: "mkDropListConnectedTo", isSignal: true, isRequired: false, transformFunction: null }, mkDropListLabel: { classPropertyName: "mkDropListLabel", publicName: "mkDropListLabel", isSignal: true, isRequired: false, transformFunction: null }, mkDropListOrientation: { classPropertyName: "mkDropListOrientation", publicName: "mkDropListOrientation", isSignal: true, isRequired: false, transformFunction: null }, mkDropListDisabled: { classPropertyName: "mkDropListDisabled", publicName: "mkDropListDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mkDropListDropped: "mkDropListDropped" }, host: { properties: { "attr.aria-orientation": "mkDropListOrientation()", "attr.aria-disabled": "mkDropListDisabled() || null", "class.mk-drop-list--horizontal": "mkDropListOrientation() === 'horizontal'", "class.mk-drop-list--disabled": "mkDropListDisabled()", "class.mk-drop-list--receiving": "_receiving()" }, classAttribute: "mk-drop-list" }, queries: [{ propertyName: "drags", predicate: MkDrag, isSignal: true }], exportAs: ["mkDropList"], ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative}:host(.mk-drop-list--receiving){outline:var(--mk-border-width-strong) solid var(--mk-primary-subtle-text);outline-offset:calc(-1 * var(--mk-border-width-strong));border-radius:var(--mk-radius-md);background-color:color-mix(in srgb,var(--mk-primary) 6%,transparent)}:host(.mk-drop-list--disabled){cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1182
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", type: MkDropList, isStandalone: true, selector: "[mkDropList]", inputs: { mkDropListData: { classPropertyName: "mkDropListData", publicName: "mkDropListData", isSignal: true, isRequired: false, transformFunction: null }, mkDropListId: { classPropertyName: "mkDropListId", publicName: "mkDropListId", isSignal: true, isRequired: false, transformFunction: null }, mkDropListConnectedTo: { classPropertyName: "mkDropListConnectedTo", publicName: "mkDropListConnectedTo", isSignal: true, isRequired: false, transformFunction: null }, mkDropListLabel: { classPropertyName: "mkDropListLabel", publicName: "mkDropListLabel", isSignal: true, isRequired: false, transformFunction: null }, mkDropListOrientation: { classPropertyName: "mkDropListOrientation", publicName: "mkDropListOrientation", isSignal: true, isRequired: false, transformFunction: null }, mkDropListDisabled: { classPropertyName: "mkDropListDisabled", publicName: "mkDropListDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mkDropListDropped: "mkDropListDropped" }, host: { properties: { "attr.role": "role()", "attr.aria-label": "ariaLabel()", "attr.aria-orientation": "orientationAllowed() ? mkDropListOrientation() : null", "attr.aria-disabled": "mkDropListDisabled() || null", "class.mk-drop-list--horizontal": "mkDropListOrientation() === 'horizontal'", "class.mk-drop-list--disabled": "mkDropListDisabled()", "class.mk-drop-list--receiving": "_receiving()" }, classAttribute: "mk-drop-list" }, queries: [{ propertyName: "drags", predicate: MkDrag, isSignal: true }], exportAs: ["mkDropList"], ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative}:host(.mk-drop-list--receiving){outline:var(--mk-border-width-strong) solid var(--mk-primary-subtle-text);outline-offset:calc(-1 * var(--mk-border-width-strong));border-radius:var(--mk-radius-md);background-color:color-mix(in srgb,var(--mk-primary) 6%,transparent)}:host(.mk-drop-list--disabled){cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1031
1183
  }
1032
1184
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDropList, decorators: [{
1033
1185
  type: Component,
1034
1186
  args: [{ selector: '[mkDropList]', exportAs: 'mkDropList', changeDetection: ChangeDetectionStrategy.OnPush, host: {
1035
1187
  class: 'mk-drop-list',
1036
- '[attr.aria-orientation]': 'mkDropListOrientation()',
1188
+ '[attr.role]': 'role()',
1189
+ '[attr.aria-label]': 'ariaLabel()',
1190
+ '[attr.aria-orientation]': 'orientationAllowed() ? mkDropListOrientation() : null',
1037
1191
  '[attr.aria-disabled]': 'mkDropListDisabled() || null',
1038
1192
  '[class.mk-drop-list--horizontal]': "mkDropListOrientation() === 'horizontal'",
1039
1193
  '[class.mk-drop-list--disabled]': 'mkDropListDisabled()',