New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
focus: rework and untangle existing focus management logic in the sdk #3718
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Ignored Deployment
|
@@ -67,7 +67,6 @@ export function BoardHistorySnapshot({ | |||
}} | |||
overrides={[fileSystemUiOverrides]} | |||
inferDarkMode | |||
autoFocus |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is already the default
ref.current?.focus() | ||
}) | ||
// Focus the input | ||
const raf = requestAnimationFrame(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this double raf doesn't seem necessary... but maybe i'm missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah your version works in chrome, safari and firefox. cc @TodePond do you remember if this double raf was intentional?
@@ -52,8 +52,8 @@ export function UserPresenceEditor() { | |||
onCancel={toggleEditingName} | |||
onBlur={handleBlur} | |||
shouldManuallyMaintainScrollPositionWhenFocused | |||
autofocus | |||
autoselect | |||
autoFocus |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drive-by rename to make autoFocus
across the SDK consistent
@@ -39,15 +39,6 @@ export function useCanvasEvents() { | |||
name: 'pointer_down', | |||
...getPointerInfo(e), | |||
}) | |||
|
|||
if (editor.getOpenMenus().length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this logic path had bitrotted, as mentioned in the PR description
@@ -97,9 +88,6 @@ export function useCanvasEvents() { | |||
|
|||
function onTouchStart(e: React.TouchEvent) { | |||
;(e as any).isKilled = true | |||
// todo: investigate whether this effects keyboard shortcuts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't look like this is necessary anymore...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did you test this on iOS? seems fine on android
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, i've been testing on the iOS simulator. will definitely test again once it's on staging.
@@ -2,16 +2,6 @@ import { Editor } from '@tldraw/editor' | |||
|
|||
export function registerDefaultSideEffects(editor: Editor) { | |||
return [ | |||
editor.sideEffects.registerAfterChangeHandler('instance', (prev, next) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved to FocusManager
@@ -58,14 +58,6 @@ export const FrameHeading = function FrameHeading({ | |||
// On iOS, we must focus here | |||
el.focus() | |||
el.select() | |||
|
|||
requestAnimationFrame(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this double raf doesn't seem necessary anymore...
// When the label blurs, deselect all of the text and complete. | ||
// This makes it so that the canvas does not have to be focused | ||
// in order to exit the editing state and complete the editing state | ||
const handleBlur = useCallback(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we were doing a lot of hoops and somersaults to account for what TldrawUiButtonPicker
should have been managing
// This is fun little micro-optimization to make sure that the focus | ||
// is retained on a text label. That way, you can continue typing | ||
// after selecting a style. | ||
const origActiveEl = rPointingOriginalActiveElement.current |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for what useEditableText was trying to accomplish was moved here
const editor = useEditor() | ||
|
||
// If the button is getting disabled while it's focused, move focus to the editor | ||
// so that the user can continue using keyboard shortcuts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in the PR desc., it looks like this isn't needed anymore...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you sure? document.activeElement still reverts to the body
when the button becomes inactive in my tests. admittedly that no longer seems to prevent keyboard shortcuts from working but maybe it could cause other issues?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think i'm specifically making sure that the original PR (#2630) is working. if it causes other issues, then i think i would rather that we know what those issues are vs. this covering them up potentially (as a fix for something unrelated)
@MitjaBezensek @SomeHats actually hold off on reviewing - there's something i have to fix first |
/** | ||
* Dispatch a focus event. | ||
* | ||
* @example | ||
* ```ts | ||
* editor.focus() | ||
* ``` | ||
* | ||
* @public | ||
*/ | ||
focus(): this { | ||
this.focusManager.focus() | ||
return this | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder whether this should set the instanceState.isFocused
to true if it's not already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or really maybe it should be the job of the focus manager
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think possibly: that it's tricky. if there are multiple editors on the page, i could see an argument that you might want to temporarily translate/move a shape on one editor (Editor A), but the focus remains on a separate editor (Editor B). and that just because you focus Editor A, it doesn't mean that you want Editor A to take over control from Editor B. but perhaps that's overthinking it?
if (shapeId === id) { | ||
rInput.current?.select() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you sure this is still working on iOS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i've been testing on the iOS sim - will definitely test on staging once it lands to double check!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel super qualified to review this but things all seem to work on mac and android in my testing so 👍🏼
The only thing I'm feeling pause on is the behaviour/api of the focus manager.
- should calling focusManager.focus update the instanceState.isFocused property, and same for the .blur method?
- should we keep the manager itself private and just have a .focus and .blur method on the editor that call the underlying manager methods? I know how steve feels about namespaces.
yeah, it's a good question - like i said above: i think possibly: that it's tricky. if there are multiple editors on the page, i could see an argument that you might want to temporarily translate/move a shape on one editor (Editor A), but the focus remains on a separate editor (Editor B). and that just because you focus Editor A, it doesn't mean that you want Editor A to take over control from Editor B. but perhaps that's overthinking it?
fair point - i think i can make the focus manager be private actually. (to be clear, no one is calling |
Focus management is really scattered across the codebase. There's sort of a battle between different code paths to make the focus the correct desired state. It seemed to grow like a knot and once I started pulling on one thread to see if it was still needed you could see underneath that it was accounting for another thing underneath that perhaps wasn't needed.
The impetus for this PR came but especially during the text label rework, now that it's much more easy to jump around from textfield to textfield. It became apparent that we were playing whack-a-mole trying to preserve the right focus conditions (especially on iOS, ugh).
This tries to remove as many hacks as possible, and bring together in place the focus logic (and in the darkness, bind them).
Places affected
useEditableText
: was able to remove a bunch of the focus logic here. In addition, it doesn't look like we need to save the selection range anymore.editingShapeId === id
, the two text labels would have just fought each other for controluseFocusEvents
: nixed and refactored — we listen to the store inFocusManager
and then take care of autoFocus thereuseSafariFocusOutFix
: nixed. not necessary anymore because we're not trying to refocus when blurring inuseEditableText
. original PR for reference: https://github.com/tldraw/brivate/pull/79defaultSideEffects
: moved logic toFocusManager
PointingShape
focus forstartTranslating
, decided to leave this alone actually.TldrawUIButton
: it doesn't look like this focus bug fix is needed anymore, original PR for reference: [draft] Keep editor focus after losing focus of an action button #2630useDocumentEvents
: left alone its manual focus after the Escape key is hitFrameHeading
: double focus/select doesn't seem necessary anymoreuseCanvasEvents
:onPointerDown
focus logic never happened b/c inEditor.ts
weclearedMenus
on pointer downonTouchStart
: looks likedocument.body.click()
is not necessary anymoreFuture Changes
pointer_down
events to see if they're necessary.useContainer
maybe? Is it really necessary to have this hook? you can just douseEditor
→editor.getContainer()
, feels superfluous.Methodology
Looked for places where we do:
body.click()
container.focus()
container.blur()
editor.updateInstanceState({ isFocused })
autofocus
document.activeElement
Change Type
sdk
— Changes the tldraw SDKdotcom
— Changes the tldraw.com web appdocs
— Changes to the documentation, examples, or templates.vs code
— Changes to the vscode plugininternal
— Does not affect user-facing stuffbugfix
— Bug fixfeature
— New featureimprovement
— Improving existing featureschore
— Updating dependencies, other boring stuffgalaxy brain
— Architectural changestests
— Changes to any test codetools
— Changes to infrastructure, CI, internal scripts, debugging tools, etc.dunno
— I don't knowTest Plan
Release Notes