-
Notifications
You must be signed in to change notification settings - Fork 8
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
Fix input focus on re-ordered elements #164
Conversation
src/observe-tag.js
Outdated
const byDistanceFromIndex = targetIndex => (indexA, indexB) => { | ||
const diffFromTargetA = Math.abs(indexA - targetIndex) | ||
const diffFromTargetB = Math.abs(indexB - targetIndex) | ||
return diffFromTargetA < diffFromTargetB ? -1 : 1 | ||
} |
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 sorting function prioritizes indices that are closest to a target.
E.g. if the target is 5
out of [1, 2, 3, 4, 5, 6, 7, 8, 9]
,
then the new sorting would be [5, 4, 6, 3, 7, 2, 8, 1, 9]
.
I don't know if there's a more elegant way to write this, but this works 👍
@@ -30,6 +40,7 @@ module.exports = tagFunction => { | |||
// if an element had focus, copy over all the selection data (so we can copy it back later) | |||
if (removedElementWithFocusData.index >= 0) { | |||
const removedElementWithFocus = parentAndChildrenNodes[removedElementWithFocusData.index] | |||
removedElementWithFocusData.tagName = removedElementWithFocus.tagName |
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.
since we now search for the element we previously had focus on, we want to store the tagName
(this way, if an input is surrounded by other different elements, we can still find it again and reapply focus)
@@ -221,4 +221,44 @@ describe('Tram-One - regressions', () => { | |||
// cleanup - remove app | |||
appContainer.remove() | |||
}) | |||
|
|||
it('should not error when reseting focus if the number of elements changed', async () => { |
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 test app needed to be updated, and actually caused a previous test to fail without the fix, but it still feels worthwhile to give this it's own test, so that if they ever get refactored, we don't forget about it.
@@ -8,14 +8,17 @@ const html = registerHtml({ | |||
* component to test url parameters | |||
*/ | |||
module.exports = () => { | |||
const mirrorable = useGlobalStore('mirrorable-input', { value: '' }) |
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 think we actually needed this , { value: '' }
, it was documented as an error #156 , but this works without it
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.
In reality, I just need to create a minimal example of this...
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "tram-one", | |||
"version": "10.1.5", | |||
"version": "10.1.6", |
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.
no API change, fixes an issue with a feature implementation, so patch is good 👍
removedElementWithFocusData.scrollLeft = removedElementWithFocus.scrollLeft | ||
removedElementWithFocusData.scrollTop = removedElementWithFocus.scrollTop |
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 preserves the scroll position in an input or textarea that you are typing in. Previously you this would get reset on re-render, so this experience is more in-line with what people would expect.
I don't know that there's a great way to test this, so I'm fine not writing an integration test - this is one thing where I think it really requires manual verification... anything else would be just checking properties, which seems disingenuous since all elements support scrollLeft
and scrollTop
, even if they don't do anything.
const elementToGiveFocus = allElements | ||
.map(toIndicies) | ||
.sort(byDistanceFromIndex(removedElementWithFocusData.index)) | ||
.map(toNodes(allElements)) | ||
.find(nodeMatchesTagName) | ||
|
||
// if the element / child exists, focus it | ||
if (elementToGiveFocus !== undefined) { |
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 the actual fix. We look through all elements now, and verify that the element even exists. Previously, even if the element was removed, we'd still try to focus on something, which caused errors to be thrown.
<text x="59" y="15" fill="#010101" fill-opacity=".3">27.59 kB</text> | ||
<text x="59" y="14">27.59 kB</text> | ||
<text x="59" y="15" fill="#010101" fill-opacity=".3">29.33 kB</text> | ||
<text x="59" y="14">29.33 kB</text> |
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.
Most of this is comments, which is fine 👍
const parentAndChildrenElements = node => { | ||
const children = node.querySelectorAll('*') | ||
return [node, ...children] | ||
} |
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.
Is this recursive/ should it be?
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.
const parentAndChildrenNodes = [oldTag, ...children] | ||
removedElementWithFocusData.index = parentAndChildrenNodes.findIndex(element => element === document.activeElement) | ||
const allElements = parentAndChildrenElements(oldTag) | ||
removedElementWithFocusData.index = allElements.findIndex(element => element === document.activeElement) |
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.
Could we do a query selector here for active element, instead of checking against a large list created by a wildcard selector?
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 goal here is to find the index of the element, not the element itself (if that's all we needed, we could just use document.activeElement
). I'm not sure that a more specific query selector would help... 🤔 thinking about it now, we could query selector on only things that have a positive tabIndex
?
This might be just another one of those things where I want to do the performance test, and then fix as necessary.
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 do agree that if there are performance issues, it's that we have to query and iterate over all the children of an element before and after. I'm not a fan either...
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.
Looks good, a few questions, and can you provide any examples of it failing before/ success after?
elementToGiveFocus.focus() | ||
|
||
// also try to set the selection, if there is a selection for this element | ||
const hasSelectionStart = removedElementWithFocusData.selectionStart !== null && removedElementWithFocusData.selectionStart !== undefined |
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.
Is there a reason why you can check to see if removedElementWithFocusData.selectionStart
is truthy? Do null and undefined come back as truthy in this case, or is there a falsy value that you want to return as true?
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.
selectionStart
can be 0
which is a falsy value, so yeah, we have to explicitly check if it's null or undefined.
Summary has been updated 👍 |
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.
LGTM
Summary
Fixes an issue where if the elements were re-ordered or changed significantly, we wouldn't be able to re-attach focus. Worse so, we never check if the element exists before trying to focus it, so we would get an error!
This PR also fixes an error where the scroll position would be reset when typing in an input.
Screenshots (Before)
When interacting with an input that has
<span>-</span>
for every letter in the input. This is what happens when typing and then deleting (this is the integration test app, which can be run locally withnpm run test:app
):The first set of errors is because we are trying to
setSelectionRange
on a<span>
, the second error is because after deleting no element exists at the index that the<input>
was on.Screenshots (After)
No errors 🎉
Checklist