Skip to content

Commit

Permalink
feat: Toolbar UX improvements (#20791)
Browse files Browse the repository at this point in the history
* Toolbar improvements

* Always show title

* Fix loading of flags
  • Loading branch information
benjackwhite authored Mar 8, 2024
1 parent 2d8d9fe commit 49b1070
Show file tree
Hide file tree
Showing 13 changed files with 85 additions and 41 deletions.
3 changes: 2 additions & 1 deletion frontend/src/toolbar/Toolbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import { listHeatmapStatsAPIResponse } from './__mocks__/list-heatmap-stats-resp
import { listMyFlagsAPIResponse } from './__mocks__/list-my-flags-response'
import { MenuState, toolbarLogic } from './bar/toolbarLogic'
import { toolbarConfigLogic } from './toolbarConfigLogic'
import { TOOLBAR_ID } from './utils'

function useToolbarStyles(): void {
useEffect(() => {
const head = document.getElementsByTagName('head')[0]
const shadowRoot = window.document.getElementById('__POSTHOG_TOOLBAR__')?.shadowRoot
const shadowRoot = window.document.getElementById(TOOLBAR_ID)?.shadowRoot
const styleTags: HTMLStyleElement[] = Array.from(head.getElementsByTagName('style'))
styleTags.forEach((tag) => {
const style = document.createElement('style')
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/toolbar/ToolbarApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { toolbarConfigLogic } from '~/toolbar/toolbarConfigLogic'
import { ToolbarContainer } from '~/toolbar/ToolbarContainer'
import { ToolbarProps } from '~/types'

import { TOOLBAR_ID } from './utils'

type HTMLElementWithShadowRoot = HTMLElement & { shadowRoot: ShadowRoot }

export function ToolbarApp(props: ToolbarProps = {}): JSX.Element {
Expand All @@ -33,14 +35,14 @@ export function ToolbarApp(props: ToolbarProps = {}): JSX.Element {
styleLink.href = `${jsURL}/static/toolbar.css?t=${timestampToNearestFiveMinutes}`
styleLink.onload = () => setDidLoadStyles(true)
const shadowRoot =
shadowRef.current?.shadowRoot || window.document.getElementById('__POSTHOG_TOOLBAR__')?.shadowRoot
shadowRef.current?.shadowRoot || window.document.getElementById(TOOLBAR_ID)?.shadowRoot
shadowRoot?.getElementById('posthog-toolbar-styles')?.appendChild(styleLink)
}
)

return (
<>
<root.div id="__POSTHOG_TOOLBAR__" className="ph-no-capture" ref={shadowRef}>
<root.div id={TOOLBAR_ID} className="ph-no-capture" ref={shadowRef}>
<div id="posthog-toolbar-styles" />
{didRender && (didLoadStyles || props.disableExternalStyles) ? <ToolbarContainer /> : null}
<ToastContainer
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/toolbar/actions/ActionsListView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { useEffect } from 'react'

import { actionsLogic } from '~/toolbar/actions/actionsLogic'
import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic'
Expand All @@ -12,14 +13,16 @@ interface ActionsListViewProps {

export function ActionsListView({ actions }: ActionsListViewProps): JSX.Element {
const { allActionsLoading, searchTerm } = useValues(actionsLogic)
const { getActions } = useActions(actionsLogic)
const { selectAction } = useActions(actionsTabLogic)

useEffect(() => {
getActions()
}, [])

return (
<div className="flex flex-col h-full overflow-y-scoll space-y-px">
{allActionsLoading ? (
<div className="flex items-center">
<Spinner className="text-4xl" />
</div>
) : actions.length ? (
{actions.length ? (
actions.map((action, index) => (
<>
<Link
Expand All @@ -35,6 +38,10 @@ export function ActionsListView({ actions }: ActionsListViewProps): JSX.Element
</Link>
</>
))
) : allActionsLoading ? (
<div className="flex items-center">
<Spinner className="text-4xl" />
</div>
) : (
<div className="p-2">No {searchTerm.length ? 'matching ' : ''}actions found.</div>
)}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/toolbar/actions/actionsLogic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Fuse from 'fuse.js'
import { actions, kea, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import { permanentlyMount } from 'lib/utils/kea-logic-builders'

import { toolbarConfigLogic, toolbarFetch } from '~/toolbar/toolbarConfigLogic'
import { ActionType } from '~/types'
Expand Down Expand Up @@ -68,4 +69,5 @@ export const actionsLogic = kea<actionsLogicType>([
],
actionCount: [(s) => [s.allActions], (allActions) => allActions.length],
}),
permanentlyMount(),
])
1 change: 0 additions & 1 deletion frontend/src/toolbar/actions/actionsTabLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ export const actionsTabLogic = kea<actionsTabLogicType>([
}
},
showButtonActions: () => {
actionsLogic.actions.getActions()
posthog.capture('toolbar mode triggered', { mode: 'actions', enabled: true })
},
hideButtonActions: () => {
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/toolbar/bar/Toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@
display: flex;
flex-direction: column;
width: 30rem;

// transition: height 0.5s ease, border 0.5s ease;

max-height: 0;
margin-left: -15rem;
overflow: hidden;
Expand All @@ -73,6 +70,7 @@
background-color: var(--bg-3000);
filter: drop-shadow(0 5px 10px var(--trace-3000));
border-radius: var(--radius);
transition: opacity 150ms ease, max-height 150ms ease;
}

&--visible {
Expand All @@ -81,6 +79,12 @@
}
}

&--blurred {
.ToolbarMenu__content {
opacity: 0;
}
}

&.ToolbarMenu--below {
.ToolbarMenu__content {
top: 0;
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/toolbar/bar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function MoreMenu(): JSX.Element {

export function ToolbarInfoMenu(): JSX.Element {
const ref = useRef<HTMLDivElement | null>(null)
const { visibleMenu, isDragging, menuProperties, minimized } = useValues(toolbarLogic)
const { visibleMenu, isDragging, menuProperties, minimized, isBlurred } = useValues(toolbarLogic)
const { setMenu } = useActions(toolbarLogic)

const content = minimized ? null : visibleMenu === 'flags' ? (
Expand All @@ -108,6 +108,7 @@ export function ToolbarInfoMenu(): JSX.Element {
'ToolbarMenu',
!!content && 'ToolbarMenu--visible',
isDragging && 'ToolbarMenu--dragging',
isBlurred && 'ToolbarMenu--blurred',
menuProperties.isBelow && 'ToolbarMenu--below'
)}
// eslint-disable-next-line react/forbid-dom-props
Expand All @@ -132,7 +133,7 @@ export function ToolbarInfoMenu(): JSX.Element {
export function Toolbar(): JSX.Element {
const ref = useRef<HTMLDivElement | null>(null)
const { minimized, dragPosition, isDragging, hedgehogMode } = useValues(toolbarLogic)
const { setVisibleMenu, toggleMinimized, onMouseDown, setElement } = useActions(toolbarLogic)
const { setVisibleMenu, toggleMinimized, onMouseDown, setElement, setIsBlurred } = useActions(toolbarLogic)
const { isAuthenticated, userIntent } = useValues(toolbarConfigLogic)
const { authenticate } = useActions(toolbarConfigLogic)

Expand Down Expand Up @@ -167,6 +168,7 @@ export function Toolbar(): JSX.Element {
isDragging && 'Toolbar--dragging'
)}
onMouseDown={(e) => onMouseDown(e as any)}
onMouseOver={() => setIsBlurred(false)}
// eslint-disable-next-line react/forbid-dom-props
style={
{
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/toolbar/bar/ToolbarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const ToolbarButton: FunctionComponent<ToolbarButtonProps> = React.forwar
</div>
)

return ((minimized && titleMinimized) || theTitle) && !active && !isDragging ? (
return ((minimized && titleMinimized) || theTitle) && !isDragging ? (
<Tooltip title={minimized ? titleMinimized : theTitle}>{theButton}</Tooltip>
) : (
theButton
Expand Down
31 changes: 26 additions & 5 deletions frontend/src/toolbar/bar/toolbarLogic.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { actions, afterMount, beforeUnmount, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { windowValues } from 'kea-window-values'
import { HedgehogActor } from 'lib/components/HedgehogBuddy/HedgehogBuddy'
import { SPRITE_SIZE } from 'lib/components/HedgehogBuddy/sprites/sprites'

import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic'
import { elementsLogic } from '~/toolbar/elements/elementsLogic'
import { heatmapLogic } from '~/toolbar/elements/heatmapLogic'
import { inBounds } from '~/toolbar/utils'
import { inBounds, TOOLBAR_ID } from '~/toolbar/utils'

import type { toolbarLogicType } from './toolbarLogicType'

Expand Down Expand Up @@ -40,6 +40,7 @@ export const toolbarLogic = kea<toolbarLogicType>([
setDragging: (dragging = true) => ({ dragging }),
setElement: (element: HTMLElement | null) => ({ element }),
setMenu: (element: HTMLElement | null) => ({ element }),
setIsBlurred: (isBlurred: boolean) => ({ isBlurred }),
})),
windowValues(() => ({
windowHeight: (window: Window) => window.innerHeight,
Expand Down Expand Up @@ -74,6 +75,14 @@ export const toolbarLogic = kea<toolbarLogicType>([
toggleMinimized: (state, { minimized }) => minimized ?? !state,
},
],
// Whether the toolbar is not in focus anymore (typically due to clicking elsewhere)
isBlurred: [
false,
{
setIsBlurred: (_, { isBlurred }) => isBlurred,
setVisibleMenu: () => false,
},
],
theme: [
'dark' as 'light' | 'dark',
{ persist: true },
Expand Down Expand Up @@ -129,8 +138,8 @@ export const toolbarLogic = kea<toolbarLogicType>([
],

menuProperties: [
(s) => [s.element, s.menu, s.dragPosition, s.windowWidth, s.windowHeight],
(element, menu, dragPosition, windowWidth, windowHeight) => {
(s) => [s.element, s.menu, s.dragPosition, s.windowWidth, s.windowHeight, s.isBlurred],
(element, menu, dragPosition, windowWidth, windowHeight, isBlurred) => {
if (!element || !menu) {
return {}
}
Expand All @@ -145,7 +154,7 @@ export const toolbarLogic = kea<toolbarLogicType>([
? windowHeight - dragPosition.y - elHeight - margin * 2
: dragPosition.y - margin * 2

maxHeight = inBounds(0, maxHeight, windowHeight * 0.6)
maxHeight = isBlurred ? 0 : inBounds(0, maxHeight, windowHeight * 0.6)

const desiredY = isBelow ? dragPosition.y + elHeight + margin : dragPosition.y - margin
const desiredX = dragPosition.x + elWidth * 0.5
Expand Down Expand Up @@ -257,4 +266,16 @@ export const toolbarLogic = kea<toolbarLogicType>([
actions.setVisibleMenu('actions')
},
})),
afterMount(({ actions, values, cache }) => {
cache.clickListener = (e: MouseEvent): void => {
const shouldBeBlurred = (e.target as HTMLElement)?.id !== TOOLBAR_ID
if (shouldBeBlurred && !values.isBlurred) {
actions.setIsBlurred(true)
}
}
window.addEventListener('mousedown', cache.clickListener)
}),
beforeUnmount(({ cache }) => {
window.removeEventListener('mousedown', cache.clickListener)
}),
])
13 changes: 10 additions & 3 deletions frontend/src/toolbar/flags/FlagsToolbarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ import { LemonRadio } from 'lib/lemon-ui/LemonRadio'
import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch'
import { Link } from 'lib/lemon-ui/Link'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { useEffect } from 'react'
import { urls } from 'scenes/urls'

import { ToolbarMenu } from '~/toolbar/bar/ToolbarMenu'
import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic'
import { flagsToolbarLogic } from '~/toolbar/flags/flagsToolbarLogic'
import { toolbarConfigLogic } from '~/toolbar/toolbarConfigLogic'

export const FlagsToolbarMenu = (): JSX.Element => {
const { searchTerm, filteredFlags, userFlagsLoading } = useValues(featureFlagsLogic)
const { setSearchTerm, setOverriddenUserFlag, deleteOverriddenUserFlag } = useActions(featureFlagsLogic)
const { searchTerm, filteredFlags, userFlagsLoading } = useValues(flagsToolbarLogic)
const { setSearchTerm, setOverriddenUserFlag, deleteOverriddenUserFlag, getUserFlags, checkLocalOverrides } =
useActions(flagsToolbarLogic)
const { apiURL } = useValues(toolbarConfigLogic)

useEffect(() => {
getUserFlags()
checkLocalOverrides()
}, [])

return (
<ToolbarMenu>
<ToolbarMenu.Header>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expectLogic } from 'kea-test-utils'

import { initKeaTests } from '~/test/init'
import { featureFlagsLogic } from '~/toolbar/flags/featureFlagsLogic'
import { flagsToolbarLogic } from '~/toolbar/flags/flagsToolbarLogic'
import { toolbarConfigLogic } from '~/toolbar/toolbarConfigLogic'
import { CombinedFeatureFlagAndValueType } from '~/types'

Expand All @@ -23,7 +23,7 @@ const featureFlagsWithExtraInfo = [
]

describe('toolbar featureFlagsLogic', () => {
let logic: ReturnType<typeof featureFlagsLogic.build>
let logic: ReturnType<typeof flagsToolbarLogic.build>
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
Expand All @@ -37,8 +37,9 @@ describe('toolbar featureFlagsLogic', () => {
beforeEach(() => {
initKeaTests()
toolbarConfigLogic({ apiURL: 'http://localhost' }).mount()
logic = featureFlagsLogic()
logic = flagsToolbarLogic()
logic.mount()
logic.actions.getUserFlags()
})

it('has expected defaults', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import Fuse from 'fuse.js'
import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea'
import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import { encodeParams } from 'kea-router'
import { permanentlyMount } from 'lib/utils/kea-logic-builders'
import type { PostHog } from 'posthog-js'

import { posthog as posthogJS } from '~/toolbar/posthog'
import { toolbarConfigLogic, toolbarFetch } from '~/toolbar/toolbarConfigLogic'
import { CombinedFeatureFlagAndValueType } from '~/types'

import type { featureFlagsLogicType } from './featureFlagsLogicType'
import type { flagsToolbarLogicType } from './flagsToolbarLogicType'

export const featureFlagsLogic = kea<featureFlagsLogicType>([
path(['toolbar', 'flags', 'featureFlagsLogic']),
export const flagsToolbarLogic = kea<flagsToolbarLogicType>([
path(['toolbar', 'flags', 'flagsToolbarLogic']),
connect(() => ({
values: [toolbarConfigLogic, ['posthog']],
})),
Expand Down Expand Up @@ -130,12 +131,7 @@ export const featureFlagsLogic = kea<featureFlagsLogicType>([
}
},
})),
events(({ actions }) => ({
afterMount: () => {
actions.getUserFlags()
actions.checkLocalOverrides()
},
})),
permanentlyMount(),
])

function getGroups(posthogInstance: PostHog | null): Record<string, any> {
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/toolbar/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { querySelectorAllDeep } from 'query-selector-shadow-dom'
import { ActionStepForm, BoxColor, ElementRect } from '~/toolbar/types'
import { ActionStepType, StringMatching } from '~/types'

export const TOOLBAR_ID = '__POSTHOG_TOOLBAR__'

export function getSafeText(el: HTMLElement): string {
if (!el.childNodes || !el.childNodes.length) {
return ''
Expand Down Expand Up @@ -70,8 +72,8 @@ export function elementToActionStep(element: HTMLElement, dataAttributes: string
}
}

export function getToolbarElement(): HTMLElement | null {
return window.document.getElementById('__POSTHOG_TOOLBAR__') || null
export function getToolbarRootElement(): HTMLElement | null {
return window.document.getElementById(TOOLBAR_ID) || null
}

export function hasCursorPointer(element: HTMLElement): boolean {
Expand All @@ -94,8 +96,8 @@ export function trimElement(element: HTMLElement): HTMLElement | null {
if (!element) {
return null
}
const toolbarElement = getToolbarElement()
if (toolbarElement && isParentOf(element, toolbarElement)) {
const rootElement = getToolbarRootElement()
if (rootElement && isParentOf(element, rootElement)) {
return null
}

Expand Down Expand Up @@ -155,7 +157,7 @@ export function getAllClickTargets(startNode: Document | HTMLElement | ShadowRoo
})

const shadowElements = allElements
.filter((el) => el.shadowRoot && el.getAttribute('id') !== '__POSTHOG_TOOLBAR__')
.filter((el) => el.shadowRoot && el.getAttribute('id') !== TOOLBAR_ID)
.map((el: HTMLElement) => (el.shadowRoot ? getAllClickTargets(el.shadowRoot) : []))
.reduce((a, b) => [...a, ...b], [])
const selectedElements = [...elements, ...pointerElements, ...shadowElements]
Expand Down

0 comments on commit 49b1070

Please sign in to comment.