blixify-ui-web 1.2.167 → 1.2.169

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.
@@ -15,6 +15,7 @@ interface Props extends ChatProps {
15
15
  resizableHeight?: boolean;
16
16
  defaultHeight?: number;
17
17
  heightStorageKey?: string;
18
+ keyboardOpen?: boolean;
18
19
  }
19
20
  interface AnchorPosition {
20
21
  right: number;
@@ -28,7 +29,15 @@ export declare class ChatAnchor extends Component<Props> {
28
29
  position: AnchorPosition | null;
29
30
  width: number | null;
30
31
  height: number | null;
32
+ keyboardVisible: boolean;
33
+ viewportHeight: number | null;
34
+ viewportTop: number;
35
+ viewportBottom: number;
31
36
  };
37
+ keyboardClosedHeight: number;
38
+ viewportWidth: number;
39
+ visualViewport: VisualViewport | null;
40
+ viewportTrackingActive: boolean;
32
41
  anchorRef: React.RefObject<HTMLDivElement | null>;
33
42
  buttonRef: React.RefObject<HTMLDivElement | null>;
34
43
  panelRef: React.RefObject<HTMLDivElement | null>;
@@ -63,6 +72,7 @@ export declare class ChatAnchor extends Component<Props> {
63
72
  handleMaxWidth: () => number;
64
73
  handleMaxHeight: () => number;
65
74
  handleClampWidth: (width: number) => number;
75
+ handleMinHeight: () => number;
66
76
  handleClampHeight: (height: number) => number;
67
77
  handleIsResizable: () => boolean;
68
78
  handleIsResizableHeight: () => boolean;
@@ -83,15 +93,21 @@ export declare class ChatAnchor extends Component<Props> {
83
93
  handleRemoveResizeListeners: () => void;
84
94
  handleResizeVertStart: (event: React.PointerEvent<HTMLDivElement>) => void;
85
95
  handleResizeVertMove: (event: PointerEvent) => void;
96
+ handleResizeHeightKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
86
97
  handleResizeVertEnd: () => void;
87
98
  handleRemoveResizeVertListeners: () => void;
88
99
  handleAnchorClick: () => void;
89
100
  handleIsDraggable: () => boolean;
90
101
  componentDidUpdate: (prevProps: Props, prevState: {
91
102
  chatCard: boolean;
103
+ isMobileWidth: boolean;
92
104
  }) => void;
93
105
  handleReclampPosition: () => void;
94
106
  handleResize: () => void;
107
+ handleRemoveViewportListeners: () => void;
108
+ handleSyncViewportListeners: () => void;
109
+ handleChatFocusChange: (event: FocusEvent) => void;
110
+ handleViewportChange: () => void;
95
111
  renderChatAnchorIcon: () => React.JSX.Element;
96
112
  render(): React.JSX.Element;
97
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/chatAnchor/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/chatAnchor/index.tsx"],"names":[],"mappings":"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"}
@@ -82,10 +82,14 @@ var WIDTH_VIEWPORT_GUTTER = 16;
82
82
  // gutter. The floor is taller than the width floor because a chat still needs
83
83
  // its header and input row to stay usable.
84
84
  var MIN_USABLE_HEIGHT = 120;
85
+ var MIN_MOBILE_HEIGHT_VIEWPORT_RATIO = 0.5;
85
86
  var HEIGHT_VIEWPORT_GUTTER = 16;
86
87
  // INFO: a drag shorter than this is a click — a tap on a touch screen always
87
88
  // travels a pixel or two, and it must still open the chat
88
89
  var DRAG_THRESHOLD = 4;
90
+ var KEYBOARD_VIEWPORT_THRESHOLD = 100;
91
+ var SAFE_AREA_TOP = "var(--safe-area-inset-top, env(safe-area-inset-top, 0px))";
92
+ var SAFE_AREA_BOTTOM = "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))";
89
93
  var ChatAnchor = /** @class */ (function (_super) {
90
94
  __extends(ChatAnchor, _super);
91
95
  function ChatAnchor() {
@@ -96,7 +100,15 @@ var ChatAnchor = /** @class */ (function (_super) {
96
100
  position: null,
97
101
  width: null,
98
102
  height: null,
99
- };
103
+ keyboardVisible: false,
104
+ viewportHeight: null,
105
+ viewportTop: 0,
106
+ viewportBottom: 0,
107
+ };
108
+ _this.keyboardClosedHeight = 0;
109
+ _this.viewportWidth = 0;
110
+ _this.visualViewport = null;
111
+ _this.viewportTrackingActive = false;
100
112
  _this.anchorRef = react_2.default.createRef();
101
113
  _this.buttonRef = react_2.default.createRef();
102
114
  // INFO: the panel's own box, not the anchor's — the anchor also holds the
@@ -125,7 +137,10 @@ var ChatAnchor = /** @class */ (function (_super) {
125
137
  return _this.props.defaultHeight;
126
138
  if (typeof window === "undefined")
127
139
  return DEFAULT_HEIGHT_FALLBACK;
128
- return Math.round(window.innerHeight * DEFAULT_HEIGHT_VIEWPORT_RATIO);
140
+ var viewportHeight = window.innerWidth < 640
141
+ ? _this.keyboardClosedHeight || window.innerHeight
142
+ : window.innerHeight;
143
+ return Math.round(viewportHeight * DEFAULT_HEIGHT_VIEWPORT_RATIO);
129
144
  };
130
145
  // INFO: the panel grows leftward from the bottom-right anchor, so the widest
131
146
  // it can be without clipping off the left edge is the distance from its own
@@ -152,6 +167,10 @@ var ChatAnchor = /** @class */ (function (_super) {
152
167
  var _a, _b;
153
168
  if (typeof window === "undefined")
154
169
  return Number.POSITIVE_INFINITY;
170
+ // INFO: mobile height resizing is pinned to the viewport bottom, independent
171
+ // of the floating button's saved position. It can grow to the full screen.
172
+ if (window.innerWidth < 640 && _this.props.resizableHeight)
173
+ return _this.keyboardClosedHeight || window.innerHeight;
155
174
  var rect = (_b = ((_a = _this.panelRef.current) !== null && _a !== void 0 ? _a : _this.anchorRef.current)) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
156
175
  var bottomEdge = rect ? rect.bottom : window.innerHeight;
157
176
  return bottomEdge - HEIGHT_VIEWPORT_GUTTER;
@@ -167,16 +186,26 @@ var ChatAnchor = /** @class */ (function (_super) {
167
186
  var cap = Math.max(MIN_USABLE_WIDTH, _this.handleMaxWidth());
168
187
  return Math.min(Math.max(MIN_USABLE_WIDTH, width), cap);
169
188
  };
189
+ _this.handleMinHeight = function () {
190
+ return typeof window !== "undefined" && window.innerWidth < 640
191
+ ? Math.round((_this.keyboardClosedHeight || window.innerHeight) *
192
+ MIN_MOBILE_HEIGHT_VIEWPORT_RATIO)
193
+ : MIN_USABLE_HEIGHT;
194
+ };
170
195
  _this.handleClampHeight = function (height) {
171
196
  // INFO: a bad pointer event (no clientY) can produce NaN — never let it
172
197
  // reach state or localStorage; fall back to the default height instead
173
198
  if (Number.isNaN(height))
174
199
  return _this.handleDefaultHeight();
175
200
  // INFO: floor keeps the header and input usable, cap keeps the panel on
176
- // screen. The floor is the hard lower bound — if the viewport is so short
177
- // the cap would fall below it, the floor still wins over the cap.
178
- var cap = Math.max(MIN_USABLE_HEIGHT, _this.handleMaxHeight());
179
- return Math.min(Math.max(MIN_USABLE_HEIGHT, height), cap);
201
+ // screen. Mobile stays between half and all of the current viewport;
202
+ // desktop keeps its existing pixel floor.
203
+ var isMobile = typeof window !== "undefined" && window.innerWidth < 640;
204
+ var minimum = _this.handleMinHeight();
205
+ var cap = isMobile
206
+ ? _this.handleMaxHeight()
207
+ : Math.max(minimum, _this.handleMaxHeight());
208
+ return Math.min(Math.max(minimum, height), cap);
180
209
  };
181
210
  _this.handleIsResizable = function () {
182
211
  return _this.props.resizableWidth === true &&
@@ -184,7 +213,7 @@ var ChatAnchor = /** @class */ (function (_super) {
184
213
  };
185
214
  _this.handleIsResizableHeight = function () {
186
215
  return _this.props.resizableHeight === true &&
187
- !(_this.props.open && _this.state.isMobileWidth);
216
+ !(_this.state.isMobileWidth && _this.state.keyboardVisible);
188
217
  };
189
218
  // INFO: keeps the anchor on screen when a position saved on a wide window is
190
219
  // restored on a narrow one, when the window is resized under it, and when the
@@ -292,30 +321,26 @@ var ChatAnchor = /** @class */ (function (_super) {
292
321
  _this.handleRestoreHeight = function () {
293
322
  if (!_this.props.resizableHeight)
294
323
  return;
324
+ // INFO: defer the default if mounted while a native keyboard is already
325
+ // visible; the closed viewport is not known until it is dismissed.
326
+ var height = _this.props.keyboardOpen && window.innerWidth < 640
327
+ ? null
328
+ : _this.handleDefaultHeight();
295
329
  try {
296
330
  var stored = window.localStorage.getItem(_this.handleHeightStorageKey());
297
- if (!stored) {
298
- // INFO: no saved height — seed the default so the panel opens at the
299
- // consumer's chosen height rather than the sizeClassName fallback
300
- _this.setState({
301
- height: _this.handleClampHeight(_this.handleDefaultHeight()),
302
- });
303
- return;
304
- }
305
- var parsed = Number(stored);
306
- if (Number.isNaN(parsed)) {
307
- // INFO: a corrupted value falls back to the default height, not nothing
308
- _this.setState({
309
- height: _this.handleClampHeight(_this.handleDefaultHeight()),
310
- });
311
- return;
312
- }
313
- _this.setState({ height: _this.handleClampHeight(parsed) });
331
+ if (stored && Number.isFinite(Number(stored)))
332
+ height = Number(stored);
314
333
  }
315
334
  catch (_a) {
316
- // INFO: private browsing and blocked site data both throw on read — the
317
- // panel just keeps its default height
335
+ // INFO: blocked storage still uses the default height.
318
336
  }
337
+ // INFO: a mobile mount can happen with the keyboard already open. Preserve
338
+ // the stored preference and apply viewport limits only when rendering it.
339
+ _this.setState({
340
+ height: height === null || window.innerWidth < 640
341
+ ? height
342
+ : _this.handleClampHeight(height),
343
+ });
319
344
  };
320
345
  _this.handlePersistHeight = function (height) {
321
346
  try {
@@ -443,7 +468,7 @@ var ChatAnchor = /** @class */ (function (_super) {
443
468
  return;
444
469
  // INFO: height is seeded on mount whenever resize is on, so state.height is
445
470
  // the panel's current height — the drag continues from what the user sees
446
- var startHeight = (_a = _this.state.height) !== null && _a !== void 0 ? _a : _this.handleDefaultHeight();
471
+ var startHeight = _this.handleClampHeight((_a = _this.state.height) !== null && _a !== void 0 ? _a : _this.handleDefaultHeight());
447
472
  event.preventDefault();
448
473
  event.stopPropagation();
449
474
  _this.resizeVertRef = {
@@ -458,7 +483,7 @@ var ChatAnchor = /** @class */ (function (_super) {
458
483
  };
459
484
  _this.handleResizeVertMove = function (event) {
460
485
  var resize = _this.resizeVertRef;
461
- if (!resize)
486
+ if (!resize || !_this.handleIsResizableHeight())
462
487
  return;
463
488
  var deltaY = event.clientY - resize.pointerY;
464
489
  if (!resize.moved && Math.abs(deltaY) < DRAG_THRESHOLD)
@@ -469,11 +494,23 @@ var ChatAnchor = /** @class */ (function (_super) {
469
494
  resize.latest = _this.handleClampHeight(resize.startHeight - deltaY);
470
495
  _this.setState({ height: resize.latest });
471
496
  };
497
+ _this.handleResizeHeightKeyDown = function (event) {
498
+ var _a;
499
+ if (!_this.handleIsResizableHeight())
500
+ return;
501
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown")
502
+ return;
503
+ event.preventDefault();
504
+ var height = _this.handleClampHeight(((_a = _this.state.height) !== null && _a !== void 0 ? _a : _this.handleDefaultHeight()) +
505
+ (event.key === "ArrowUp" ? 24 : -24));
506
+ _this.setState({ height: height });
507
+ _this.handlePersistHeight(height);
508
+ };
472
509
  _this.handleResizeVertEnd = function () {
473
510
  var resize = _this.resizeVertRef;
474
511
  _this.resizeVertRef = null;
475
512
  _this.handleRemoveResizeVertListeners();
476
- if (!(resize === null || resize === void 0 ? void 0 : resize.moved))
513
+ if (!(resize === null || resize === void 0 ? void 0 : resize.moved) || !_this.handleIsResizableHeight())
477
514
  return;
478
515
  if (resize.latest != null)
479
516
  _this.handlePersistHeight(resize.latest);
@@ -490,18 +527,33 @@ var ChatAnchor = /** @class */ (function (_super) {
490
527
  }
491
528
  _this.props.onClick();
492
529
  };
493
- // INFO: no dragging while the panel is full screen on mobile — there is no
530
+ // INFO: no button dragging while the panel is open on mobile — there is no
494
531
  // free-floating button to move at that point
495
532
  _this.handleIsDraggable = function () {
496
533
  return _this.props.draggable !== false &&
497
534
  !(_this.props.open && _this.state.isMobileWidth);
498
535
  };
499
536
  _this.componentDidUpdate = function (prevProps, prevState) {
537
+ var _a;
500
538
  if (_this.props.open !== prevProps.open && _this.props.open) {
501
539
  _this.setState({
502
540
  chatCard: false,
503
541
  });
504
542
  }
543
+ if (_this.props.open !== prevProps.open ||
544
+ _this.props.keyboardOpen !== prevProps.keyboardOpen ||
545
+ _this.state.isMobileWidth !== prevState.isMobileWidth) {
546
+ _this.handleSyncViewportListeners();
547
+ }
548
+ if (_this.props.resizableHeight &&
549
+ !_this.state.isMobileWidth &&
550
+ (_this.state.isMobileWidth !== prevState.isMobileWidth ||
551
+ _this.props.open !== prevProps.open ||
552
+ _this.props.resizableHeight !== prevProps.resizableHeight)) {
553
+ var height = _this.handleClampHeight((_a = _this.state.height) !== null && _a !== void 0 ? _a : _this.handleDefaultHeight());
554
+ if (height !== _this.state.height)
555
+ _this.setState({ height: height });
556
+ }
505
557
  // INFO: opening, closing or dismissing the greeting changes the anchor's
506
558
  // size, so a position that fit the old size can leave the new one off
507
559
  // screen — re-clamp it against the layout that was just committed
@@ -520,17 +572,137 @@ var ChatAnchor = /** @class */ (function (_super) {
520
572
  // INFO: kept too, or the next visit restores the off-screen position
521
573
  _this.handlePersistPosition(next);
522
574
  };
575
+ // INFO: only real window resizes need floating-position and size measurements.
523
576
  _this.handleResize = function () {
524
- _this.setState(function (previous) { return ({
525
- isMobileWidth: window.innerWidth < 640,
526
- position: previous.position
577
+ var _a, _b;
578
+ var isMobileWidth = window.innerWidth < 640;
579
+ if (!_this.props.open || !isMobileWidth) {
580
+ _this.keyboardClosedHeight = Math.min(window.innerHeight, (_b = (_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) !== null && _b !== void 0 ? _b : window.innerHeight);
581
+ _this.viewportWidth = window.innerWidth;
582
+ }
583
+ _this.setState(function (previous) {
584
+ var _a, _b;
585
+ var position = previous.position
527
586
  ? _this.handleClampPosition(previous.position)
528
- : null,
529
- // INFO: shrink a saved width that no longer fits the narrower window
530
- width: previous.width ? _this.handleClampWidth(previous.width) : null,
531
- // INFO: shrink a saved height that no longer fits the shorter window
532
- height: previous.height ? _this.handleClampHeight(previous.height) : null,
533
- }); });
587
+ : null;
588
+ var width = previous.width ? _this.handleClampWidth(previous.width) : null;
589
+ var height = !isMobileWidth && previous.height
590
+ ? _this.handleClampHeight(previous.height)
591
+ : previous.height;
592
+ if (previous.isMobileWidth === isMobileWidth &&
593
+ ((_a = previous.position) === null || _a === void 0 ? void 0 : _a.right) === (position === null || position === void 0 ? void 0 : position.right) &&
594
+ ((_b = previous.position) === null || _b === void 0 ? void 0 : _b.bottom) === (position === null || position === void 0 ? void 0 : position.bottom) &&
595
+ previous.width === width &&
596
+ previous.height === height)
597
+ return null;
598
+ return { isMobileWidth: isMobileWidth, position: position, width: width, height: height };
599
+ });
600
+ _this.handleSyncViewportListeners();
601
+ };
602
+ _this.handleRemoveViewportListeners = function () {
603
+ var _a, _b;
604
+ if (!_this.viewportTrackingActive)
605
+ return;
606
+ (_a = _this.visualViewport) === null || _a === void 0 ? void 0 : _a.removeEventListener("resize", _this.handleViewportChange);
607
+ (_b = _this.visualViewport) === null || _b === void 0 ? void 0 : _b.removeEventListener("scroll", _this.handleViewportChange);
608
+ document.removeEventListener("focusin", _this.handleChatFocusChange);
609
+ document.removeEventListener("focusout", _this.handleChatFocusChange);
610
+ _this.visualViewport = null;
611
+ _this.viewportTrackingActive = false;
612
+ };
613
+ _this.handleSyncViewportListeners = function () {
614
+ var _a, _b;
615
+ // INFO: fullscreen and resizable mobile panels both need keyboard tracking.
616
+ var shouldTrack = _this.props.open && window.innerWidth < 640;
617
+ if (shouldTrack && !_this.viewportTrackingActive) {
618
+ _this.viewportTrackingActive = true;
619
+ _this.visualViewport = window.visualViewport;
620
+ (_a = _this.visualViewport) === null || _a === void 0 ? void 0 : _a.addEventListener("resize", _this.handleViewportChange);
621
+ (_b = _this.visualViewport) === null || _b === void 0 ? void 0 : _b.addEventListener("scroll", _this.handleViewportChange);
622
+ document.addEventListener("focusin", _this.handleChatFocusChange);
623
+ document.addEventListener("focusout", _this.handleChatFocusChange);
624
+ }
625
+ else if (!shouldTrack && _this.viewportTrackingActive) {
626
+ _this.handleRemoveViewportListeners();
627
+ _this.setState({
628
+ keyboardVisible: false,
629
+ viewportHeight: null,
630
+ viewportTop: 0,
631
+ viewportBottom: 0,
632
+ });
633
+ }
634
+ if (shouldTrack)
635
+ _this.handleViewportChange();
636
+ };
637
+ _this.handleChatFocusChange = function (event) {
638
+ var _a;
639
+ // INFO: an input elsewhere on the page must not trigger chat layout work.
640
+ // Focusout from this chat still runs so keyboard dismissal remains latched.
641
+ if (event.target instanceof Node &&
642
+ ((_a = _this.anchorRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target)))
643
+ _this.handleViewportChange();
644
+ };
645
+ _this.handleViewportChange = function () {
646
+ var _a, _b, _c, _d, _e;
647
+ if (!_this.viewportTrackingActive || !_this.props.open || window.innerWidth >= 640)
648
+ return;
649
+ var viewport = window.visualViewport;
650
+ // INFO: pinch zoom changes the visual viewport too; it is not a keyboard.
651
+ if (viewport && Math.abs(viewport.scale - 1) > 0.01)
652
+ return;
653
+ var isMobileWidth = window.innerWidth < 640;
654
+ var viewportHeight = Math.min(window.innerHeight, (_a = viewport === null || viewport === void 0 ? void 0 : viewport.height) !== null && _a !== void 0 ? _a : window.innerHeight);
655
+ var viewportTop = Math.max(0, (_b = viewport === null || viewport === void 0 ? void 0 : viewport.offsetTop) !== null && _b !== void 0 ? _b : 0);
656
+ var viewportBottom = Math.max(0, window.innerHeight - viewportHeight - viewportTop);
657
+ if (!_this.keyboardClosedHeight || _this.viewportWidth !== window.innerWidth) {
658
+ _this.keyboardClosedHeight = viewportHeight;
659
+ _this.viewportWidth = window.innerWidth;
660
+ }
661
+ var active = document.activeElement;
662
+ var inputFocused = active instanceof HTMLElement &&
663
+ ((_c = _this.anchorRef.current) === null || _c === void 0 ? void 0 : _c.contains(active)) &&
664
+ (active.matches("input:not([readonly]):not([disabled]), textarea:not([readonly]):not([disabled])") || active.isContentEditable);
665
+ // INFO: latch detection while the viewport stays reduced, including the
666
+ // interval after blur but before the keyboard's closing animation finishes.
667
+ var detectedKeyboard = isMobileWidth &&
668
+ (inputFocused || _this.state.keyboardVisible) &&
669
+ _this.keyboardClosedHeight - viewportHeight >
670
+ Math.max(KEYBOARD_VIEWPORT_THRESHOLD, _this.keyboardClosedHeight * 0.15);
671
+ var keyboardVisible = isMobileWidth && ((_d = _this.props.keyboardOpen) !== null && _d !== void 0 ? _d : !!detectedKeyboard);
672
+ if (!keyboardVisible)
673
+ _this.keyboardClosedHeight = viewportHeight;
674
+ // INFO: a keyboard interrupting a drag must not save a temporary height.
675
+ var interruptedHeight = keyboardVisible
676
+ ? (_e = _this.resizeVertRef) === null || _e === void 0 ? void 0 : _e.startHeight
677
+ : undefined;
678
+ if (keyboardVisible) {
679
+ _this.resizeVertRef = null;
680
+ _this.handleRemoveResizeVertListeners();
681
+ }
682
+ // INFO: focus and viewport scroll can repeat identical metrics. Avoid both
683
+ // geometry measurements and state updates when nothing actually changed.
684
+ if (_this.state.keyboardVisible === keyboardVisible &&
685
+ _this.state.viewportHeight === viewportHeight &&
686
+ _this.state.viewportTop === viewportTop &&
687
+ _this.state.viewportBottom === viewportBottom &&
688
+ interruptedHeight === undefined)
689
+ return;
690
+ _this.setState(function (previous) {
691
+ var height = interruptedHeight !== null && interruptedHeight !== void 0 ? interruptedHeight : previous.height;
692
+ if (previous.keyboardVisible === keyboardVisible &&
693
+ previous.viewportHeight === viewportHeight &&
694
+ previous.viewportTop === viewportTop &&
695
+ previous.viewportBottom === viewportBottom &&
696
+ previous.height === height)
697
+ return null;
698
+ return {
699
+ keyboardVisible: keyboardVisible,
700
+ viewportHeight: viewportHeight,
701
+ viewportTop: viewportTop,
702
+ viewportBottom: viewportBottom,
703
+ height: height,
704
+ };
705
+ });
534
706
  };
535
707
  _this.renderChatAnchorIcon = function () {
536
708
  var _a = _this.props, open = _a.open, openIcon = _a.openIcon, closeIcon = _a.closeIcon;
@@ -556,22 +728,27 @@ var ChatAnchor = /** @class */ (function (_super) {
556
728
  };
557
729
  ChatAnchor.prototype.componentWillUnmount = function () {
558
730
  window.removeEventListener("resize", this.handleResize);
731
+ this.handleRemoveViewportListeners();
559
732
  this.handleRemoveDragListeners();
560
733
  this.handleRemoveResizeListeners();
561
734
  this.handleRemoveResizeVertListeners();
562
735
  };
563
736
  ChatAnchor.prototype.render = function () {
564
737
  var _this = this;
565
- var _a, _b;
566
- var useAbsolutePosition = this.props.open && this.state.isMobileWidth;
738
+ var _a, _b, _c, _d, _e, _f, _g, _h;
739
+ var isMobileOpen = this.props.open && this.state.isMobileWidth;
740
+ var useMobileResize = isMobileOpen && this.props.resizableHeight === true;
741
+ var useAbsolutePosition = isMobileOpen && !useMobileResize;
567
742
  // INFO: a dragged position is an inline style — Tailwind only emits the
568
743
  // classes it can see at build time, so a runtime bottom-[Npx] never exists
569
- var draggedPosition = !useAbsolutePosition && this.state.position ? this.state.position : null;
744
+ var draggedPosition = !isMobileOpen && this.state.position ? this.state.position : null;
570
745
  var positionOffset = useAbsolutePosition
571
746
  ? "h-[calc(100dvh)] w-screen top-0 left-0 overflow-hidden"
572
- : draggedPosition
573
- ? ""
574
- : ((_a = this.props.positionOffset) !== null && _a !== void 0 ? _a : exports.DEFAULT_POSITION_OFFSET);
747
+ : useMobileResize
748
+ ? "bottom-0 left-0 w-screen max-h-[100dvh]"
749
+ : draggedPosition
750
+ ? ""
751
+ : ((_a = this.props.positionOffset) !== null && _a !== void 0 ? _a : exports.DEFAULT_POSITION_OFFSET);
575
752
  var chatAnchorVisible = this.props.open ? "hidden sm:block" : "block";
576
753
  var icon = this.props.open ? this.props.closeIcon : this.props.openIcon;
577
754
  // INFO: only feed a pixel width when resize is on and the panel is not
@@ -580,33 +757,68 @@ var ChatAnchor = /** @class */ (function (_super) {
580
757
  ? this.state.width
581
758
  : undefined;
582
759
  // INFO: same for height — a full-screen mobile panel is the whole viewport
583
- var heightPx = this.handleIsResizableHeight() && this.state.height
584
- ? this.state.height
760
+ var heightPx = this.props.resizableHeight
761
+ ? useMobileResize
762
+ ? this.handleClampHeight((_b = this.state.height) !== null && _b !== void 0 ? _b : this.handleDefaultHeight())
763
+ : (_c = this.state.height) !== null && _c !== void 0 ? _c : undefined
585
764
  : undefined;
765
+ var availableHeight = (_d = this.state.viewportHeight) !== null && _d !== void 0 ? _d : this.handleDefaultHeight();
766
+ var mobileHeight = Math.min(heightPx !== null && heightPx !== void 0 ? heightPx : this.handleDefaultHeight(), availableHeight);
767
+ var mobileTopInset = mobileHeight >= availableHeight - 1 ? SAFE_AREA_TOP : "0px";
768
+ var mobileBottomInset = this.state.keyboardVisible ? "0px" : SAFE_AREA_BOTTOM;
586
769
  // INFO: the handle only makes sense while the resizable panel is open and
587
770
  // not full-screen on mobile — it needs a panel edge to sit against. Gated
588
771
  // on the same useAbsolutePosition the layout uses so the two never disagree.
589
- var showResizeHandle = this.props.open && this.props.resizableWidth && !useAbsolutePosition;
590
- var showResizeVertHandle = this.props.open && this.props.resizableHeight && !useAbsolutePosition;
772
+ var showResizeHandle = this.props.open && this.props.resizableWidth && !isMobileOpen;
773
+ var showResizeVertHandle = this.props.open && this.handleIsResizableHeight() && !useAbsolutePosition;
591
774
  return (react_2.default.createElement("div", { ref: this.anchorRef,
592
775
  // INFO: no child spacing when full screen — the hidden bubble row's
593
776
  // space-y margin made the panel 16px taller than the screen, so the
594
777
  // container scrolled on top of the message list
595
- className: "z-40 flex flex-col items-end fixed ".concat(useAbsolutePosition ? "" : "space-y-4", " ").concat(positionOffset), style: draggedPosition !== null && draggedPosition !== void 0 ? draggedPosition : undefined },
778
+ className: "z-40 flex flex-col items-end fixed ".concat(isMobileOpen ? "" : "space-y-4", " ").concat(positionOffset), style: isMobileOpen
779
+ ? useMobileResize
780
+ ? { bottom: this.state.viewportBottom }
781
+ : {
782
+ top: this.state.viewportTop,
783
+ height: (_e = this.state.viewportHeight) !== null && _e !== void 0 ? _e : undefined,
784
+ paddingTop: SAFE_AREA_TOP,
785
+ paddingBottom: mobileBottomInset,
786
+ }
787
+ : draggedPosition !== null && draggedPosition !== void 0 ? draggedPosition : undefined },
596
788
  react_2.default.createElement(react_1.Transition, { show: this.props.open, as: react_2.Fragment, enter: "transform transition duration-[400ms]", enterFrom: "opacity-0 scale-50", leave: "transform duration-200 transition ease-in-out", leaveTo: "opacity-0 scale-95 " },
597
- react_2.default.createElement("div", { ref: this.panelRef, className: "relative w-full h-full" },
789
+ react_2.default.createElement("div", { ref: this.panelRef, className: "relative w-full h-full ".concat(useMobileResize
790
+ ? "pt-8 max-h-[100dvh] rounded-t-2xl overflow-hidden ".concat(this.props.darkMode ? "bg-gray-900" : "bg-white")
791
+ : ""), style: useMobileResize
792
+ ? {
793
+ height: mobileHeight,
794
+ paddingTop: "calc(2rem + ".concat(mobileTopInset, ")"),
795
+ paddingBottom: mobileBottomInset,
796
+ paddingLeft: "var(--safe-area-inset-left, env(safe-area-inset-left, 0px))",
797
+ paddingRight: "var(--safe-area-inset-right, env(safe-area-inset-right, 0px))",
798
+ }
799
+ : undefined },
598
800
  showResizeHandle && (react_2.default.createElement("div", { "data-testid": "chat-anchor-resize-handle", onPointerDown: this.handleResizeStart, className: "absolute left-0 top-0 h-full w-2 -ml-1 z-10 flex items-center justify-center touch-none select-none cursor-ew-resize group" },
599
801
  react_2.default.createElement("div", { className: "h-10 w-1 rounded-full bg-gray-300 group-hover:bg-primary-500 transition-colors" }))),
600
- showResizeVertHandle && (react_2.default.createElement("div", { "data-testid": "chat-anchor-resize-handle-vertical", onPointerDown: this.handleResizeVertStart,
802
+ showResizeVertHandle && (react_2.default.createElement("div", { "data-testid": "chat-anchor-resize-handle-vertical", onPointerDown: this.handleResizeVertStart, style: useMobileResize ? { top: mobileTopInset } : undefined,
601
803
  // INFO: sits a hair right of the left width handle so the two do
602
804
  // not fight for the top-left corner; z-20 keeps it above the
603
805
  // width strip where they still overlap
604
- className: "absolute left-3 right-0 top-0 h-2 -mt-1 z-20 flex items-center justify-center touch-none select-none cursor-ns-resize group" },
806
+ role: "separator", "aria-label": "Resize chat height", "aria-orientation": "horizontal", "aria-valuemin": this.handleMinHeight(), "aria-valuemax": this.handleMaxHeight(), "aria-valuenow": heightPx, tabIndex: 0, onKeyDown: this.handleResizeHeightKeyDown, className: "absolute top-0 z-20 flex items-center justify-center touch-none select-none cursor-ns-resize group ".concat(useMobileResize
807
+ ? "left-0 h-8 ".concat(this.props.hideAdditionalAction ? "right-10" : "right-0")
808
+ : "left-3 right-0 h-2 -mt-1") },
605
809
  react_2.default.createElement("div", { className: "h-1 w-10 rounded-full bg-gray-300 group-hover:bg-primary-500 transition-colors" }))),
606
- react_2.default.createElement(chatBase_1.ChatBase, __assign({}, this.props, { widthPx: widthPx, heightPx: heightPx,
607
- // INFO: square corners when the panel is the whole screen
608
- className: "".concat((_b = this.props.className) !== null && _b !== void 0 ? _b : "", " ").concat(useAbsolutePosition ? "!rounded-none" : "") })))),
609
- react_2.default.createElement("div", { className: "flex flex-row" },
810
+ useMobileResize && this.props.hideAdditionalAction && (react_2.default.createElement("button", { type: "button", "aria-label": "Close chat", style: { top: mobileTopInset }, onClick: (_f = this.props.onClose) !== null && _f !== void 0 ? _f : this.props.onClick, className: "absolute top-0 right-0 z-20 h-8 w-10 flex items-center justify-center text-gray-500" },
811
+ react_2.default.createElement(solid_1.XMarkIcon, { className: "h-5 w-5" }))),
812
+ react_2.default.createElement(chatBase_1.ChatBase, __assign({}, this.props, { onClose: useMobileResize
813
+ ? (_g = this.props.onClose) !== null && _g !== void 0 ? _g : this.props.onClick
814
+ : this.props.onClose, disableMobileResize: useMobileResize ? false : this.props.disableMobileResize, sizeClassName: useMobileResize ? "h-full w-full min-h-0" : this.props.sizeClassName, widthPx: widthPx, heightPx: useMobileResize ? undefined : heightPx,
815
+ // INFO: clip custom headers to the mobile panel's rounded top corners.
816
+ className: "".concat((_h = this.props.className) !== null && _h !== void 0 ? _h : "", " ").concat(useMobileResize
817
+ ? "!rounded-t-2xl !rounded-b-none overflow-hidden"
818
+ : useAbsolutePosition
819
+ ? "!rounded-none"
820
+ : "") })))),
821
+ react_2.default.createElement("div", { className: isMobileOpen ? "hidden sm:flex flex-row" : "flex flex-row" },
610
822
  react_2.default.createElement("div", { ref: this.buttonRef, onClick: this.handleAnchorClick, onPointerDown: this.handleDragStart, className: "".concat(chatAnchorVisible, " ").concat(icon ? "" : "p-3", " ").concat(this.handleIsDraggable()
611
823
  ? "touch-none cursor-grab active:cursor-grabbing"
612
824
  : "hover:cursor-pointer", " rounded-full select-none h-fit w-fit bg-primary-500 shadow-lg hover:bg-primary-600") }, this.renderChatAnchorIcon()),