0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 09:14:46 +01:00

feat(navigation): Add "Command palette" button (#20018)

* Clarify search/actions bar copy

* Add command palette to the navbar

* Remove `keyboardShortcut` from `NavbarButton`

* Remove `systemStatusHealthy`

* Fix side action styling

* Enforce top-right placement of navbar side action tooltips

* Move command palette info to tooltip

* Restore pnpm-lock.yaml

* Revert "System issue!" removal

* Rename "Switch theme" to "Change theme"

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (2)

* Fix tooltip

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (2)

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Michael Matloka 2024-03-01 15:15:09 +01:00 committed by GitHub
parent 57edf362c8
commit e12629626e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 135 additions and 160 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -127,10 +127,6 @@
.NavbarButton {
position: relative;
.LemonButton__icon {
transition: color 100ms ease, transform 100ms ease;
}
&.NavbarButton--here {
&::after {
position: absolute;

View File

@ -1,8 +1,4 @@
.KeyboardShortcut {
margin: 0 0.125rem;
}
.KeyboardShortcut__key {
display: inline-flex;
align-items: center;
justify-content: center;

View File

@ -1,7 +1,7 @@
import './KeyboardShortcut.scss'
import clsx from 'clsx'
import { isMac } from 'lib/utils'
import { isMac, isMobile } from 'lib/utils'
import { HotKeyOrModifier } from '~/types'
@ -26,19 +26,24 @@ const MODIFIER_PRIORITY: HotKeyOrModifier[] = ['shift', 'command', 'option']
export interface KeyboardShortcutProps extends Partial<Record<HotKeyOrModifier, true>> {
/** Whether this shortcut should be shown with muted opacity. */
muted?: boolean
className?: string
}
export function KeyboardShortcut({ muted, ...keys }: KeyboardShortcutProps): JSX.Element {
export function KeyboardShortcut({ muted, className, ...keys }: KeyboardShortcutProps): JSX.Element | null {
const sortedKeys = Object.keys(keys).sort(
(a, b) =>
(-MODIFIER_PRIORITY.indexOf(a as HotKeyOrModifier) || 0) -
(-MODIFIER_PRIORITY.indexOf(b as HotKeyOrModifier) || 0)
) as HotKeyOrModifier[]
if (isMobile()) {
// If the user agent says we're on mobile, then it's unlikely - though of course not impossible -
// that there's a physical keyboard. Hence in that case we don't show the keyboard shortcut
return null
}
return (
<span
className={clsx('KeyboardShortcut KeyboardShortcut__key space-x-0.5', muted && 'KeyboardShortcut--muted')}
>
<span className={clsx('KeyboardShortcut space-x-0.5', muted && 'KeyboardShortcut--muted', className)}>
{sortedKeys.map((key) => (
<span key={key}>{KEY_TO_SYMBOL[key] || key}</span>
))}

View File

@ -17,6 +17,7 @@ import { navigationLogic } from '~/layout/navigation/navigationLogic'
import { AccountPopoverOverlay } from '~/layout/navigation/TopBar/AccountPopover'
import { navigation3000Logic } from '../navigationLogic'
import { KeyboardShortcut } from './KeyboardShortcut'
import { NavbarButton } from './NavbarButton'
export function Navbar(): JSX.Element {
@ -74,9 +75,20 @@ export function Navbar(): JSX.Element {
<NavbarButton
identifier="search-button"
icon={<IconSearch />}
title="Search"
shortTitle="Search"
title={
<div className="flex flex-col gap-0.5">
<span>
For search, press <KeyboardShortcut command k />
</span>
<span>
For commands, press <KeyboardShortcut command shift k />
</span>
</div>
}
forceTooltipOnHover
sideIcon={<KeyboardShortcut command k />}
onClick={toggleSearchBar}
keyboardShortcut={{ command: true, k: true }}
/>
<NavbarButton
icon={<IconToolbar />}

View File

@ -4,7 +4,6 @@ import { useActions, useValues } from 'kea'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { isMobile } from 'lib/utils'
import React, { FunctionComponent, ReactElement, useState } from 'react'
import { sceneLogic } from 'scenes/sceneLogic'
@ -13,7 +12,6 @@ import { SidebarChangeNoticeContent, useSidebarChangeNotices } from '~/layout/na
import { navigation3000Logic } from '../navigationLogic'
import { NavbarItem } from '../types'
import { KeyboardShortcut, KeyboardShortcutProps } from './KeyboardShortcut'
export interface NavbarButtonProps extends Pick<LemonButtonProps, 'onClick' | 'icon' | 'sideIcon' | 'to' | 'active'> {
identifier: string
@ -22,136 +20,110 @@ export interface NavbarButtonProps extends Pick<LemonButtonProps, 'onClick' | 'i
shortTitle?: string
forceTooltipOnHover?: boolean
tag?: 'alpha' | 'beta' | 'new'
keyboardShortcut?: KeyboardShortcutProps
sideAction?: NavbarItem['sideAction']
}
export const NavbarButton: FunctionComponent<NavbarButtonProps> = React.forwardRef<
HTMLButtonElement,
NavbarButtonProps
>(
(
{
identifier,
shortTitle,
title,
forceTooltipOnHover,
tag,
onClick,
keyboardShortcut,
sideAction,
sideIcon,
...rest
},
ref
): JSX.Element => {
const { activeScene } = useValues(sceneLogic)
const { sceneBreadcrumbKeys } = useValues(breadcrumbsLogic)
const { hideNavOnMobile } = useActions(navigation3000Logic)
const { isNavCollapsed } = useValues(navigation3000Logic)
const isUsingNewNav = useFeatureFlag('POSTHOG_3000_NAV')
>(({ identifier, shortTitle, title, forceTooltipOnHover, tag, onClick, sideAction, ...rest }, ref): JSX.Element => {
const { activeScene } = useValues(sceneLogic)
const { sceneBreadcrumbKeys } = useValues(breadcrumbsLogic)
const { hideNavOnMobile } = useActions(navigation3000Logic)
const { isNavCollapsed } = useValues(navigation3000Logic)
const isUsingNewNav = useFeatureFlag('POSTHOG_3000_NAV')
const [hasBeenClicked, setHasBeenClicked] = useState(false)
const [hasBeenClicked, setHasBeenClicked] = useState(false)
const here = activeScene === identifier || sceneBreadcrumbKeys.includes(identifier)
const isNavCollapsedActually = isNavCollapsed || isUsingNewNav
const here = activeScene === identifier || sceneBreadcrumbKeys.includes(identifier)
const isNavCollapsedActually = isNavCollapsed || isUsingNewNav
const buttonProps: LemonButtonProps = rest
if (!isUsingNewNav) {
buttonProps.active = here
}
if (!isNavCollapsedActually) {
if (sideAction) {
// @ts-expect-error - in this case we are perfectly okay with assigning a sideAction
buttonProps.sideAction = {
...sideAction,
divider: true,
'data-attr': `menu-item-${sideAction.identifier.toLowerCase()}`,
}
buttonProps.sideIcon = null
} else if (keyboardShortcut && !isMobile()) {
// If the user agent says we're on mobile, then it's unlikely - but not impossible -
// that there's a physical keyboard. Hence in that case we don't show the keyboard shortcut
buttonProps.sideIcon = (
<span className="text-xs">
<KeyboardShortcut {...keyboardShortcut} />
</span>
)
}
}
let content: JSX.Element | string | undefined
if (!isNavCollapsedActually) {
content = shortTitle || title
if (tag) {
content = (
<>
<span className="grow">{content}</span>
<LemonTag
type={tag === 'alpha' ? 'completion' : tag === 'beta' ? 'warning' : 'success'}
size="small"
className="ml-2"
>
{tag.toUpperCase()}
</LemonTag>
</>
)
}
}
const buttonContent = (
<LemonButton
ref={ref}
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
onMouseEnter={() => setHasBeenClicked(false)}
onClick={(e) => {
if (buttonProps.to) {
hideNavOnMobile()
}
setHasBeenClicked(true)
onClick?.(e)
}}
className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')}
fullWidth
type="secondary"
status="alt"
{...buttonProps}
>
{content}
</LemonButton>
)
const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier })
return (
<li className="w-full">
{notices.length ? (
<Tooltip
title={<SidebarChangeNoticeContent notices={notices} onAcknowledged={onAcknowledged} />}
placement={notices[0].placement ?? 'right'}
delayMs={0}
visible={true}
>
{buttonContent}
</Tooltip>
) : (
<Tooltip
title={
forceTooltipOnHover || isNavCollapsedActually
? here
? `${title} (you are here)`
: title
: null
}
placement="right"
delayMs={0}
visible={hasBeenClicked ? false : undefined} // Force-hide tooltip after button click
>
{buttonContent}
</Tooltip>
)}
</li>
)
const buttonProps: LemonButtonProps = rest
if (!isUsingNewNav) {
buttonProps.active = here
}
)
let content: JSX.Element | string | undefined
if (!isNavCollapsedActually) {
content = shortTitle || title
if (tag) {
content = (
<>
<span className="grow">{content}</span>
<LemonTag
type={tag === 'alpha' ? 'completion' : tag === 'beta' ? 'warning' : 'success'}
size="small"
className="ml-2"
>
{tag.toUpperCase()}
</LemonTag>
</>
)
}
if (sideAction) {
// @ts-expect-error - in this case we are perfectly okay with assigning a sideAction
buttonProps.sideAction = {
...sideAction,
divider: true,
'data-attr': `menu-item-${sideAction.identifier.toLowerCase()}`,
}
buttonProps.sideIcon = null
}
} else {
buttonProps.sideIcon = null
}
const buttonContent = (
<LemonButton
ref={ref}
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
onMouseEnter={() => setHasBeenClicked(false)}
onClick={(e) => {
if (buttonProps.to) {
hideNavOnMobile()
}
setHasBeenClicked(true)
onClick?.(e)
}}
className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')}
fullWidth
type="secondary"
status="alt"
{...buttonProps}
>
{content}
</LemonButton>
)
const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier })
return (
<li className="w-full">
{notices.length ? (
<Tooltip
title={<SidebarChangeNoticeContent notices={notices} onAcknowledged={onAcknowledged} />}
placement={notices[0].placement ?? 'right'}
delayMs={0}
visible={true}
>
{buttonContent}
</Tooltip>
) : (
<Tooltip
title={
forceTooltipOnHover || isNavCollapsedActually
? here
? `${title} (you are here)`
: title
: null
}
placement="right"
delayMs={0}
visible={hasBeenClicked ? false : undefined} // Force-hide tooltip after button click
>
{buttonContent}
</Tooltip>
)}
</li>
)
})
NavbarButton.displayName = 'NavbarButton'

View File

@ -30,7 +30,7 @@ interface NavbarItemBase {
icon: JSX.Element
featureFlag?: (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
tag?: 'alpha' | 'beta' | 'new'
sideAction?: Pick<SideAction, 'icon' | 'to' | 'onClick' | 'tooltip' | 'dropdown'> & { identifier: string }
sideAction?: Omit<SideAction, 'divider' | 'data-attr' | 'tooltipPlacement'> & { identifier: string }
}
export interface SceneNavbarItem extends NavbarItemBase {
to: string

View File

@ -3,6 +3,7 @@ import { LemonInput } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { CommandFlow } from 'lib/components/CommandPalette/commandPaletteLogic'
import { IconChevronRight } from 'lib/lemon-ui/icons'
import { isMac } from 'lib/utils'
import React from 'react'
import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'
@ -33,7 +34,9 @@ export const ActionInput = (): JSX.Element => {
fullWidth
prefix={<PrefixIcon activeFlow={activeFlow} />}
suffix={<KeyboardShortcut escape />}
placeholder={activeFlow?.instruction ?? 'Run a command…'}
placeholder={
activeFlow?.instruction ?? `Run a command… or press ${isMac() ? 'Delete' : 'Backspace'} for search`
}
autoFocus
value={input}
onChange={setInput}

View File

@ -1,6 +1,5 @@
import { LemonButton, LemonInput } from '@posthog/lemon-ui'
import { LemonInput } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { isMac } from 'lib/utils'
import { forwardRef, Ref } from 'react'
import { teamLogic } from 'scenes/teamLogic'
@ -8,15 +7,12 @@ import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardSh
import { searchBarLogic } from './searchBarLogic'
export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref<HTMLInputElement>): JSX.Element {
export const SearchInput = forwardRef(function SearchInput(_, ref: Ref<HTMLInputElement>): JSX.Element {
const { currentTeam } = useValues(teamLogic)
const { searchQuery } = useValues(searchBarLogic)
const { setSearchQuery, hideCommandBar } = useActions(searchBarLogic)
const { setSearchQuery } = useActions(searchBarLogic)
const modifierKey = isMac() ? '⌘' : '^'
const placeholder = currentTeam
? `Search the ${currentTeam.name} project or press ${modifierKey}⇧K to go to commands…`
: `Search or press ${modifierKey}⇧K to go to commands…`
const placeholder = `Search ${currentTeam ? 'the ' + currentTeam.name : 'this'} project… or enter > for commands`
return (
<div className="border-b">
@ -26,11 +22,7 @@ export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref<HTMLInpu
type="search"
className="CommandBar__input"
fullWidth
suffix={
<LemonButton onClick={() => hideCommandBar()} noPadding>
<KeyboardShortcut escape />
</LemonButton>
}
suffix={<KeyboardShortcut escape />}
placeholder={placeholder}
autoFocus
value={searchQuery}

View File

@ -839,10 +839,10 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
icon: IconEye,
display: 'Switch theme',
display: 'Change theme',
synonyms: ['toggle theme', 'dark mode', 'light mode'],
executor: () => ({
scope: 'Switch theme',
scope: 'Change theme',
resolver: [
{
icon: IconDay,

View File

@ -379,7 +379,7 @@
var(--lemon-button-side-action-width) -
var(--lemon-button-padding-right, var(--lemon-button-padding-horizontal))
);
height: 1.25rem;
height: calc(var(--lemon-button-height) - 1.0625rem);
color: var(--muted);
&--divider {
@ -403,7 +403,6 @@
.LemonButton {
--lemon-button-depth: 0px;
--lemon-button-icon-opacity: 0.5;
width: var(--lemon-button-side-action-width);
height: 100%;

View File

@ -259,12 +259,12 @@ export const LemonButton: React.FunctionComponent<LemonButtonProps & React.RefAt
{workingButton}
<div className="LemonButtonWithSideAction__side-button">
<SideComponent
// We don't want secondary style as it creates double borders
type={type !== 'secondary' ? type : undefined}
type={type}
size={size}
status={status}
dropdown={sideDropdown as LemonButtonDropdown}
noPadding
active={active}
{...sideActionRest}
/>
</div>