From 49b1070c5f745bfe544d7b53d43b942be0cb1b2b Mon Sep 17 00:00:00 2001 From: Ben White Date: Fri, 8 Mar 2024 20:26:10 +0100 Subject: [PATCH] feat: Toolbar UX improvements (#20791) * Toolbar improvements * Always show title * Fix loading of flags --- frontend/src/toolbar/Toolbar.stories.tsx | 3 +- frontend/src/toolbar/ToolbarApp.tsx | 6 ++-- .../src/toolbar/actions/ActionsListView.tsx | 17 +++++++--- frontend/src/toolbar/actions/actionsLogic.ts | 2 ++ .../src/toolbar/actions/actionsTabLogic.tsx | 1 - frontend/src/toolbar/bar/Toolbar.scss | 10 ++++-- frontend/src/toolbar/bar/Toolbar.tsx | 6 ++-- frontend/src/toolbar/bar/ToolbarButton.tsx | 2 +- frontend/src/toolbar/bar/toolbarLogic.ts | 31 ++++++++++++++++--- .../src/toolbar/flags/FlagsToolbarMenu.tsx | 13 ++++++-- ...ogic.test.ts => flagsToolbarLogic.test.ts} | 7 +++-- ...tureFlagsLogic.ts => flagsToolbarLogic.ts} | 16 ++++------ frontend/src/toolbar/utils.ts | 12 ++++--- 13 files changed, 85 insertions(+), 41 deletions(-) rename frontend/src/toolbar/flags/{featureFlagsLogic.test.ts => flagsToolbarLogic.test.ts} (93%) rename frontend/src/toolbar/flags/{featureFlagsLogic.ts => flagsToolbarLogic.ts} (92%) diff --git a/frontend/src/toolbar/Toolbar.stories.tsx b/frontend/src/toolbar/Toolbar.stories.tsx index d3fdabe243a26..dd60ccec1e7c8 100644 --- a/frontend/src/toolbar/Toolbar.stories.tsx +++ b/frontend/src/toolbar/Toolbar.stories.tsx @@ -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') diff --git a/frontend/src/toolbar/ToolbarApp.tsx b/frontend/src/toolbar/ToolbarApp.tsx index 65aac3ed51608..39d2d15afe8b7 100644 --- a/frontend/src/toolbar/ToolbarApp.tsx +++ b/frontend/src/toolbar/ToolbarApp.tsx @@ -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 { @@ -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 ( <> - +
{didRender && (didLoadStyles || props.disableExternalStyles) ? : null} { + getActions() + }, []) + return (
- {allActionsLoading ? ( -
- -
- ) : actions.length ? ( + {actions.length ? ( actions.map((action, index) => ( <> )) + ) : allActionsLoading ? ( +
+ +
) : (
No {searchTerm.length ? 'matching ' : ''}actions found.
)} diff --git a/frontend/src/toolbar/actions/actionsLogic.ts b/frontend/src/toolbar/actions/actionsLogic.ts index f2311a949cc87..88e9893cad502 100644 --- a/frontend/src/toolbar/actions/actionsLogic.ts +++ b/frontend/src/toolbar/actions/actionsLogic.ts @@ -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' @@ -68,4 +69,5 @@ export const actionsLogic = kea([ ], actionCount: [(s) => [s.allActions], (allActions) => allActions.length], }), + permanentlyMount(), ]) diff --git a/frontend/src/toolbar/actions/actionsTabLogic.tsx b/frontend/src/toolbar/actions/actionsTabLogic.tsx index 4c37a5d10d1ec..dc49fa26042e0 100644 --- a/frontend/src/toolbar/actions/actionsTabLogic.tsx +++ b/frontend/src/toolbar/actions/actionsTabLogic.tsx @@ -292,7 +292,6 @@ export const actionsTabLogic = kea([ } }, showButtonActions: () => { - actionsLogic.actions.getActions() posthog.capture('toolbar mode triggered', { mode: 'actions', enabled: true }) }, hideButtonActions: () => { diff --git a/frontend/src/toolbar/bar/Toolbar.scss b/frontend/src/toolbar/bar/Toolbar.scss index 6f3b593bd7d46..ff83b7c9d922a 100644 --- a/frontend/src/toolbar/bar/Toolbar.scss +++ b/frontend/src/toolbar/bar/Toolbar.scss @@ -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; @@ -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 { @@ -81,6 +79,12 @@ } } + &--blurred { + .ToolbarMenu__content { + opacity: 0; + } + } + &.ToolbarMenu--below { .ToolbarMenu__content { top: 0; diff --git a/frontend/src/toolbar/bar/Toolbar.tsx b/frontend/src/toolbar/bar/Toolbar.tsx index 61ffd96a47b21..3bdda9e523e79 100644 --- a/frontend/src/toolbar/bar/Toolbar.tsx +++ b/frontend/src/toolbar/bar/Toolbar.tsx @@ -84,7 +84,7 @@ function MoreMenu(): JSX.Element { export function ToolbarInfoMenu(): JSX.Element { const ref = useRef(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' ? ( @@ -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 @@ -132,7 +133,7 @@ export function ToolbarInfoMenu(): JSX.Element { export function Toolbar(): JSX.Element { const ref = useRef(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) @@ -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={ { diff --git a/frontend/src/toolbar/bar/ToolbarButton.tsx b/frontend/src/toolbar/bar/ToolbarButton.tsx index cdff56303d1e6..add0e5f2580ce 100644 --- a/frontend/src/toolbar/bar/ToolbarButton.tsx +++ b/frontend/src/toolbar/bar/ToolbarButton.tsx @@ -61,7 +61,7 @@ export const ToolbarButton: FunctionComponent = React.forwar
) - return ((minimized && titleMinimized) || theTitle) && !active && !isDragging ? ( + return ((minimized && titleMinimized) || theTitle) && !isDragging ? ( {theButton} ) : ( theButton diff --git a/frontend/src/toolbar/bar/toolbarLogic.ts b/frontend/src/toolbar/bar/toolbarLogic.ts index 8f80eaac4c08a..7b01c978dcd92 100644 --- a/frontend/src/toolbar/bar/toolbarLogic.ts +++ b/frontend/src/toolbar/bar/toolbarLogic.ts @@ -1,4 +1,4 @@ -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' @@ -6,7 +6,7 @@ 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' @@ -40,6 +40,7 @@ export const toolbarLogic = kea([ setDragging: (dragging = true) => ({ dragging }), setElement: (element: HTMLElement | null) => ({ element }), setMenu: (element: HTMLElement | null) => ({ element }), + setIsBlurred: (isBlurred: boolean) => ({ isBlurred }), })), windowValues(() => ({ windowHeight: (window: Window) => window.innerHeight, @@ -74,6 +75,14 @@ export const toolbarLogic = kea([ 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 }, @@ -129,8 +138,8 @@ export const toolbarLogic = kea([ ], 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 {} } @@ -145,7 +154,7 @@ export const toolbarLogic = kea([ ? 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 @@ -257,4 +266,16 @@ export const toolbarLogic = kea([ 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) + }), ]) diff --git a/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx b/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx index a74884e2faa2a..2ffc0cd9666b3 100644 --- a/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx +++ b/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx @@ -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 ( diff --git a/frontend/src/toolbar/flags/featureFlagsLogic.test.ts b/frontend/src/toolbar/flags/flagsToolbarLogic.test.ts similarity index 93% rename from frontend/src/toolbar/flags/featureFlagsLogic.test.ts rename to frontend/src/toolbar/flags/flagsToolbarLogic.test.ts index 0841606815f44..5ac9502c27d75 100644 --- a/frontend/src/toolbar/flags/featureFlagsLogic.test.ts +++ b/frontend/src/toolbar/flags/flagsToolbarLogic.test.ts @@ -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' @@ -23,7 +23,7 @@ const featureFlagsWithExtraInfo = [ ] describe('toolbar featureFlagsLogic', () => { - let logic: ReturnType + let logic: ReturnType beforeEach(() => { global.fetch = jest.fn(() => Promise.resolve({ @@ -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', () => { diff --git a/frontend/src/toolbar/flags/featureFlagsLogic.ts b/frontend/src/toolbar/flags/flagsToolbarLogic.ts similarity index 92% rename from frontend/src/toolbar/flags/featureFlagsLogic.ts rename to frontend/src/toolbar/flags/flagsToolbarLogic.ts index 747937ab6e19f..ba385dc1c2a19 100644 --- a/frontend/src/toolbar/flags/featureFlagsLogic.ts +++ b/frontend/src/toolbar/flags/flagsToolbarLogic.ts @@ -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([ - path(['toolbar', 'flags', 'featureFlagsLogic']), +export const flagsToolbarLogic = kea([ + path(['toolbar', 'flags', 'flagsToolbarLogic']), connect(() => ({ values: [toolbarConfigLogic, ['posthog']], })), @@ -130,12 +131,7 @@ export const featureFlagsLogic = kea([ } }, })), - events(({ actions }) => ({ - afterMount: () => { - actions.getUserFlags() - actions.checkLocalOverrides() - }, - })), + permanentlyMount(), ]) function getGroups(posthogInstance: PostHog | null): Record { diff --git a/frontend/src/toolbar/utils.ts b/frontend/src/toolbar/utils.ts index 368e569bb84b5..20441d2d385a9 100644 --- a/frontend/src/toolbar/utils.ts +++ b/frontend/src/toolbar/utils.ts @@ -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 '' @@ -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 { @@ -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 } @@ -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]