diff --git a/src/activities/Activities.tsx b/src/activities/Activities.tsx index a9ca71e9..dbe83fa0 100644 --- a/src/activities/Activities.tsx +++ b/src/activities/Activities.tsx @@ -123,6 +123,7 @@ const Activities: React.VoidFunctionComponent = () => { } panel={} panelClassName="pb-activities-tabview" + onMouseDown={(e) => e.stopPropagation()} /> { } panel={} panelClassName="pb-activities-tabview" + onMouseDown={(e) => e.stopPropagation()} /> ); diff --git a/src/app/App.tsx b/src/app/App.tsx index 267981f7..83f5d0ad 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -2,7 +2,14 @@ // Copyright (c) 2020-2022 The Pybricks Authors import { Classes } from '@blueprintjs/core'; -import React, { useEffect, useState } from 'react'; +import { getFocusableTreeWalker } from '@react-aria/focus'; +import React, { + FocusEventHandler, + MouseEventHandler, + useCallback, + useEffect, + useState, +} from 'react'; import SplitterLayout from 'react-splitter-layout'; import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts'; import Activities from '../activities/Activities'; @@ -155,13 +162,57 @@ const App: React.VFC = () => { return () => removeEventListener('keydown', listener); }, []); + // keep track of last focused element in the activities area and restore + // focus to that element if any non-interactive area is clicked + + const [lastActivitiesFocusChild, setLastActivitiesFocusChild] = + useState(null); + + const handleFocus = useCallback( + (e) => { + if (e.target instanceof HTMLElement) { + setLastActivitiesFocusChild(e.target); + } + }, + [setLastActivitiesFocusChild], + ); + + const handleActivitiesMouseDown = useCallback>( + (e) => { + if ( + lastActivitiesFocusChild && + e.currentTarget.contains(lastActivitiesFocusChild) + ) { + // if the last focused child exists and it is still inside of + // the activities area, focus it + lastActivitiesFocusChild.focus(); + } else { + // otherwise, focus the first focusable element + const walker = getFocusableTreeWalker(e.currentTarget); + const first = walker.nextNode(); + + if (first instanceof HTMLElement) { + first.focus(); + } + } + + // prevent document body from getting focus + e.stopPropagation(); + e.preventDefault(); + }, + [lastActivitiesFocusChild], + ); + return ( -
e.preventDefault()} - > +
e.preventDefault()}>
- +
+ +
{/* need a container with position: relative; for SplitterLayout since it uses position: absolute; */}
{
-
+
-
- {isDragging &&
} +
+ {isDragging &&
}
diff --git a/src/app/app.scss b/src/app/app.scss index 2e754498..51d57ae6 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -7,11 +7,39 @@ @use '../variables' as pb; .pb-app { + width: 100%; + height: 100%; background-color: bp.$pt-app-background-color; .#{bp.$ns}-dark & { background-color: bp.$pt-dark-app-background-color; } + + &-activities { + display: flex; + @include pb.focus-within(); + } + + &-terminal { + height: 100%; + padding-left: 8px; + background-color: white; + @include pb.focus-within(); + + .#{bp.$ns}-dark & { + background-color: black; + } + } + + &-docs { + height: 100%; + + &-drag-helper { + height: 100%; + width: 100%; + position: absolute; + } + } } .pb-app-body { @@ -52,15 +80,6 @@ overflow: hidden; } -.pb-app-terminal-padding { - padding-left: 10px; - background-color: white; - - .#{bp.$ns}-dark & { - background-color: black; - } -} - // hide the docs and resize separator div.pb-hide-docs > :not(.layout-pane-primary) { diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 4bf04e72..63b13b21 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -20,7 +20,7 @@ type ButtonProps = { minimal?: boolean; /** Icon that will be displayed to the left of the button content. */ icon: IconName; - /** A refernece to the underlying - {/* This can't be inside the buttton element, otherwise it messes up the styling */} + {/* This can't be inside the button element, otherwise it messes up the styling */} {description && (