improve focus styles

This commit is contained in:
David Lechner
2022-05-16 21:59:58 -05:00
parent 9e092ca469
commit ef7f6d15c4
11 changed files with 160 additions and 41 deletions
+2
View File
@@ -123,6 +123,7 @@ const Activities: React.VoidFunctionComponent = () => {
}
panel={<Explorer />}
panelClassName="pb-activities-tabview"
onMouseDown={(e) => e.stopPropagation()}
/>
<Tab
aria-label={i18n.translate(I18nId.Settings)}
@@ -137,6 +138,7 @@ const Activities: React.VoidFunctionComponent = () => {
}
panel={<Settings />}
panelClassName="pb-activities-tabview"
onMouseDown={(e) => e.stopPropagation()}
/>
</Tabs>
);
+60 -9
View File
@@ -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<HTMLElement | null>(null);
const handleFocus = useCallback<FocusEventHandler>(
(e) => {
if (e.target instanceof HTMLElement) {
setLastActivitiesFocusChild(e.target);
}
},
[setLastActivitiesFocusChild],
);
const handleActivitiesMouseDown = useCallback<MouseEventHandler<HTMLDivElement>>(
(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 (
<div
className="pb-app h-100 w-100 p-absolute"
onContextMenu={(e) => e.preventDefault()}
>
<div className="pb-app" onContextMenu={(e) => e.preventDefault()}>
<div className="pb-app-body">
<Activities />
<div
className="pb-app-activities"
onFocus={handleFocus}
onMouseDown={handleActivitiesMouseDown}
>
<Activities />
</div>
{/* need a container with position: relative; for SplitterLayout since it uses position: absolute; */}
<div className="pb-app-main" style={{ position: 'relative' }}>
<SplitterLayout
@@ -184,12 +235,12 @@ const App: React.VFC = () => {
<Toolbar />
<Editor />
</div>
<div className="pb-app-terminal-padding h-100">
<div className="pb-app-terminal">
<Terminal />
</div>
</SplitterLayout>
<div className="h-100 w-100">
{isDragging && <div className="h-100 w-100 p-absolute" />}
<div className="pb-app-docs">
{isDragging && <div className="pb-app-docs-drag-helper" />}
<Docs />
</div>
</SplitterLayout>
+28 -9
View File
@@ -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) {
+5 -5
View File
@@ -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 <button> HTML element. */
/** A reference to the underlying <button> HTML element. */
elementRef?: React.ForwardedRef<HTMLButtonElement>;
/** Called when the button is pressed. */
onPress?: () => void;
@@ -56,10 +56,10 @@ export const Button: React.VoidFunctionComponent<ButtonProps> = ({
return (
<>
{/* The blueprint focus manage doesn't always get it right, so we ignore it. */}
<FocusRing focusRingClass={Classes.FOCUS_STYLE_MANAGER_IGNORE}>
{/* useButton() breaks the native browser :focus-visible :-/ */}
<FocusRing focusRingClass="pb-focus-ring">
<button
className={classNames(Classes.BUTTON, {
className={classNames(Classes.BUTTON, 'pb-focus-managed', {
[Classes.ACTIVE]: isPressed,
[Classes.LOADING]: loading,
[Classes.MINIMAL]: minimal,
@@ -84,7 +84,7 @@ export const Button: React.VoidFunctionComponent<ButtonProps> = ({
)}
</button>
</FocusRing>
{/* 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 && (
<div id={descriptionId} hidden>
{description}
+1
View File
@@ -80,6 +80,7 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
elementRef={elementRef}
onFocus={focusable === false ? (e) => e.preventDefault() : undefined}
onClick={handleClick}
onMouseDown={(e) => e.stopPropagation()}
/>
);
};
+30 -14
View File
@@ -16,26 +16,46 @@
--pb-vh: 100vh;
}
// use :focus-visible instead of :focus (so we don't need blueprint.js focus style manager)
:focus {
outline: none;
}
.#{bp.$ns}-control input:focus ~ .#{bp.$ns}-control-indicator {
outline: none;
.#{bp.$ns}-dark & {
outline: none;
}
}
// show focus for tree nodes even if mouse click it
.#{bp.$ns}-tree-node:focus,
// react-aria managed focus visibility
.pb-focus-ring,
// broswer managed focus visibility
:focus-visible:not(.pb-focus-managed) {
@include pb.focus-outline(0);
}
// these controls need extra separation for better visibility
.#{bp.$ns}-control input:focus-visible ~ .#{bp.$ns}-control-indicator {
@include pb.focus-outline(2px);
}
body {
// no scrolling of the page
overflow: hidden;
user-select: none;
}
// Utility classes
// Utility classes - do not use these in new code!
.h-100 {
height: 100%;
}
.w-100 {
width: 100%;
}
.p-absolute {
position: absolute;
}
.no-wrap {
white-space: nowrap;
}
@@ -112,11 +132,7 @@ a.#{bp.$ns}-button {
min-width: math.div(bp.$pt-grid-size, 2);
}
.#{bp.$ns}-focus-disabled :focus.#{bp.$ns}-focus-style-manager-ignore {
outline: bp.$pt-outline-color auto 2px !important;
outline-offset: 2px !important;
}
// make scrollbars fit our style
::-webkit-scrollbar {
width: 16px;
-3
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { FocusStyleManager } from '@blueprintjs/core';
import { I18nContext } from '@shopify/react-i18n';
import React from 'react';
import { OverlayProvider } from 'react-aria';
@@ -47,8 +46,6 @@ if (appVersion.match(/beta/)) {
sagaMiddleware.run(rootSaga);
FocusStyleManager.onlyShowFocusOnTabs();
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
+1 -1
View File
@@ -26,7 +26,7 @@
flex-flow: column;
overflow: auto;
// to allow for focus outline
padding: 6px;
padding: 2px;
}
.pb-license-info {
+1
View File
@@ -137,6 +137,7 @@ const Settings: React.VoidFunctionComponent = () => {
value={hubName}
onChange={(e) => setHubName(e.currentTarget.value)}
onMouseOver={(e) => e.preventDefault()}
onMouseDown={(e) => e.stopPropagation()}
intent={isHubNameValid ? Intent.NONE : Intent.DANGER}
placeholder="Pybricks Hub"
rightElement={
+1
View File
@@ -72,6 +72,7 @@ export const renderers: Omit<
(props.context.isSelected || props.context.isDraggingOver) &&
Classes.TREE_NODE_SELECTED,
)}
onMouseDown={(e) => e.stopPropagation()}
{...props.context.itemContainerWithChildrenProps}
{...props.context.interactiveElementProps}
>
+31
View File
@@ -12,6 +12,9 @@ $narrow-screen-limit: 700px;
// Official Pybricks branding color.
$pybricks-blue: #0088ce;
// blueprint doesn't define this
$pt-dark-outline-color: rgba(bp.$blue5, 0.6);
/**
* Adjusts the background contrast by $background-adjust (usually a percent).
*
@@ -31,3 +34,31 @@ $pybricks-blue: #0088ce;
);
}
}
// This is usually used with :focus-visible, but can be use with :focus if
// we wan't focust to be visible, even on mouse click.
@mixin focus-outline($offset) {
outline: bp.$pt-outline-color solid 2px;
outline-offset: $offset;
.#{bp.$ns}-dark & {
outline: bp.$pt-outline-color solid 2px;
outline-offset: $offset;
}
}
// add a focus ring when focus is within a container
@mixin focus-within() {
position: relative;
&:focus-within::after {
content: '';
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
@include focus-outline(-3px);
}
}