Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"copy": "Copy selected item(s)",
"paste": "Paste item(s)",
"cut": "Cut selected item(s)",
"toggleSearch": "Toggle search filter",
"showShortcuts": "Show keyboard shortcuts"
},
"pinningModal": {
Expand Down
21 changes: 18 additions & 3 deletions src/files/FilesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ const FilesPage = ({
setViewMode(newMode)
}

const toggleSearch = () => setShowSearch(prev => {
// Toggle search filter visibility. When hiding, clear the filter text.
const toggleSearch = useCallback(() => setShowSearch(prev => {
if (prev) filterRef.current = ''
return !prev
})
}), [])

useEffect(() => {
doFetchPinningServices()
Expand All @@ -81,11 +82,25 @@ const FilesPage = ({
e.preventDefault()
showModal(SHORTCUTS)
}

if (e.key === '/') {
e.preventDefault()
if (showSearch) {
// Search is visible but input lost focus, re-focus it.
document.getElementById('search-filter-input')?.focus()
} else {
// Open search and focus the input after React renders it.
toggleSearch()
requestAnimationFrame(() => {
document.getElementById('search-filter-input')?.focus()
})
}
}
}

document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
}, [toggleSearch, showSearch])

// Persist view mode changes to localStorage
useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions src/files/modals/shortcut-modal/shortcut-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const ShortcutModal = ({ onLeave, className, ...props }) => {
]

const otherShortcuts = [
{ shortcut: '/', description: t('shortcutModal.toggleSearch') },
{ shortcut: ['Shift', '?'], description: t('shortcutModal.showShortcuts') }
]

Expand Down
10 changes: 10 additions & 0 deletions src/files/search-filter/SearchFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,25 @@ const SearchFilter = ({ initialValue = '', onFilterChange, filteredCount, totalC
onFilterChange('')
}, [onFilterChange])

// Allow users to quickly clear the filter text by pressing Escape,
// so the full file listing is restored without needing to click the clear button.
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
clearFilter()
}
}, [clearFilter])

