diff --git a/README.md b/README.md index 0099704..b5c076c 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,8 @@ a breadcrumb trail, a hidden-entries toggle — driven through stored in the form that keeps its meaning: `/work/root/src/shared` becomes `src/shared` when the session started in `/work/root`. The picker's own path field stays editable, because the Host lists folders and an extra may legitimately name -a file. +a file — which is why that action reads **Add path or file** and its field **Path +or file**, rather than promising a folder the list would then fail to show. Because the panel owns widening, `facet action=add` is the *other* path and the narrower one: it is gated on an approval channel, and a deployment with none — the diff --git a/client/client.js b/client/client.js index 7171f0c..149d312 100644 --- a/client/client.js +++ b/client/client.js @@ -680,7 +680,7 @@ window.__ModuleLoader__.load({ return h(Modal, { open: true, onClose, - title: target === 'focus' ? 'Choose the focus folder' : 'Add a folder or a path', + title: target === 'focus' ? 'Choose the focus folder' : 'Add a path or a file', closeLabel: 'Close', className: PICKER_CLASS, // One scroll region around everything but the footer, so the card is @@ -694,7 +694,7 @@ window.__ModuleLoader__.load({ h( 'div', { key: 'path' }, - h('label', { style: labelStyle, htmlFor: 'facet-picker-path' }, target === 'focus' ? 'Folder' : 'Path'), + h('label', { style: labelStyle, htmlFor: 'facet-picker-path' }, target === 'focus' ? 'Folder' : 'Path or file'), h('input', { id: 'facet-picker-path', type: 'text', @@ -713,7 +713,16 @@ window.__ModuleLoader__.load({ model.open(typed) }, }), - h('p', { style: hintStyle }, 'Relative to the session folder, or absolute. Enter lists whatever is there.'), + h( + 'p', + { style: hintStyle }, + target === 'focus' + ? 'Relative to the session folder, or absolute. Enter lists whatever is there.' + : // The list holds folders only — the Host's browse primitive has no + // file rows — so the box is where a file is named, and saying so + // is the difference between a dead end and an instruction. + 'Relative to the session folder, or absolute. The list holds folders; type a file\u2019s name to add the file itself.', + ), ), h( 'div', @@ -1015,7 +1024,7 @@ window.__ModuleLoader__.load({ h( 'button', { type: 'button', style: quietButtonStyle, disabled: !canBrowse, onClick: () => openPicker('extra') }, - '+ Add folder or path\u2026', + '+ Add path or file\u2026', ), ), h( diff --git a/test/client.test.js b/test/client.test.js index 4827694..183c0db 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -423,7 +423,7 @@ test('the panel renders the scope, the guidance, and a way to add and remove', a // already did. assert.ok(buttons.some((button) => labelOf(button) === 'Browse\u2026')) assert.ok(buttons.some((button) => labelOf(button) === 'Clear')) - assert.ok(buttons.some((button) => labelOf(button) === '+ Add folder or path\u2026')) + assert.ok(buttons.some((button) => labelOf(button) === '+ Add path or file\u2026')) assert.ok(!buttons.some((button) => labelOf(button) === 'Set')) assert.deepEqual(inputs, [], 'the panel itself has no free-text box left') }) @@ -456,7 +456,7 @@ test('a deployment with no directory browser keeps the panel and says so', async ) assert.deepEqual( buttonsOf(tree) - .filter((button) => label(button) === '+ Add folder or path\u2026') + .filter((button) => label(button) === '+ Add path or file\u2026') .map((button) => button.props.disabled), [true], ) @@ -520,7 +520,10 @@ function walkTree(node, visit) { } visit(node) if (Array.isArray(node.children)) node.children.forEach((child) => walkTree(child, visit)) + // `footer` is a prop rather than a child, and it is rendered: a control there + // is as much part of the tree as one in the body. walkTree(node.props?.children, visit) + walkTree(node.props?.footer, visit) } test('a path is declared relative to the session folder wherever it can be', async () => { @@ -661,3 +664,37 @@ test('the picker lists folders, hides hidden ones until asked, and answers in de await settle() }) +test('the extras picker names what it accepts and says what the list holds', async () => { + const { module: mod } = await loadClient() + + const model = new mod.DirectoryPickerModel({ + list: () => Promise.resolve(listingFixture()), + root: '/work/root', + start: '/work/root/src', + }) + await settle() + + const tree = mod.DirectoryPicker({ + model, + root: '/work/root', + target: 'extra', + disabled: false, + onClose: () => {}, + onPick: () => {}, + }) + + // "folder or path" promised one thing and offered another: the rows are + // folders, and a file is added by naming it. The copy says both. + assert.equal(tree.props.title, 'Add a path or a file') + const texts = textsOf(tree) + assert.ok(texts.includes('Path or file'), 'the field is named for what it accepts') + assert.ok( + texts.some((text) => text.includes('The list holds folders')), + 'the copy explains why no file rows are shown', + ) + assert.ok( + buttonsOf(tree).some((button) => (button.children ?? [])[0] === 'Add'), + 'the action is Add for an extra, not Set focus', + ) +}) +