Add cmd k to navigate pages (#254)
* Begin styled command bar * Add 2 commands and improve styling * Add storybook * Move z-index to variables and use hotkey hook
This commit is contained in:
64
front/src/modules/search/components/CommandMenu.tsx
Normal file
64
front/src/modules/search/components/CommandMenu.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
StyledDialog,
|
||||
StyledEmpty,
|
||||
StyledGroup,
|
||||
StyledInput,
|
||||
StyledItem,
|
||||
StyledList,
|
||||
// StyledSeparator,
|
||||
} from './CommandMenuStyles';
|
||||
|
||||
export const CommandMenu = ({ initiallyOpen = false }) => {
|
||||
const [open, setOpen] = React.useState(initiallyOpen);
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+k,meta+k',
|
||||
() => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<StyledDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
label="Global Command Menu"
|
||||
>
|
||||
<StyledInput />
|
||||
<StyledList>
|
||||
<StyledEmpty>No results found.</StyledEmpty>
|
||||
|
||||
<StyledGroup heading="Go to">
|
||||
<StyledItem
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
navigate('/people');
|
||||
}}
|
||||
>
|
||||
People
|
||||
</StyledItem>
|
||||
<StyledItem
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
navigate('/companies');
|
||||
}}
|
||||
>
|
||||
Companies
|
||||
</StyledItem>
|
||||
</StyledGroup>
|
||||
</StyledList>
|
||||
</StyledDialog>
|
||||
);
|
||||
};
|
||||
104
front/src/modules/search/components/CommandMenuStyles.tsx
Normal file
104
front/src/modules/search/components/CommandMenuStyles.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Command } from 'cmdk';
|
||||
|
||||
export const StyledDialog = styled(Command.Dialog)`
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 640px;
|
||||
padding: 25px;
|
||||
width: 100%;
|
||||
background: ${(props) => props.theme.primaryBackground};
|
||||
border-radius: ${(props) => props.theme.borderRadius};
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
font-family: ${(props) => props.theme.fontFamily};
|
||||
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
|
||||
`;
|
||||
|
||||
export const StyledInput = styled(Command.Input)`
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-size: ${(props) => props.theme.fontSizeLarge};
|
||||
padding: ${(props) => props.theme.spacing(5)};
|
||||
outline: none;
|
||||
background: ${(props) => props.theme.primaryBackground};
|
||||
color: ${(props) => props.theme.text100};
|
||||
border-bottom: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
border-radius: 0;
|
||||
caret-color: ${(props) => props.theme.blue};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const StyledItem = styled(Command.Item)`
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
font-size: ${(props) => props.theme.fontSizeMedium};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${(props) => props.theme.spacing(3)};
|
||||
padding: 0 ${(props) => props.theme.spacing(4)};
|
||||
color: ${(props) => props.theme.text100};
|
||||
user-select: none;
|
||||
transition: all 150ms ease;
|
||||
transition-property: none;
|
||||
position: relative;
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.clickableElementBackgroundHover};
|
||||
}
|
||||
&[data-selected='true'] {
|
||||
background: ${(props) => props.theme.secondaryBackground};
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: ${(props) => props.theme.lastLayerZIndex};
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: ${(props) => props.theme.blue};
|
||||
}
|
||||
}
|
||||
&[data-disabled='true'] {
|
||||
color: ${(props) => props.theme.text30};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: ${(props) => props.theme.text80};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledList = styled(Command.List)`
|
||||
height: min(300px, var(--cmdk-list-height));
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
transition: 100ms ease;
|
||||
transition-property: height;
|
||||
background: ${(props) => props.theme.secondaryBackground};
|
||||
`;
|
||||
|
||||
export const StyledGroup = styled(Command.Group)`
|
||||
[cmdk-group-heading] {
|
||||
user-select: none;
|
||||
font-size: ${(props) => props.theme.fontSizeExtraSmall};
|
||||
color: ${(props) => props.theme.text30};
|
||||
padding: ${(props) => props.theme.spacing(2)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledEmpty = styled(Command.Empty)`
|
||||
font-size: ${(props) => props.theme.fontSizeMedium};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64px;
|
||||
white-space: pre-wrap;
|
||||
color: ${(props) => props.theme.text30};
|
||||
`;
|
||||
|
||||
export const StyledSeparator = styled(Command.Separator)``;
|
||||
@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||
|
||||
import { CommandMenu } from '../CommandMenu';
|
||||
|
||||
const meta: Meta<typeof CommandMenu> = {
|
||||
title: 'Pages/Search/CommandMenu',
|
||||
component: CommandMenu,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommandMenu>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: getRenderWrapperForPage(<CommandMenu initiallyOpen={true} />),
|
||||
};
|
||||
@ -2,6 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { CommandMenu } from '@/search/components/CommandMenu';
|
||||
|
||||
import { Navbar } from './navbar/Navbar';
|
||||
|
||||
@ -34,6 +35,7 @@ export function AppLayout({ children }: OwnProps) {
|
||||
<StyledLayout>
|
||||
{userIsAuthenticated ? (
|
||||
<>
|
||||
<CommandMenu />
|
||||
<Navbar />
|
||||
<MainContainer>{children}</MainContainer>
|
||||
</>
|
||||
|
||||
@ -26,6 +26,8 @@ const commonTheme = {
|
||||
borderRadius: '4px',
|
||||
|
||||
rightDrawerWidth: '300px',
|
||||
|
||||
lastLayerZIndex: 2147483647,
|
||||
};
|
||||
|
||||
const lightThemeSpecific = {
|
||||
|
||||
Reference in New Issue
Block a user