diff --git a/.storybook/preview.js b/.storybook/preview.js
index 764545437..a98b125e5 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -9,7 +9,7 @@ import getStore from '../src/bundles/index.js'
import i18n from '../src/i18n.js'
import DndBackend from '../src/lib/dnd-backend.js'
import { HeliaProvider, ExploreProvider } from 'ipld-explorer-components/providers'
-
+import { ShortcutsProvider } from '../src/contexts/ShortcutsContext.js'
/**
* @type {import('@storybook/addons').BaseAnnotations}
*/
@@ -21,7 +21,9 @@ const baseAnnotations = {
-
+
+
+
diff --git a/package-lock.json b/package-lock.json
index eb91cb7c2..d293be8e6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -108,6 +108,8 @@
"@types/jest": "^29.5.14",
"@types/node": "^14.18.36",
"@types/path-browserify": "^1.0.0",
+ "@types/prop-types": "^15.7.14",
+ "@types/react-overlays": "^3.1.0",
"@typescript-eslint/parser": "^5.62.0",
"aegir": "^42.2.2",
"autoprefixer": "^10.4.7",
@@ -18245,9 +18247,9 @@
"license": "MIT"
},
"node_modules/@types/prop-types": {
- "version": "15.7.13",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
- "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/q": {
@@ -18299,6 +18301,17 @@
"@types/reactcss": "*"
}
},
+ "node_modules/@types/react-overlays": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/react-overlays/-/react-overlays-3.1.0.tgz",
+ "integrity": "sha512-NzZZHFLj7M7+I+p5rDdVHtm6AeVYQPShVxALiLYhR9leJSX8XujPsYuY+vh7/mzFjv6XR7PxHBAdlFGNaN6QDQ==",
+ "deprecated": "This is a stub types definition. react-overlays provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "react-overlays": "*"
+ }
+ },
"node_modules/@types/react-syntax-highlighter": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz",
diff --git a/package.json b/package.json
index a3eb69bdf..d0dd69f78 100644
--- a/package.json
+++ b/package.json
@@ -138,6 +138,8 @@
"@types/jest": "^29.5.14",
"@types/node": "^14.18.36",
"@types/path-browserify": "^1.0.0",
+ "@types/prop-types": "^15.7.14",
+ "@types/react-overlays": "^3.1.0",
"@typescript-eslint/parser": "^5.62.0",
"aegir": "^42.2.2",
"autoprefixer": "^10.4.7",
diff --git a/public/locales/en/app.json b/public/locales/en/app.json
index c40e0a833..311723785 100644
--- a/public/locales/en/app.json
+++ b/public/locales/en/app.json
@@ -105,5 +105,13 @@
"skip": "Skip",
"tooltip": "Click this button any time for a guided tour on the current page."
},
+ "shortcutModal": {
+ "title": "Keyboard Shortcuts",
+ "description": "The following keyboard shortcuts are available in the Files section:",
+ "showShortcuts": "Show keyboard shortcuts",
+ "general": "General",
+ "tourHelp": "Show tour help",
+ "ipfsPath": "Enter QmHash or CID"
+ },
"startTourHelper": "Start tour"
}
diff --git a/public/locales/en/files.json b/public/locales/en/files.json
index 4a2dee8bd..fce82687f 100644
--- a/public/locales/en/files.json
+++ b/public/locales/en/files.json
@@ -49,28 +49,6 @@
"checkboxRemoveLocalPin": "Also remove local pin (recommended)",
"checkboxUnpinFromServices": "Unpin from all pinning services"
},
- "shortcutModal": {
- "title": "Keyboard Shortcuts",
- "description": "The following keyboard shortcuts are available in the Files section:",
- "navigation": "Navigation",
- "selection": "Selection",
- "actions": "Actions",
- "other": "Other",
- "moveDown": "Move down",
- "moveUp": "Move up",
- "moveLeft": "Move left",
- "moveRight": "Move right",
- "navigate": "Navigate to selected item",
- "rename": "Rename selected item",
- "delete": "Delete selected item(s)",
- "toggleSelection": "Toggle selection",
- "selectAll": "Select all items",
- "deselectAll": "Deselect all items",
- "copy": "Copy selected item(s)",
- "paste": "Paste item(s)",
- "cut": "Cut selected item(s)",
- "showShortcuts": "Show keyboard shortcuts"
- },
"pinningModal": {
"title": "Select where you would like to pin these items.",
"complianceLabel": "🔍 Check pinning services' compliance",
@@ -185,5 +163,24 @@
},
"noPinsInProgress": "All done, no remote pins in progress.",
"remotePinningInProgress": "Remote pinning in progress:",
- "selectAllEntries": "Select all entries"
+ "selectAllEntries": "Select all entries",
+ "shortcutModal": {
+ "navigation": "Navigation",
+ "selection": "Selection",
+ "actions": "Actions",
+ "other": "Other",
+ "moveDown": "Move down",
+ "moveUp": "Move up",
+ "moveLeft": "Move left",
+ "moveRight": "Move right",
+ "navigate": "Navigate to selected item",
+ "rename": "Rename selected item",
+ "delete": "Delete selected item(s)",
+ "toggleSelection": "Toggle selection",
+ "selectAll": "Select all items",
+ "deselectAll": "Deselect all items",
+ "copy": "Copy selected item(s)",
+ "paste": "Paste item(s)",
+ "cut": "Cut selected item(s)"
+ }
}
diff --git a/public/locales/en/peers.json b/public/locales/en/peers.json
index 4ccec8abf..6591fe759 100644
--- a/public/locales/en/peers.json
+++ b/public/locales/en/peers.json
@@ -3,6 +3,7 @@
"localNetwork": "Local Network",
"nearby": "nearby",
"protocols": "Open streams",
+ "filterPeers": "Filter peers",
"addConnection": "Add connection",
"insertPeerAddress": "Insert the peer address you want to connect to.",
"addPermanentPeer": "Add to the permanent peering configuration",
diff --git a/src/components/modal/Modal.js b/src/components/modal/Modal.js
index 05695bb3d..0989f4013 100644
--- a/src/components/modal/Modal.js
+++ b/src/components/modal/Modal.js
@@ -1,7 +1,9 @@
-import React from 'react'
+// @ts-nocheck
+import * as React from 'react'
import PropTypes from 'prop-types'
-import CancelIcon from '../../icons/GlyphSmallCancel.js'
+import CancelIcon from '../../icons/GlyphSmallCancel'
+// @ts-ignore
export const ModalActions = ({ justify = 'between', className = '', children, ...props }) => (
{ children }
@@ -10,9 +12,11 @@ export const ModalActions = ({ justify = 'between', className = '', children, ..
ModalActions.propTypes = {
justify: PropTypes.string,
- className: PropTypes.string
+ className: PropTypes.string,
+ children: PropTypes.node
}
+// @ts-ignore
export const ModalBody = ({ className = '', Icon, title, children, ...props }) => (
{ Icon && (
@@ -32,9 +36,11 @@ ModalBody.propTypes = {
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
- ])
+ ]),
+ children: PropTypes.node
}
+// @ts-ignore
export const Modal = ({ onCancel, children, className, ...props }) => {
return (
diff --git a/src/components/notify/Toast.js b/src/components/notify/Toast.js
index b4e9ea89e..d9e169046 100644
--- a/src/components/notify/Toast.js
+++ b/src/components/notify/Toast.js
@@ -1,5 +1,5 @@
import React from 'react'
-import CancelIcon from '../../icons/GlyphSmallCancel.js'
+import CancelIcon from '../../icons/GlyphSmallCancel'
const Toast = ({ error, children, onDismiss }) => {
const bg = error ? 'bg-yellow' : 'bg-green'
diff --git a/src/components/overlay/Overlay.js b/src/components/overlay/Overlay.js
deleted file mode 100644
index 3820ed355..000000000
--- a/src/components/overlay/Overlay.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { Modal } from 'react-overlays'
-
-function Overlay ({ children, show, onLeave, className, hidden, ...props }) {
- const handleKeyDown = (e) => {
- if (e.key !== 'Escape') return
-
- e.stopPropagation()
- e.nativeEvent.stopImmediatePropagation()
-
- onLeave()
- }
-
- const renderBackdrop = (props) => (
-
- )
-
- return (
-
- {children}
-
- )
-}
-
-Overlay.propTypes = {
- show: PropTypes.bool.isRequired,
- onLeave: PropTypes.func.isRequired
-}
-
-Overlay.defaultProps = {
- className: ''
-}
-
-export default Overlay
diff --git a/src/components/overlay/Overlay.tsx b/src/components/overlay/Overlay.tsx
new file mode 100644
index 000000000..76b6eeaeb
--- /dev/null
+++ b/src/components/overlay/Overlay.tsx
@@ -0,0 +1,41 @@
+import React, { ReactElement } from 'react'
+import { Modal } from 'react-overlays'
+
+interface OverlayProps {
+ children: React.ReactNode
+ show: boolean
+ onLeave: () => void
+ className: string
+ hidden: boolean
+}
+
+const Overlay: React.FC
= ({ children, show, onLeave, className, hidden, ...props }): ReactElement => {
+ const handleKeyDown = (e: React.KeyboardEvent): null | void => {
+ if (e.key !== 'Escape') return null
+
+ e.stopPropagation()
+ e.nativeEvent.stopImmediatePropagation()
+
+ onLeave()
+ }
+
+ const renderBackdrop = (): React.ReactNode => {
+ return (
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default Overlay
diff --git a/src/components/tour/TourHelper.js b/src/components/tour/TourHelper.js
index 91417c0b5..fcac34f69 100644
--- a/src/components/tour/TourHelper.js
+++ b/src/components/tour/TourHelper.js
@@ -8,7 +8,7 @@ export const TourHelper = ({ doEnableTours, className = '', size = 23, t }) => {
}
return (
-
diff --git a/src/files/modals/Modals.js b/src/files/modals/Modals.js
index 04f66f8fd..e09348fa1 100644
--- a/src/files/modals/Modals.js
+++ b/src/files/modals/Modals.js
@@ -12,7 +12,6 @@ import RemoveModal from './remove-modal/RemoveModal.js'
import AddByPathModal from './add-by-path-modal/AddByPathModal.js'
import BulkImportModal from './bulk-import-modal/bulk-import-modal.tsx'
import PublishModal from './publish-modal/PublishModal.js'
-import ShortcutModal from './shortcut-modal/shortcut-modal.js'
import CliTutorMode from '../../components/cli-tutor-mode/CliTutorMode.js'
import { cliCommandList, cliCmdKeys } from '../../bundles/files/consts.js'
import { realMfsPath } from '../../bundles/files/actions.js'
@@ -28,7 +27,6 @@ const BULK_CID_IMPORT = 'bulk_cid_import'
const CLI_TUTOR_MODE = 'cli_tutor_mode'
const PINNING = 'pinning'
const PUBLISH = 'publish'
-const SHORTCUTS = 'shortcuts'
export {
NEW_FOLDER,
@@ -40,8 +38,7 @@ export {
BULK_CID_IMPORT,
CLI_TUTOR_MODE,
PINNING,
- PUBLISH,
- SHORTCUTS
+ PUBLISH
}
class Modals extends React.Component {
@@ -196,9 +193,6 @@ class Modals extends React.Component {
publish: { file }
})
}
- case SHORTCUTS:
- this.setState({ readyToShow: true })
- break
default:
// do nothing
}
@@ -311,12 +305,6 @@ class Modals extends React.Component {
onLeave={this.leave}
onSubmit={this.publish} />
-
-
-
-
)
}
diff --git a/src/files/modals/shortcut-modal/shortcut-modal.js b/src/files/modals/shortcut-modal/shortcut-modal.js
deleted file mode 100644
index 0caeab681..000000000
--- a/src/files/modals/shortcut-modal/shortcut-modal.js
+++ /dev/null
@@ -1,184 +0,0 @@
-import React, { useEffect, useState } from 'react'
-import PropTypes from 'prop-types'
-import { useTranslation } from 'react-i18next'
-import { Modal } from '../../../components/modal/Modal.js'
-import CancelIcon from '../../../icons/GlyphSmallCancel.js'
-
-const keySymbols = {
- ArrowUp: '↑',
- ArrowDown: '↓',
- ArrowLeft: '←',
- ArrowRight: '→',
- Enter: '↵',
- Space: '␣',
- Escape: 'Esc',
- Delete: 'Del',
- Backspace: '⌫',
- mac: {
- Meta: '⌘',
- Alt: '⌥',
- Shift: '⇧',
- Control: '⌃',
- Ctrl: '⌃'
- },
- other: {
- Meta: 'Win',
- Alt: 'Alt',
- Shift: 'Shift',
- Control: 'Ctrl',
- Ctrl: 'Ctrl'
- }
-}
-
-const KeyboardKey = ({ children, platform }) => {
- const getKeySymbol = (key) => {
- if (keySymbols[key]) return keySymbols[key]
- if (platform === 'mac' && keySymbols.mac[key]) return keySymbols.mac[key]
- if (platform !== 'mac' && keySymbols.other[key]) return keySymbols.other[key]
- return key
- }
-
- return (
-
- {getKeySymbol(children)}
-
- )
-}
-
-KeyboardKey.propTypes = {
- children: PropTypes.node.isRequired,
- platform: PropTypes.string.isRequired
-}
-
-const ShortcutItem = ({ shortcut, description, platform }) => (
-
-
{description}
-
- {Array.isArray(shortcut)
- ? shortcut.map((key, i) => (
-
- {key}
- {i < shortcut.length - 1 && +}
- ))
- : {shortcut}}
-
-
-)
-
-ShortcutItem.propTypes = {
- shortcut: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.array
- ]).isRequired,
- description: PropTypes.string.isRequired,
- platform: PropTypes.string.isRequired
-}
-
-const ShortcutSection = ({ title, shortcuts, platform }) => (
-
-
{title}
-
- {shortcuts.map((shortcut, i) => (
-
- ))}
-
-
-)
-
-ShortcutSection.propTypes = {
- title: PropTypes.string.isRequired,
- shortcuts: PropTypes.array.isRequired,
- platform: PropTypes.string.isRequired
-}
-
-const ShortcutModal = ({ onLeave, className, ...props }) => {
- const [platform, setPlatform] = useState('other')
- const { t } = useTranslation('files')
-
- // Detect platform on component mount
- useEffect(() => {
- const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ||
- (navigator.userAgent.includes('Mac') && !navigator.userAgent.includes('Mobile'))
- setPlatform(isMac ? 'mac' : 'other')
- }, [])
-
- const navigationShortcuts = [
- { shortcut: 'ArrowDown', description: t('shortcutModal.moveDown') },
- { shortcut: 'ArrowUp', description: t('shortcutModal.moveUp') },
- { shortcut: 'ArrowLeft', description: t('shortcutModal.moveLeft') },
- { shortcut: 'ArrowRight', description: t('shortcutModal.moveRight') },
- { shortcut: 'Enter', description: t('shortcutModal.navigate') }
- ]
-
- const selectionShortcuts = [
- { shortcut: 'Space', description: t('shortcutModal.toggleSelection') },
- { shortcut: 'Escape', description: t('shortcutModal.deselectAll') }
- ]
-
- const actionShortcuts = [
- { shortcut: 'F2', description: t('shortcutModal.rename') },
- { shortcut: 'Delete', description: t('shortcutModal.delete') }
- ]
-
- const otherShortcuts = [
- { shortcut: ['Shift', '?'], description: t('shortcutModal.showShortcuts') }
- ]
-
- return (
-
-
-
{t('shortcutModal.title')}
-
-
-
-
-
-
-
- )
-}
-
-ShortcutModal.propTypes = {
- onLeave: PropTypes.func.isRequired,
- className: PropTypes.string
-}
-
-ShortcutModal.defaultProps = {
- className: ''
-}
-
-export default ShortcutModal
diff --git a/src/files/modals/shortcut-modal/shortcut-modal.tsx b/src/files/modals/shortcut-modal/shortcut-modal.tsx
new file mode 100644
index 000000000..b9e63debe
--- /dev/null
+++ b/src/files/modals/shortcut-modal/shortcut-modal.tsx
@@ -0,0 +1,191 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+// @ts-ignore
+import { Modal } from '../../../components/modal/Modal'
+import { useShortcuts } from '../../../contexts/ShortcutsContext'
+
+interface KeySymbols {
+ [key: string]: string | {
+ [key: string]: string
+ }
+ mac: {
+ [key: string]: string
+ }
+ other: {
+ [key: string]: string
+ }
+}
+
+const keySymbols: KeySymbols = {
+ ArrowUp: '↑',
+ ArrowDown: '↓',
+ ArrowLeft: '←',
+ ArrowRight: '→',
+ Enter: '↵',
+ Space: 'Space',
+ Escape: 'Esc',
+ Delete: 'Del',
+ Backspace: '⌫',
+ mac: {
+ Meta: '⌘',
+ Alt: '⌥',
+ Shift: 'Shift',
+ Control: 'Ctrl',
+ Ctrl: 'Ctrl'
+ },
+ other: {
+ Meta: 'Win',
+ Alt: 'Alt',
+ Shift: 'Shift',
+ Control: 'Ctrl',
+ Ctrl: 'Ctrl'
+ }
+}
+
+type PlatformType = 'mac' | 'other'
+
+interface KeyboardKeyProps {
+ children: string
+ platform: PlatformType
+}
+
+const KeyboardKey: React.FC
= ({ children, platform }) => {
+ const getKeySymbol = (key: string): string => {
+ if (keySymbols[key]) return keySymbols[key] as string
+ if (platform === 'mac' && keySymbols.mac[key]) return keySymbols.mac[key]
+ if (platform !== 'mac' && keySymbols.other[key]) return keySymbols.other[key]
+ return key
+ }
+
+ return (
+
+ {getKeySymbol(children)}
+
+ )
+}
+
+interface ShortcutItemProps {
+ shortcut: string | string[]
+ description: string
+ platform: PlatformType,
+ hidden?: boolean
+}
+
+const ShortcutItem: React.FC = ({ shortcut, description, platform }) => (
+
+
{description}
+
+ {Array.isArray(shortcut)
+ ? shortcut.map((key, i) => (
+
+ {key}
+ {i < shortcut.length - 1 && +}
+ ))
+ : {shortcut}}
+
+
+)
+
+interface ShortcutData {
+ shortcut: string | string[]
+ description: string
+ hidden?: boolean
+}
+
+interface ShortcutSectionProps {
+ title: string
+ shortcuts: ShortcutData[]
+ platform: PlatformType
+}
+
+const ShortcutSection: React.FC = ({ title, shortcuts, platform }) => (
+
+
{title}
+
+ {shortcuts.filter(shortcut => !shortcut.hidden).map((shortcut, i) => (
+
+ ))}
+
+
+)
+
+interface ShortcutModalProps {
+ onLeave: () => void
+ className?: string
+}
+
+const ShortcutModal: React.FC = ({ onLeave, className = '', ...props }) => {
+ const [platform, setPlatform] = useState('other')
+ const { t } = useTranslation('app')
+ const shortcuts = useShortcuts()
+
+ useEffect(() => {
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ||
+ (navigator.userAgent.includes('Mac') && !navigator.userAgent.includes('Mobile'))
+ setPlatform(isMac ? 'mac' : 'other')
+ }, [])
+
+ const groupedShortcuts = useMemo(() => {
+ return shortcuts.reduce((acc, shortcut) => {
+ const group = shortcut.group || 'Other'
+ if (!acc[group]) {
+ acc[group] = []
+ }
+ acc[group].push({
+ shortcut: shortcut.keys,
+ description: shortcut.label,
+ hidden: shortcut.hidden
+ })
+ return acc
+ }, {} as Record)
+ }, [shortcuts])
+
+ const groupOrder = ['General', 'Navigation', 'Selection', 'Actions', 'Other']
+
+ const sortedGroups = useMemo(() => {
+ return Object.entries(groupedShortcuts)
+ .sort(([a], [b]) => {
+ const indexA = groupOrder.indexOf(a)
+ const indexB = groupOrder.indexOf(b)
+ if (indexA === -1) return 1
+ if (indexB === -1) return -1
+ return indexA - indexB
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [groupedShortcuts])
+
+ return (
+
+
+
{t('shortcutModal.title')}
+
+
+
+
+ {sortedGroups.map(([group, shortcuts]) => (
+
+ ))}
+
+
+
+ )
+}
+
+export default ShortcutModal
diff --git a/src/icons/GlyphSmallCancel.js b/src/icons/GlyphSmallCancel.tsx
similarity index 74%
rename from src/icons/GlyphSmallCancel.js
rename to src/icons/GlyphSmallCancel.tsx
index f745ab6ef..3739f83d0 100644
--- a/src/icons/GlyphSmallCancel.js
+++ b/src/icons/GlyphSmallCancel.tsx
@@ -1,6 +1,6 @@
-import * as React from 'react'
+import { SVGProps } from 'react'
-function SvgGlyphSmallCancel (props) {
+function SvgGlyphSmallCancel (props: SVGProps) {
return (