@brickclay-org/ui 0.1.70 → 0.1.71

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/index.d.ts CHANGED
@@ -239,7 +239,28 @@ declare class BkCustomCalendar implements OnInit, OnDestroy, OnChanges, ControlV
239
239
  /** Format to "H:MM AM/PM" string. */
240
240
  private formatTimeToAmPm;
241
241
  onClickOutside(event: MouseEvent): void;
242
- onWindowEvents(): void;
242
+ /** True while capture-phase scroll + resize listeners are attached (only while the popup is open). */
243
+ private viewportListenersAttached;
244
+ /** Pending rAF id so bursts of scroll events collapse into one layout pass. */
245
+ private viewportRafId;
246
+ /**
247
+ * Recipe steps 2 & 4: while the popup is open, keep it glued to the trigger as the page
248
+ * (or any nested scroll container) scrolls, and dismiss it once the trigger leaves the viewport.
249
+ * Throttled through requestAnimationFrame to avoid layout thrash during fast scrolling.
250
+ */
251
+ private readonly onViewportChange;
252
+ private handleViewportChange;
253
+ /**
254
+ * Trigger is no longer visible and the fixed popup should be dismissed. True when the trigger is
255
+ * either entirely outside the viewport, or clipped out of ANY ancestor scroll container — e.g.
256
+ * scrolled behind the header/footer of a modal body. This makes step 4 work both on the plain
257
+ * page and inside a dialog when `appendToBody` positions the popup with `fixed`.
258
+ */
259
+ private isTriggerOutOfView;
260
+ /** An ancestor that clips overflowing content on an axis where its content actually overflows. */
261
+ private isScrollClippingContainer;
262
+ private attachViewportListeners;
263
+ private detachViewportListeners;
243
264
  ngOnInit(): void;
244
265
  ngOnChanges(changes: SimpleChanges): void;
245
266
  private regenerateCalendarsForWeekStart;
@@ -249,6 +270,18 @@ declare class BkCustomCalendar implements OnInit, OnDestroy, OnChanges, ControlV
249
270
  toggle(): void;
250
271
  /** Update popup position when appendToBody is true (fixed positioning relative to viewport). */
251
272
  updatePosition(): void;
273
+ /**
274
+ * Horizontal placement for the fixed (appendToBody) popup.
275
+ *
276
+ * The base edge follows the author's {@link opens} preference:
277
+ * • 'left' → align left edges, panel opens rightwards (default)
278
+ * • 'right' → align right edges, panel opens leftwards
279
+ * • 'center' → centred on the trigger
280
+ * When {@link autoPosition} is on, the panel auto-flips to the opposite edge if the preferred
281
+ * side would overflow the viewport, then clamps to an 8px margin so it can never leave the screen.
282
+ * When autoPosition is off, the explicit `opens` value is honoured as-is.
283
+ */
284
+ private computePopupLeft;
252
285
  /** Non–append-to-body: set `popupPlacementAbove` for CSS `drop-up` with viewport flip. */
253
286
  refreshPopupPlacement(): void;
254
287
  private computePlacementAbove;
@@ -422,7 +455,7 @@ declare class BkScheduledDatePicker implements OnInit {
422
455
  static ɵcmp: i0.ɵɵComponentDeclaration<BkScheduledDatePicker, "bk-scheduled-date-picker", never, { "timeFormat": { "alias": "timeFormat"; "required": false; }; "enableSeconds": { "alias": "enableSeconds"; "required": false; }; }, { "scheduled": "scheduled"; "cleared": "cleared"; }, never, never, true, never>;
423
456
  }
424
457
 