return (
<div className={classnames('flex items-center pa2 bg-snow-muted', className)}>
<div className='flex items-center relative flex-auto'>
<input
id='search-filter-input'
className='input-reset ba b--black-20 pa2 db w-100 br1'
type='text'
placeholder={t('searchFiles')}
value={filter}
onChange={handleFilterChange}
onKeyDown={handleKeyDown}
aria-label={t('searchFiles')}
/>
{filter && (
Expand Down
22 changes: 19 additions & 3 deletions src/peers/PeersPage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { connect } from 'redux-bundler-react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
Expand All @@ -15,7 +15,22 @@ import AddConnection from './AddConnection/AddConnection.js'
import CliTutorMode from '../components/cli-tutor-mode/CliTutorMode.js'
import { cliCmdKeys, cliCommandList } from '../bundles/files/consts.js'

const PeersPage = ({ t, toursEnabled, handleJoyrideCallback }) => (
const PeersPage = ({ t, toursEnabled, handleJoyrideCallback }) => {
useEffect(() => {
const handleKeyDown = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return
}
if (e.key === '/') {
e.preventDefault()
document.getElementById('peers-filter-input')?.focus()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])

return (
<div data-id='PeersPage' className='overflow-hidden'>
<Helmet>
<title>{t('title')} | IPFS</title>
Expand All @@ -41,7 +56,8 @@ const PeersPage = ({ t, toursEnabled, handleJoyrideCallback }) => (
locale={getJoyrideLocales(t)}
showProgress />
</div>
)
)
}

export default connect(
'selectToursEnabled',
Expand Down
13 changes: 11 additions & 2 deletions src/peers/PeersTable/PeersTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,23 @@ const rowClassRenderer = ({ index }, peers = [], selectedPeers) => {
return classNames('bb b--near-white peersTableItem', index === -1 && 'bg-near-white', shouldAddHoverEffect && 'bg-light-gray')
}

const FilterInput = ({ setFilter, t, filteredCount }) => {
const FilterInput = ({ filter, setFilter, t, filteredCount }) => {
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
setFilter('')
}
}

return (
<div className='flex items-center justify-between pa2'>
<input
id='peers-filter-input'
className='input-reset ba b--black-20 pa2 mb2 db w-100'
type='text'
placeholder='Filter peers'
value={filter}
onChange={(e) => setFilter(e.target.value)}
onKeyDown={handleKeyDown}
/>
{/* Now to display the total number of peers filtered out on the right side of the inside of the input */}
<div className='f4 charcoal-muted absolute top-1 right-1'>{filteredCount}</div>
Expand Down Expand Up @@ -187,7 +196,7 @@ export const PeersTable = ({ className, t, peerLocationsForSwarm, selectedPeers

return (
<div className={`bg-white-70 center ${className}`} style={{ height: `${tableHeight}px`, maxWidth: 1764 }}>
<FilterInput setFilter={filterCb} t={t} filteredCount={sortedList.length} />
<FilterInput filter={filter} setFilter={filterCb} t={t} filteredCount={sortedList.length} />
{ awaitedPeerLocationsForSwarm && <AutoSizer disableHeight>
{({ width }) => (
<>
Expand Down
86 changes: 85 additions & 1 deletion test/e2e/files.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from './setup/coverage.js'
import { fixtureData } from './fixtures/index.js'
import { files, explore, dismissImportNotification } from './setup/locators.js'
import { files, explore, nav, dismissImportNotification } from './setup/locators.js'
import { selectViewMode, toggleSearchFilter } from '../helpers/grid'
import all from 'it-all'
import filesize from 'filesize'
Expand Down Expand Up @@ -404,6 +404,90 @@ test.describe('Files screen', () => {
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).toBeVisible()
})

test('pressing Escape clears the filter', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)

await files.searchInput(page).fill('orange')

// only the orange file should be visible
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()

// press Escape while focused on the search input
await files.searchInput(page).press('Escape')

// filter should be cleared and both files visible again
await expect(files.searchInput(page)).toHaveValue('')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).toBeVisible()
})

test('pressing / opens search and auto-focuses input', async ({ page }) => {
await selectViewMode(page, 'list')

// search should be hidden initially
await expect(files.searchInput(page)).not.toBeVisible()

// press / to open search (focus must not be in an input)
await page.keyboard.press('/')

// search input should appear and be focused
await expect(files.searchInput(page)).toBeVisible()
await expect(files.searchInput(page)).toBeFocused()

// type a filter term directly (input is focused)
await page.keyboard.type('orange')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
})

test('pressing / re-focuses input when search is visible but not focused', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)

await files.searchInput(page).fill('orange')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()

// move focus out of the search input
await files.searchInput(page).blur()
await expect(files.searchInput(page)).not.toBeFocused()

// press / should re-focus the input, not hide search
await page.keyboard.press('/')
await expect(files.searchInput(page)).toBeVisible()
await expect(files.searchInput(page)).toBeFocused()

// filter text should still be intact
await expect(files.searchInput(page)).toHaveValue('orange')
})

test('pressing / focuses search after navigating away and back', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)

// confirm search input is visible
await expect(files.searchInput(page)).toBeVisible()

// navigate to Status page
await nav.status(page).click()
await page.waitForURL('/#/')

// navigate back to Files
await nav.files(page).click()
await page.waitForURL('/#/files')

// search input should still be visible (persisted in localStorage)
await expect(files.searchInput(page)).toBeVisible()

// but it should NOT be focused on page load
await expect(files.searchInput(page)).not.toBeFocused()

// press / to focus the search input
await page.keyboard.press('/')
await expect(files.searchInput(page)).toBeFocused()
})

test('no matches message', async ({ page }) => {
await toggleSearchFilter(page, true)

Expand Down
21 changes: 21 additions & 0 deletions test/e2e/peers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,25 @@ test.describe('Peers screen', () => {
test('should have a peer from a "Local Network"', async ({ page }) => {
await expect(peers.localNetwork(page).first()).toBeVisible()
})

test('pressing / focuses the filter input', async ({ page }) => {
await expect(peers.filterInput(page)).toBeVisible()
await expect(peers.filterInput(page)).not.toBeFocused()

await page.keyboard.press('/')
await expect(peers.filterInput(page)).toBeFocused()
})

test('pressing Escape clears the filter input', async ({ page }) => {
const filterInput = peers.filterInput(page)
await expect(filterInput).toBeVisible()

// type a filter term
await filterInput.fill('nonexistent-peer-xyz')
await expect(filterInput).toHaveValue('nonexistent-peer-xyz')

// press Escape to clear
await filterInput.press('Escape')
await expect(filterInput).toHaveValue('')
})
})
3 changes: 2 additions & 1 deletion test/e2e/setup/locators.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export const peers = {
modal: (page) => page.getByTestId('ipfs-modal'),
multiAddrInput: (page) => page.locator('input[name="maddr"]'),
successIndicator: (page) => page.locator('.bg-green'),
localNetwork: (page) => page.getByText('Local Network')
localNetwork: (page) => page.getByText('Local Network'),
filterInput: (page) => page.locator('#peers-filter-input')
}

// Explore page locators
Expand Down