@memberjunction/ng-whiteboard 5.51.2 → 5.51.3
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 +5 -0
- package/dist/lib/whiteboard-board.component.d.ts +11 -3
- package/dist/lib/whiteboard-board.component.d.ts.map +1 -1
- package/dist/lib/whiteboard-board.component.js +12 -3
- package/dist/lib/whiteboard-board.component.js.map +1 -1
- package/dist/lib/whiteboard-context-menu.d.ts +2 -1
- package/dist/lib/whiteboard-context-menu.d.ts.map +1 -1
- package/dist/lib/whiteboard-context-menu.js +11 -7
- package/dist/lib/whiteboard-context-menu.js.map +1 -1
- package/dist/lib/whiteboard-host.component.d.ts +72 -3
- package/dist/lib/whiteboard-host.component.d.ts.map +1 -1
- package/dist/lib/whiteboard-host.component.dom.test.d.ts +2 -0
- package/dist/lib/whiteboard-host.component.dom.test.d.ts.map +1 -0
- package/dist/lib/whiteboard-host.component.dom.test.js +187 -0
- package/dist/lib/whiteboard-host.component.dom.test.js.map +1 -0
- package/dist/lib/whiteboard-host.component.js +132 -12
- package/dist/lib/whiteboard-host.component.js.map +1 -1
- package/dist/lib/whiteboard-tool-roster.d.ts +55 -0
- package/dist/lib/whiteboard-tool-roster.d.ts.map +1 -0
- package/dist/lib/whiteboard-tool-roster.js +58 -0
- package/dist/lib/whiteboard-tool-roster.js.map +1 -0
- package/dist/lib/whiteboard-toolbar.component.d.ts +11 -4
- package/dist/lib/whiteboard-toolbar.component.d.ts.map +1 -1
- package/dist/lib/whiteboard-toolbar.component.dom.test.js +13 -0
- package/dist/lib/whiteboard-toolbar.component.dom.test.js.map +1 -1
- package/dist/lib/whiteboard-toolbar.component.js +18 -3
- package/dist/lib/whiteboard-toolbar.component.js.map +1 -1
- package/dist/public-api.d.ts +1 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +2 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WhiteboardItem, WhiteboardPageInfo } from './whiteboard-state';
|
|
2
|
+
import { WhiteboardToolRoster } from './whiteboard-tool-roster';
|
|
2
3
|
/**
|
|
3
4
|
* LIVE WHITEBOARD — right-click CONTEXT MENU model (pure, Angular-free).
|
|
4
5
|
*
|
|
@@ -35,7 +36,7 @@ export interface WhiteboardContextMenuAction {
|
|
|
35
36
|
* (text/sticky), Duplicate (not connectors/highlights), Bring to front / Send to
|
|
36
37
|
* back, and Delete.
|
|
37
38
|
*/
|
|
38
|
-
export declare function BuildWhiteboardContextMenu(item: WhiteboardItem | null): WhiteboardContextMenuAction[];
|
|
39
|
+
export declare function BuildWhiteboardContextMenu(item: WhiteboardItem | null, roster?: WhiteboardToolRoster): WhiteboardContextMenuAction[];
|
|
39
40
|
/** Options for {@link BuildWhiteboardPageContextMenu}. */
|
|
40
41
|
export interface WhiteboardPageContextMenuOptions {
|
|
41
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-context-menu.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-context-menu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExE;;;;;;;;;;;GAWG;AAEH,qFAAqF;AACrF,MAAM,MAAM,6BAA6B,GACrC,MAAM,GACN,SAAS,GACT,WAAW,GACX,aAAa,GACb,WAAW,GACX,QAAQ,GACR,YAAY,GACZ,UAAU,GACV,cAAc,GACd,UAAU,GACV,UAAU,GACV,aAAa,GACb,aAAa,CAAC;AAElB,8CAA8C;AAC9C,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,6BAA6B,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,GAAG,2BAA2B,EAAE,
|
|
1
|
+
{"version":3,"file":"whiteboard-context-menu.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-context-menu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExE,OAAO,EAAE,oBAAoB,EAAiB,MAAM,0BAA0B,CAAC;AAE/E;;;;;;;;;;;GAWG;AAEH,qFAAqF;AACrF,MAAM,MAAM,6BAA6B,GACrC,MAAM,GACN,SAAS,GACT,WAAW,GACX,aAAa,GACb,WAAW,GACX,QAAQ,GACR,YAAY,GACZ,UAAU,GACV,cAAc,GACd,UAAU,GACV,UAAU,GACV,aAAa,GACb,aAAa,CAAC;AAElB,8CAA8C;AAC9C,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,6BAA6B,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,EAAE,MAAM,GAAE,oBAA2B,GAAG,2BAA2B,EAAE,CAgC1I;AAED,0DAA0D;AAC1D,MAAM,WAAW,gCAAgC;IAC/C;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,mFAAmF;IACnF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,gCAAgC,GACxC,2BAA2B,EAAE,CAkB/B"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IsToolAllowed } from './whiteboard-tool-roster';
|
|
1
2
|
/** Kinds whose dblclick opens an editor — the menu's Edit action mirrors that path. */
|
|
2
3
|
const EDITABLE_KINDS = new Set(['sticky', 'shape', 'text', 'markdown', 'html']);
|
|
3
4
|
/** Kinds whose text styling the toolbar flyout can restyle. */
|
|
@@ -15,15 +16,18 @@ const NON_DUPLICABLE_KINDS = new Set(['connector', 'highlight']);
|
|
|
15
16
|
* (text/sticky), Duplicate (not connectors/highlights), Bring to front / Send to
|
|
16
17
|
* back, and Delete.
|
|
17
18
|
*/
|
|
18
|
-
export function BuildWhiteboardContextMenu(item) {
|
|
19
|
+
export function BuildWhiteboardContextMenu(item, roster = null) {
|
|
19
20
|
if (item === null) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ ID: 'add-
|
|
24
|
-
{ ID: 'add-
|
|
25
|
-
{ ID: 'add-
|
|
21
|
+
// Each "add … here" action is the context-menu door to a TOOL, so it follows the roster the
|
|
22
|
+
// toolbar and the keyboard follow. "New page" is not a tool and is always offered.
|
|
23
|
+
const canvas = [
|
|
24
|
+
{ Tool: 'sticky', Action: { ID: 'add-sticky', Label: 'Add sticky note here', Icon: 'fa-regular fa-note-sticky' } },
|
|
25
|
+
{ Tool: 'text', Action: { ID: 'add-text', Label: 'Add text here', Icon: 'fa-solid fa-font' } },
|
|
26
|
+
{ Tool: 'markdown', Action: { ID: 'add-markdown', Label: 'Add markdown panel here', Icon: 'fa-brands fa-markdown' } },
|
|
27
|
+
{ Tool: 'html', Action: { ID: 'add-html', Label: 'Add widget here', Icon: 'fa-solid fa-code' } }
|
|
26
28
|
];
|
|
29
|
+
const adds = canvas.filter((c) => IsToolAllowed(roster, c.Tool)).map((c) => c.Action);
|
|
30
|
+
return [...adds, { ID: 'add-page', Label: 'New page', Icon: 'fa-regular fa-file', SeparatorBefore: adds.length > 0 }];
|
|
27
31
|
}
|
|
28
32
|
if (item.Kind === 'highlight') {
|
|
29
33
|
return [{ ID: 'delete', Label: 'Delete', Icon: 'fa-solid fa-trash-can', Danger: true }];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-context-menu.js","sourceRoot":"","sources":["../../src/lib/whiteboard-context-menu.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"whiteboard-context-menu.js","sourceRoot":"","sources":["../../src/lib/whiteboard-context-menu.ts"],"names":[],"mappings":"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","sourcesContent":["import { WhiteboardItem, WhiteboardPageInfo } from './whiteboard-state';\nimport type { WhiteboardTool } from './whiteboard-tool-roster';\nimport { WhiteboardToolRoster, IsToolAllowed } from './whiteboard-tool-roster';\n\n/**\n * LIVE WHITEBOARD — right-click CONTEXT MENU model (pure, Angular-free).\n *\n * {@link BuildWhiteboardContextMenu} derives the action list for a right-click target:\n * an ITEM menu (edit / restyle / duplicate / z-order / delete, gated per kind), the\n * delete-only HIGHLIGHT menu, or the EMPTY-CANVAS menu (click-point \"add … here\" actions\n * that reuse the board's existing placement paths, plus \"New page\").\n * {@link BuildWhiteboardPageContextMenu} derives the menu for a right-click on a PAGE\n * CHIP in the page strip (rename / delete, honoring the last-page guard and ReadOnly).\n * The board component renders the model and dispatches the picked action; this module\n * decides only WHAT is offered.\n */\n\n/** Identifier of one context-menu action — the board's dispatcher switches on it. */\nexport type WhiteboardContextMenuActionID =\n | 'edit'\n | 'restyle'\n | 'duplicate'\n | 'bring-front'\n | 'send-back'\n | 'delete'\n | 'add-sticky'\n | 'add-text'\n | 'add-markdown'\n | 'add-html'\n | 'add-page'\n | 'page-rename'\n | 'page-delete';\n\n/** One entry of the rendered context menu. */\nexport interface WhiteboardContextMenuAction {\n ID: WhiteboardContextMenuActionID;\n Label: string;\n /** Font Awesome icon classes. */\n Icon: string;\n /** Render a divider line ABOVE this entry. */\n SeparatorBefore?: boolean;\n /** Destructive styling (delete). */\n Danger?: boolean;\n}\n\n/** Kinds whose dblclick opens an editor — the menu's Edit action mirrors that path. */\nconst EDITABLE_KINDS: ReadonlySet<WhiteboardItem['Kind']> = new Set(['sticky', 'shape', 'text', 'markdown', 'html']);\n/** Kinds whose text styling the toolbar flyout can restyle. */\nconst RESTYLABLE_KINDS: ReadonlySet<WhiteboardItem['Kind']> = new Set(['sticky', 'text']);\n/** Kinds that cannot be duplicated (connectors reference other items; highlights are transient). */\nconst NON_DUPLICABLE_KINDS: ReadonlySet<WhiteboardItem['Kind']> = new Set(['connector', 'highlight']);\n\n/**\n * Build the context-menu model for a right-click target.\n *\n * - `item === null` → empty-canvas menu: add sticky / text / markdown panel / widget\n * at the click point (the board routes these through its existing placement paths),\n * plus \"New page\" (the same path as the strip's \"+\" button);\n * - highlight → Delete only (highlights are transient \"pointing\" chrome);\n * - any other item → Edit (same path as dblclick; editable kinds only), Restyle…\n * (text/sticky), Duplicate (not connectors/highlights), Bring to front / Send to\n * back, and Delete.\n */\nexport function BuildWhiteboardContextMenu(item: WhiteboardItem | null, roster: WhiteboardToolRoster = null): WhiteboardContextMenuAction[] {\n if (item === null) {\n // Each \"add … here\" action is the context-menu door to a TOOL, so it follows the roster the\n // toolbar and the keyboard follow. \"New page\" is not a tool and is always offered.\n const canvas: Array<{ Tool: WhiteboardTool; Action: WhiteboardContextMenuAction }> = [\n { Tool: 'sticky', Action: { ID: 'add-sticky', Label: 'Add sticky note here', Icon: 'fa-regular fa-note-sticky' } },\n { Tool: 'text', Action: { ID: 'add-text', Label: 'Add text here', Icon: 'fa-solid fa-font' } },\n { Tool: 'markdown', Action: { ID: 'add-markdown', Label: 'Add markdown panel here', Icon: 'fa-brands fa-markdown' } },\n { Tool: 'html', Action: { ID: 'add-html', Label: 'Add widget here', Icon: 'fa-solid fa-code' } }\n ];\n const adds = canvas.filter((c) => IsToolAllowed(roster, c.Tool)).map((c) => c.Action);\n return [...adds, { ID: 'add-page', Label: 'New page', Icon: 'fa-regular fa-file', SeparatorBefore: adds.length > 0 }];\n }\n if (item.Kind === 'highlight') {\n return [{ ID: 'delete', Label: 'Delete', Icon: 'fa-solid fa-trash-can', Danger: true }];\n }\n const actions: WhiteboardContextMenuAction[] = [];\n if (EDITABLE_KINDS.has(item.Kind)) {\n actions.push({ ID: 'edit', Label: 'Edit', Icon: 'fa-solid fa-pen' });\n }\n if (RESTYLABLE_KINDS.has(item.Kind)) {\n actions.push({ ID: 'restyle', Label: 'Restyle…', Icon: 'fa-solid fa-palette' });\n }\n if (!NON_DUPLICABLE_KINDS.has(item.Kind)) {\n actions.push({ ID: 'duplicate', Label: 'Duplicate', Icon: 'fa-regular fa-clone' });\n }\n actions.push(\n { ID: 'bring-front', Label: 'Bring to front', Icon: 'fa-solid fa-arrow-up-from-bracket', SeparatorBefore: actions.length > 0 },\n { ID: 'send-back', Label: 'Send to back', Icon: 'fa-solid fa-arrow-down-long' },\n { ID: 'delete', Label: 'Delete', Icon: 'fa-solid fa-trash-can', Danger: true, SeparatorBefore: true }\n );\n return actions;\n}\n\n/** Options for {@link BuildWhiteboardPageContextMenu}. */\nexport interface WhiteboardPageContextMenuOptions {\n /**\n * Whether the page may be deleted — mirrors the engine's last-page guard (the only\n * remaining page can never be removed). When false the Delete entry is OMITTED\n * (not disabled), matching the strip's hidden \"×\" affordance.\n */\n CanDelete: boolean;\n /** Read-only board: page mutations are unavailable, so NO menu is offered ([]). */\n ReadOnly: boolean;\n}\n\n/**\n * Build the context-menu model for a right-click on a PAGE CHIP in the page strip:\n * Rename (routes to the strip's existing inline-rename editor) and Delete (guarded by\n * the engine's last-page rule via {@link WhiteboardPageContextMenuOptions.CanDelete}).\n * Read-only boards get an empty model — the caller shows no menu at all.\n */\nexport function BuildWhiteboardPageContextMenu(\n page: WhiteboardPageInfo,\n options: WhiteboardPageContextMenuOptions\n): WhiteboardContextMenuAction[] {\n if (options.ReadOnly) {\n return [];\n }\n const actions: WhiteboardContextMenuAction[] = [\n { ID: 'page-rename', Label: 'Rename page', Icon: 'fa-solid fa-i-cursor' }\n ];\n if (options.CanDelete) {\n const name = page.Name.length > 20 ? `${page.Name.slice(0, 20).trimEnd()}…` : page.Name;\n actions.push({\n ID: 'page-delete',\n Label: `Delete \"${name}\"`,\n Icon: 'fa-solid fa-trash-can',\n Danger: true,\n SeparatorBefore: true\n });\n }\n return actions;\n}\n"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EventEmitter, OnDestroy, OnInit } from '@angular/core';
|
|
2
2
|
import { WhiteboardFontFamily, WhiteboardItemAddedEventArgs, WhiteboardItemAddingEventArgs, WhiteboardItemRemovedEventArgs, WhiteboardItemRemovingEventArgs, WhiteboardItemUpdatedEventArgs, WhiteboardItemUpdatingEventArgs, WhiteboardState } from './whiteboard-state';
|
|
3
|
+
import { WhiteboardTool, WhiteboardToolRoster } from './whiteboard-tool-roster';
|
|
3
4
|
import { RealtimeWhiteboardBoardComponent, WhiteboardAgentPresence, WhiteboardContentAppliedEventArgs, WhiteboardContentApplyingEventArgs } from './whiteboard-board.component';
|
|
4
5
|
import { WhiteboardWidgetInteractionEvent, WhiteboardWidgetSubmitEvent, WhiteboardWidgetSubmittingEventArgs } from './whiteboard-widget-bridge';
|
|
5
|
-
import { WhiteboardTool } from './whiteboard-toolbar.component';
|
|
6
6
|
import * as i0 from "@angular/core";
|
|
7
7
|
/**
|
|
8
8
|
* LIVE WHITEBOARD host (`mj-realtime-whiteboard-host`): board header (title / saved chip /
|
|
@@ -36,6 +36,41 @@ export declare class RealtimeWhiteboardHostComponent implements OnInit, OnDestro
|
|
|
36
36
|
BoardTitle: string;
|
|
37
37
|
/** Persistence chip text (e.g. "Saved to session · v14"). */
|
|
38
38
|
SavedLabel: string;
|
|
39
|
+
/**
|
|
40
|
+
* Opt back in to DOCUMENT-WIDE keyboard shortcuts.
|
|
41
|
+
*
|
|
42
|
+
* The host binds eleven bare single-character tool shortcuts (v h p r s t m w i c e). While
|
|
43
|
+
* those listened on `document` unconditionally they fired wherever the user was on the page,
|
|
44
|
+
* which fails WCAG 2.1.4 (Character Key Shortcuts): a speech-input user saying an ordinary
|
|
45
|
+
* word, or anyone typing in a non-input control elsewhere, silently switched the board tool.
|
|
46
|
+
* They are now scoped to focus being inside this host, which satisfies the criterion's
|
|
47
|
+
* "active only on focus" branch.
|
|
48
|
+
*
|
|
49
|
+
* Scoping covers the host's ENTIRE keydown handler, not only the letters: undo/redo
|
|
50
|
+
* (Cmd/Ctrl+Z, +Y), Escape and Delete/Backspace are focus-gated as well.
|
|
51
|
+
*
|
|
52
|
+
* Set `true` only for a surface where the whiteboard is the entire page AND the 2.1.4
|
|
53
|
+
* exposure has been accepted — it is an accessibility regression, not a convenience flag.
|
|
54
|
+
*/
|
|
55
|
+
EnableGlobalShortcuts: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Which tools this surface offers. `null` (default) is all eleven: today's rendering.
|
|
58
|
+
*
|
|
59
|
+
* The roster governs which tools are AVAILABLE, closing every door to a tool it leaves out:
|
|
60
|
+
* the toolbar button, the single-letter shortcut, and the canvas "add … here" action. It is
|
|
61
|
+
* deliberately not a content policy — it does not restrict what already exists on the board,
|
|
62
|
+
* what the agent places, or authoring on existing items (Restyle, Duplicate, paste). An empty
|
|
63
|
+
* roster narrows the palette to nothing; it does NOT make the board read-only, which is a
|
|
64
|
+
* separate input.
|
|
65
|
+
*
|
|
66
|
+
* A setter rather than `ngOnChanges` (packages/Angular/CLAUDE.md): a realtime channel's
|
|
67
|
+
* `BindSurface` assigns this property directly on a dynamically-created component, and
|
|
68
|
+
* Angular fires no `ngOnChanges` for a plain assignment, so a clamp implemented there would
|
|
69
|
+
* silently never run on the path real consumers use.
|
|
70
|
+
*/
|
|
71
|
+
set ToolRoster(value: WhiteboardToolRoster);
|
|
72
|
+
get ToolRoster(): WhiteboardToolRoster;
|
|
73
|
+
private _toolRoster;
|
|
39
74
|
/** Debounced (750 ms), coalesced scene-delta JSON — the live perception feed. */
|
|
40
75
|
SceneDelta: EventEmitter<string>;
|
|
41
76
|
/** The user clicked Undo on the agent-action toast (the undo itself already applied). */
|
|
@@ -84,7 +119,23 @@ export declare class RealtimeWhiteboardHostComponent implements OnInit, OnDestro
|
|
|
84
119
|
/** AFTER mirror of {@link WhiteboardState.ItemRemoved$}. */
|
|
85
120
|
ItemRemoved: EventEmitter<WhiteboardItemRemovedEventArgs>;
|
|
86
121
|
Board?: RealtimeWhiteboardBoardComponent;
|
|
87
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The active tool, and the ONE place the roster invariant is enforced on a write.
|
|
124
|
+
*
|
|
125
|
+
* "The active tool is always one the roster allows" is a property of every write, not of the
|
|
126
|
+
* handful of call sites that exist today (the roster setter, Escape, the key map, and the two
|
|
127
|
+
* template `Tool = $event` bindings). Guarding each of those would leave the next writer —
|
|
128
|
+
* a new shortcut, a gesture, a host API — to remember the rule. Guarding here closes them all
|
|
129
|
+
* by construction, and makes assignment order irrelevant: setting `ToolRoster` and `Tool` in
|
|
130
|
+
* either order still lands on an allowed tool.
|
|
131
|
+
*
|
|
132
|
+
* A DISALLOWED request is ignored rather than clamped: pressing Escape under `['pen','eraser']`
|
|
133
|
+
* should leave you on pen, not bounce you to the roster's fallback. The clamp is only for the
|
|
134
|
+
* case where the tool you are already holding stopped being allowed.
|
|
135
|
+
*/
|
|
136
|
+
get Tool(): WhiteboardTool;
|
|
137
|
+
set Tool(value: WhiteboardTool);
|
|
138
|
+
private _tool;
|
|
88
139
|
PenColor: string;
|
|
89
140
|
PenWidth: number;
|
|
90
141
|
ShapeKind: 'rect' | 'ellipse' | 'diamond';
|
|
@@ -153,6 +204,24 @@ export declare class RealtimeWhiteboardHostComponent implements OnInit, OnDestro
|
|
|
153
204
|
private downloadBlob;
|
|
154
205
|
private static exportDateStamp;
|
|
155
206
|
OnToastUndo(event: Event): void;
|
|
207
|
+
/**
|
|
208
|
+
* The board is made focusable so its shortcuts have a focus scope to be bound to. `-1`, not
|
|
209
|
+
* `0`: the canvas has no keyboard interaction model yet (see the whiteboard keyboard-
|
|
210
|
+
* operability work), so adding a real Tab stop would put a stop in the page that a keyboard
|
|
211
|
+
* user can reach and do nothing with. Click focuses it; that is the whole contract today.
|
|
212
|
+
*/
|
|
213
|
+
readonly HostTabIndex = "-1";
|
|
214
|
+
private readonly host;
|
|
215
|
+
/**
|
|
216
|
+
* Clicking the board takes focus, so the shortcuts below apply. The browser would usually do
|
|
217
|
+
* this on its own by focusing the nearest focusable ancestor, but the board's pointer
|
|
218
|
+
* pipeline is elaborate enough (drag, marquee, handles, transient shapes) that relying on
|
|
219
|
+
* that default is fragile — one `preventDefault()` added later would silently kill every
|
|
220
|
+
* shortcut. This makes it explicit.
|
|
221
|
+
*/
|
|
222
|
+
OnHostPointerDown(): void;
|
|
223
|
+
/** Whether focus currently sits on, or inside, this whiteboard host. */
|
|
224
|
+
private hasFocusWithin;
|
|
156
225
|
OnKeydown(event: KeyboardEvent): void;
|
|
157
226
|
get ElementCount(): number;
|
|
158
227
|
get UserCount(): number;
|
|
@@ -169,6 +238,6 @@ export declare class RealtimeWhiteboardHostComponent implements OnInit, OnDestro
|
|
|
169
238
|
private static toolPhrase;
|
|
170
239
|
private static toolForKey;
|
|
171
240
|
static ɵfac: i0.ɵɵFactoryDeclaration<RealtimeWhiteboardHostComponent, never>;
|
|
172
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<RealtimeWhiteboardHostComponent, "mj-realtime-whiteboard-host", never, { "State": { "alias": "State"; "required": true; }; "AgentName": { "alias": "AgentName"; "required": false; }; "BoardTitle": { "alias": "BoardTitle"; "required": false; }; "SavedLabel": { "alias": "SavedLabel"; "required": false; }; }, { "SceneDelta": "SceneDelta"; "AgentUndo": "AgentUndo"; "FocusModeChange": "FocusModeChange"; "SaveToArtifactsRequested": "SaveToArtifactsRequested"; "WidgetSubmitting": "WidgetSubmitting"; "WidgetSubmitted": "WidgetSubmitted"; "WidgetInteraction": "WidgetInteraction"; "ContentApplying": "ContentApplying"; "ContentApplied": "ContentApplied"; "ItemAdding": "ItemAdding"; "ItemAdded": "ItemAdded"; "ItemUpdating": "ItemUpdating"; "ItemUpdated": "ItemUpdated"; "ItemRemoving": "ItemRemoving"; "ItemRemoved": "ItemRemoved"; }, never, never, true, never>;
|
|
241
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<RealtimeWhiteboardHostComponent, "mj-realtime-whiteboard-host", never, { "State": { "alias": "State"; "required": true; }; "AgentName": { "alias": "AgentName"; "required": false; }; "BoardTitle": { "alias": "BoardTitle"; "required": false; }; "SavedLabel": { "alias": "SavedLabel"; "required": false; }; "EnableGlobalShortcuts": { "alias": "EnableGlobalShortcuts"; "required": false; }; "ToolRoster": { "alias": "ToolRoster"; "required": false; }; }, { "SceneDelta": "SceneDelta"; "AgentUndo": "AgentUndo"; "FocusModeChange": "FocusModeChange"; "SaveToArtifactsRequested": "SaveToArtifactsRequested"; "WidgetSubmitting": "WidgetSubmitting"; "WidgetSubmitted": "WidgetSubmitted"; "WidgetInteraction": "WidgetInteraction"; "ContentApplying": "ContentApplying"; "ContentApplied": "ContentApplied"; "ItemAdding": "ItemAdding"; "ItemAdded": "ItemAdded"; "ItemUpdating": "ItemUpdating"; "ItemUpdated": "ItemUpdated"; "ItemRemoving": "ItemRemoving"; "ItemRemoved": "ItemRemoved"; }, never, never, true, never>;
|
|
173
242
|
}
|
|
174
243
|
//# sourceMappingURL=whiteboard-host.component.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-host.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-host.component.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"whiteboard-host.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-host.component.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-host.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-host.component.dom.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderComponentFixture } from '@memberjunction/ng-test-utils';
|
|
3
|
+
import { RealtimeWhiteboardHostComponent } from './whiteboard-host.component';
|
|
4
|
+
import { WhiteboardState } from './whiteboard-state';
|
|
5
|
+
/**
|
|
6
|
+
* DOM spec for <mj-realtime-whiteboard-host> — the FIRST host-level spec in the package,
|
|
7
|
+
* and it exists for one reason: `ToolRoster` is a single fact the host reads in three
|
|
8
|
+
* places, and two of those three (the `document:keydown` tool-key gate and the
|
|
9
|
+
* setter clamp) live on the host itself, over pure helpers that are unit-tested
|
|
10
|
+
* but never wired end to end. This renders the real host — header, board and toolbar
|
|
11
|
+
* together — and drives the roster through every door a user has: the toolbar button,
|
|
12
|
+
* the single-letter shortcut, and the canvas right-click "add ... here" action.
|
|
13
|
+
*
|
|
14
|
+
* The keyboard cases focus the host element first rather than setting
|
|
15
|
+
* `EnableGlobalShortcuts`, because the focus scope (#4121, WCAG 2.1.4) is the shipped
|
|
16
|
+
* path and the opt-out is discouraged; every key case carries a positive control so a
|
|
17
|
+
* dead listener cannot pass as "correctly gated".
|
|
18
|
+
*/
|
|
19
|
+
describe('RealtimeWhiteboardHostComponent (DOM)', () => {
|
|
20
|
+
/** The six-tool roster LXP will set: the eleven minus shapes, markdown, widget, image, connector. */
|
|
21
|
+
const LXP_ROSTER = ['select', 'pan', 'pen', 'sticky', 'text', 'eraser'];
|
|
22
|
+
const render = (roster = null, state = new WhiteboardState()) => renderComponentFixture(RealtimeWhiteboardHostComponent, {
|
|
23
|
+
inputs: roster === null ? { State: state } : { State: state, ToolRoster: roster },
|
|
24
|
+
});
|
|
25
|
+
/** Toolbar buttons: the tool palette PLUS the undo and redo buttons, which share `.tool`. */
|
|
26
|
+
const toolButtons = (f) => Array.from(f.nativeElement.querySelectorAll('button.tool'));
|
|
27
|
+
const toolTitles = (f) => toolButtons(f).map((b) => b.getAttribute('title'));
|
|
28
|
+
/** Focus the host so the #4121-scoped `document:keydown` handler is live. */
|
|
29
|
+
const focusHost = (f) => f.nativeElement.focus();
|
|
30
|
+
const pressKey = (f, key) => {
|
|
31
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
32
|
+
f.detectChanges();
|
|
33
|
+
};
|
|
34
|
+
const rightClick = (f, selector) => {
|
|
35
|
+
const el = f.nativeElement.querySelector(selector);
|
|
36
|
+
if (!el)
|
|
37
|
+
throw new Error(`rightClick(): no element matched "${selector}"`);
|
|
38
|
+
el.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: 40, clientY: 40 }));
|
|
39
|
+
f.detectChanges();
|
|
40
|
+
};
|
|
41
|
+
const menuLabels = (f) => Array.from(f.nativeElement.querySelectorAll('.wb-context-menu__item')).map((b) => b.textContent?.trim());
|
|
42
|
+
// ── 1. the control: no roster reproduces today's chrome ────────────────────────────
|
|
43
|
+
it('renders today toolbar and default tool when no roster is supplied', () => {
|
|
44
|
+
const f = render();
|
|
45
|
+
// eleven tools plus undo and redo
|
|
46
|
+
expect(toolButtons(f).length).toBe(13);
|
|
47
|
+
expect(f.componentInstance.Tool).toBe('select');
|
|
48
|
+
});
|
|
49
|
+
// ── 2. the roster narrows the toolbar THROUGH the host ─────────────────────────────
|
|
50
|
+
it('narrows the toolbar to the roster, in Tools order (not roster order)', () => {
|
|
51
|
+
const f = render(['eraser', 'select', 'text', 'pen', 'pan', 'sticky']); // deliberately scrambled
|
|
52
|
+
expect(toolTitles(f)).toEqual([
|
|
53
|
+
'Select / move',
|
|
54
|
+
'Pan',
|
|
55
|
+
'Pen',
|
|
56
|
+
'Sticky note',
|
|
57
|
+
'Text',
|
|
58
|
+
'Eraser',
|
|
59
|
+
'Undo',
|
|
60
|
+
'Redo',
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
// ── 3. a hidden tool's keyboard shortcut is a no-op ────────────────────────────────
|
|
64
|
+
it('ignores the shortcut of a tool outside the roster, and still honors one inside it', () => {
|
|
65
|
+
const f = render(LXP_ROSTER);
|
|
66
|
+
focusHost(f);
|
|
67
|
+
pressKey(f, 'w'); // html widget — not in the roster
|
|
68
|
+
expect(f.componentInstance.Tool).toBe('select');
|
|
69
|
+
pressKey(f, 'm'); // markdown — not in the roster
|
|
70
|
+
expect(f.componentInstance.Tool).toBe('select');
|
|
71
|
+
// positive control in the same test: the listener IS live, the keys above were gated
|
|
72
|
+
pressKey(f, 'p');
|
|
73
|
+
expect(f.componentInstance.Tool).toBe('pen');
|
|
74
|
+
});
|
|
75
|
+
it('leaves every shortcut live when there is no roster', () => {
|
|
76
|
+
const f = render();
|
|
77
|
+
focusHost(f);
|
|
78
|
+
pressKey(f, 'w');
|
|
79
|
+
expect(f.componentInstance.Tool).toBe('html');
|
|
80
|
+
});
|
|
81
|
+
// ── 4. the setter clamp ────────────────────────────────────────────────────────────
|
|
82
|
+
it('clamps the active tool to the roster first entry when a roster arrives that excludes it', () => {
|
|
83
|
+
const f = render();
|
|
84
|
+
focusHost(f);
|
|
85
|
+
pressKey(f, 'w');
|
|
86
|
+
expect(f.componentInstance.Tool).toBe('html');
|
|
87
|
+
f.componentRef.setInput('ToolRoster', ['pan', 'pen']);
|
|
88
|
+
f.detectChanges();
|
|
89
|
+
// the roster's FIRST entry, not 'select' — a roster without 'select' is legal
|
|
90
|
+
expect(f.componentInstance.Tool).toBe('pan');
|
|
91
|
+
});
|
|
92
|
+
it('never widens on clamp: dropping the roster leaves the active tool where it was', () => {
|
|
93
|
+
const f = render(['pan', 'pen']);
|
|
94
|
+
expect(f.componentInstance.Tool).toBe('pan'); // clamped on the first change
|
|
95
|
+
f.componentRef.setInput('ToolRoster', null);
|
|
96
|
+
f.detectChanges();
|
|
97
|
+
expect(f.componentInstance.Tool).toBe('pan');
|
|
98
|
+
expect(toolButtons(f).length).toBe(13); // the palette is whole again
|
|
99
|
+
});
|
|
100
|
+
it('clamps when a host assigns ToolRoster DIRECTLY, not through an Angular binding', () => {
|
|
101
|
+
// The realtime channel wires this host by creating it dynamically and assigning
|
|
102
|
+
// `instance.ToolRoster = ...`. Angular fires NO ngOnChanges for a plain property
|
|
103
|
+
// assignment, so a clamp implemented there never runs on the path real consumers
|
|
104
|
+
// use — verified failing in the browser before the clamp moved into the setter.
|
|
105
|
+
// `setup` runs after construction but BEFORE the first change detection, which is
|
|
106
|
+
// exactly when BindSurface assigns its inputs.
|
|
107
|
+
const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {
|
|
108
|
+
inputs: { State: new WhiteboardState() },
|
|
109
|
+
setup: (instance) => {
|
|
110
|
+
expect(instance.Tool).toBe('select');
|
|
111
|
+
instance.ToolRoster = ['pan', 'pen'];
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
expect(f.componentInstance.Tool).toBe('pan');
|
|
115
|
+
expect(toolTitles(f)).toEqual(['Pan', 'Pen', 'Undo', 'Redo']);
|
|
116
|
+
});
|
|
117
|
+
it('ignores a disallowed WRITE rather than clamping: Escape stays put under a roster without select', () => {
|
|
118
|
+
// Escape assigns 'select'. Under ['pen','eraser'] that is disallowed, and the right answer
|
|
119
|
+
// is to stay on pen, NOT to bounce to the roster fallback.
|
|
120
|
+
const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {
|
|
121
|
+
inputs: { State: new WhiteboardState() },
|
|
122
|
+
setup: (instance) => { instance.ToolRoster = ['pen', 'eraser']; },
|
|
123
|
+
});
|
|
124
|
+
expect(f.componentInstance.Tool).toBe('pen'); // clamped on mount: select is not on the roster
|
|
125
|
+
f.nativeElement.focus();
|
|
126
|
+
pressKey(f, 'Escape');
|
|
127
|
+
expect(f.componentInstance.Tool).toBe('pen');
|
|
128
|
+
});
|
|
129
|
+
it('enforces the roster on a direct Tool write, in either assignment order', () => {
|
|
130
|
+
const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {
|
|
131
|
+
inputs: { State: new WhiteboardState() },
|
|
132
|
+
setup: (instance) => {
|
|
133
|
+
instance.ToolRoster = ['pan', 'pen'];
|
|
134
|
+
instance.Tool = 'html'; // disallowed: must be ignored, not applied
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
expect(f.componentInstance.Tool).toBe('pan');
|
|
138
|
+
});
|
|
139
|
+
it('does not re-clamp when the same roster value is set again', () => {
|
|
140
|
+
// A consumer re-binding an identical array must not fight the user for the active tool.
|
|
141
|
+
const roster = ['pan', 'pen'];
|
|
142
|
+
const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {
|
|
143
|
+
inputs: { State: new WhiteboardState() },
|
|
144
|
+
setup: (instance) => { instance.ToolRoster = roster; },
|
|
145
|
+
});
|
|
146
|
+
f.componentInstance.Tool = 'pen';
|
|
147
|
+
expect(f.componentInstance.Tool).toBe('pen');
|
|
148
|
+
f.componentRef.setInput('ToolRoster', roster);
|
|
149
|
+
f.detectChanges();
|
|
150
|
+
expect(f.componentInstance.Tool).toBe('pen');
|
|
151
|
+
});
|
|
152
|
+
// ── 5. the right-click door on the canvas ──────────────────────────────────────────
|
|
153
|
+
it('offers every add action plus New page on the canvas menu when there is no roster', () => {
|
|
154
|
+
const f = render();
|
|
155
|
+
rightClick(f, '.board-canvas');
|
|
156
|
+
expect(menuLabels(f)).toEqual([
|
|
157
|
+
'Add sticky note here',
|
|
158
|
+
'Add text here',
|
|
159
|
+
'Add markdown panel here',
|
|
160
|
+
'Add widget here',
|
|
161
|
+
'New page',
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
it('offers only the roster add actions on the canvas menu (the #845 hole)', () => {
|
|
165
|
+
const f = render(LXP_ROSTER);
|
|
166
|
+
rightClick(f, '.board-canvas');
|
|
167
|
+
expect(menuLabels(f)).toEqual(['Add sticky note here', 'Add text here', 'New page']);
|
|
168
|
+
});
|
|
169
|
+
it('renders no separator above New page when the roster leaves no add actions', () => {
|
|
170
|
+
const f = render([]);
|
|
171
|
+
rightClick(f, '.board-canvas');
|
|
172
|
+
expect(menuLabels(f)).toEqual(['New page']);
|
|
173
|
+
expect(f.nativeElement.querySelectorAll('.wb-context-menu__sep').length).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
// ── 6. item menus are deliberately untouched by the roster ─────────────────────────
|
|
176
|
+
it('leaves the item menu Edit and Delete actions in place under a narrow roster', () => {
|
|
177
|
+
const state = new WhiteboardState();
|
|
178
|
+
state.AddItem({ Kind: 'sticky', X: 10, Y: 20, Text: 'hello' }, 'user');
|
|
179
|
+
const f = render(LXP_ROSTER, state);
|
|
180
|
+
rightClick(f, '.bi.sticky');
|
|
181
|
+
const labels = menuLabels(f);
|
|
182
|
+
// authoring controls are the NEXT input (ShowAuthoringControls), not tool selection
|
|
183
|
+
expect(labels).toContain('Edit');
|
|
184
|
+
expect(labels).toContain('Delete');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
//# sourceMappingURL=whiteboard-host.component.dom.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-host.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-host.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardHostComponent } from './whiteboard-host.component';\nimport { WhiteboardState } from './whiteboard-state';\nimport { WhiteboardTool, WhiteboardToolRoster } from './whiteboard-tool-roster';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-host> — the FIRST host-level spec in the package,\n * and it exists for one reason: `ToolRoster` is a single fact the host reads in three\n * places, and two of those three (the `document:keydown` tool-key gate and the\n * setter clamp) live on the host itself, over pure helpers that are unit-tested\n * but never wired end to end. This renders the real host — header, board and toolbar\n * together — and drives the roster through every door a user has: the toolbar button,\n * the single-letter shortcut, and the canvas right-click \"add ... here\" action.\n *\n * The keyboard cases focus the host element first rather than setting\n * `EnableGlobalShortcuts`, because the focus scope (#4121, WCAG 2.1.4) is the shipped\n * path and the opt-out is discouraged; every key case carries a positive control so a\n * dead listener cannot pass as \"correctly gated\".\n */\ndescribe('RealtimeWhiteboardHostComponent (DOM)', () => {\n type Fix = ReturnType<typeof renderComponentFixture<RealtimeWhiteboardHostComponent>>;\n\n /** The six-tool roster LXP will set: the eleven minus shapes, markdown, widget, image, connector. */\n const LXP_ROSTER: WhiteboardToolRoster = ['select', 'pan', 'pen', 'sticky', 'text', 'eraser'];\n\n const render = (roster: WhiteboardToolRoster = null, state: WhiteboardState = new WhiteboardState()): Fix =>\n renderComponentFixture(RealtimeWhiteboardHostComponent, {\n inputs: roster === null ? { State: state } : { State: state, ToolRoster: roster },\n });\n\n /** Toolbar buttons: the tool palette PLUS the undo and redo buttons, which share `.tool`. */\n const toolButtons = (f: Fix) => Array.from(f.nativeElement.querySelectorAll('button.tool')) as HTMLButtonElement[];\n const toolTitles = (f: Fix) => toolButtons(f).map((b) => b.getAttribute('title'));\n\n /** Focus the host so the #4121-scoped `document:keydown` handler is live. */\n const focusHost = (f: Fix): void => (f.nativeElement as HTMLElement).focus();\n\n const pressKey = (f: Fix, key: string): void => {\n document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));\n f.detectChanges();\n };\n\n const rightClick = (f: Fix, selector: string): void => {\n const el = f.nativeElement.querySelector(selector);\n if (!el) throw new Error(`rightClick(): no element matched \"${selector}\"`);\n el.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: 40, clientY: 40 }));\n f.detectChanges();\n };\n\n const menuLabels = (f: Fix) =>\n Array.from(f.nativeElement.querySelectorAll('.wb-context-menu__item')).map((b) =>\n (b as HTMLElement).textContent?.trim(),\n );\n\n // ── 1. the control: no roster reproduces today's chrome ────────────────────────────\n\n it('renders today toolbar and default tool when no roster is supplied', () => {\n const f = render();\n // eleven tools plus undo and redo\n expect(toolButtons(f).length).toBe(13);\n expect(f.componentInstance.Tool).toBe('select');\n });\n\n // ── 2. the roster narrows the toolbar THROUGH the host ─────────────────────────────\n\n it('narrows the toolbar to the roster, in Tools order (not roster order)', () => {\n const f = render(['eraser', 'select', 'text', 'pen', 'pan', 'sticky']); // deliberately scrambled\n expect(toolTitles(f)).toEqual([\n 'Select / move',\n 'Pan',\n 'Pen',\n 'Sticky note',\n 'Text',\n 'Eraser',\n 'Undo',\n 'Redo',\n ]);\n });\n\n // ── 3. a hidden tool's keyboard shortcut is a no-op ────────────────────────────────\n\n it('ignores the shortcut of a tool outside the roster, and still honors one inside it', () => {\n const f = render(LXP_ROSTER);\n focusHost(f);\n\n pressKey(f, 'w'); // html widget — not in the roster\n expect(f.componentInstance.Tool).toBe('select');\n pressKey(f, 'm'); // markdown — not in the roster\n expect(f.componentInstance.Tool).toBe('select');\n\n // positive control in the same test: the listener IS live, the keys above were gated\n pressKey(f, 'p');\n expect(f.componentInstance.Tool).toBe('pen');\n });\n\n it('leaves every shortcut live when there is no roster', () => {\n const f = render();\n focusHost(f);\n pressKey(f, 'w');\n expect(f.componentInstance.Tool).toBe('html');\n });\n\n // ── 4. the setter clamp ────────────────────────────────────────────────────────────\n\n it('clamps the active tool to the roster first entry when a roster arrives that excludes it', () => {\n const f = render();\n focusHost(f);\n pressKey(f, 'w');\n expect(f.componentInstance.Tool).toBe('html');\n\n f.componentRef.setInput('ToolRoster', ['pan', 'pen']);\n f.detectChanges();\n // the roster's FIRST entry, not 'select' — a roster without 'select' is legal\n expect(f.componentInstance.Tool).toBe('pan');\n });\n\n it('never widens on clamp: dropping the roster leaves the active tool where it was', () => {\n const f = render(['pan', 'pen']);\n expect(f.componentInstance.Tool).toBe('pan'); // clamped on the first change\n\n f.componentRef.setInput('ToolRoster', null);\n f.detectChanges();\n expect(f.componentInstance.Tool).toBe('pan');\n expect(toolButtons(f).length).toBe(13); // the palette is whole again\n });\n\n it('clamps when a host assigns ToolRoster DIRECTLY, not through an Angular binding', () => {\n // The realtime channel wires this host by creating it dynamically and assigning\n // `instance.ToolRoster = ...`. Angular fires NO ngOnChanges for a plain property\n // assignment, so a clamp implemented there never runs on the path real consumers\n // use — verified failing in the browser before the clamp moved into the setter.\n // `setup` runs after construction but BEFORE the first change detection, which is\n // exactly when BindSurface assigns its inputs.\n const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {\n inputs: { State: new WhiteboardState() },\n setup: (instance) => {\n expect(instance.Tool).toBe('select');\n instance.ToolRoster = ['pan', 'pen'];\n },\n });\n\n expect(f.componentInstance.Tool).toBe('pan');\n expect(toolTitles(f)).toEqual(['Pan', 'Pen', 'Undo', 'Redo']);\n });\n\n it('ignores a disallowed WRITE rather than clamping: Escape stays put under a roster without select', () => {\n // Escape assigns 'select'. Under ['pen','eraser'] that is disallowed, and the right answer\n // is to stay on pen, NOT to bounce to the roster fallback.\n const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {\n inputs: { State: new WhiteboardState() },\n setup: (instance) => { instance.ToolRoster = ['pen', 'eraser']; },\n });\n expect(f.componentInstance.Tool).toBe('pen'); // clamped on mount: select is not on the roster\n\n (f.nativeElement as HTMLElement).focus();\n pressKey(f, 'Escape');\n\n expect(f.componentInstance.Tool).toBe('pen');\n });\n\n it('enforces the roster on a direct Tool write, in either assignment order', () => {\n const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {\n inputs: { State: new WhiteboardState() },\n setup: (instance) => {\n instance.ToolRoster = ['pan', 'pen'];\n instance.Tool = 'html'; // disallowed: must be ignored, not applied\n },\n });\n expect(f.componentInstance.Tool).toBe('pan');\n });\n\n it('does not re-clamp when the same roster value is set again', () => {\n // A consumer re-binding an identical array must not fight the user for the active tool.\n const roster: WhiteboardTool[] = ['pan', 'pen'];\n const f = renderComponentFixture(RealtimeWhiteboardHostComponent, {\n inputs: { State: new WhiteboardState() },\n setup: (instance) => { instance.ToolRoster = roster; },\n });\n f.componentInstance.Tool = 'pen';\n expect(f.componentInstance.Tool).toBe('pen');\n\n f.componentRef.setInput('ToolRoster', roster);\n f.detectChanges();\n\n expect(f.componentInstance.Tool).toBe('pen');\n });\n\n // ── 5. the right-click door on the canvas ──────────────────────────────────────────\n\n it('offers every add action plus New page on the canvas menu when there is no roster', () => {\n const f = render();\n rightClick(f, '.board-canvas');\n expect(menuLabels(f)).toEqual([\n 'Add sticky note here',\n 'Add text here',\n 'Add markdown panel here',\n 'Add widget here',\n 'New page',\n ]);\n });\n\n it('offers only the roster add actions on the canvas menu (the #845 hole)', () => {\n const f = render(LXP_ROSTER);\n rightClick(f, '.board-canvas');\n expect(menuLabels(f)).toEqual(['Add sticky note here', 'Add text here', 'New page']);\n });\n\n it('renders no separator above New page when the roster leaves no add actions', () => {\n const f = render([]);\n rightClick(f, '.board-canvas');\n expect(menuLabels(f)).toEqual(['New page']);\n expect(f.nativeElement.querySelectorAll('.wb-context-menu__sep').length).toBe(0);\n });\n\n // ── 6. item menus are deliberately untouched by the roster ─────────────────────────\n\n it('leaves the item menu Edit and Delete actions in place under a narrow roster', () => {\n const state = new WhiteboardState();\n state.AddItem({ Kind: 'sticky', X: 10, Y: 20, Text: 'hello' }, 'user');\n const f = render(LXP_ROSTER, state);\n\n rightClick(f, '.bi.sticky');\n const labels = menuLabels(f);\n // authoring controls are the NEXT input (ShowAuthoringControls), not tool selection\n expect(labels).toContain('Edit');\n expect(labels).toContain('Delete');\n });\n});\n"]}
|