@memberjunction/ng-ui-components 5.51.0 → 6.1.0-edge.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +51 -2
  3. package/dist/lib/bottom-sheet/bottom-sheet.component.d.ts +96 -0
  4. package/dist/lib/bottom-sheet/bottom-sheet.component.d.ts.map +1 -0
  5. package/dist/lib/bottom-sheet/bottom-sheet.component.js +300 -0
  6. package/dist/lib/bottom-sheet/bottom-sheet.component.js.map +1 -0
  7. package/dist/lib/left-nav/left-nav.component.d.ts +142 -3
  8. package/dist/lib/left-nav/left-nav.component.d.ts.map +1 -1
  9. package/dist/lib/left-nav/left-nav.component.js +391 -117
  10. package/dist/lib/left-nav/left-nav.component.js.map +1 -1
  11. package/dist/lib/slide-panel/slide-panel.component.js +6 -6
  12. package/dist/lib/slide-panel/slide-panel.component.js.map +1 -1
  13. package/dist/lib/tabs/tab-chrome-guard.d.ts +14 -0
  14. package/dist/lib/tabs/tab-chrome-guard.d.ts.map +1 -0
  15. package/dist/lib/tabs/tab-chrome-guard.js +36 -0
  16. package/dist/lib/tabs/tab-chrome-guard.js.map +1 -0
  17. package/dist/lib/tabs/tab-list.directive.d.ts +93 -0
  18. package/dist/lib/tabs/tab-list.directive.d.ts.map +1 -0
  19. package/dist/lib/tabs/tab-list.directive.js +202 -0
  20. package/dist/lib/tabs/tab-list.directive.js.map +1 -0
  21. package/dist/lib/tabs/tabs.scss +242 -0
  22. package/dist/lib/workspace-tabs/workspace-card.component.d.ts +63 -0
  23. package/dist/lib/workspace-tabs/workspace-card.component.d.ts.map +1 -0
  24. package/dist/lib/workspace-tabs/workspace-card.component.js +256 -0
  25. package/dist/lib/workspace-tabs/workspace-card.component.js.map +1 -0
  26. package/dist/lib/workspace-tabs/workspace-tab-store.d.ts +47 -0
  27. package/dist/lib/workspace-tabs/workspace-tab-store.d.ts.map +1 -0
  28. package/dist/lib/workspace-tabs/workspace-tab-store.js +112 -0
  29. package/dist/lib/workspace-tabs/workspace-tab-store.js.map +1 -0
  30. package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts +75 -0
  31. package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts.map +1 -0
  32. package/dist/lib/workspace-tabs/workspace-tab-strip.component.js +252 -0
  33. package/dist/lib/workspace-tabs/workspace-tab-strip.component.js.map +1 -0
  34. package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts +40 -0
  35. package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts.map +1 -0
  36. package/dist/lib/workspace-tabs/workspace-tabs.types.js +13 -0
  37. package/dist/lib/workspace-tabs/workspace-tabs.types.js.map +1 -0
  38. package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts +34 -0
  39. package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts.map +1 -0
  40. package/dist/lib/workspace-tabs/workspace-tip.directive.js +123 -0
  41. package/dist/lib/workspace-tabs/workspace-tip.directive.js.map +1 -0
  42. package/dist/public-api.d.ts +8 -0
  43. package/dist/public-api.d.ts.map +1 -1
  44. package/dist/public-api.js +8 -0
  45. package/dist/public-api.js.map +1 -1
  46. package/package.json +14 -12
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2023 MemberJunction
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -26,7 +26,7 @@ Components attach styles one of three ways, and one of them needs host cooperati
26
26
 
27
27
  1. **Inline `styles` in the component** (most components) — nothing to do.
28
28
  2. **`styleUrls` compiled into the component** (`page-header`, `slide-panel`) — nothing to do.
29
- 3. **Global stylesheets shipped as assets** — the form controls and overlay chrome (`button`, `dialog`, `dropdown`, `combobox`, `input`, `datepicker`, `splitter`, `accordion`, `window`, `chip`, `switch`/`progress`) ship their `.scss` to `dist` and expect the **host application** to import them globally.
29
+ 3. **Global stylesheets shipped as assets** — the form controls and overlay chrome (`button`, `dialog`, `dropdown`, `combobox`, `input`, `datepicker`, `splitter`, `accordion`, `window`, `chip`, `switch`/`progress`, `tabs`) ship their `.scss` to `dist` and expect the **host application** to import them globally.
30
30
 
31
31
  **If your app runs inside MJ Explorer (`explorer-app`), category 3 is already handled** — the shell imports all of them. A standalone host must add these to its global stylesheet:
32
32
 
@@ -42,8 +42,11 @@ Components attach styles one of three ways, and one of them needs host cooperati
42
42
  @import '@memberjunction/ng-ui-components/dist/lib/splitter/splitter';
43
43
  @import '@memberjunction/ng-ui-components/dist/lib/accordion/accordion';
44
44
  @import '@memberjunction/ng-ui-components/dist/lib/window/window';
