mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-24 16:32:45 -04:00
ENG-449 - Keyboard support for Search & Component Extraction (#683)
* Extracted components within TopBar to their own files as components. Added support for for cmd/ctrl + f and Esc for keyboard searchbar. * Fixed misplaced event prevent default
This commit is contained in:
75
interface/app/$libraryId/Explorer/SearchBar.tsx
Normal file
75
interface/app/$libraryId/Explorer/SearchBar.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import clsx from 'clsx';
|
||||
import { ComponentPropsWithRef, forwardRef, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Input, Shortcut } from '@sd/ui';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
|
||||
export default forwardRef<HTMLInputElement, ComponentPropsWithRef<'input'>>(
|
||||
(props, forwardedRef) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { dirtyFields }
|
||||
} = useForm();
|
||||
|
||||
const { ref, ...searchField } = register('searchField', {
|
||||
onBlur: () => {
|
||||
// if there's no text in the search bar, don't mark it as dirty so the key hint shows
|
||||
if (!dirtyFields.searchField) reset();
|
||||
}
|
||||
});
|
||||
|
||||
const platform = useOperatingSystem(false);
|
||||
const os = useOperatingSystem(true);
|
||||
|
||||
useEffect(() => {
|
||||
const keyboardSearchFocus = (event: KeyboardEvent) => {
|
||||
if (typeof forwardedRef !== 'function') {
|
||||
if ((event.key === 'f' && event.metaKey) || event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
forwardedRef?.current?.focus();
|
||||
} else if (forwardedRef?.current === document.activeElement && event.key === 'Escape') {
|
||||
forwardedRef.current?.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keyboardSearchFocus);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyboardSearchFocus);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(() => null)} className="relative flex h-7">
|
||||
<Input
|
||||
ref={(el) => {
|
||||
ref(el);
|
||||
if (typeof forwardedRef === 'function') forwardedRef(el);
|
||||
else if (forwardedRef) forwardedRef.current = el;
|
||||
}}
|
||||
placeholder="Search"
|
||||
className={clsx('w-32 transition-all focus-within:w-52', props.className)}
|
||||
size="sm"
|
||||
{...searchField}
|
||||
right={
|
||||
<div
|
||||
className={clsx(
|
||||
'pointer-events-none flex h-7 items-center space-x-1 opacity-70 group-focus-within:hidden'
|
||||
)}
|
||||
>
|
||||
{platform === 'browser' ? (
|
||||
<Shortcut chars="⌘F" aria-label={'Press Command-F to focus search bar'} />
|
||||
) : os === 'macOS' ? (
|
||||
<Shortcut chars="⌘F" aria-label={'Press Command-F to focus search bar'} />
|
||||
) : (
|
||||
<Shortcut chars="CTRL+F" aria-label={'Press CTRL-F to focus search bar'} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
CaretRight,
|
||||
Columns,
|
||||
Key,
|
||||
List,
|
||||
MonitorPlay,
|
||||
Rows,
|
||||
SidebarSimple,
|
||||
@@ -13,128 +12,26 @@ import {
|
||||
SquaresFour,
|
||||
Tag
|
||||
} from 'phosphor-react';
|
||||
import { ComponentProps, forwardRef, useEffect, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Input, Popover, Shortcut, Tooltip, cva } from '@sd/ui';
|
||||
import { Popover, Tooltip } from '@sd/ui';
|
||||
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import { KeybindEvent } from '~/util/keybind';
|
||||
import { KeyManager } from '../KeyManager';
|
||||
import OptionsPanel from './OptionsPanel';
|
||||
|
||||
export interface TopBarButtonProps {
|
||||
children: React.ReactNode;
|
||||
rounding?: 'none' | 'left' | 'right' | 'both';
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
// export const TopBarIcon = (icon: any) => tw(icon)`m-0.5 w-5 h-5 text-ink-dull`;
|
||||
|
||||
const topBarButtonStyle = cva(
|
||||
'text-ink hover:text-ink text-md hover:bg-app-selected radix-state-open:bg-app-selected mr-[1px] flex border-none !p-0.5 font-medium outline-none transition-colors duration-100',
|
||||
{
|
||||
variants: {
|
||||
active: {
|
||||
true: 'bg-app-selected',
|
||||
false: 'bg-transparent'
|
||||
},
|
||||
rounding: {
|
||||
none: 'rounded-none',
|
||||
left: 'rounded-l-md rounded-r-none',
|
||||
right: 'rounded-r-md rounded-l-none',
|
||||
both: 'rounded-md'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
active: false,
|
||||
rounding: 'both'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const TOP_BAR_ICON_STYLE = 'm-0.5 w-5 h-5 text-ink-dull';
|
||||
|
||||
const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>(
|
||||
({ active, rounding, className, ...props }, ref) => {
|
||||
return (
|
||||
<Button
|
||||
// size="sm"
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={topBarButtonStyle({ active, rounding, className })}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const SearchBar = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
|
||||
(props, forwardedRef) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { dirtyFields }
|
||||
} = useForm();
|
||||
|
||||
const { ref, ...searchField } = register('searchField', {
|
||||
onBlur: () => {
|
||||
// if there's no text in the search bar, don't mark it as dirty so the key hint shows
|
||||
if (!dirtyFields.searchField) reset();
|
||||
}
|
||||
});
|
||||
|
||||
const platform = useOperatingSystem(false);
|
||||
const os = useOperatingSystem(true);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(() => null)} className="relative flex h-7">
|
||||
<Input
|
||||
ref={(el) => {
|
||||
ref(el);
|
||||
|
||||
if (typeof forwardedRef === 'function') forwardedRef(el);
|
||||
else if (forwardedRef) forwardedRef.current = el;
|
||||
}}
|
||||
placeholder="Search"
|
||||
className={clsx('w-32 transition-all focus-within:w-52', props.className)}
|
||||
size="sm"
|
||||
{...searchField}
|
||||
right={
|
||||
<div
|
||||
className={clsx(
|
||||
'pointer-events-none flex h-7 items-center space-x-1 opacity-70 group-focus-within:hidden'
|
||||
)}
|
||||
>
|
||||
{platform === 'browser' ? (
|
||||
<Shortcut chars="⌘F" aria-label={'Press Command-F to focus search bar'} />
|
||||
) : os === 'macOS' ? (
|
||||
<Shortcut chars="⌘F" aria-label={'Press Command-F to focus search bar'} />
|
||||
) : (
|
||||
<Shortcut chars="CTRL+F" aria-label={'Press CTRL-F to focus search bar'} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
);
|
||||
import SearchBar from './SearchBar';
|
||||
import TopBarButton from './TopBarButton';
|
||||
|
||||
export type TopBarProps = {
|
||||
showSeparator?: boolean;
|
||||
};
|
||||
|
||||
export default (props: TopBarProps) => {
|
||||
const TOP_BAR_ICON_STYLE = 'm-0.5 w-5 h-5 text-ink-dull';
|
||||
const platform = useOperatingSystem(false);
|
||||
const os = useOperatingSystem(true);
|
||||
|
||||
const store = useExplorerStore();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
//create function to focus on search box when cmd+k is pressed
|
||||
@@ -216,12 +113,6 @@ export default (props: TopBarProps) => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex mx-8 space-x-[1px]">
|
||||
<TopBarButton active group left icon={List} />
|
||||
<TopBarButton group icon={Columns} />
|
||||
<TopBarButton group right icon={SquaresFour} />
|
||||
</div> */}
|
||||
|
||||
<div data-tauri-drag-region className="flex grow flex-row justify-center">
|
||||
<div className="mx-8 flex">
|
||||
<Tooltip label="Grid view">
|
||||
@@ -251,16 +142,6 @@ export default (props: TopBarProps) => {
|
||||
<Columns className={TOP_BAR_ICON_STYLE} />
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
{/* <Tooltip label="Timeline view">
|
||||
<TopBarButton
|
||||
rounding="none"
|
||||
active={store.layoutMode === 'timeline'}
|
||||
onClick={() => (getExplorerStore().layoutMode = 'timeline')}
|
||||
>
|
||||
<ClockCounterClockwise className={TOP_BAR_ICON_STYLE} />
|
||||
</TopBarButton>
|
||||
</Tooltip> */}
|
||||
|
||||
<Tooltip label="Media view">
|
||||
<TopBarButton
|
||||
rounding="right"
|
||||
@@ -279,15 +160,13 @@ export default (props: TopBarProps) => {
|
||||
<Popover
|
||||
className="focus:outline-none"
|
||||
trigger={
|
||||
// <Tooltip label="Major Key Alert">
|
||||
<TopBarButton>
|
||||
<Key className={TOP_BAR_ICON_STYLE} />
|
||||
</TopBarButton>
|
||||
// </Tooltip>
|
||||
}
|
||||
>
|
||||
<div className="block w-[350px]">
|
||||
<KeyManager /* className={TOP_BAR_ICON_STYLE} */ />
|
||||
<KeyManager />
|
||||
</div>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
@@ -303,12 +182,7 @@ export default (props: TopBarProps) => {
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Regenerate thumbs (temp)">
|
||||
<TopBarButton
|
||||
// onClick={() =>
|
||||
// store.locationId &&
|
||||
// generateThumbsForLocation.mutate({ id: store.locationId, path: '' })
|
||||
// }
|
||||
>
|
||||
<TopBarButton>
|
||||
<ArrowsClockwise className={TOP_BAR_ICON_STYLE} />
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
@@ -345,34 +219,6 @@ export default (props: TopBarProps) => {
|
||||
/>
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
{/* <Dropdown
|
||||
// className="absolute block h-6 w-44 top-2 right-4"
|
||||
align="right"
|
||||
items={[
|
||||
[
|
||||
{
|
||||
name: 'Generate Thumbs',
|
||||
icon: ArrowsClockwise,
|
||||
onPress: () =>
|
||||
store.locationId &&
|
||||
generateThumbsForLocation({ id: store.locationId, path: '' })
|
||||
},
|
||||
{
|
||||
name: 'Identify Unique',
|
||||
icon: ArrowsClockwise,
|
||||
onPress: () =>
|
||||
store.locationId && identifyUniqueFiles({ id: store.locationId, path: '' })
|
||||
},
|
||||
{
|
||||
name: 'Validate Objects',
|
||||
icon: ArrowsClockwise,
|
||||
onPress: () =>
|
||||
store.locationId && objectValidator({ id: store.locationId, path: '' })
|
||||
}
|
||||
]
|
||||
]}
|
||||
buttonComponent={<TopBarButton icon={List} />}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
43
interface/app/$libraryId/Explorer/TopBarButton.tsx
Normal file
43
interface/app/$libraryId/Explorer/TopBarButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { forwardRef } from 'react';
|
||||
import { Button } from '@sd/ui';
|
||||
|
||||
export interface TopBarButtonProps {
|
||||
children: React.ReactNode;
|
||||
rounding?: 'none' | 'left' | 'right' | 'both';
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const topBarButtonStyle = cva(
|
||||
'text-ink hover:text-ink text-md hover:bg-app-selected radix-state-open:bg-app-selected mr-[1px] flex border-none !p-0.5 font-medium outline-none transition-colors duration-100',
|
||||
{
|
||||
variants: {
|
||||
active: {
|
||||
true: 'bg-app-selected',
|
||||
false: 'bg-transparent'
|
||||
},
|
||||
rounding: {
|
||||
none: 'rounded-none',
|
||||
left: 'rounded-l-md rounded-r-none',
|
||||
right: 'rounded-r-md rounded-l-none',
|
||||
both: 'rounded-md'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
active: false,
|
||||
rounding: 'both'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default forwardRef<HTMLButtonElement, TopBarButtonProps>(
|
||||
({ active, rounding, className, ...props }, ref) => {
|
||||
return (
|
||||
<Button {...props} ref={ref} className={topBarButtonStyle({ active, rounding, className })}>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -2,11 +2,11 @@ import { GoogleDrive, Mega, iCloud } from '@sd/assets/images';
|
||||
import clsx from 'clsx';
|
||||
import { DeviceMobile, HardDrives, Icon, Laptop, User } from 'phosphor-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Label, Select, SelectOption, forms, tw } from '@sd/ui';
|
||||
import { Button, Select, SelectOption, forms, tw } from '@sd/ui';
|
||||
import { PeerMetadata, useBridgeMutation, useBridgeSubscription } from '~/../packages/client/src';
|
||||
import { SubtleButton, SubtleButtonContainer } from '~/components/SubtleButton';
|
||||
import { OperatingSystem } from '~/util/Platform';
|
||||
import { SearchBar } from './Explorer/TopBar';
|
||||
import SearchBar from './Explorer/SearchBar';
|
||||
import * as PageLayout from './PageLayout';
|
||||
import classes from './spacedrop.module.scss';
|
||||
|
||||
@@ -107,8 +107,6 @@ function TemporarySpacedropDemo() {
|
||||
}
|
||||
});
|
||||
|
||||
console.log({ discoveredPeers });
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
doSpacedrop.mutate({
|
||||
peer_id: data.target_peer,
|
||||
@@ -160,16 +158,12 @@ function TemporarySpacedropDemo() {
|
||||
|
||||
export const Component = () => {
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="i2ems-center relative bottom-[11.5px] flex w-full flex-row justify-center">
|
||||
<SearchBar className="ml-[13px]" ref={searchRef} />
|
||||
</div>
|
||||
<TemporarySpacedropDemo />
|
||||
<PageLayout.DragChildren>
|
||||
<div className="flex h-8 w-full flex-row items-center justify-center pt-3">
|
||||
<SearchBar className="ml-[13px]" ref={searchRef} />
|
||||
{/* <Button variant="outline">Add</Button> */}
|
||||
</div>
|
||||
</PageLayout.DragChildren>
|
||||
<div className={classes.honeycombOuter}>
|
||||
<div className={clsx(classes.honeycombContainer, 'mt-8')}>
|
||||
<DropItem
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VariantProps, cva, cx } from 'class-variance-authority';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import clsx from 'clsx';
|
||||
import { Eye, EyeSlash, Icon, IconProps, MagnifyingGlass } from 'phosphor-react';
|
||||
import { PropsWithChildren, createElement, forwardRef, isValidElement, useState } from 'react';
|
||||
|
||||
Reference in New Issue
Block a user