mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
improve focus styles
This commit is contained in:
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
flex-flow: column;
|
||||
overflow: auto;
|
||||
// to allow for focus outline
|
||||
padding: 6px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.pb-license-info {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user