Skip to content
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

Merged
merged 19 commits into from May 17, 2024

Conversation

mimecuvalo
Copy link
Collaborator

@mimecuvalo mimecuvalo commented May 8, 2024

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.
    • lingering footgun that needed to be fixed anyway: if there are two labels in the same shape, because we were just checking editingShapeId === id, the two text labels would have just fought each other for control
  • useFocusEvents: nixed and refactored — we listen to the store in FocusManager and then take care of autoFocus there
  • useSafariFocusOutFix: nixed. not necessary anymore because we're not trying to refocus when blurring in useEditableText. original PR for reference: https://github.com/tldraw/brivate/pull/79
  • defaultSideEffects: moved logic to FocusManager
  • PointingShape focus for startTranslating, 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 #2630
  • useDocumentEvents: left alone its manual focus after the Escape key is hit
  • FrameHeading: double focus/select doesn't seem necessary anymore
  • useCanvasEvents: onPointerDown focus logic never happened b/c in Editor.ts we clearedMenus on pointer down
    • onTouchStart: looks like document.body.click() is not necessary anymore

Future Changes

  • a11y: work on having an accessebility focus ring
  • Page visibility API: (https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API) events when tab is back in focus vs. background, different kind of focus
  • Reexamine places we manually dispatch pointer_down events to see if they're necessary.
  • Minor: get rid of useContainer maybe? Is it really necessary to have this hook? you can just do useEditoreditor.getContainer(), feels superfluous.

Methodology

Looked for places where we do:

  • body.click()
  • places we do container.focus()
  • places we do container.blur()
  • places we do editor.updateInstanceState({ isFocused })
  • places we do autofocus
  • searched for document.activeElement

Change Type

  • sdk — Changes the tldraw SDK
  • dotcom — Changes the tldraw.com web app
  • docs — Changes to the documentation, examples, or templates.
  • vs code — Changes to the vscode plugin
  • internal — Does not affect user-facing stuff
  • bugfix — Bug fix
  • feature — New feature
  • improvement — Improving existing features
  • chore — Updating dependencies, other boring stuff
  • galaxy brain — Architectural changes
  • tests — Changes to any test code
  • tools — Changes to infrastructure, CI, internal scripts, debugging tools, etc.
  • dunno — I don't know

Test Plan

  • run test-focus.spec.ts
  • check MultipleExample
  • check EditorFocusExample
  • check autoFocus
  • check style panel usage and focus events in general
  • check text editing focus, lots of different devices, mobile/desktop

Release Notes

  • Focus: rework and untangle existing focus management logic in the SDK

@huppy-bot huppy-bot bot added improvement Improves existing features sdk Affects the tldraw sdk labels May 8, 2024
Copy link

vercel bot commented May 8, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
examples ✅ Ready (Inspect) Visit Preview May 15, 2024 2:10pm
1 Ignored Deployment
Name Status Preview Updated (UTC)
tldraw-docs ⬜️ Ignored (Inspect) Visit Preview May 15, 2024 2:10pm

@@ -67,7 +67,6 @@ export function BoardHistorySnapshot({
}}
overrides={[fileSystemUiOverrides]}
inferDarkMode
autoFocus
Copy link
Collaborator Author

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(() => {
Copy link
Collaborator Author

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.

Copy link
Collaborator

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
Copy link
Collaborator Author

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) {
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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...

Copy link
Collaborator

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

Copy link
Collaborator Author

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) => {
Copy link
Collaborator Author

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(() => {
Copy link
Collaborator Author

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(() => {
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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...

Copy link
Collaborator

@ds300 ds300 May 13, 2024

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?

Copy link
Collaborator Author

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)

@mimecuvalo
Copy link
Collaborator Author

@MitjaBezensek @SomeHats actually hold off on reviewing - there's something i have to fix first

Comment on lines +8040 to +8053
/**
* Dispatch a focus event.
*
* @example
* ```ts
* editor.focus()
* ```
*
* @public
*/
focus(): this {
this.focusManager.focus()
return this
}
Copy link
Collaborator

@ds300 ds300 May 13, 2024

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?

Copy link
Collaborator

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

Copy link
Collaborator Author

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?

Comment on lines +23 to +25
if (shapeId === id) {
rInput.current?.select()
}
Copy link
Collaborator

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?

Copy link
Collaborator Author

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!

Copy link
Collaborator

@ds300 ds300 left a 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.

@mimecuvalo
Copy link
Collaborator Author

should calling focusManager.focus update the instanceState.isFocused property, and same for the .blur method?

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?

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.

fair point - i think i can make the focus manager be private actually. (to be clear, no one is calling blur() explicitly at the moment, it's only used in FocusManager at the moment)

@mimecuvalo mimecuvalo added this pull request to the merge queue May 17, 2024
Merged via the queue into main with commit b4c1f60 May 17, 2024
10 checks passed
@mimecuvalo mimecuvalo deleted the mime/focus-management branch May 17, 2024 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
improvement Improves existing features sdk Affects the tldraw sdk
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants