diff --git a/pkg/grid/src/components/Button.tsx b/pkg/grid/src/components/Button.tsx index 5fa06dd6d3..d38aeef3f6 100644 --- a/pkg/grid/src/components/Button.tsx +++ b/pkg/grid/src/components/Button.tsx @@ -22,7 +22,7 @@ const variants: Record = { secondary: 'text-black bg-gray-100', caution: 'text-white bg-orange-400', destructive: 'text-white bg-red-500', - 'alt-primary': 'text-white bg-blue-400', + 'alt-primary': 'text-white bg-blue-400 ring-blue-300', 'alt-secondary': 'text-blue-400 bg-blue-50' }; diff --git a/pkg/grid/src/nav/Leap.tsx b/pkg/grid/src/nav/Leap.tsx index ec10101198..d754b1793b 100644 --- a/pkg/grid/src/nav/Leap.tsx +++ b/pkg/grid/src/nav/Leap.tsx @@ -35,212 +35,215 @@ type LeapProps = { menu: MenuState; dropdown: string; navOpen: boolean; + shouldDim: boolean; } & HTMLAttributes; -export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: LeapProps, ref) => { - const { push } = useHistory(); - const match = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>( - `/leap/${menu}/:query?/(apps)?/:desk?` - ); - const appsMatch = useRouteMatch(`/leap/${menu}/${match?.params.query}/apps`); - const inputRef = useRef(null); - useImperativeHandle(ref, () => inputRef.current); - const { rawInput, selectedMatch, matches, selection, select } = useLeapStore(); +export const Leap = React.forwardRef( + ({ menu, dropdown, navOpen, shouldDim, className }: LeapProps, ref) => { + const { push } = useHistory(); + const match = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>( + `/leap/${menu}/:query?/(apps)?/:desk?` + ); + const appsMatch = useRouteMatch(`/leap/${menu}/${match?.params.query}/apps`); + const inputRef = useRef(null); + useImperativeHandle(ref, () => inputRef.current); + const { rawInput, selectedMatch, matches, selection, select } = useLeapStore(); - useEffect(() => { - if (selection && rawInput === '') { - inputRef.current?.focus(); - } - }, [selection, rawInput]); + useEffect(() => { + if (selection && rawInput === '') { + inputRef.current?.focus(); + } + }, [selection, rawInput]); - const toggleSearch = useCallback(() => { - if (selection || menu === 'search') { - return; - } - - push('/leap/search'); - }, [selection, menu]); - - const onFocus = useCallback( - (e: FocusEvent) => { - // refocusing tab with input focused is false trigger - const windowFocus = e.nativeEvent.currentTarget === document.body; - if (windowFocus) { + const toggleSearch = useCallback(() => { + if (selection || menu === 'search') { return; } - toggleSearch(); - }, - [toggleSearch] - ); + push('/leap/search'); + }, [selection, menu]); - const getMatch = useCallback( - (value: string) => { - const normValue = value.toLocaleLowerCase(); - return matches.find( - (m) => - m.display?.toLocaleLowerCase().startsWith(normValue) || - m.value.toLocaleLowerCase().startsWith(normValue) - ); - }, - [matches] - ); + const onFocus = useCallback( + (e: FocusEvent) => { + // refocusing tab with input focused is false trigger + const windowFocus = e.nativeEvent.currentTarget === document.body; + if (windowFocus) { + return; + } - const navigateByInput = useCallback( - (input: string) => { - const normalizedValue = input.trim().replace(/(~?[\w^_-]{3,13})\//, '$1/apps/'); - push(`/leap/${menu}/${normalizedValue}`); - }, - [menu] - ); + toggleSearch(); + }, + [toggleSearch] + ); - const debouncedSearch = useDebounce( - (input: string) => { - if (!match || appsMatch) { - return; - } + const getMatch = useCallback( + (value: string) => { + const normValue = value.toLocaleLowerCase(); + return matches.find( + (m) => + m.display?.toLocaleLowerCase().startsWith(normValue) || + m.value.toLocaleLowerCase().startsWith(normValue) + ); + }, + [matches] + ); - useLeapStore.setState({ searchInput: input }); - navigateByInput(input); - }, - 300, - { leading: true } - ); + const navigateByInput = useCallback( + (input: string) => { + const normalizedValue = input.trim().replace(/(~?[\w^_-]{3,13})\//, '$1/apps/'); + push(`/leap/${menu}/${normalizedValue}`); + }, + [menu] + ); - const handleSearch = useCallback(debouncedSearch, [match]); + const debouncedSearch = useDebounce( + (input: string) => { + if (!match || appsMatch) { + return; + } - const onChange = useCallback( - (e: ChangeEvent) => { - const input = e.target as HTMLInputElement; - const value = input.value.trim(); - const isDeletion = (e.nativeEvent as InputEvent).inputType === 'deleteContentBackward'; - const inputMatch = getMatch(value); - const matchValue = inputMatch?.display || inputMatch?.value; + useLeapStore.setState({ searchInput: input }); + navigateByInput(input); + }, + 300, + { leading: true } + ); - if (matchValue && inputRef.current && !isDeletion) { - inputRef.current.value = matchValue; - inputRef.current.setSelectionRange(value.length, matchValue.length); - useLeapStore.setState({ - rawInput: matchValue, - selectedMatch: inputMatch - }); - } else { - useLeapStore.setState({ - rawInput: value, - selectedMatch: matches[0] - }); - } + const handleSearch = useCallback(debouncedSearch, [match]); - handleSearch(value); - }, - [matches] - ); + const onChange = useCallback( + (e: ChangeEvent) => { + const input = e.target as HTMLInputElement; + const value = input.value.trim(); + const isDeletion = (e.nativeEvent as InputEvent).inputType === 'deleteContentBackward'; + const inputMatch = getMatch(value); + const matchValue = inputMatch?.display || inputMatch?.value; - const onSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); + if (matchValue && inputRef.current && !isDeletion) { + inputRef.current.value = matchValue; + inputRef.current.setSelectionRange(value.length, matchValue.length); + useLeapStore.setState({ + rawInput: matchValue, + selectedMatch: inputMatch + }); + } else { + useLeapStore.setState({ + rawInput: value, + selectedMatch: matches[0] + }); + } - const value = inputRef.current?.value.trim(); - const currentMatch = selectedMatch || (value && getMatch(value)); + handleSearch(value); + }, + [matches] + ); - if (!currentMatch) { - return; - } - - if (currentMatch?.openInNewTab) { - window.open(currentMatch.url, currentMatch.value); - return; - } - - push(currentMatch.url); - useLeapStore.setState({ rawInput: '' }); - }, - [match, selectedMatch] - ); - - const onKeyDown = useCallback( - (e: KeyboardEvent) => { - const deletion = e.key === 'Backspace' || e.key === 'Delete'; - const arrow = e.key === 'ArrowDown' || e.key === 'ArrowUp'; - - if (deletion && !rawInput && selection) { - e.preventDefault(); - select(null, appsMatch && !appsMatch.isExact ? undefined : match?.params.query); - const pathBack = createPreviousPath(match?.url || ''); - push(pathBack); - } - - if (arrow) { + const onSubmit = useCallback( + (e: FormEvent) => { e.preventDefault(); - const currentIndex = selectedMatch - ? matches.findIndex((m) => { - const matchValue = m.display || m.value; - const searchValue = selectedMatch.display || selectedMatch.value; - return matchValue === searchValue; - }) - : 0; - const unsafeIndex = e.key === 'ArrowUp' ? currentIndex - 1 : currentIndex + 1; - const index = (unsafeIndex + matches.length) % matches.length; + const value = inputRef.current?.value.trim(); + const currentMatch = selectedMatch || (value && getMatch(value)); - const newMatch = matches[index]; - const matchValue = newMatch.display || newMatch.value; - useLeapStore.setState({ - rawInput: matchValue, - // searchInput: matchValue, - selectedMatch: newMatch - }); - } - }, - [selection, rawInput, match, matches, selectedMatch] - ); + if (!currentMatch) { + return; + } - return ( -
-
-
+ ); + } +); diff --git a/pkg/grid/src/nav/Nav.tsx b/pkg/grid/src/nav/Nav.tsx index 1c5b661c2a..e50bfac40d 100644 --- a/pkg/grid/src/nav/Nav.tsx +++ b/pkg/grid/src/nav/Nav.tsx @@ -52,33 +52,6 @@ export type MenuState = | 'help-and-support' | 'system-preferences'; -export function createNextPath(current: string, nextPart?: string): string { - let end = nextPart; - const parts = current.split('/').reverse(); - if (parts[1] === 'search') { - end = 'apps'; - } - - if (parts[0] === 'leap') { - end = `search/${nextPart}`; - } - - return `${current}/${end}`; -} - -export function createPreviousPath(current: string): string { - const parts = current.split('/'); - parts.pop(); - - if (parts[parts.length - 1] === 'leap') { - parts.push('search'); - } - if (parts[parts.length - 2] === 'apps') { - parts.pop(); - } - return parts.join('/'); -} - interface NavProps { menu?: MenuState; } @@ -123,6 +96,15 @@ export const Nav: FunctionComponent = ({ menu }) => { } }, []); + const preventClose = useCallback((e) => { + const target = e.target as HTMLElement; + const hasNavAncestor = target.closest('#dialog-nav'); + + if (hasNavAncestor) { + e.preventDefault(); + } + }, []); + return ( <> {/* Using portal so that we can retain the same nav items both in the dialog and in the base header */} @@ -132,12 +114,20 @@ export const Nav: FunctionComponent = ({ menu }) => { > - - + +
= ({ menu }) => { /> = ({ menu }) => { aria-expanded={isOpen} >
diff --git a/pkg/grid/src/nav/NotificationsLink.tsx b/pkg/grid/src/nav/NotificationsLink.tsx index 75f61e28f1..4d0cd1d12b 100644 --- a/pkg/grid/src/nav/NotificationsLink.tsx +++ b/pkg/grid/src/nav/NotificationsLink.tsx @@ -4,7 +4,6 @@ import { Link, LinkProps } from 'react-router-dom'; import { Bullet } from '../components/icons/Bullet'; import { Notification } from '../state/hark-types'; import { useNotifications } from '../state/notifications'; -import { MenuState } from './Nav'; type NotificationsState = 'empty' | 'unread' | 'attention-needed'; @@ -25,11 +24,11 @@ function getNotificationsState( } type NotificationsLinkProps = Omit, 'to'> & { - menu: MenuState; navOpen: boolean; + shouldDim: boolean; }; -export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) => { +export const NotificationsLink = ({ navOpen, shouldDim }: NotificationsLinkProps) => { const { notifications, systemNotifications } = useNotifications(); const state = getNotificationsState(notifications, systemNotifications); @@ -39,7 +38,7 @@ export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) => className={classNames( 'relative z-50 flex-none circle-button h4 default-ring', navOpen && 'text-opacity-60', - navOpen && menu !== 'notifications' && 'opacity-60', + shouldDim && 'opacity-60', state === 'empty' && !navOpen && 'text-gray-400 bg-gray-50', state === 'empty' && navOpen && 'text-gray-400 bg-white', state === 'unread' && 'bg-blue-400 text-white', diff --git a/pkg/grid/src/nav/SystemMenu.tsx b/pkg/grid/src/nav/SystemMenu.tsx index 1939ce5a97..8397f7755b 100644 --- a/pkg/grid/src/nav/SystemMenu.tsx +++ b/pkg/grid/src/nav/SystemMenu.tsx @@ -7,13 +7,11 @@ import { Vat } from '@urbit/api/hood'; import { Adjust } from '../components/icons/Adjust'; import { useVat } from '../state/kiln'; import { disableDefault, handleDropdownLink } from '../state/util'; -import { MenuState } from './Nav'; import { useMedia } from '../logic/useMedia'; type SystemMenuProps = HTMLAttributes & { - menu: MenuState; open: boolean; - navOpen: boolean; + shouldDim: boolean; }; function getHash(vat: Vat): string { @@ -21,7 +19,7 @@ function getHash(vat: Vat): string { return parts[parts.length - 1]; } -export const SystemMenu = ({ className, menu, open, navOpen }: SystemMenuProps) => { +export const SystemMenu = ({ className, open, shouldDim }: SystemMenuProps) => { const { push } = useHistory(); const [copied, setCopied] = useState(false); const garden = useVat('garden'); @@ -61,10 +59,7 @@ export const SystemMenu = ({ className, menu, open, navOpen }: SystemMenuProps) className={classNames( 'appearance-none circle-button default-ring', open && 'text-gray-300', - navOpen && - menu !== 'system-preferences' && - menu !== 'help-and-support' && - 'opacity-60', + shouldDim && 'opacity-60', className )} > diff --git a/pkg/grid/src/nav/SystemPreferences.tsx b/pkg/grid/src/nav/SystemPreferences.tsx index 82773a5065..6e674b8790 100644 --- a/pkg/grid/src/nav/SystemPreferences.tsx +++ b/pkg/grid/src/nav/SystemPreferences.tsx @@ -53,8 +53,8 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string } ); return ( -
-