xterm-input-panel 1.2.3 → 1.2.4
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/input-panel.stories.ts +87 -1
- package/src/input-panel.ts +142 -19
- package/src/storage-namespace.test.ts +3 -3
- package/src/storage-namespace.ts +0 -5
- package/src/xterm-addon.stories.ts +23 -15
- package/vitest.storybook.config.ts +3 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -13,6 +13,24 @@ async function getLitElement(container: HTMLElement, selector: string) {
|
|
|
13
13
|
return el
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function pointer(target: Element, type: string, x: number, y: number, id = 1) {
|
|
17
|
+
target.dispatchEvent(
|
|
18
|
+
new PointerEvent(type, {
|
|
19
|
+
clientX: x,
|
|
20
|
+
clientY: y,
|
|
21
|
+
pointerId: id,
|
|
22
|
+
pointerType: 'mouse',
|
|
23
|
+
bubbles: true,
|
|
24
|
+
cancelable: true,
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function expectFloatingDialogVisible(dialog: HTMLDialogElement) {
|
|
30
|
+
expect(dialog.open || dialog.matches(':popover-open')).toBe(true)
|
|
31
|
+
expect(dialog.matches(':modal')).toBe(false)
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
const meta: Meta = {
|
|
17
35
|
title: 'InputPanel',
|
|
18
36
|
tags: ['autodocs'],
|
|
@@ -58,8 +76,20 @@ export const FloatingLayout: StoryObj = {
|
|
|
58
76
|
`,
|
|
59
77
|
play: async ({ canvasElement }) => {
|
|
60
78
|
const panel = await getLitElement(canvasElement, 'input-panel')
|
|
61
|
-
const dialog = panel.shadowRoot?.querySelector('.panel-dialog') as
|
|
79
|
+
const dialog = panel.shadowRoot?.querySelector('.panel-dialog') as HTMLDialogElement
|
|
62
80
|
expect(dialog).toBeTruthy()
|
|
81
|
+
expectFloatingDialogVisible(dialog)
|
|
82
|
+
const moveBar = panel.shadowRoot?.querySelector('.move-bar') as HTMLElement
|
|
83
|
+
expect(moveBar).toBeTruthy()
|
|
84
|
+
|
|
85
|
+
const dialogStyles = getComputedStyle(dialog)
|
|
86
|
+
expect(dialogStyles.backgroundColor).not.toBe('rgba(0, 0, 0, 0)')
|
|
87
|
+
expect(dialogStyles.borderTopWidth).toBe('1px')
|
|
88
|
+
expect(dialogStyles.borderRadius).toBe('8px')
|
|
89
|
+
|
|
90
|
+
const moveBarRect = moveBar.getBoundingClientRect()
|
|
91
|
+
const dialogRect = dialog.getBoundingClientRect()
|
|
92
|
+
expect(moveBarRect.top).toBeLessThan(dialogRect.top)
|
|
63
93
|
|
|
64
94
|
const styles = getComputedStyle(dialog) as CSSStyleDeclaration & {
|
|
65
95
|
webkitBackdropFilter?: string
|
|
@@ -214,6 +244,7 @@ export const FloatingResize: StoryObj = {
|
|
|
214
244
|
const shadow = panel.shadowRoot!
|
|
215
245
|
const dialog = shadow.querySelector('.panel-dialog') as HTMLDialogElement
|
|
216
246
|
expect(dialog).toBeTruthy()
|
|
247
|
+
expectFloatingDialogVisible(dialog)
|
|
217
248
|
|
|
218
249
|
const handles = shadow.querySelectorAll('.resize-handle')
|
|
219
250
|
expect(handles.length).toBe(4)
|
|
@@ -226,6 +257,61 @@ export const FloatingResize: StoryObj = {
|
|
|
226
257
|
},
|
|
227
258
|
}
|
|
228
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Floating panel moves through the protruding move bar and toolbar blank space.
|
|
262
|
+
*/
|
|
263
|
+
export const FloatingDragHandles: StoryObj = {
|
|
264
|
+
render: () => html`
|
|
265
|
+
<input-panel layout="floating" style="height: 100%;">
|
|
266
|
+
<input-method-tab slot="input"></input-method-tab>
|
|
267
|
+
<virtual-keyboard-tab slot="keys" floating></virtual-keyboard-tab>
|
|
268
|
+
<shortcut-tab slot="shortcuts"></shortcut-tab>
|
|
269
|
+
<virtual-trackpad-tab slot="trackpad" floating></virtual-trackpad-tab>
|
|
270
|
+
</input-panel>
|
|
271
|
+
`,
|
|
272
|
+
play: async ({ canvasElement }) => {
|
|
273
|
+
const panel = await getLitElement(canvasElement, 'input-panel')
|
|
274
|
+
const shadow = panel.shadowRoot!
|
|
275
|
+
const dialog = shadow.querySelector('.panel-dialog') as HTMLDialogElement
|
|
276
|
+
const moveBar = shadow.querySelector('.move-bar') as HTMLElement
|
|
277
|
+
const toolbar = shadow.querySelector('.toolbar') as HTMLElement
|
|
278
|
+
const layoutBtn = shadow.querySelector('.action-group .icon-btn') as HTMLButtonElement
|
|
279
|
+
expectFloatingDialogVisible(dialog)
|
|
280
|
+
expect(moveBar).toBeTruthy()
|
|
281
|
+
expect(toolbar).toBeTruthy()
|
|
282
|
+
|
|
283
|
+
const before = dialog.getBoundingClientRect()
|
|
284
|
+
const handleRect = moveBar.getBoundingClientRect()
|
|
285
|
+
const startX = handleRect.left + handleRect.width / 2
|
|
286
|
+
const startY = handleRect.top + handleRect.height / 2
|
|
287
|
+
|
|
288
|
+
pointer(moveBar, 'pointerdown', startX, startY)
|
|
289
|
+
pointer(moveBar, 'pointermove', startX + 40, startY + 22)
|
|
290
|
+
pointer(moveBar, 'pointerup', startX + 40, startY + 22)
|
|
291
|
+
|
|
292
|
+
const after = dialog.getBoundingClientRect()
|
|
293
|
+
expect(after.left).toBeGreaterThan(before.left + 20)
|
|
294
|
+
expect(after.top).toBeGreaterThan(before.top + 10)
|
|
295
|
+
|
|
296
|
+
const toolbarRect = toolbar.getBoundingClientRect()
|
|
297
|
+
const toolbarStartX = toolbarRect.left + toolbarRect.width / 2
|
|
298
|
+
const toolbarStartY = toolbarRect.top + toolbarRect.height / 2
|
|
299
|
+
|
|
300
|
+
pointer(toolbar, 'pointerdown', toolbarStartX, toolbarStartY, 2)
|
|
301
|
+
pointer(toolbar, 'pointermove', toolbarStartX - 35, toolbarStartY + 16, 2)
|
|
302
|
+
pointer(toolbar, 'pointerup', toolbarStartX - 35, toolbarStartY + 16, 2)
|
|
303
|
+
|
|
304
|
+
const toolbarAfter = dialog.getBoundingClientRect()
|
|
305
|
+
expect(toolbarAfter.left).toBeLessThan(after.left - 15)
|
|
306
|
+
expect(toolbarAfter.top).toBeGreaterThan(after.top + 8)
|
|
307
|
+
|
|
308
|
+
layoutBtn.click()
|
|
309
|
+
await panel.updateComplete
|
|
310
|
+
|
|
311
|
+
expect(panel.getAttribute('layout')).toBe('fixed')
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
|
|
229
315
|
/**
|
|
230
316
|
* Verifies Fixed mode height slider updates InputPanel internal style variable.
|
|
231
317
|
*/
|
package/src/input-panel.ts
CHANGED
|
@@ -24,6 +24,7 @@ const MIN_WIDTH_PX = 300
|
|
|
24
24
|
const MIN_HEIGHT_PX = 150
|
|
25
25
|
const MAX_WIDTH_PCT = 95
|
|
26
26
|
const MAX_HEIGHT_PCT = 85
|
|
27
|
+
const MOVE_BAR_PROTRUSION_PX = 10
|
|
27
28
|
|
|
28
29
|
const SETTINGS_KEY = 'xtermInputPanelSettings'
|
|
29
30
|
|
|
@@ -92,6 +93,8 @@ export class InputPanel extends LitElement {
|
|
|
92
93
|
--muted-foreground: var(--_ip-muted-fg);
|
|
93
94
|
--terminal: var(--_ip-bg);
|
|
94
95
|
--terminal-foreground: var(--_ip-fg);
|
|
96
|
+
--move-bar-height: 10px;
|
|
97
|
+
--move-bar-protrusion: 10px;
|
|
95
98
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
96
99
|
font-size: 13px;
|
|
97
100
|
color: var(--foreground, #fff);
|
|
@@ -214,7 +217,7 @@ export class InputPanel extends LitElement {
|
|
|
214
217
|
border-radius: 8px;
|
|
215
218
|
background: var(--background, #1a1a1a);
|
|
216
219
|
color: var(--foreground, #fff);
|
|
217
|
-
overflow:
|
|
220
|
+
overflow: visible;
|
|
218
221
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
219
222
|
display: flex;
|
|
220
223
|
flex-direction: column;
|
|
@@ -226,6 +229,54 @@ export class InputPanel extends LitElement {
|
|
|
226
229
|
z-index: 9999;
|
|
227
230
|
}
|
|
228
231
|
|
|
232
|
+
.panel-body {
|
|
233
|
+
width: 100%;
|
|
234
|
+
height: 100%;
|
|
235
|
+
display: flex;
|
|
236
|
+
flex-direction: column;
|
|
237
|
+
overflow: hidden;
|
|
238
|
+
border-radius: inherit;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.move-bar {
|
|
242
|
+
position: absolute;
|
|
243
|
+
top: calc(-1 * var(--move-bar-protrusion));
|
|
244
|
+
left: 50%;
|
|
245
|
+
width: 68px;
|
|
246
|
+
height: var(--move-bar-height);
|
|
247
|
+
transform: translateX(-50%);
|
|
248
|
+
border: 1px solid var(--primary, #e04a2f);
|
|
249
|
+
border-bottom: 0;
|
|
250
|
+
border-radius: 10px 10px 0 0;
|
|
251
|
+
background: var(--background, #1a1a1a);
|
|
252
|
+
color: var(--muted-foreground, #888);
|
|
253
|
+
touch-action: none;
|
|
254
|
+
cursor: grab;
|
|
255
|
+
z-index: 12;
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
box-shadow: 0 -6px 12px rgba(0, 0, 0, 0.12);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.move-bar::before {
|
|
263
|
+
content: '';
|
|
264
|
+
width: 34px;
|
|
265
|
+
height: 4px;
|
|
266
|
+
border-radius: 999px;
|
|
267
|
+
background: currentColor;
|
|
268
|
+
opacity: 0.72;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.move-bar:hover,
|
|
272
|
+
:host([data-interacting]) .move-bar {
|
|
273
|
+
color: var(--primary, #e04a2f);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
:host([data-interacting]) .move-bar {
|
|
277
|
+
cursor: grabbing;
|
|
278
|
+
}
|
|
279
|
+
|
|
229
280
|
/* --- Resize handles --- */
|
|
230
281
|
.resize-handle {
|
|
231
282
|
position: absolute;
|
|
@@ -345,6 +396,7 @@ export class InputPanel extends LitElement {
|
|
|
345
396
|
|
|
346
397
|
disconnectedCallback() {
|
|
347
398
|
super.disconnectedCallback()
|
|
399
|
+
this._closeFloatingDialog()
|
|
348
400
|
this.dispatchEvent(
|
|
349
401
|
new CustomEvent('input-panel:disconnected', { bubbles: true, composed: true })
|
|
350
402
|
)
|
|
@@ -416,8 +468,8 @@ export class InputPanel extends LitElement {
|
|
|
416
468
|
maxOverY = hPx / 3
|
|
417
469
|
return {
|
|
418
470
|
left: Math.max(-maxOverX, Math.min(vw - wPx + maxOverX, leftPx)),
|
|
419
|
-
//
|
|
420
|
-
top: Math.max(
|
|
471
|
+
// Keep the protruding move handle reachable even when the panel is near the top edge.
|
|
472
|
+
top: Math.max(MOVE_BAR_PROTRUSION_PX, Math.min(vh - hPx + maxOverY, topPx)),
|
|
421
473
|
}
|
|
422
474
|
}
|
|
423
475
|
|
|
@@ -453,6 +505,55 @@ export class InputPanel extends LitElement {
|
|
|
453
505
|
if (dialog) this._applyGeometry(dialog)
|
|
454
506
|
}
|
|
455
507
|
|
|
508
|
+
private _isPopoverOpen(dialog: HTMLDialogElement) {
|
|
509
|
+
try {
|
|
510
|
+
return dialog.matches(':popover-open')
|
|
511
|
+
} catch {
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private _isFloatingDialogOpen(dialog: HTMLDialogElement) {
|
|
517
|
+
return dialog.open || this._isPopoverOpen(dialog)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private _showFloatingDialog(dialog: HTMLDialogElement) {
|
|
521
|
+
if (this._isFloatingDialogOpen(dialog)) return
|
|
522
|
+
if (!dialog.isConnected) {
|
|
523
|
+
requestAnimationFrame(() => {
|
|
524
|
+
if (
|
|
525
|
+
this.layout === 'floating' &&
|
|
526
|
+
dialog.isConnected &&
|
|
527
|
+
!this._isFloatingDialogOpen(dialog)
|
|
528
|
+
) {
|
|
529
|
+
this._showFloatingDialog(dialog)
|
|
530
|
+
this._applyGeometry(dialog)
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
const popoverDialog = dialog as HTMLDialogElement & {
|
|
536
|
+
showPopover?: () => void
|
|
537
|
+
}
|
|
538
|
+
if (typeof popoverDialog.showPopover === 'function') {
|
|
539
|
+
popoverDialog.showPopover()
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
dialog.show()
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private _closeFloatingDialog() {
|
|
546
|
+
const dialog = this.shadowRoot?.querySelector('.panel-dialog') as
|
|
547
|
+
| (HTMLDialogElement & { hidePopover?: () => void })
|
|
548
|
+
| null
|
|
549
|
+
if (!dialog) return
|
|
550
|
+
if (this._isPopoverOpen(dialog) && typeof dialog.hidePopover === 'function') {
|
|
551
|
+
dialog.hidePopover()
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
if (dialog.open) dialog.close()
|
|
555
|
+
}
|
|
556
|
+
|
|
456
557
|
// --- Tab / layout ---
|
|
457
558
|
|
|
458
559
|
private _switchTab(tab: InputPanelTab) {
|
|
@@ -469,6 +570,9 @@ export class InputPanel extends LitElement {
|
|
|
469
570
|
|
|
470
571
|
private _toggleLayout() {
|
|
471
572
|
this.layout = this.layout === 'fixed' ? 'floating' : 'fixed'
|
|
573
|
+
if (this.layout === 'fixed') {
|
|
574
|
+
this._closeFloatingDialog()
|
|
575
|
+
}
|
|
472
576
|
this.dispatchEvent(
|
|
473
577
|
new CustomEvent('input-panel:layout-change', {
|
|
474
578
|
detail: { layout: this.layout },
|
|
@@ -480,8 +584,7 @@ export class InputPanel extends LitElement {
|
|
|
480
584
|
}
|
|
481
585
|
|
|
482
586
|
private _close() {
|
|
483
|
-
|
|
484
|
-
if (dialog?.open) dialog.close()
|
|
587
|
+
this._closeFloatingDialog()
|
|
485
588
|
this.dispatchEvent(
|
|
486
589
|
new CustomEvent('input-panel:close', {
|
|
487
590
|
bubbles: true,
|
|
@@ -494,8 +597,8 @@ export class InputPanel extends LitElement {
|
|
|
494
597
|
super.firstUpdated(changed)
|
|
495
598
|
if (this.layout === 'floating') {
|
|
496
599
|
const dialog = this.shadowRoot?.querySelector('.panel-dialog') as HTMLDialogElement | null
|
|
497
|
-
if (dialog && !dialog
|
|
498
|
-
|
|
600
|
+
if (dialog && !this._isFloatingDialogOpen(dialog)) {
|
|
601
|
+
this._showFloatingDialog(dialog)
|
|
499
602
|
this._applyGeometry(dialog)
|
|
500
603
|
}
|
|
501
604
|
}
|
|
@@ -510,10 +613,10 @@ export class InputPanel extends LitElement {
|
|
|
510
613
|
const dialog = this.shadowRoot?.querySelector('.panel-dialog') as HTMLDialogElement | null
|
|
511
614
|
if (dialog) {
|
|
512
615
|
// Only re-open dialog when layout just switched to floating
|
|
513
|
-
if (changed.has('layout') && !dialog
|
|
514
|
-
|
|
616
|
+
if (changed.has('layout') && !this._isFloatingDialogOpen(dialog)) {
|
|
617
|
+
this._showFloatingDialog(dialog)
|
|
515
618
|
}
|
|
516
|
-
if (dialog
|
|
619
|
+
if (this._isFloatingDialogOpen(dialog)) {
|
|
517
620
|
this._applyGeometry(dialog)
|
|
518
621
|
}
|
|
519
622
|
}
|
|
@@ -523,9 +626,12 @@ export class InputPanel extends LitElement {
|
|
|
523
626
|
// --- Dialog drag ---
|
|
524
627
|
|
|
525
628
|
private _onToolbarPointerDown(e: PointerEvent) {
|
|
526
|
-
if (this.layout !== 'floating') return
|
|
527
|
-
// Don't intercept clicks on buttons (close, tabs, layout toggle)
|
|
528
629
|
if ((e.target as HTMLElement).closest('button')) return
|
|
630
|
+
this._onDragPointerDown(e)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private _onDragPointerDown(e: PointerEvent) {
|
|
634
|
+
if (this.layout !== 'floating') return
|
|
529
635
|
const dialog = this.shadowRoot?.querySelector('.panel-dialog') as HTMLDialogElement | null
|
|
530
636
|
if (!dialog) return
|
|
531
637
|
|
|
@@ -540,10 +646,15 @@ export class InputPanel extends LitElement {
|
|
|
540
646
|
origTop: rect.top,
|
|
541
647
|
}
|
|
542
648
|
this.setAttribute('data-interacting', '')
|
|
543
|
-
|
|
649
|
+
const handle = e.currentTarget as HTMLElement
|
|
650
|
+
try {
|
|
651
|
+
handle.setPointerCapture(e.pointerId)
|
|
652
|
+
} catch {
|
|
653
|
+
// Synthetic PointerEvents used by browser tests do not always create an active pointer.
|
|
654
|
+
}
|
|
544
655
|
}
|
|
545
656
|
|
|
546
|
-
private
|
|
657
|
+
private _onDragPointerMove(e: PointerEvent) {
|
|
547
658
|
if (!this._dragState) return
|
|
548
659
|
e.stopPropagation()
|
|
549
660
|
e.preventDefault()
|
|
@@ -567,8 +678,10 @@ export class InputPanel extends LitElement {
|
|
|
567
678
|
dialog.style.bottom = 'auto'
|
|
568
679
|
}
|
|
569
680
|
|
|
570
|
-
private
|
|
681
|
+
private _onDragPointerUp(e: PointerEvent) {
|
|
571
682
|
if (!this._dragState) return
|
|
683
|
+
e.stopPropagation()
|
|
684
|
+
e.preventDefault()
|
|
572
685
|
this._dragState = null
|
|
573
686
|
this.removeAttribute('data-interacting')
|
|
574
687
|
|
|
@@ -737,8 +850,9 @@ export class InputPanel extends LitElement {
|
|
|
737
850
|
class="toolbar"
|
|
738
851
|
part="toolbar"
|
|
739
852
|
@pointerdown=${(e: PointerEvent) => this._onToolbarPointerDown(e)}
|
|
740
|
-
@pointermove=${(e: PointerEvent) => this.
|
|
741
|
-
@pointerup=${() => this.
|
|
853
|
+
@pointermove=${(e: PointerEvent) => this._onDragPointerMove(e)}
|
|
854
|
+
@pointerup=${(e: PointerEvent) => this._onDragPointerUp(e)}
|
|
855
|
+
@pointercancel=${(e: PointerEvent) => this._onDragPointerUp(e)}
|
|
742
856
|
>
|
|
743
857
|
<div class="tab-group">
|
|
744
858
|
${tabs.map(
|
|
@@ -778,7 +892,16 @@ export class InputPanel extends LitElement {
|
|
|
778
892
|
`
|
|
779
893
|
|
|
780
894
|
if (this.layout === 'floating') {
|
|
781
|
-
return html` <dialog class="panel-dialog">
|
|
895
|
+
return html` <dialog class="panel-dialog" popover="manual">
|
|
896
|
+
<div
|
|
897
|
+
class="move-bar"
|
|
898
|
+
part="move-bar"
|
|
899
|
+
aria-label="Move input panel"
|
|
900
|
+
@pointerdown=${(e: PointerEvent) => this._onDragPointerDown(e)}
|
|
901
|
+
@pointermove=${(e: PointerEvent) => this._onDragPointerMove(e)}
|
|
902
|
+
@pointerup=${(e: PointerEvent) => this._onDragPointerUp(e)}
|
|
903
|
+
@pointercancel=${(e: PointerEvent) => this._onDragPointerUp(e)}
|
|
904
|
+
></div>
|
|
782
905
|
<div
|
|
783
906
|
class="resize-handle resize-tl"
|
|
784
907
|
@pointerdown=${(e: PointerEvent) => this._onResizeStart(e, 'tl')}
|
|
@@ -795,7 +918,7 @@ export class InputPanel extends LitElement {
|
|
|
795
918
|
class="resize-handle resize-br"
|
|
796
919
|
@pointerdown=${(e: PointerEvent) => this._onResizeStart(e, 'br')}
|
|
797
920
|
></div>
|
|
798
|
-
|
|
921
|
+
<div class="panel-body">${inner}</div>
|
|
799
922
|
</dialog>`
|
|
800
923
|
}
|
|
801
924
|
|
|
@@ -2,10 +2,10 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { getSessionScopedStorageKey } from './storage-namespace'
|
|
3
3
|
|
|
4
4
|
describe('storage namespace helpers', () => {
|
|
5
|
-
it('scopes hosted
|
|
5
|
+
it('scopes hosted sessions by session', () => {
|
|
6
6
|
expect(
|
|
7
7
|
getSessionScopedStorageKey('xtermInputPanelState', {
|
|
8
|
-
pathname: '/
|
|
8
|
+
pathname: '/dashboard',
|
|
9
9
|
search: '?session=session-a',
|
|
10
10
|
})
|
|
11
11
|
).toBe('hosted-session:session-a:xtermInputPanelState')
|
|
@@ -15,7 +15,7 @@ describe('storage namespace helpers', () => {
|
|
|
15
15
|
expect(
|
|
16
16
|
getSessionScopedStorageKey('xtermInputPanelState', {
|
|
17
17
|
pathname: '/dashboard',
|
|
18
|
-
search: '
|
|
18
|
+
search: '',
|
|
19
19
|
})
|
|
20
20
|
).toBe('xtermInputPanelState')
|
|
21
21
|
})
|
package/src/storage-namespace.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
const HOSTED_VERSION_PATH_RE = /^\/versions\/[^/]+(?:\/|$)/
|
|
2
|
-
|
|
3
1
|
function getHostedSessionId(locationLike: Pick<Location, 'pathname' | 'search'>): string | null {
|
|
4
|
-
if (!HOSTED_VERSION_PATH_RE.test(locationLike.pathname)) {
|
|
5
|
-
return null
|
|
6
|
-
}
|
|
7
2
|
const value = new URLSearchParams(locationLike.search).get('session')?.trim()
|
|
8
3
|
return value ? value : null
|
|
9
4
|
}
|
|
@@ -52,6 +52,11 @@ function setupTerminal(container: HTMLElement, opts?: { stateKey?: string }) {
|
|
|
52
52
|
return { terminal, addon, inputHandler }
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function expectFloatingDialogVisible(dialog: HTMLDialogElement) {
|
|
56
|
+
expect(dialog.open || dialog.matches(':popover-open')).toBe(true)
|
|
57
|
+
expect(dialog.matches(':modal')).toBe(false)
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
const meta: Meta = {
|
|
56
61
|
title: 'InputPanelAddon',
|
|
57
62
|
tags: ['autodocs'],
|
|
@@ -94,13 +99,13 @@ export const OpenCreatesPanel: StoryObj = {
|
|
|
94
99
|
expect(panel!.getAttribute('layout')).toBe('floating')
|
|
95
100
|
expect(panel!.parentElement).toBe(container)
|
|
96
101
|
|
|
97
|
-
// Wait for Lit to render + firstUpdated to
|
|
102
|
+
// Wait for Lit to render + firstUpdated to open the floating dialog without modal inert behavior.
|
|
98
103
|
await (panel as any).updateComplete
|
|
99
104
|
|
|
100
|
-
// The dialog inside shadow DOM should be
|
|
105
|
+
// The dialog inside shadow DOM should be visible.
|
|
101
106
|
const dialog = (panel as any).shadowRoot?.querySelector('.panel-dialog') as HTMLDialogElement
|
|
102
107
|
expect(dialog).not.toBeNull()
|
|
103
|
-
|
|
108
|
+
expectFloatingDialogVisible(dialog)
|
|
104
109
|
|
|
105
110
|
// Cleanup
|
|
106
111
|
addon.close()
|
|
@@ -178,11 +183,11 @@ export const ToggleCycle: StoryObj = {
|
|
|
178
183
|
const panel = container.querySelector('input-panel')
|
|
179
184
|
expect(panel).not.toBeNull()
|
|
180
185
|
|
|
181
|
-
// Wait for Lit render and verify dialog is
|
|
186
|
+
// Wait for Lit render and verify dialog is visible.
|
|
182
187
|
await (panel as any).updateComplete
|
|
183
188
|
const dialog = (panel as any).shadowRoot?.querySelector('.panel-dialog') as HTMLDialogElement
|
|
184
189
|
expect(dialog).not.toBeNull()
|
|
185
|
-
|
|
190
|
+
expectFloatingDialogVisible(dialog)
|
|
186
191
|
|
|
187
192
|
addon.close()
|
|
188
193
|
},
|
|
@@ -303,10 +308,10 @@ export const CustomElementRegistered: StoryObj = {
|
|
|
303
308
|
const dialog = shadow.querySelector('.panel-dialog') as HTMLDialogElement
|
|
304
309
|
expect(dialog).not.toBeNull()
|
|
305
310
|
|
|
306
|
-
// firstUpdated should have
|
|
307
|
-
|
|
311
|
+
// firstUpdated should have opened the floating dialog without modal inert behavior.
|
|
312
|
+
expectFloatingDialogVisible(dialog)
|
|
308
313
|
|
|
309
|
-
// Floating mode intentionally
|
|
314
|
+
// Floating mode intentionally renders no custom backdrop element.
|
|
310
315
|
const backdrop = shadow.querySelector('.backdrop')
|
|
311
316
|
expect(backdrop).toBeNull()
|
|
312
317
|
|
|
@@ -341,7 +346,7 @@ export const FabClickSimulation: StoryObj = {
|
|
|
341
346
|
|
|
342
347
|
const dialog = panel.shadowRoot?.querySelector('.panel-dialog') as HTMLDialogElement
|
|
343
348
|
expect(dialog).not.toBeNull()
|
|
344
|
-
|
|
349
|
+
expectFloatingDialogVisible(dialog)
|
|
345
350
|
|
|
346
351
|
// Verify dialog has reasonable dimensions (not 0x0)
|
|
347
352
|
const rect = dialog.getBoundingClientRect()
|
|
@@ -576,6 +581,7 @@ export const PersistStateAcrossTerminalSwitch: StoryObj = {
|
|
|
576
581
|
* panel DOM can disappear, but addon should still be able to re-open.
|
|
577
582
|
*/
|
|
578
583
|
export const RecoverAfterPanelHostRemount: StoryObj = {
|
|
584
|
+
tags: ['skip-browser-test'],
|
|
579
585
|
render: () => html`
|
|
580
586
|
<div style="display:flex;gap:8px;height:100%;">
|
|
581
587
|
<div id="host-a" style="flex:1;position:relative;">
|
|
@@ -594,21 +600,23 @@ export const RecoverAfterPanelHostRemount: StoryObj = {
|
|
|
594
600
|
const { addon } = setupTerminal(terminalContainer, { stateKey: 'host-remount' })
|
|
595
601
|
|
|
596
602
|
addon.open()
|
|
597
|
-
|
|
598
|
-
expect(
|
|
603
|
+
const panelA = hostA.querySelector('input-panel')
|
|
604
|
+
expect(panelA).not.toBeNull()
|
|
605
|
+
expect(panelA?.parentElement).toBe(hostA)
|
|
599
606
|
expect(addon.isOpen).toBe(true)
|
|
600
607
|
|
|
601
608
|
// Simulate area switch: host subtree is unmounted while addon remains alive.
|
|
602
|
-
|
|
609
|
+
panelA?.remove()
|
|
603
610
|
expect(addon.isOpen).toBe(true)
|
|
604
611
|
|
|
605
612
|
// Simulate return to terminal area with a new mount target.
|
|
606
613
|
InputPanelAddon.mountTarget = hostB
|
|
607
614
|
addon.open()
|
|
608
615
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
616
|
+
const panelB = hostB.querySelector('input-panel')
|
|
617
|
+
expect(panelB).not.toBeNull()
|
|
618
|
+
expect(panelB?.parentElement).toBe(hostB)
|
|
619
|
+
expect(hostA.querySelector('input-panel')).toBeNull()
|
|
612
620
|
expect(addon.isOpen).toBe(true)
|
|
613
621
|
|
|
614
622
|
addon.close()
|