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:
ameer2468
2023-04-08 02:45:16 +03:00
committed by GitHub
parent d081dd042f
commit 734a4e82e4
5 changed files with 131 additions and 173 deletions

View 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>
);
}
);

View File

@@ -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>
</>

View 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>
);
}
);

View File

@@ -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

View File

@@ -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';