@gridkitjs/dom 0.1.0 → 0.2.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
@@ -2,8 +2,8 @@
2
2
 
3
3
  The browser behaviours a GridKit renderer needs, as framework-free
4
4
  TypeScript: pointer gestures, keyboard focus movement, the observers behind
5
- fit-to-width and infinite scroll, the row virtualizer's measuring, overlays,
6
- and a live-region announcer. Built on
5
+ fit-to-width and infinite scroll, the row virtualizer's measuring, overlays
6
+ and the menus shown in them, and a live-region announcer. Built on
7
7
  [`@gridkitjs/core`](https://www.npmjs.com/package/@gridkitjs/core) and used by
8
8
  [`@gridkitjs/react`](https://www.npmjs.com/package/@gridkitjs/react).
9
9
 
@@ -47,6 +47,8 @@ announcer.destroy();
47
47
  | `createGridFocus` | Keeps the grid's single tab stop and moves the browser's focus with the keys. |
48
48
  | `bodyCellFrom` and others | Resolve a delegated body event to the cell, row or group header it addresses. |
49
49
  | `attachOverlay` | Positions a panel beside an anchor or point, and reports when to dismiss it. |
50
+ | `attachMenu` | Moves a menu's focus by key, typeahead and pointer, and reports choices. |
51
+ | `attachContextMenuTrigger` | Reports a right-click, Shift+F10 or the ContextMenu key, once per gesture. |
50
52
 
51
53
  Full documentation: [github.com/blagojablazhevski/gridkit](https://github.com/blagojablazhevski/gridkit#readme)
52
54
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ColumnResizeSession, ColumnOrderState, GroupByState, GridFocus, NavigationModifiers, VisibleRange } from '@gridkitjs/core';
1
+ import { ColumnResizeSession, ColumnOrderState, GroupByState, GridFocus, NavigationModifiers, VisibleRange, ResolvedToolbarItem } from '@gridkitjs/core';
2
2
 
3
3
  /** A live region to report changes through. */
4
4
  interface LiveAnnouncer {
@@ -65,6 +65,54 @@ declare function groupRowFrom(target: EventTarget | null): GroupRowTarget | null
65
65
  */
66
66
  declare function bodyRowFrom(target: EventTarget | null): BodyRowTarget | null;
67
67
 
68
+ /**
69
+ * How a context menu was asked for: by a pointer — a right-click, or a touch
70
+ * held still — or by the keyboard.
71
+ */
72
+ type ContextMenuSource = "pointer" | "keyboard";
73
+ /** A context menu asked for inside the root, as `onRequest` hears of it. */
74
+ interface ContextMenuRequest {
75
+ /** The element it was asked for on: the one under the pointer, or the one focused for a key. */
76
+ readonly target: Element;
77
+ /**
78
+ * Where the pointer was, in viewport coordinates — where a menu opened by
79
+ * it goes. `null` from the keyboard, which has no point: a menu it opens
80
+ * goes beside the focused element instead.
81
+ */
82
+ readonly point: {
83
+ readonly x: number;
84
+ readonly y: number;
85
+ } | null;
86
+ readonly source: ContextMenuSource;
87
+ }
88
+ interface ContextMenuTriggerOptions {
89
+ /**
90
+ * A context menu was asked for inside the root. Returns whether the caller
91
+ * opened one of its own: if so the browser's menu is prevented, and if not
92
+ * it shows as it would have.
93
+ */
94
+ readonly onRequest: (request: ContextMenuRequest) => boolean;
95
+ }
96
+ /**
97
+ * Reports every context menu the user asks for inside `root` — a right-click,
98
+ * Shift+F10, the ContextMenu key, or a touch held still for half a second —
99
+ * through `onRequest`, and returns the function that stops it.
100
+ *
101
+ * Each gesture asks once. Pressing a key, or holding a touch, also makes some
102
+ * browsers fire `contextmenu` of their own, and that event is taken, not
103
+ * reported again, once the gesture opened a menu. The click a touch brings
104
+ * as it lifts is taken too: the menu opened under the finger, and the click
105
+ * would land in it.
106
+ *
107
+ * An event something already prevented is left alone, so a cell template
108
+ * that shows a menu of its own keeps it. That is why the listeners are on the
109
+ * document, in the bubble phase: a framework that handles events at its own
110
+ * root container — React does — hears them before the document does, where
111
+ * a listener on `root` itself would hear them first. Only events whose target
112
+ * is inside `root` count.
113
+ */
114
+ declare function attachContextMenuTrigger(root: Element, options: ContextMenuTriggerOptions): () => void;
115
+
68
116
  /**
69
117
  * The `pointerdown` a gesture starts from. A native `PointerEvent` is one,
70
118
  * and so is a framework's wrapper around it, such as React's synthetic
@@ -370,6 +418,80 @@ interface InfiniteScroll {
370
418
  */
371
419
  declare function attachInfiniteScroll(root: Element, options: InfiniteScrollOptions): InfiniteScroll;
372
420
 
421
+ /**
422
+ * Where focus goes when a submenu opens: its first item, or nowhere — it
423
+ * stays on the item that opened it, as when the pointer opened it.
424
+ */
425
+ type MenuSubmenuFocus = "first" | "none";
426
+ /** Why a menu asked for every menu to close. */
427
+ type MenuCloseReason = "tab";
428
+ interface MenuOptions {
429
+ /**
430
+ * An enabled item that opens no submenu was chosen — Enter, Space or a
431
+ * click. The caller runs it, and closes every menu.
432
+ */
433
+ readonly onActivate: (id: string) => void;
434
+ /**
435
+ * An enabled submenu item asks for its submenu: Enter, Space or ArrowRight
436
+ * on it want its first item focused (`"first"`), a click or the pointer
437
+ * resting on it want focus left where it is (`"none"`). It may come for a
438
+ * submenu already open — ArrowRight on one the pointer opened — when a
439
+ * `"first"` asks for focus to move into it (`focusItem(0)` on its menu).
440
+ * Opening it closes any other submenu open from this menu.
441
+ */
442
+ readonly onOpenSubmenu?: ((id: string, options: {
443
+ readonly focus: MenuSubmenuFocus;
444
+ }) => void) | undefined;
445
+ /**
446
+ * The pointer came to rest on an item that opens no submenu, or a disabled
447
+ * one: close whatever submenu is open from this menu, if any. Moving on to
448
+ * another item is how a pointer user leaves a submenu they did not want.
449
+ */
450
+ readonly onCollapseSubmenu?: (() => void) | undefined;
451
+ /**
452
+ * ArrowLeft was pressed in this menu, which is a submenu: close it, and
453
+ * focus goes back to the item that opened it. Passed only for a submenu —
454
+ * in a menu without it, ArrowLeft does nothing.
455
+ */
456
+ readonly onCloseSubmenu?: (() => void) | undefined;
457
+ /**
458
+ * Tab or Shift+Tab was pressed: close every menu, so focus returns to where
459
+ * the first one was opened from rather than moving on past it.
460
+ */
461
+ readonly onClose: (reason: MenuCloseReason) => void;
462
+ }
463
+ interface AttachedMenu {
464
+ /**
465
+ * Focuses an item, by its `data-gridkit-menu-item` id or its place among
466
+ * the items. Does nothing for an item the menu does not have.
467
+ */
468
+ readonly focusItem: (item: string | number) => void;
469
+ /** Removes every listener. The items keep the `tabindex` they had. */
470
+ readonly detach: () => void;
471
+ }
472
+ /**
473
+ * Makes `panel` — a `role="menu"` element the renderer drew — behave as a
474
+ * menu: the arrow keys, Home, End and typeahead move focus among its items,
475
+ * Enter, Space and a click choose one, ArrowRight and ArrowLeft open and
476
+ * close submenus, and the pointer moves focus to the item under it. It
477
+ * decides; the caller is told through the callbacks and draws the outcome —
478
+ * runs the item, opens or closes a submenu.
479
+ *
480
+ * Items are the panel's descendants with a menu item role (`menuitem`,
481
+ * `menuitemcheckbox`, `menuitemradio`) and a `data-gridkit-menu-item` id,
482
+ * read afresh on every key so items drawn since attaching count. A submenu
483
+ * item says so with `aria-haspopup="menu"`; an item with
484
+ * `aria-disabled="true"` takes focus, so a screen reader finds it, but is
485
+ * never chosen. One item at a time has `tabindex="0"` — the focused one, or
486
+ * the first — and the rest `-1`: every item can take focus, and the first is
487
+ * what an overlay's initial focus finds when the menu is attached before it.
488
+ *
489
+ * Escape is not handled: a menu is shown with `attachOverlay`, whose stack
490
+ * gives Escape to the topmost overlay. A submenu is a menu of its own, in an
491
+ * overlay of its own anchored to its item, attached when it opens.
492
+ */
493
+ declare function attachMenu(panel: HTMLElement, options: MenuOptions): AttachedMenu;
494
+
373
495
  /** Which side of its anchor an overlay opens on. */
374
496
  type OverlaySide = "top" | "bottom" | "left" | "right";
375
497
  /**
@@ -395,6 +517,14 @@ interface OverlayPlacementOptions {
395
517
  readonly placement?: OverlayPlacement | undefined;
396
518
  /** Space between the anchor and the overlay, in pixels. Defaults to `0`. */
397
519
  readonly gap?: number | undefined;
520
+ /**
521
+ * Pixels to shift the overlay along the anchor's edge — down beside a
522
+ * `left`/`right` side, right beside `top`/`bottom`; negative goes the other
523
+ * way. A submenu opened `right-start` takes its own top padding, negated,
524
+ * so its first item lines up with the item that opened it. The overlay is
525
+ * still kept on screen. Defaults to `0`.
526
+ */
527
+ readonly offset?: number | undefined;
398
528
  }
399
529
  /** Where an overlay goes: its top-left corner, and the placement it ended up with. */
400
530
  interface OverlayPosition {
@@ -421,21 +551,46 @@ type OverlayAnchor = Element | {
421
551
  readonly x: number;
422
552
  readonly y: number;
423
553
  };
554
+ /**
555
+ * Where focus goes when an overlay attaches: its first focusable descendant,
556
+ * the panel itself, nowhere — focus stays where it was — or a given element.
557
+ */
558
+ type OverlayInitialFocus = "first" | "panel" | "none" | HTMLElement;
424
559
  /** Why an overlay asked to close. */
425
- type OverlayDismissReason = "escape" | "outside" | "scroll" | "resize" | "blur";
560
+ type OverlayDismissReason = "escape" | "outside" | "scroll" | "resize" | "blur" | "parent";
426
561
  interface OverlayOptions extends OverlayPlacementOptions {
427
562
  /**
428
- * The overlay asks to close: Escape was pressed, a pointer went down
429
- * outside it, something outside it scrolled, the window resized or lost
430
- * focus, or focus moved outside it. The caller closes it — removes the
431
- * panel and detaches — or keeps it open by doing nothing.
563
+ * The overlay asks to close: Escape was pressed while it was the topmost
564
+ * overlay, a pointer went down outside it, something outside it scrolled,
565
+ * the window resized or lost focus, focus moved outside it, or the overlay
566
+ * it was opened above detached. The caller closes it — removes the panel
567
+ * and detaches — or keeps it open by doing nothing, except after
568
+ * `"parent"`, which detaches it regardless.
432
569
  */
433
570
  readonly onDismiss: (reason: OverlayDismissReason) => void;
571
+ /**
572
+ * Where focus goes on attaching. Defaults to `"first"`: a menu focuses its
573
+ * first item. A panel whose first control is not where the user starts — a
574
+ * filter editor's operator picker before its value box — passes that
575
+ * element, or `"panel"` to land on the panel itself; `"none"` leaves focus
576
+ * where it was, as a suggestion list does while typing goes on in the
577
+ * field that opened it.
578
+ */
579
+ readonly initialFocus?: OverlayInitialFocus | undefined;
434
580
  /**
435
581
  * Where focus goes when the overlay detaches. Defaults to the element that
436
582
  * had focus when it attached; `null` leaves focus alone.
437
583
  */
438
584
  readonly returnFocusTo?: HTMLElement | null | undefined;
585
+ /**
586
+ * Whether a scroll outside the panel just after input inside it — an
587
+ * `input` or `change` event, in the two animation frames that follow it —
588
+ * leaves it open. A panel whose controls change the page under it opts
589
+ * in: a column chooser hiding a column narrows the grid it opened from,
590
+ * and the browser scrolls that grid back within its narrower content.
591
+ * Defaults to `false`, where every scroll outside the panel dismisses it.
592
+ */
593
+ readonly ignoreScrollAfterInput?: boolean | undefined;
439
594
  }
440
595
  /**
441
596
  * Shows `panel` as an overlay beside `anchor` — a menu, a popover, a filter
@@ -447,17 +602,27 @@ interface OverlayOptions extends OverlayPlacementOptions {
447
602
  * The panel is positioned `fixed`, at the place `placeOverlay` picks from the
448
603
  * anchor's and the panel's measured boxes, and carries that placement in
449
604
  * `data-gridkit-placement` (`"top-start"` once flipped above, say) for a
450
- * stylesheet to point an arrow by. Focus moves to the panel's first focusable
451
- * descendant, or to the panel itself — given `tabindex="-1"` if it has no
452
- * `tabindex` — when there is none.
605
+ * stylesheet to point an arrow by. Whenever the panel's size changes — a list
606
+ * that shrinks under a search — it is placed again beside the anchor, so a
607
+ * panel flipped above its anchor keeps its bottom edge there — or beside
608
+ * where the anchor was, once it has left the page. Focus moves
609
+ * where `initialFocus` says.
453
610
  *
454
611
  * Nothing that happens on or in the anchor dismisses the overlay, so a button
455
612
  * that toggles it can close it with its own click rather than having the
456
613
  * overlay close and the click reopen it.
457
614
  *
615
+ * Overlays stack in the order they attach. Escape belongs to the topmost
616
+ * alone, which takes it: it closes a submenu without closing the menu under
617
+ * it, and nothing else on the page — a grid that clears its selection on
618
+ * Escape — hears it. An overlay counts everything in the overlays above it
619
+ * as inside itself, so a click in a submenu keeps its menu open while a click
620
+ * elsewhere in the menu closes only the submenu.
621
+ *
458
622
  * Detaching removes every listener and returns focus, but only when focus is
459
- * still in the panel or nowhere — a click that dismissed it by landing on
460
- * another control leaves focus on that control.
623
+ * still in the panel, in an overlay above it, or nowhere — a click that
624
+ * dismissed it by landing on another control leaves focus on that control.
625
+ * Any overlay still above it is dismissed with `"parent"` and detached first.
461
626
  */
462
627
  declare function attachOverlay(anchor: OverlayAnchor, panel: HTMLElement, options: OverlayOptions): () => void;
463
628
 
@@ -512,4 +677,79 @@ interface RowVirtualizer {
512
677
  */
513
678
  declare function createRowVirtualizer(scrollElement: HTMLElement, options: RowVirtualizerOptions): RowVirtualizer;
514
679
 
515
- export { type BodyCellTarget, type BodyRowTarget, type ColumnDropContext, type ColumnDropTarget, type ColumnResizeGestureHandlers, type DragReorderHandlers, type DragReorderState, type FocusVirtualRange, type GridFocusController, type GridFocusOptions, type GridFocusRows, type GroupByDropTarget, type GroupRowTarget, type InfiniteScroll, type InfiniteScrollOptions, type LiveAnnouncer, NO_DRAG, type OverlayAlign, type OverlayAnchor, type OverlayDismissReason, type OverlayOptions, type OverlayPlacement, type OverlayPlacementOptions, type OverlayPosition, type OverlayRect, type OverlaySide, type OverlaySize, type PointerPress, type RowVirtualizer, type RowVirtualizerOptions, attachInfiniteScroll, attachOverlay, bodyCellFrom, bodyRowFrom, createGridFocus, createLiveAnnouncer, createRowVirtualizer, findBodyRow, groupRowFrom, measureColumnContent, measurePageSize, observeElementWidth, placeOverlay, resolveColumnDropTarget, resolveGroupByDropTarget, startColumnResizeGesture, startDragReorder };
680
+ /**
681
+ * Makes `toolbar` — a `role="toolbar"` element the renderer drew — one tab
682
+ * stop that the arrow keys move within, following the WAI-ARIA toolbar
683
+ * pattern: ArrowRight and ArrowLeft move focus to the next and previous
684
+ * control, wrapping round the ends, and Home and End to the first and last.
685
+ * Returns the function that stops it.
686
+ *
687
+ * The toolbar's entries are its descendants with a `data-gridkit-toolbar-item`,
688
+ * and each entry's control is the entry itself when it can take focus, else
689
+ * the first element in it that can; an entry with none — a separator, a
690
+ * spacer — is passed over. They are read afresh on every key. One control at
691
+ * a time has `tabindex="0"` — the focused one, or the first — and the rest
692
+ * `-1`, kept so as entries come and go; a control with
693
+ * `aria-disabled="true"` stays one of them, so a screen reader finds it.
694
+ *
695
+ * A text field keeps the keys it needs: Home and End, and an arrow while
696
+ * the caret has text to move through in that direction. At the start or end
697
+ * of its text, the arrow moves on to the next control.
698
+ */
699
+ declare function attachToolbar(toolbar: HTMLElement): () => void;
700
+
701
+ /**
702
+ * A toolbar entry as the overflow reads it: its kind, the key its element
703
+ * carries and, for a button or a dropdown, its label — a change to which,
704
+ * while the entry is in "More" and not drawn, makes the width remembered for
705
+ * it stale.
706
+ */
707
+ type ToolbarOverflowItem = Pick<ResolvedToolbarItem, "type" | "key"> & {
708
+ readonly label?: string | undefined;
709
+ };
710
+ interface ToolbarOverflowOptions {
711
+ /**
712
+ * Every entry of the toolbar, in order — those drawn and those moved into
713
+ * "More" alike — as `resolveToolbar` gave them.
714
+ */
715
+ readonly items: readonly ToolbarOverflowItem[];
716
+ /**
717
+ * How many entries fit, as `fitToolbarItems` counts them: called once
718
+ * straight away, then whenever the count changes.
719
+ */
720
+ readonly onChange: (fit: number) => void;
721
+ }
722
+ interface ToolbarOverflow {
723
+ /**
724
+ * Call whenever the items change, with the current options: measures
725
+ * again, and reports a changed count.
726
+ */
727
+ readonly update: (options: ToolbarOverflowOptions) => void;
728
+ /** Stops observing. */
729
+ readonly detach: () => void;
730
+ }
731
+ /**
732
+ * Measures how many of a toolbar's entries fit in it, reporting the count
733
+ * through `onChange` straight away — so a renderer calling this before
734
+ * paint draws the right entries from the first frame — and again whenever a
735
+ * resize changes it. The arithmetic is @gridkitjs/core's `fitToolbarItems`:
736
+ * the search box and custom entries always stay, and room for "More" is kept
737
+ * only when something does not fit.
738
+ *
739
+ * Entries are found by their `data-gridkit-toolbar-item`, holding each
740
+ * item's `key`, and the "More" button by `data-gridkit-toolbar-more`. An
741
+ * entry moved into "More" is no longer drawn, so each width is remembered
742
+ * by key from the last time it was; one never drawn yet counts as nothing,
743
+ * which lets it be drawn, measured, and moved back out if it does not fit —
744
+ * as one whose label changed while it was not drawn counts again.
745
+ * A spacer counts as nothing too: its width is only the room left over. The
746
+ * toolbar's padding and the `column-gap` between entries are allowed for.
747
+ *
748
+ * It measures again whenever the toolbar or an entry resizes and whenever
749
+ * entries are drawn or taken away — the ones the count it reported let in,
750
+ * or the "More" button — so a renderer only has to draw what it is told,
751
+ * and to pass new items to `update`.
752
+ */
753
+ declare function observeToolbarOverflow(toolbar: HTMLElement, options: ToolbarOverflowOptions): ToolbarOverflow;
754
+
755
+ export { type AttachedMenu, type BodyCellTarget, type BodyRowTarget, type ColumnDropContext, type ColumnDropTarget, type ColumnResizeGestureHandlers, type ContextMenuRequest, type ContextMenuSource, type ContextMenuTriggerOptions, type DragReorderHandlers, type DragReorderState, type FocusVirtualRange, type GridFocusController, type GridFocusOptions, type GridFocusRows, type GroupByDropTarget, type GroupRowTarget, type InfiniteScroll, type InfiniteScrollOptions, type LiveAnnouncer, type MenuCloseReason, type MenuOptions, type MenuSubmenuFocus, NO_DRAG, type OverlayAlign, type OverlayAnchor, type OverlayDismissReason, type OverlayInitialFocus, type OverlayOptions, type OverlayPlacement, type OverlayPlacementOptions, type OverlayPosition, type OverlayRect, type OverlaySide, type OverlaySize, type PointerPress, type RowVirtualizer, type RowVirtualizerOptions, type ToolbarOverflow, type ToolbarOverflowItem, type ToolbarOverflowOptions, attachContextMenuTrigger, attachInfiniteScroll, attachMenu, attachOverlay, attachToolbar, bodyCellFrom, bodyRowFrom, createGridFocus, createLiveAnnouncer, createRowVirtualizer, findBodyRow, groupRowFrom, measureColumnContent, measurePageSize, observeElementWidth, observeToolbarOverflow, placeOverlay, resolveColumnDropTarget, resolveGroupByDropTarget, startColumnResizeGesture, startDragReorder };