feat(ext/popup): hide popout-to-tab button in fullscreen forms
The ⤴ popout button is meaningless when the form is already in vault.html — gate it on !isInTab(). Affects all seven type forms plus the type-selection screen. Regression tests cover both popup (button present) and fullscreen (button absent) contexts via it.each across all 7 forms. Plan 2026-04-30 fullscreen UX phase 1 task 7. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/// Typed-item add/edit form dispatcher. Each type's renderForm lives in
|
||||
/// its own module under ./types/. Document stays "coming soon" until γ.
|
||||
|
||||
import { navigate, getState, setState, escapeHtml, popOutToTab } from '../../shared/state';
|
||||
import { navigate, getState, setState, escapeHtml, popOutToTab, isInTab } from '../../shared/state';
|
||||
import type { Item, ItemType } from '../../shared/types';
|
||||
|
||||
const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string }> = [
|
||||
@@ -58,7 +58,7 @@ function renderTypeSelection(app: HTMLElement): void {
|
||||
<button class="btn" id="back-btn">← back</button>
|
||||
<h3 style="margin:0;">new item</h3>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
<div class="type-select-list">
|
||||
${TYPE_OPTIONS.map((opt) => `
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('../../../../shared/state', () => ({
|
||||
sendMessage: vi.fn(),
|
||||
getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }),
|
||||
setState: vi.fn(),
|
||||
navigate: vi.fn(),
|
||||
escapeHtml: (s: string) => s,
|
||||
popOutToTab: vi.fn(),
|
||||
isInTab: () => true, // FULLSCREEN context
|
||||
openVaultTab: vi.fn(),
|
||||
registerHost: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../generator-panel', () => ({
|
||||
openGeneratorPanel: vi.fn(),
|
||||
closeGeneratorPanel: vi.fn(),
|
||||
isGeneratorPanelOpen: () => false,
|
||||
}));
|
||||
|
||||
import * as login from '../login';
|
||||
import * as secureNote from '../secure-note';
|
||||
import * as identity from '../identity';
|
||||
import * as card from '../card';
|
||||
import * as key from '../key';
|
||||
import * as totp from '../totp';
|
||||
import * as documentType from '../document';
|
||||
|
||||
const forms: Array<[string, (app: HTMLElement, mode: 'add' | 'edit', existing: null) => void]> = [
|
||||
['login', login.renderForm],
|
||||
['secure-note', secureNote.renderForm],
|
||||
['identity', identity.renderForm],
|
||||
['card', card.renderForm],
|
||||
['key', key.renderForm],
|
||||
['totp', totp.renderForm],
|
||||
['document', documentType.renderForm],
|
||||
];
|
||||
|
||||
describe('popout-to-tab button (fullscreen context)', () => {
|
||||
beforeEach(() => { document.body.innerHTML = '<div id="app"></div>'; });
|
||||
|
||||
it.each(forms)('%s form does NOT render the popout button', (_name, render) => {
|
||||
render(document.getElementById('app')!, 'add', null);
|
||||
expect(document.getElementById('popout-btn')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('../../../../shared/state', () => ({
|
||||
sendMessage: vi.fn(),
|
||||
getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }),
|
||||
setState: vi.fn(),
|
||||
navigate: vi.fn(),
|
||||
escapeHtml: (s: string) => s,
|
||||
popOutToTab: vi.fn(),
|
||||
isInTab: () => false, // POPUP context
|
||||
openVaultTab: vi.fn(),
|
||||
registerHost: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../generator-panel', () => ({
|
||||
openGeneratorPanel: vi.fn(),
|
||||
closeGeneratorPanel: vi.fn(),
|
||||
isGeneratorPanelOpen: () => false,
|
||||
}));
|
||||
|
||||
import * as login from '../login';
|
||||
import * as secureNote from '../secure-note';
|
||||
import * as identity from '../identity';
|
||||
import * as card from '../card';
|
||||
import * as key from '../key';
|
||||
import * as totp from '../totp';
|
||||
import * as documentType from '../document';
|
||||
|
||||
const forms: Array<[string, (app: HTMLElement, mode: 'add' | 'edit', existing: null) => void]> = [
|
||||
['login', login.renderForm],
|
||||
['secure-note', secureNote.renderForm],
|
||||
['identity', identity.renderForm],
|
||||
['card', card.renderForm],
|
||||
['key', key.renderForm],
|
||||
['totp', totp.renderForm],
|
||||
['document', documentType.renderForm],
|
||||
];
|
||||
|
||||
describe('popout-to-tab button (popup context)', () => {
|
||||
beforeEach(() => { document.body.innerHTML = '<div id="app"></div>'; });
|
||||
|
||||
it.each(forms)('%s form renders the popout button', (_name, render) => {
|
||||
render(document.getElementById('app')!, 'add', null);
|
||||
expect(document.getElementById('popout-btn')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Card: number / holder / expiry MonthYear / cvv / pin / kind.
|
||||
/// Detail view has a styled card-silhouette signature block.
|
||||
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
||||
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||
import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types';
|
||||
import {
|
||||
@@ -177,7 +177,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
||||
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||
<div class="detail-title">${mode === 'add' ? 'new card' : 'edit card'}</div>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// notes/tags + optional supplementary attachments.
|
||||
/// Primary attachment is referenced by ID from the item's attachments array.
|
||||
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
||||
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||
import {
|
||||
@@ -88,7 +88,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
||||
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||
<div class="detail-title">${isEdit ? 'edit document' : 'new document'}</div>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div class="form-group">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Identity: full_name, address (multiline), phone, email, date_of_birth.
|
||||
/// Detail view shows a "profile card" signature block + plain rows.
|
||||
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
||||
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||
import {
|
||||
@@ -137,7 +137,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
||||
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||
<div class="detail-title">${mode === 'add' ? 'new identity' : 'edit identity'}</div>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// Form's key_material textarea uses CSS text-security to mask characters
|
||||
/// since <textarea type="password"> isn't a thing.
|
||||
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
||||
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||
import {
|
||||
@@ -126,7 +126,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
||||
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||
<div class="detail-title">${mode === 'add' ? 'new key' : 'edit key'}</div>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
||||
|
||||
@@ -247,7 +247,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
||||
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||
<div class="detail-title">${mode === 'add' ? 'new login' : 'edit login'}</div>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
||||
|
||||
@@ -115,7 +115,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
||||
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||
<div class="detail-title">${mode === 'add' ? 'new secure note' : 'edit secure note'}</div>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// signature block with a thin SVG countdown ring; form has a kind toggle
|
||||
/// (TOTP vs Steam Guard) and a single secret input.
|
||||
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
||||
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||
import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types';
|
||||
import { base32Decode, base32Encode } from '../../../shared/base32';
|
||||
@@ -216,7 +216,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
||||
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
||||
<div class="detail-title">${mode === 'add' ? 'new totp' : 'edit totp'}</div>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="popout-btn" title="Open in tab">⤴</button>
|
||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||
</div>
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
||||
|
||||
Reference in New Issue
Block a user