425
- declare class BkTimePicker implements OnInit, OnChanges, AfterViewInit, ControlValueAccessor {
458
+ declare class BkTimePicker implements OnInit, OnChanges, OnDestroy, AfterViewInit, ControlValueAccessor {
426
459
  required: boolean;
427
460
  /** @deprecated Prefer [(ngModel)] */
428
461
  value: string | null;
@@ -435,6 +468,11 @@ declare class BkTimePicker implements OnInit, OnChanges, AfterViewInit, ControlV
435
468
  closePicker: number;
436
469
  timeFormat: 12 | 24;
437
470
  showSeconds: boolean;
471
+ /** When true, auto-flip the dropdown above/below and left/right based on available viewport space. */
472
+ autoPosition: boolean;
473
+ /** When true, position the dropdown with `fixed` so it escapes ancestor overflow (dialogs, scroll containers),
474
+ * follows the trigger on scroll, and closes when the trigger is scrolled out of view. */
475
+ appendToBody: boolean;
438
476
  change: EventEmitter<string | null>;
439
477
  timeChange: EventEmitter<string | null>;
440
478
  pickerOpened: EventEmitter<string>;
@@ -442,20 +480,35 @@ declare class BkTimePicker implements OnInit, OnChanges, AfterViewInit, ControlV
442
480
  private onChange;
443
481
  private onTouched;
444
482
  disabled: boolean;
445
- timeScrollElements: QueryList<ElementRef>;
446
- minuteScrollElements: QueryList<ElementRef>;
447
- secondScrollElements: QueryList<ElementRef>;
448
- private readonly WHEEL_CYCLE;
483
+ hourScrollEl?: ElementRef<HTMLElement>;
484
+ minuteScrollEl?: ElementRef<HTMLElement>;
485
+ secondScrollEl?: ElementRef<HTMLElement>;
486
+ /** The positioned trigger (its rect anchors the dropdown). */
487
+ tpWrapper?: ElementRef<HTMLElement>;
488
+ /** The dropdown panel element (measured for flip decisions). */
489
+ tpDropdown?: ElementRef<HTMLElement>;
490
+ /** Resolved placement state, driven by updatePosition(). */
491
+ placeAbove: boolean;
492
+ resolvedPosition: 'left' | 'right';
493
+ dropdownStyle: Record<string, string>;
449
494
  /** Row height in px. MUST match the .time-item height in CSS for the active variation. */
450
495
  private get ITEM_HEIGHT();
451
- private get MIDDLE_OFFSET();
452
- wheelMinutes: number[];
453
- wheelSeconds: number[];
496
+ /** Finite lists (no cyclic loop) — same as the hours column, so 00..59 has a hard start/end. */
497
+ minutes: number[];
498
+ seconds: number[];
454
499
  showPicker: boolean;
455
500
  currentHour: number;
456
501
  currentMinute: number;
457
502
  currentSecond: number;
458
503
  currentAMPM: string;
504
+ /**
505
+ * Highlight that follows a column while scrolling. It is purely visual: scrolling moves it
506
+ * but does NOT commit the value. The value is only selected/committed when the user clicks
507
+ * an item (see onHourChange / onMinuteChange / onSecondChange).
508
+ */
509
+ activeHour: number;
510
+ activeMinute: number;
511
+ activeSecond: number;
459
512
  private _modelValue;
460
513
  brickclayIcons: {
461
514
  readonly arrowleft: "assets/icons/chevron-left.svg";
@@ -484,22 +537,45 @@ declare class BkTimePicker implements OnInit, OnChanges, AfterViewInit, ControlV
484
537
  };
485
538
  formatTimeFromComponents(hour: number, minute: number, second: number, ampm: string): string;
486
539
  togglePicker(): void;
540
+ /** Central close path so listeners are always detached and events emitted once. */
541
+ private dismiss;
487
542
  onHourChange(hour: number): void;
488
543
  onMinuteChange(minute: number): void;
489
544
  onSecondChange(second: number): void;
490
545
  onAMPMChange(ampm: string): void;
491
546
  updateTime(): void;
547
+ /** Position each column so the committed value sits at the top of its viewport. */
492
548
  scrollToSelectedTimes(): void;
493
- private initMinuteWheelScroll;
494
- private initSecondWheelScroll;
549
+ private scrollColumnTo;
550
+ /** Item nearest the viewport center for the current scroll position, clamped to [0, count - 1]. */
551
+ private scrollIndex;
495
552
  onDocumentClick(event: MouseEvent): void;
496
553
  private previousCloseCounter;
497
554
  getHours(): number[];
498
555
  getAMPMOptions(): string[];
499
- onMinuteWheelScroll(): void;
500
- onSecondWheelScroll(): void;
556
+ onHourScroll(): void;
557
+ onMinuteScroll(): void;
558
+ onSecondScroll(): void;
559
+ /**
560
+ * Resolve the dropdown placement against the current viewport.
561
+ * - autoPosition: flip above when there isn't room below, and flip the horizontal
562
+ * side when the preferred side would overflow.
563
+ * - appendToBody: additionally anchor with `position: fixed` (inline style) so the
564
+ * dropdown escapes ancestor overflow and follows the trigger on scroll.
565
+ */
566
+ private updatePosition;
567
+ private viewportListenersAttached;
568
+ private viewportRafId;
569
+ private readonly onViewportChange;
570
+ private handleViewportChange;
571
+ /** True when the trigger has scrolled out of the viewport or out of any clipping ancestor. */
572
+ private isTriggerOutOfView;
573
+ private isScrollClippingContainer;
574
+ private attachViewportListeners;
575
+ private detachViewportListeners;
576
+ ngOnDestroy(): void;
501
577
  static ɵfac: i0.ɵɵFactoryDeclaration<BkTimePicker, never>;
502
- static ɵcmp: i0.ɵɵComponentDeclaration<BkTimePicker, "bk-time-picker", never, { "required": { "alias": "required"; "required": false; }; "value": { "alias": "value"; "required": false; }; "label": { "alias": "label"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "clearable": { "alias": "clearable"; "required": false; }; "position": { "alias": "position"; "required": false; }; "variation": { "alias": "variation"; "required": false; }; "pickerId": { "alias": "pickerId"; "required": false; }; "closePicker": { "alias": "closePicker"; "required": false; }; "timeFormat": { "alias": "timeFormat"; "required": false; }; "showSeconds": { "alias": "showSeconds"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "change": "change"; "timeChange": "timeChange"; "pickerOpened": "pickerOpened"; "pickerClosed": "pickerClosed"; }, never, never, true, never>;
578
+ static ɵcmp: i0.ɵɵComponentDeclaration<BkTimePicker, "bk-time-picker", never, { "required": { "alias": "required"; "required": false; }; "value": { "alias": "value"; "required": false; }; "label": { "alias": "label"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "clearable": { "alias": "clearable"; "required": false; }; "position": { "alias": "position"; "required": false; }; "variation": { "alias": "variation"; "required": false; }; "pickerId": { "alias": "pickerId"; "required": false; }; "closePicker": { "alias": "closePicker"; "required": false; }; "timeFormat": { "alias": "timeFormat"; "required": false; }; "showSeconds": { "alias": "showSeconds"; "required": false; }; "autoPosition": { "alias": "autoPosition"; "required": false; }; "appendToBody": { "alias": "appendToBody"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "change": "change"; "timeChange": "timeChange"; "pickerOpened": "pickerOpened"; "pickerClosed": "pickerClosed"; }, never, never, true, never>;
503
579
  }
504
580
 
505
581
  /**
@@ -885,6 +961,7 @@ declare class BkSelect implements ControlValueAccessor, AfterViewInit, OnDestroy
885
961
  optionsListContainer: ElementRef<HTMLDivElement>;
886
962
  optionsRef: QueryList<ElementRef>;
887
963
  controlWrapper: ElementRef<HTMLDivElement>;
964
+ dropdownPanel?: ElementRef<HTMLDivElement>;
888
965
  chipsViewport?: ElementRef<HTMLDivElement>;
889
966
  measureChips?: QueryList<ElementRef<HTMLElement>>;
890
967
  measurePlus?: ElementRef<HTMLElement>;
@@ -894,6 +971,10 @@ declare class BkSelect implements ControlValueAccessor, AfterViewInit, OnDestroy
894
971
  selectedOptions: i0.WritableSignal<any[]>;
895
972
  searchTerm: i0.WritableSignal<string>;
896
973
  markedIndex: i0.WritableSignal<number>;
974
+ placement: i0.WritableSignal<"top" | "bottom">;
975
+ private originalPanelParent;
976
+ private originalPanelAnchor;
977
+ private panelInBody;
897
978
  visibleCount: i0.WritableSignal<number>;
898
979
  private resizeObserver?;
899
980
  dropdownStyle: i0.WritableSignal<{
@@ -907,6 +988,25 @@ declare class BkSelect implements ControlValueAccessor, AfterViewInit, OnDestroy
907
988
  constructor();
908
989
  ngAfterViewInit(): void;
909
990
  ngOnDestroy(): void;
991
+ private reposition;
992
+ /**
993
+ * True when the control is fully outside the visible area — either the
994
+ * viewport or a scrollable/clipping ancestor (e.g. a dialog's scroll body).
995
+ * Used to close the floating (append-to-body) panel once its anchor is no
996
+ * longer on screen, instead of leaving it hanging.
997
+ */
998
+ private isControlClipped;
999
+ /**
1000
+ * Relocate the panel node to <body> so it truly escapes any transformed or
1001
+ * overflow-clipped ancestor (dialogs, drawers, cards with overflow, etc.).
1002
+ * Setting position:fixed alone is not enough when an ancestor has a transform.
1003
+ */
1004
+ private movePanelToBody;
1005
+ /**
1006
+ * Return the panel to its original DOM position so Angular's @if can remove
1007
+ * it cleanly on close. Must run before isOpen is set to false.
1008
+ */
1009
+ private restorePanel;
910
1010
  private scheduleRecompute;
911
1011
  /**
912
1012
  * Measures the off-screen chip row and works out how many chips fit on a
@@ -925,8 +1025,13 @@ declare class BkSelect implements ControlValueAccessor, AfterViewInit, OnDestroy
925
1025
  closeDropdown(): void;
926
1026
  getTop(): string | null;
927
1027
  getBottom(): string | null;
928
- updatePosition(): void;
929
- onWindowEvents(): void;
1028
+ /**
1029
+ * Decides which side the panel should open on (auto-flip) and, for
1030
+ * append-to-body mode, computes the fixed coordinates. Shared by both modes
1031
+ * and re-run on open and on every scroll/resize so the panel snaps to
1032
+ * whichever side has room — honouring `dropdownPosition` when it fits.
1033
+ */
1034
+ applyPlacement(): void;
930
1035
  toggleSelectAll(event: Event): void;
931
1036
  handleSelection(item: any, event?: Event): void;
932
1037
  removeOption(item: any, event: Event): void;
@@ -1929,7 +2034,7 @@ interface HierarchicalNode {
1929
2034
  children?: HierarchicalNode[];
1930
2035
  disabled?: boolean;
1931
2036
  }
1932
- declare class BkHierarchicalSelect implements ControlValueAccessor {
2037
+ declare class BkHierarchicalSelect implements ControlValueAccessor, AfterViewInit, OnDestroy {
1933
2038
  /** Tree data (root-level array). */
1934
2039
  items: i0.InputSignal<HierarchicalNode[]>;
1935
2040
  /** Key for display label (e.g. 'noteType'). */
@@ -1979,8 +2084,30 @@ declare class BkHierarchicalSelect implements ControlValueAccessor {
1979
2084
  clear: i0.OutputEmitterRef<any>;
1980
2085
  searchInput: ElementRef<HTMLInputElement>;
1981
2086
  controlWrapper: ElementRef<HTMLDivElement>;
2087
+ dropdownPanel?: ElementRef<HTMLDivElement>;
1982
2088
  private el;
2089
+ placement: i0.WritableSignal<"top" | "bottom">;
2090
+ private originalPanelParent;
2091
+ private originalPanelAnchor;
2092
+ private panelInBody;
1983
2093
  constructor();
2094
+ ngAfterViewInit(): void;
2095
+ ngOnDestroy(): void;
2096
+ private reposition;
2097
+ /**
2098
+ * True when the control is fully outside the visible area — either the
2099
+ * viewport or a scrollable/clipping ancestor (e.g. a dialog's scroll body).
2100
+ */
2101
+ private isControlClipped;
2102
+ /**
2103
+ * Relocate the panel node to <body> so it truly escapes any transformed or
2104
+ * overflow-clipped ancestor (dialogs, drawers, overflow cards, etc.).
2105
+ */
2106
+ private movePanelToBody;
2107
+ /** Return the panel to its original DOM position before Angular removes it. */
2108
+ private restorePanel;
2109
+ /** Runs after the panel has rendered: relocate (append mode) + position + focus. */
2110
+ private afterOpenInit;
1984
2111
  isOpen: i0.WritableSignal<boolean>;
1985
2112
  dropdownStyle: i0.WritableSignal<{
1986
2113
  top?: string;
@@ -2012,11 +2139,16 @@ declare class BkHierarchicalSelect implements ControlValueAccessor {
2012
2139
  private buildPathTo;
2013
2140
  toggleDropdown(event?: Event): void;
2014
2141
  openDropdown(): void;
2015
- updatePosition(): void;
2142
+ /**
2143
+ * Decides which side the panel opens on (auto-flip) and, for append-to-body
2144
+ * mode, computes the fixed coordinates. Re-run on open and on scroll/resize so
2145
+ * the panel snaps to whichever side has room — honouring `dropdownPosition`
2146
+ * when it fits. Inline mode is positioned by CSS via the `placement` signal.
2147
+ */
2148
+ applyPlacement(): void;
2016
2149
  /** Fixed positioning when appendToBody; inset placement uses CSS + .hierarchical-select-field. */
2017
2150
  getTop(): string | null;
2018
2151
  getBottom(): string | null;
2019
- onWindowScrollOrResize(): void;
2020
2152
  closeDropdown(): void;
2021
2153
  goBack(): void;
2022
2154
  onSearchInput(event: Event): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brickclay-org/ui",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "schematics": "./schematics/collection.json",
5
5
  "ng-add": {
6
6
  "save": "dependencies"