45
+ @import '@memberjunction/ng-ui-components/dist/lib/tabs/tabs';
45
46
  ```
46
47
 
48
+ `tabs` is the shared `.mj-tabs*` chrome consumed by BOTH `mj-workspace-tab-strip` (this package) and `mj-tabstrip` (`@memberjunction/ng-tabstrip`) — without it either strip renders as unstyled divs, so a standalone host using tab strips must include it.
49
+
47
50
  All colors resolve through `--mj-*` semantic tokens, so light/dark theming requires zero component-level work.
48
51
 
49
52
  ## Forms integration
@@ -159,11 +162,48 @@ Inputs: `Padding` (default true; false removes the gutter), `Flex` (default fals
159
162
  The same pair for **sub-pages inside a left-nav shell** — card-like chrome, responsive padding. Header-interior adds `Role`/`AriaLabel` inputs and the same three slots.
160
163
 
161
164
  ### `mj-left-nav` / `mj-left-nav-content` — MJLeftNavComponent
162
- Canonical left rail: sections, flat + tree items (badges, descriptions, disabled), off-canvas drawer at ≤700px. Inputs: `Sections: MJLeftNavSection[]`, `ActiveId`, `ExpandedIds`, `Width` (240), `MobileTitle`. Outputs: `ItemClicked`, `ItemToggled`. Slots: `[header]`, `[footer]`. Pair with `mj-left-nav-content`, which provides `Loading`/`Error` states and hides (not destroys) content while busy.
165
+ Canonical left rail: sections, flat + tree items (badges, descriptions, disabled), off-canvas drawer at ≤700px. Inputs: `Sections: MJLeftNavSection[]`, `ActiveId`, `ExpandedIds`, `Width` (240), `MobileTitle`, `IconOnly` (per-item tooltips + accessible names for an externally-narrowed rail). Outputs: `ItemClicked`, `ItemToggled`. Slots: `[header]`, `[footer]`. Pair with `mj-left-nav-content`, which provides `Loading`/`Error` states and hides (not destroys) content while busy.
166
+
167
+ **Desktop collapse (opt-in):** set `[Collapsible]="true"` and bind `[(Collapsed)]` — the rail renders a locked-position double-angle toggle chip and, when collapsed, becomes a `CollapsedWidth` (60px) icons-only strip: labels visually hidden but kept in the a11y tree, section labels folded to divider lines, badges docked on the icon corner, item tooltips auto-enabled. The component owns presentation only — the consumer owns + persists the state (same split as `ExpandedIds`). Desktop-only; the ≤700px drawer is unaffected. There is deliberately no hover-to-peek.
168
+
169
+ ```html
170
+ <mj-left-nav [Sections]="sections" [ActiveId]="activeId" [Collapsible]="true"
171
+ [(Collapsed)]="railCollapsed" (CollapsedChange)="persist($event)" />
172
+ ```
173
+
174
+ Collapsing a rail whose content is richer than a flat icon list:
175
+
176
+ | Content | Collapsed behavior |
177
+ |---|---|
178
+ | **Tree sections** (`item.children`) | Fold to top-level items only — no chevrons, no indentation, children hidden. A top-level item paints active (`aria-current="true"`) when the active item is one of its descendants, so the rail still shows where you are. `ExpandedIds` is untouched, so the tree returns exactly as the user left it on expand. |
179
+ | **Items with no `icon`** | Render an uppercase monogram from the label, so an item never collapses to a blank-but-clickable button. |
180
+ | **`[header]` / `[footer]` slots** | **Yours to handle.** The rail can't restyle projected content, so a wide header overflows a 60px strip — bind the same collapsed flag in your consumer and project a compact variant. |
163
181
 
164
182
  ### `mj-tab-nav` — MJTabNavComponent
165
183
  Data-driven tab strip. `Tabs: TabConfig[]` (`key`, `label`, `icon?`, `badge?`, `badgeVariant?: 'default'|'error'|'warning'|'success'`), `ActiveKey`, `(TabChange)` emits the key.
166
184
 
185
+ ### `mjTabList` — MJTabListDirective
186
+ The ARIA tabs keyboard contract, shared by every MJ tab strip. Apply to the element holding the tabs; mark each tab `role="tab"` and keep `aria-selected` truthful. Owns `role="tablist"`, the roving `tabindex` (one stop per strip), Arrow/Home/End navigation with focus-follows-selection, Enter/Space activation, and Delete/Backspace close. Emits `(TabActivateRequested)` / `(TabCloseRequested)` with the tab's index in the full list plus the element, so index-addressed and id-addressed hosts both map it onto their own model. Skips `display:none` tabs; leaves keys alone inside editable content.
187
+
188
+ ### `mj-workspace-card` / `mj-workspace-tab-strip` — MJWorkspaceCardComponent, MJWorkspaceTabStripComponent
189
+ The **workspace** pattern: a screen where the user builds several drafts in parallel, each in a browser-style tab (open, switch, drag-reorder via CDK, close, dirty-dot, rejected/complete states). `MJWorkspaceTabStore<TState>` is the pure state machine (open-activates-existing, neighbour activation on close, reorder, lifecycle + dirty tracking — exhaustively unit-tested); `mj-workspace-tab-strip` is dumb presentation over it (renders tabs, emits intent); `mj-workspace-card` is the slotted frame — card surface, tab-strip row, `[workspaceHeader]` identity band, a single scrolling body (default slot), and an opt-in standardized footer (`ShowFooter`: primary confirm + save-as-draft + discard, only the labels vary; `[workspaceFooterNote]` caption slot). Tabs carry an opaque `State` the host owns — no app types in the framework. v1 is session-scoped (no DB persistence).
190
+
191
+ The card is a **query container** (`container-type: size`): size content inside it with `cqh`/`cqw`, never `vh`/`vw` (viewport units break the moment the card sits in a split or a smaller pane).
192
+
193
+ ```html
194
+ <mj-workspace-card [Tabs]="store.Tabs" [ActiveId]="store.ActiveId" NewTabLabel="New entry"
195
+ [ShowFooter]="true" ConfirmLabel="Create entry"
196
+ (TabSelected)="store.Activate($event)" (TabClosed)="onClose($event)"
197
+ (NewTabRequested)="openBlank()" (TabReordered)="store.Reorder($event.previousIndex, $event.currentIndex)"
198
+ (Confirm)="create()" (SaveDraft)="keep()" (Discard)="discard()">
199
+ <div workspaceHeader><!-- per-workshop identity badges --></div>
200
+ <!-- the workshop's form/body -->
201
+ </mj-workspace-card>
202
+ ```
203
+
204
+ ### `mjTip` — MJWorkspaceTipDirective
205
+ Delayed (~450ms still-pointer), non-interactive, token-themed hover tooltip that by default shows only when the host is truncated (`scrollWidth > clientWidth`); `mjTipAlways` forces it for hosts whose truncation can't be measured (a native `<select>`). Used by the workspace tab strip for full labels on capped tabs.
206
+
167
207
  ### `mj-page-search` — MJPageSearchComponent
168
208
  Compact toolbar search input. `Placeholder`, `Value`, `Icon`; `(ValueChange)` on input.
169
209
 
@@ -225,6 +265,15 @@ Floating, **non-modal** panel (no backdrop): `Draggable`, `Resizable` (8 handles
225
265
  ### `mj-filter-popover` — MJFilterPopoverComponent
226
266
  Filter trigger button + CDK popover on desktop, docked bottom-sheet on mobile. Inputs: `Label`, `Icon`, `ActiveCount` (badge), `ShowClearAll`. Output: `ClearAllRequested`. Project your filter UI (dropdowns, chips) as content.
227
267
 
268
+ ### `mj-bottom-sheet` — MJBottomSheetComponent
269
+ Generic mobile bottom sheet: scrim + panel sliding up from the bottom edge with a grab handle. Pure chrome, no breakpoint logic — the caller decides when a sheet is the right presentation. Fully modal: `role="dialog"` + `aria-modal`, focus moves in on open and is **trapped** (Tab/Shift+Tab cycle within), body scroll locks while open, focus returns to the opener on close. Owns enter/exit transitions (class-driven, dismissal animates), Escape, scrim-click close, `prefers-reduced-motion`. Inputs: `[(Visible)]` (two-way), `Title` (optional header), `AriaLabel`. Output: `Closed` (after the exit transition). Sits in the mobile-chrome z band (9998/9999, below `mj-window`); settled open state is `transform: none` so `position: fixed` descendants (dropdowns) aren't displaced.
270
+
271
+ ```html
272
+ <mj-bottom-sheet [(Visible)]="sheetOpen" Title="Open Records">
273
+ <div class="rows">…</div>
274
+ </mj-bottom-sheet>
275
+ ```
276
+
228
277
  ## Patterns
229
278
 
230
279
  ### `mj-empty-state` — MJEmptyStateComponent
@@ -0,0 +1,96 @@
1
+ import { EventEmitter, OnDestroy } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Generic bottom sheet — the canonical mobile overlay surface (scrim + panel
5
+ * sliding up from the bottom edge with a grab handle).
6
+ *
7
+ * Pure chrome, no breakpoint logic: the CALLER decides when a sheet is the
8
+ * right presentation (typically below its mobile breakpoint). Content is
9
+ * projected; the sheet owns the scrim, enter/exit transitions, Escape,
10
+ * focus capture/restore, and the open/close lifecycle.
11
+ *
12
+ * Modality contract (this is a shared primitive — the a11y baseline is the
13
+ * full one, not the historic house minimum): `role="dialog"` +
14
+ * `aria-modal="true"`, focus moves INTO the sheet on open and is trapped
15
+ * there (Tab/Shift+Tab cycle within), body scroll is locked while open, and
16
+ * focus returns to the opener on close.
17
+ *
18
+ * Mechanics notes (learned from the earlier hand-rolled sheets):
19
+ * - Enter AND exit are class-driven `transition`s (the filter-popover sheet's
20
+ * mount-only `animation` cannot animate dismissal).
21
+ * - The settled open state is `transform: none`, NOT `translateY(0)` — any
22
+ * non-none transform makes the sheet a containing block and breaks
23
+ * `position: fixed` descendants (dropdowns/popovers projected into it).
24
+ * - z-index 9998/9999: the established mobile-chrome band (same as the
25
+ * left-nav drawer and filter sheet; below mj-window at 10000, and
26
+ * MJConfirmService dialogs at 20000 still land above).
27
+ *
28
+ * @example
29
+ * ```html
30
+ * <mj-bottom-sheet [(Visible)]="sheetOpen" Title="Open Records">
31
+ * <div>...rows...</div>
32
+ * </mj-bottom-sheet>
33
+ * ```
34
+ */
35
+ export declare class MJBottomSheetComponent implements OnDestroy {
36
+ private cdr;
37
+ private sheetEl?;
38
+ /** Optional header row text; also the default accessible name */
39
+ Title: string;
40
+ /** Accessible name override (falls back to Title) */
41
+ AriaLabel: string;
42
+ set Visible(value: boolean);
43
+ get Visible(): boolean;
44
+ private _visible;
45
+ VisibleChange: EventEmitter<boolean>;
46
+ /** Emitted after the exit transition completes */
47
+ Closed: EventEmitter<void>;
48
+ /** @if gate — outlives Visible through the exit transition */
49
+ IsRendered: boolean;
50
+ /** Drives the enter/exit transition classes */
51
+ IsOpen: boolean;
52
+ /** Open transition finished — transform settles to `none` */
53
+ IsSettled: boolean;
54
+ /** Focus restore target captured at open */
55
+ private previousFocus;
56
+ /** body overflow value before the scroll lock; null = not locked by us */
57
+ private previousBodyOverflow;
58
+ private closeFallbackTimer;
59
+ /** Pending open rAFs — canceled on close so a rapid open→close can't
60
+ * resurrect the open class after the close started */
61
+ private openRafIds;
62
+ /** Settle fallback: under prefers-reduced-motion no transitionend fires,
63
+ * and the sheet must still reach transform:none (containing-block fix) */
64
+ private settleFallbackTimer;
65
+ OnEscape(): void;
66
+ /** Scrim click / programmatic dismiss */
67
+ Close(): void;
68
+ /**
69
+ * Focus trap: aria-modal promises assistive tech the background is inert —
70
+ * Tab must cycle within the sheet, never walk out behind the scrim.
71
+ */
72
+ OnSheetKeydown(event: KeyboardEvent): void;
73
+ OnTransitionEnd(event: TransitionEvent): void;
74
+ private open;
75
+ /**
76
+ * @param emit true when the sheet itself initiated the dismissal (Escape,
77
+ * scrim) — the Visible setter path passes false because the caller's
78
+ * binding is already up to date.
79
+ */
80
+ private close;
81
+ private finishClose;
82
+ /**
83
+ * Body scroll lock while open: aria-modal content must not let the page
84
+ * scroll behind the scrim. Restores the EXACT prior inline value (which is
85
+ * usually '') so a host page's own overflow styling survives.
86
+ */
87
+ private lockBodyScroll;
88
+ private unlockBodyScroll;
89
+ private clearCloseFallback;
90
+ private clearSettleFallback;
91
+ private cancelOpenRafs;
92
+ ngOnDestroy(): void;
93
+ static ɵfac: i0.ɵɵFactoryDeclaration<MJBottomSheetComponent, never>;
94
+ static ɵcmp: i0.ɵɵComponentDeclaration<MJBottomSheetComponent, "mj-bottom-sheet", never, { "Title": { "alias": "Title"; "required": false; }; "AriaLabel": { "alias": "AriaLabel"; "required": false; }; "Visible": { "alias": "Visible"; "required": false; }; }, { "VisibleChange": "VisibleChange"; "Closed": "Closed"; }, never, ["*"], true, never>;
95
+ }
96
+ //# sourceMappingURL=bottom-sheet.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bottom-sheet.component.d.ts","sourceRoot":"","sources":["../../../src/lib/bottom-sheet/bottom-sheet.component.ts"],"names":[],"mappings":"AAAA,OAAO,EACqB,YAAY,EACZ,SAAS,EACpC,MAAM,eAAe,CAAC;;AAEvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAOa,sBAAuB,YAAW,SAAS;IACtD,OAAO,CAAC,GAAG,CAA6B;IAEpB,OAAO,CAAC,OAAO,CAAC,CAA0B;IAE9D,iEAAiE;IACxD,KAAK,SAAM;IACpB,qDAAqD;IAC5C,SAAS,SAAM;IAExB,IACI,OAAO,CAAC,KAAK,EAAE,OAAO,EAUzB;IACD,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,OAAO,CAAC,QAAQ,CAAS;IAEf,aAAa,wBAA+B;IACtD,kDAAkD;IACxC,MAAM,qBAA4B;IAE5C,8DAA8D;IACvD,UAAU,UAAS;IAC1B,+CAA+C;IACxC,MAAM,UAAS;IACtB,6DAA6D;IACtD,SAAS,UAAS;IAEzB,4CAA4C;IAC5C,OAAO,CAAC,aAAa,CAA4B;IACjD,0EAA0E;IAC1E,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,kBAAkB,CAA8C;IACxE;2DACuD;IACvD,OAAO,CAAC,UAAU,CAAgB;IAClC;+EAC2E;IAC3E,OAAO,CAAC,mBAAmB,CAA8C;IAGlE,QAAQ,IAAI,IAAI;IAMvB,yCAAyC;IAClC,KAAK,IAAI,IAAI;IAMpB;;;OAGG;IACI,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IA0B1C,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAapD,OAAO,CAAC,IAAI;IA8BZ;;;;OAIG;IACH,OAAO,CAAC,KAAK;IAgBb,OAAO,CAAC,WAAW;IAanB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAKtB,WAAW,IAAI,IAAI;yCAjNR,sBAAsB;2CAAtB,sBAAsB;CAyNlC"}
@@ -0,0 +1,300 @@
1
+ import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy, HostListener, ViewChild, inject } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ const _c0 = ["sheet"];
4
+ const _c1 = ["*"];
5
+ function MJBottomSheetComponent_Conditional_0_Conditional_4_Template(rf, ctx) { if (rf & 1) {
6
+ i0.ɵɵdomElementStart(0, "div", 4);
7
+ i0.ɵɵtext(1);
8
+ i0.ɵɵdomElementEnd();
9
+ } if (rf & 2) {
10
+ const ctx_r1 = i0.ɵɵnextContext(2);
11
+ i0.ɵɵadvance();
12
+ i0.ɵɵtextInterpolate(ctx_r1.Title);
13
+ } }
14
+ function MJBottomSheetComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
15
+ const _r1 = i0.ɵɵgetCurrentView();
16
+ i0.ɵɵdomElementStart(0, "div", 1);
17
+ i0.ɵɵdomListener("click", function MJBottomSheetComponent_Conditional_0_Template_div_click_0_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.Close()); });
18
+ i0.ɵɵdomElementEnd();
19
+ i0.ɵɵdomElementStart(1, "div", 2, 0);
20
+ i0.ɵɵdomListener("keydown", function MJBottomSheetComponent_Conditional_0_Template_div_keydown_1_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnSheetKeydown($event)); })("transitionend", function MJBottomSheetComponent_Conditional_0_Template_div_transitionend_1_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnTransitionEnd($event)); });
21
+ i0.ɵɵdomElement(3, "div", 3);
22
+ i0.ɵɵconditionalCreate(4, MJBottomSheetComponent_Conditional_0_Conditional_4_Template, 2, 1, "div", 4);
23
+ i0.ɵɵdomElementStart(5, "div", 5);
24
+ i0.ɵɵprojection(6);
25
+ i0.ɵɵdomElementEnd()();
26
+ } if (rf & 2) {
27
+ const ctx_r1 = i0.ɵɵnextContext();
28
+ i0.ɵɵclassProp("mj-bottom-sheet-scrim--open", ctx_r1.IsOpen);
29
+ i0.ɵɵadvance();
30
+ i0.ɵɵclassProp("mj-bottom-sheet--open", ctx_r1.IsOpen)("mj-bottom-sheet--settled", ctx_r1.IsSettled);
31
+ i0.ɵɵattribute("aria-label", ctx_r1.AriaLabel || ctx_r1.Title || null);
32
+ i0.ɵɵadvance(3);
33
+ i0.ɵɵconditional(ctx_r1.Title ? 4 : -1);
34
+ } }
35
+ /**
36
+ * Generic bottom sheet — the canonical mobile overlay surface (scrim + panel
37
+ * sliding up from the bottom edge with a grab handle).
38
+ *
39
+ * Pure chrome, no breakpoint logic: the CALLER decides when a sheet is the
40
+ * right presentation (typically below its mobile breakpoint). Content is
41
+ * projected; the sheet owns the scrim, enter/exit transitions, Escape,
42
+ * focus capture/restore, and the open/close lifecycle.
43
+ *
44
+ * Modality contract (this is a shared primitive — the a11y baseline is the
45
+ * full one, not the historic house minimum): `role="dialog"` +
46
+ * `aria-modal="true"`, focus moves INTO the sheet on open and is trapped
47
+ * there (Tab/Shift+Tab cycle within), body scroll is locked while open, and
48
+ * focus returns to the opener on close.
49
+ *
50
+ * Mechanics notes (learned from the earlier hand-rolled sheets):
51
+ * - Enter AND exit are class-driven `transition`s (the filter-popover sheet's
52
+ * mount-only `animation` cannot animate dismissal).
53
+ * - The settled open state is `transform: none`, NOT `translateY(0)` — any
54
+ * non-none transform makes the sheet a containing block and breaks
55
+ * `position: fixed` descendants (dropdowns/popovers projected into it).
56
+ * - z-index 9998/9999: the established mobile-chrome band (same as the
57
+ * left-nav drawer and filter sheet; below mj-window at 10000, and
58
+ * MJConfirmService dialogs at 20000 still land above).
59
+ *
60
+ * @example
61
+ * ```html
62
+ * <mj-bottom-sheet [(Visible)]="sheetOpen" Title="Open Records">
63
+ * <div>...rows...</div>
64
+ * </mj-bottom-sheet>
65
+ * ```
66
+ */
67
+ export class MJBottomSheetComponent {
68
+ cdr = inject(ChangeDetectorRef);
69
+ sheetEl;
70
+ /** Optional header row text; also the default accessible name */
71
+ Title = '';
72
+ /** Accessible name override (falls back to Title) */
73
+ AriaLabel = '';
74
+ set Visible(value) {
75
+ if (value === this._visible) {
76
+ return;
77
+ }
78
+ this._visible = value;
79
+ if (value) {
80
+ this.open();
81
+ }
82
+ else {
83
+ this.close(false);
84
+ }
85
+ }
86
+ get Visible() {
87
+ return this._visible;
88
+ }
89
+ _visible = false;
90
+ VisibleChange = new EventEmitter();
91
+ /** Emitted after the exit transition completes */
92
+ Closed = new EventEmitter();
93
+ /** @if gate — outlives Visible through the exit transition */
94
+ IsRendered = false;
95
+ /** Drives the enter/exit transition classes */
96
+ IsOpen = false;
97
+ /** Open transition finished — transform settles to `none` */
98
+ IsSettled = false;
99
+ /** Focus restore target captured at open */
100
+ previousFocus = null;
101
+ /** body overflow value before the scroll lock; null = not locked by us */
102
+ previousBodyOverflow = null;
103
+ closeFallbackTimer = null;
104
+ /** Pending open rAFs — canceled on close so a rapid open→close can't
105
+ * resurrect the open class after the close started */
106
+ openRafIds = [];
107
+ /** Settle fallback: under prefers-reduced-motion no transitionend fires,
108
+ * and the sheet must still reach transform:none (containing-block fix) */
109
+ settleFallbackTimer = null;
110
+ OnEscape() {
111
+ if (this.IsOpen) {
112
+ this.close(true);
113
+ }
114
+ }
115
+ /** Scrim click / programmatic dismiss */
116
+ Close() {
117
+ if (this.IsOpen) {
118
+ this.close(true);
119
+ }
120
+ }
121
+ /**
122
+ * Focus trap: aria-modal promises assistive tech the background is inert —
123
+ * Tab must cycle within the sheet, never walk out behind the scrim.
124
+ */
125
+ OnSheetKeydown(event) {
126
+ if (event.key !== 'Tab' || !this.sheetEl) {
127
+ return;
128
+ }
129
+ const sheet = this.sheetEl.nativeElement;
130
+ const focusables = Array.from(sheet.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'));
131
+ if (focusables.length === 0) {
132
+ event.preventDefault(); // nothing to move to — keep focus on the sheet
133
+ return;
134
+ }
135
+ const first = focusables[0];
136
+ const last = focusables[focusables.length - 1];
137
+ const active = document.activeElement;
138
+ if (event.shiftKey) {
139
+ if (active === first || active === sheet) {
140
+ event.preventDefault();
141
+ last.focus();
142
+ }
143
+ }
144
+ else if (active === last || active === sheet) {
145
+ event.preventDefault();
146
+ first.focus();
147
+ }
148
+ }
149
+ OnTransitionEnd(event) {
150
+ if (event.target !== this.sheetEl?.nativeElement || event.propertyName !== 'transform') {
151
+ return;
152
+ }
153
+ if (this.IsOpen) {
154
+ // Settled: transform → none so fixed-position descendants aren't trapped
155
+ this.IsSettled = true;
156
+ this.cdr.markForCheck();
157
+ }
158
+ else if (this.IsRendered) {
159
+ this.finishClose();
160
+ }
161
+ }
162
+ open() {
163
+ this.clearCloseFallback();
164
+ this.previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
165
+ this.lockBodyScroll();
166
+ this.IsRendered = true;
167
+ this.cdr.markForCheck();
168
+ // Two frames: one for the @if to render at translateY(100%), one so the
169
+ // class flip actually transitions (same-frame flips snap instead)
170
+ this.openRafIds.push(requestAnimationFrame(() => {
171
+ this.openRafIds.push(requestAnimationFrame(() => {
172
+ this.openRafIds = [];
173
+ if (!this._visible) {
174
+ return; // closed again before the frames ran — stay closed
175
+ }
176
+ this.IsOpen = true;
177
+ this.cdr.markForCheck();
178
+ this.sheetEl?.nativeElement?.focus();
179
+ // Reduced-motion (transition:none) never fires transitionend — the
180
+ // fallback still settles the transform to none
181
+ this.clearSettleFallback();
182
+ this.settleFallbackTimer = setTimeout(() => {
183
+ if (this.IsOpen && !this.IsSettled) {
184
+ this.IsSettled = true;
185
+ this.cdr.markForCheck();
186
+ }
187
+ }, 300);
188
+ }));
189
+ }));
190
+ }
191
+ /**
192
+ * @param emit true when the sheet itself initiated the dismissal (Escape,
193
+ * scrim) — the Visible setter path passes false because the caller's
194
+ * binding is already up to date.
195
+ */
196
+ close(emit) {
197
+ this._visible = false;
198
+ this.cancelOpenRafs();
199
+ this.clearSettleFallback();
200
+ this.IsOpen = false;
201
+ this.IsSettled = false;
202
+ this.cdr.markForCheck();
203
+ if (emit) {
204
+ this.VisibleChange.emit(false);
205
+ }
206
+ // transitionend normally finishes the close; the timer covers
207
+ // prefers-reduced-motion (no transition event) and detached nodes
208
+ this.clearCloseFallback();
209
+ this.closeFallbackTimer = setTimeout(() => this.finishClose(), 300);
210
+ }
211
+ finishClose() {
212
+ this.clearCloseFallback();
213
+ if (!this.IsRendered) {
214
+ return;
215
+ }
216
+ this.IsRendered = false;
217
+ this.unlockBodyScroll();
218
+ this.cdr.markForCheck();
219
+ this.Closed.emit();
220
+ this.previousFocus?.focus();
221
+ this.previousFocus = null;
222
+ }
223
+ /**
224
+ * Body scroll lock while open: aria-modal content must not let the page
225
+ * scroll behind the scrim. Restores the EXACT prior inline value (which is
226
+ * usually '') so a host page's own overflow styling survives.
227
+ */
228
+ lockBodyScroll() {
229
+ if (this.previousBodyOverflow === null) {
230
+ this.previousBodyOverflow = document.body.style.overflow;
231
+ document.body.style.overflow = 'hidden';
232
+ }
233
+ }
234
+ unlockBodyScroll() {
235
+ if (this.previousBodyOverflow !== null) {
236
+ document.body.style.overflow = this.previousBodyOverflow;
237
+ this.previousBodyOverflow = null;
238
+ }
239
+ }
240
+ clearCloseFallback() {
241
+ if (this.closeFallbackTimer !== null) {
242
+ clearTimeout(this.closeFallbackTimer);
243
+ this.closeFallbackTimer = null;
244
+ }
245
+ }
246
+ clearSettleFallback() {
247
+ if (this.settleFallbackTimer !== null) {
248
+ clearTimeout(this.settleFallbackTimer);
249
+ this.settleFallbackTimer = null;
250
+ }
251
+ }
252
+ cancelOpenRafs() {
253
+ this.openRafIds.forEach(id => cancelAnimationFrame(id));
254
+ this.openRafIds = [];
255
+ }
256
+ ngOnDestroy() {
257
+ this.clearCloseFallback();
258
+ this.clearSettleFallback();
259
+ this.cancelOpenRafs();
260
+ // Destroyed while open (route change, parent teardown): never leave the
261
+ // page unscrollable
262
+ this.unlockBodyScroll();
263
+ }
264
+ static ɵfac = function MJBottomSheetComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MJBottomSheetComponent)(); };
265
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MJBottomSheetComponent, selectors: [["mj-bottom-sheet"]], viewQuery: function MJBottomSheetComponent_Query(rf, ctx) { if (rf & 1) {
266
+ i0.ɵɵviewQuery(_c0, 5);
267
+ } if (rf & 2) {
268
+ let _t;
269
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.sheetEl = _t.first);
270
+ } }, hostBindings: function MJBottomSheetComponent_HostBindings(rf, ctx) { if (rf & 1) {
271
+ i0.ɵɵlistener("keydown.escape", function MJBottomSheetComponent_keydown_escape_HostBindingHandler() { return ctx.OnEscape(); }, i0.ɵɵresolveDocument);
272
+ } }, inputs: { Title: "Title", AriaLabel: "AriaLabel", Visible: "Visible" }, outputs: { VisibleChange: "VisibleChange", Closed: "Closed" }, ngContentSelectors: _c1, decls: 1, vars: 1, consts: [["sheet", ""], ["aria-hidden", "true", 1, "mj-bottom-sheet-scrim", 3, "click"], ["role", "dialog", "aria-modal", "true", "tabindex", "-1", 1, "mj-bottom-sheet", 3, "keydown", "transitionend"], ["aria-hidden", "true", 1, "mj-bottom-sheet-grab"], [1, "mj-bottom-sheet-header"], [1, "mj-bottom-sheet-content"]], template: function MJBottomSheetComponent_Template(rf, ctx) { if (rf & 1) {
273
+ i0.ɵɵprojectionDef();
274
+ i0.ɵɵconditionalCreate(0, MJBottomSheetComponent_Conditional_0_Template, 7, 8);
275
+ } if (rf & 2) {
276
+ i0.ɵɵconditional(ctx.IsRendered ? 0 : -1);
277
+ } }, styles: ["\n\n\n\n\n.mj-bottom-sheet-scrim[_ngcontent-%COMP%] {\n position: fixed;\n inset: 0;\n z-index: 9998;\n background: var(--mj-bg-overlay);\n opacity: 0;\n transition: opacity 0.24s ease;\n}\n\n.mj-bottom-sheet-scrim--open[_ngcontent-%COMP%] {\n opacity: 1;\n}\n\n.mj-bottom-sheet[_ngcontent-%COMP%] {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n max-height: 85vh;\n background: var(--mj-bg-surface);\n border-top-left-radius: 16px;\n border-top-right-radius: 16px;\n box-shadow: var(--mj-shadow-lg);\n transform: translateY(100%);\n transition: transform 0.24s ease;\n outline: none;\n}\n\n.mj-bottom-sheet--open[_ngcontent-%COMP%] {\n transform: translateY(0);\n}\n\n\n\n\n\n.mj-bottom-sheet--settled[_ngcontent-%COMP%] {\n transform: none;\n}\n\n.mj-bottom-sheet-grab[_ngcontent-%COMP%] {\n width: 36px;\n height: 4px;\n border-radius: 2px;\n background: var(--mj-border-strong);\n margin: 10px auto 2px;\n flex-shrink: 0;\n}\n\n.mj-bottom-sheet-header[_ngcontent-%COMP%] {\n flex-shrink: 0;\n padding: 8px 16px 10px;\n font-size: var(--mj-text-base, 15px);\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.mj-bottom-sheet-content[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n padding-bottom: var(--mj-space-6);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .mj-bottom-sheet-scrim[_ngcontent-%COMP%], \n .mj-bottom-sheet[_ngcontent-%COMP%] {\n transition: none;\n }\n}"], changeDetection: 0 });
278
+ }
279
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJBottomSheetComponent, [{
280
+ type: Component,
281
+ args: [{ standalone: true, selector: 'mj-bottom-sheet', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (IsRendered) {\n <div\n class=\"mj-bottom-sheet-scrim\"\n [class.mj-bottom-sheet-scrim--open]=\"IsOpen\"\n (click)=\"Close()\"\n aria-hidden=\"true\"></div>\n <div\n #sheet\n class=\"mj-bottom-sheet\"\n [class.mj-bottom-sheet--open]=\"IsOpen\"\n [class.mj-bottom-sheet--settled]=\"IsSettled\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"AriaLabel || Title || null\"\n tabindex=\"-1\"\n (keydown)=\"OnSheetKeydown($event)\"\n (transitionend)=\"OnTransitionEnd($event)\">\n <div class=\"mj-bottom-sheet-grab\" aria-hidden=\"true\"></div>\n @if (Title) {\n <div class=\"mj-bottom-sheet-header\">{{ Title }}</div>\n }\n <div class=\"mj-bottom-sheet-content\">\n <ng-content></ng-content>\n </div>\n </div>\n}\n", styles: ["/* z-index 9998/9999: the mobile-chrome overlay band (left-nav drawer,\n filter sheet). Below mj-window (10000); MJConfirmService (20000) and\n toasts (100000) still land above a sheet. */\n\n.mj-bottom-sheet-scrim {\n position: fixed;\n inset: 0;\n z-index: 9998;\n background: var(--mj-bg-overlay);\n opacity: 0;\n transition: opacity 0.24s ease;\n}\n\n.mj-bottom-sheet-scrim--open {\n opacity: 1;\n}\n\n.mj-bottom-sheet {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n max-height: 85vh;\n background: var(--mj-bg-surface);\n border-top-left-radius: 16px;\n border-top-right-radius: 16px;\n box-shadow: var(--mj-shadow-lg);\n transform: translateY(100%);\n transition: transform 0.24s ease;\n outline: none;\n}\n\n.mj-bottom-sheet--open {\n transform: translateY(0);\n}\n\n/* Settled open state: transform must be NONE, not translateY(0) \u2014 a non-none\n transform creates a containing block that displaces position:fixed\n descendants (dropdowns/popovers projected into the sheet). */\n.mj-bottom-sheet--settled {\n transform: none;\n}\n\n.mj-bottom-sheet-grab {\n width: 36px;\n height: 4px;\n border-radius: 2px;\n background: var(--mj-border-strong);\n margin: 10px auto 2px;\n flex-shrink: 0;\n}\n\n.mj-bottom-sheet-header {\n flex-shrink: 0;\n padding: 8px 16px 10px;\n font-size: var(--mj-text-base, 15px);\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.mj-bottom-sheet-content {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n padding-bottom: var(--mj-space-6);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .mj-bottom-sheet-scrim,\n .mj-bottom-sheet {\n transition: none;\n }\n}\n"] }]
282
+ }], null, { sheetEl: [{
283
+ type: ViewChild,
284
+ args: ['sheet']
285
+ }], Title: [{
286
+ type: Input
287
+ }], AriaLabel: [{
288
+ type: Input
289
+ }], Visible: [{
290
+ type: Input
291
+ }], VisibleChange: [{
292
+ type: Output
293
+ }], Closed: [{
294
+ type: Output
295
+ }], OnEscape: [{
296
+ type: HostListener,
297
+ args: ['document:keydown.escape']
298
+ }] }); })();
299
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MJBottomSheetComponent, { className: "MJBottomSheetComponent", filePath: "lib/bottom-sheet/bottom-sheet.component.ts", lineNumber: 45 }); })();
300
+ //# sourceMappingURL=bottom-sheet.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bottom-sheet.component.js","sourceRoot":"","sources":["../../../src/lib/bottom-sheet/bottom-sheet.component.ts","../../../src/lib/bottom-sheet/bottom-sheet.component.html"],"names":[],"mappings":"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","sourcesContent":["import {\n Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy,\n ElementRef, HostListener, OnDestroy, ViewChild, inject\n} from '@angular/core';\n\n/**\n * Generic bottom sheet — the canonical mobile overlay surface (scrim + panel\n * sliding up from the bottom edge with a grab handle).\n *\n * Pure chrome, no breakpoint logic: the CALLER decides when a sheet is the\n * right presentation (typically below its mobile breakpoint). Content is\n * projected; the sheet owns the scrim, enter/exit transitions, Escape,\n * focus capture/restore, and the open/close lifecycle.\n *\n * Modality contract (this is a shared primitive — the a11y baseline is the\n * full one, not the historic house minimum): `role=\"dialog\"` +\n * `aria-modal=\"true\"`, focus moves INTO the sheet on open and is trapped\n * there (Tab/Shift+Tab cycle within), body scroll is locked while open, and\n * focus returns to the opener on close.\n *\n * Mechanics notes (learned from the earlier hand-rolled sheets):\n * - Enter AND exit are class-driven `transition`s (the filter-popover sheet's\n * mount-only `animation` cannot animate dismissal).\n * - The settled open state is `transform: none`, NOT `translateY(0)` — any\n * non-none transform makes the sheet a containing block and breaks\n * `position: fixed` descendants (dropdowns/popovers projected into it).\n * - z-index 9998/9999: the established mobile-chrome band (same as the\n * left-nav drawer and filter sheet; below mj-window at 10000, and\n * MJConfirmService dialogs at 20000 still land above).\n *\n * @example\n * ```html\n * <mj-bottom-sheet [(Visible)]=\"sheetOpen\" Title=\"Open Records\">\n * <div>...rows...</div>\n * </mj-bottom-sheet>\n * ```\n */\n@Component({\n standalone: true,\n selector: 'mj-bottom-sheet',\n templateUrl: './bottom-sheet.component.html',\n styleUrls: ['./bottom-sheet.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class MJBottomSheetComponent implements OnDestroy {\n private cdr = inject(ChangeDetectorRef);\n\n @ViewChild('sheet') private sheetEl?: ElementRef<HTMLElement>;\n\n /** Optional header row text; also the default accessible name */\n @Input() Title = '';\n /** Accessible name override (falls back to Title) */\n @Input() AriaLabel = '';\n\n @Input()\n set Visible(value: boolean) {\n if (value === this._visible) {\n return;\n }\n this._visible = value;\n if (value) {\n this.open();\n } else {\n this.close(false);\n }\n }\n get Visible(): boolean {\n return this._visible;\n }\n private _visible = false;\n\n @Output() VisibleChange = new EventEmitter<boolean>();\n /** Emitted after the exit transition completes */\n @Output() Closed = new EventEmitter<void>();\n\n /** @if gate — outlives Visible through the exit transition */\n public IsRendered = false;\n /** Drives the enter/exit transition classes */\n public IsOpen = false;\n /** Open transition finished — transform settles to `none` */\n public IsSettled = false;\n\n /** Focus restore target captured at open */\n private previousFocus: HTMLElement | null = null;\n /** body overflow value before the scroll lock; null = not locked by us */\n private previousBodyOverflow: string | null = null;\n private closeFallbackTimer: ReturnType<typeof setTimeout> | null = null;\n /** Pending open rAFs — canceled on close so a rapid open→close can't\n * resurrect the open class after the close started */\n private openRafIds: number[] = [];\n /** Settle fallback: under prefers-reduced-motion no transitionend fires,\n * and the sheet must still reach transform:none (containing-block fix) */\n private settleFallbackTimer: ReturnType<typeof setTimeout> | null = null;\n\n @HostListener('document:keydown.escape')\n public OnEscape(): void {\n if (this.IsOpen) {\n this.close(true);\n }\n }\n\n /** Scrim click / programmatic dismiss */\n public Close(): void {\n if (this.IsOpen) {\n this.close(true);\n }\n }\n\n /**\n * Focus trap: aria-modal promises assistive tech the background is inert —\n * Tab must cycle within the sheet, never walk out behind the scrim.\n */\n public OnSheetKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Tab' || !this.sheetEl) {\n return;\n }\n const sheet = this.sheetEl.nativeElement;\n const focusables = Array.from(sheet.querySelectorAll<HTMLElement>(\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n ));\n if (focusables.length === 0) {\n event.preventDefault(); // nothing to move to — keep focus on the sheet\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (event.shiftKey) {\n if (active === first || active === sheet) {\n event.preventDefault();\n last.focus();\n }\n } else if (active === last || active === sheet) {\n event.preventDefault();\n first.focus();\n }\n }\n\n public OnTransitionEnd(event: TransitionEvent): void {\n if (event.target !== this.sheetEl?.nativeElement || event.propertyName !== 'transform') {\n return;\n }\n if (this.IsOpen) {\n // Settled: transform → none so fixed-position descendants aren't trapped\n this.IsSettled = true;\n this.cdr.markForCheck();\n } else if (this.IsRendered) {\n this.finishClose();\n }\n }\n\n private open(): void {\n this.clearCloseFallback();\n this.previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n this.lockBodyScroll();\n this.IsRendered = true;\n this.cdr.markForCheck();\n // Two frames: one for the @if to render at translateY(100%), one so the\n // class flip actually transitions (same-frame flips snap instead)\n this.openRafIds.push(requestAnimationFrame(() => {\n this.openRafIds.push(requestAnimationFrame(() => {\n this.openRafIds = [];\n if (!this._visible) {\n return; // closed again before the frames ran — stay closed\n }\n this.IsOpen = true;\n this.cdr.markForCheck();\n this.sheetEl?.nativeElement?.focus();\n // Reduced-motion (transition:none) never fires transitionend — the\n // fallback still settles the transform to none\n this.clearSettleFallback();\n this.settleFallbackTimer = setTimeout(() => {\n if (this.IsOpen && !this.IsSettled) {\n this.IsSettled = true;\n this.cdr.markForCheck();\n }\n }, 300);\n }));\n }));\n }\n\n /**\n * @param emit true when the sheet itself initiated the dismissal (Escape,\n * scrim) — the Visible setter path passes false because the caller's\n * binding is already up to date.\n */\n private close(emit: boolean): void {\n this._visible = false;\n this.cancelOpenRafs();\n this.clearSettleFallback();\n this.IsOpen = false;\n this.IsSettled = false;\n this.cdr.markForCheck();\n if (emit) {\n this.VisibleChange.emit(false);\n }\n // transitionend normally finishes the close; the timer covers\n // prefers-reduced-motion (no transition event) and detached nodes\n this.clearCloseFallback();\n this.closeFallbackTimer = setTimeout(() => this.finishClose(), 300);\n }\n\n private finishClose(): void {\n this.clearCloseFallback();\n if (!this.IsRendered) {\n return;\n }\n this.IsRendered = false;\n this.unlockBodyScroll();\n this.cdr.markForCheck();\n this.Closed.emit();\n this.previousFocus?.focus();\n this.previousFocus = null;\n }\n\n /**\n * Body scroll lock while open: aria-modal content must not let the page\n * scroll behind the scrim. Restores the EXACT prior inline value (which is\n * usually '') so a host page's own overflow styling survives.\n */\n private lockBodyScroll(): void {\n if (this.previousBodyOverflow === null) {\n this.previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n }\n\n private unlockBodyScroll(): void {\n if (this.previousBodyOverflow !== null) {\n document.body.style.overflow = this.previousBodyOverflow;\n this.previousBodyOverflow = null;\n }\n }\n\n private clearCloseFallback(): void {\n if (this.closeFallbackTimer !== null) {\n clearTimeout(this.closeFallbackTimer);\n this.closeFallbackTimer = null;\n }\n }\n\n private clearSettleFallback(): void {\n if (this.settleFallbackTimer !== null) {\n clearTimeout(this.settleFallbackTimer);\n this.settleFallbackTimer = null;\n }\n }\n\n private cancelOpenRafs(): void {\n this.openRafIds.forEach(id => cancelAnimationFrame(id));\n this.openRafIds = [];\n }\n\n ngOnDestroy(): void {\n this.clearCloseFallback();\n this.clearSettleFallback();\n this.cancelOpenRafs();\n // Destroyed while open (route change, parent teardown): never leave the\n // page unscrollable\n this.unlockBodyScroll();\n }\n}\n","@if (IsRendered) {\n <div\n class=\"mj-bottom-sheet-scrim\"\n [class.mj-bottom-sheet-scrim--open]=\"IsOpen\"\n (click)=\"Close()\"\n aria-hidden=\"true\"></div>\n <div\n #sheet\n class=\"mj-bottom-sheet\"\n [class.mj-bottom-sheet--open]=\"IsOpen\"\n [class.mj-bottom-sheet--settled]=\"IsSettled\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"AriaLabel || Title || null\"\n tabindex=\"-1\"\n (keydown)=\"OnSheetKeydown($event)\"\n (transitionend)=\"OnTransitionEnd($event)\">\n <div class=\"mj-bottom-sheet-grab\" aria-hidden=\"true\"></div>\n @if (Title) {\n <div class=\"mj-bottom-sheet-header\">{{ Title }}</div>\n }\n <div class=\"mj-bottom-sheet-content\">\n <ng-content></ng-content>\n </div>\n </div>\n}\n"]}