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 internal/local navigation links scrolling #5381
Conversation
Demo here try it on iOS simulator - second tab, open sidebar and click section 1 and 2 links to see it in effect, close the side bar notice everything works as expected. |
// the scrollbar jumping the user back to the top for failing to calculate | ||
// the new jumped offset. | ||
// See https://github.com/ampproject/amphtml/issues/5334 for more details. | ||
let oldAttribute = {}; | ||
if (hash) { | ||
elem = ampdoc.getRootNode().getElementById(hash); | ||
if (!elem) { |
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.
Please swap this conditional.
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.
Dropped. Using ||
instead.
win.location.replace(`#${hash}`); | ||
// for more details. Do this only if fragment has changed. | ||
if (tgtLoc.hash != curLoc.hash) { | ||
win.location.replace(`#${hash}`); |
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.
Do we want to preserve the share tracking hash?
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.
Not sure. @dvoytenko do you know how do we handle this here?
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 it's a good idea to have both document tag and share-tracking hash together in the url hash. Maybe we could only have document tag in this case. So I think this is good for now.
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, don't preserve. It's up to the publisher on whether they use hash-based links.
} | ||
} else { | ||
oldAttribute = {key: 'id', value: elem.id}; | ||
elem.removeAttribute('id'); |
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 need to ensure there's not a [name=${hash}]
element, too. And there could be multiple of both.
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.
Good points. Will fix.
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.
Done
} else { | ||
dev().warn('documentElement', | ||
`failed to find element with id=${hash} or a[name=${hash}]`); | ||
} | ||
|
||
// Has fragment really changed? | ||
if (tgtLoc.hash != curLoc.hash) { |
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 ==
(caught by a test). Fixing.
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.
Fixed
oldAttribute = {key: 'name', value: elem.name}; | ||
elem.removeAttribute('name'); | ||
} | ||
} else { |
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 you please revert if/else
here to remove double negative?
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.
Dropped.
} else { | ||
dev().warn('documentElement', | ||
`failed to find element with id=${hash} or a[name=${hash}]`); | ||
} | ||
|
||
// Has fragment really changed? | ||
if (tgtLoc.hash != curLoc.hash) { |
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.
It should be ==
here I believe. Also, please remove return
and wrap the history.push
inside the if
.
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, caught by a test. Will fix
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.
Fixed.
PTAL 👀 |
function removeAttrsWithMatchingHash_(ampdoc, hash) { | ||
const restoreElementsAttrs = []; | ||
const targetElements = ampdoc.getRootNode().querySelectorAll( | ||
`#${hash},a[name=${hash}]`) || []; |
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 hash should be escaped and quoted for the name
part.
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.
By the hash you mean the literal #
sign or the variable ${hash}
? If the earlier, why do we need to escape it?
If the latter, what values do we expect to break this? And what does escape means here?
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.
hash = something]
will break this. @dvoytenko recently did something with CSS escaping.
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.
Used dom.escapeCssSelectorIdent
to escape it. @dvoytenko is this accurate? Or if we should just use the css-escape.cssEscape
instead?
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, dom.escapeCssSelectorIdent
would do the trick.
`#${hash},a[name=${hash}]`) || []; | ||
for (let i = 0; i < targetElements.length; i++) { | ||
const element = targetElements[i]; | ||
const attributes = {}; |
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 can simplify to:
attributes = {
name: element.name === hash,
id: element.id === name,
};
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.
Then I'd have to check the value when looping over attrs. I also still need the if else to remove attributes when matching.
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.
Ohh, I see the loop now.
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 doing loop here is a bit too much: we should just focus on a most likely case here and leave edge-cases out. The fidelity requirement is somewhat lower here.
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.
So just go back to only managing attributes on the found elem
?
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 he's saying the for attr in attrs
loop is too much. We only care about name
and id
, nothing else.
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.
@dvoytenko can you clarify?
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.
Both:
- It's enough to just have
RestoreElementAttributesDef
be{element, attrName, attrValue}
and thus you can just take care of everything with a single loop. - We could even simplify this and only look for a single first element that matches either id search or name search. It seems to me that if there are several competing elements - we could live with that. But, obviously, a more foolproof solution is good as well :)
PTAL 👀 |
@@ -14,11 +14,14 @@ | |||
* limitations under the License. | |||
*/ | |||
|
|||
import {closestByTag} from './dom'; | |||
import { | |||
closestByTag, |
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.
-2sp
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.
Fixed
So @dvoytenko found few problems with this solution,
I am exploring other solutions, one being calling viewport.scrollIntoView(elem);
setTimeout(() => viewport.scrollIntoView(elem), 0); |
Why can't we use |
viewport./*OK*/scrollIntoView(elem); | ||
setTimeout(() => viewport./*OK*/scrollIntoView(elem), 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.
@dvoytenko here's the updated approach.
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.
Can we use timer.delay
here (which uses a micro task for 0
delays)?
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'm afraid it actually won't work in microtask :( We probably need to do it in its own event loop. That being said, timer.delay
is a good idea, but probably with time 1?
|
||
// Push/pop history. | ||
history.push(() => { | ||
win.location.replace(`${curLoc.hash || '#'}`); |
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.
@dvoytenko dropping this to fix #4184 See discussion there. Let me know what do you think about 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.
Don't yet understand why this is being dropped. Can you re-sum this up in either thread? This exists so that users can click on back button and get to normal state, especially given the :target
support.
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.
Don't yet understand why this is being dropped.
Agree, keeping this is a must.
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.
OK, as I replied to that bug, I want to just focus on fixing this bug now. So I'll bring this line back and stick with the timer fix. And will address the other issue in a followup PR.
@dvoytenko PTAL 👀 Please ignore tests - outdated. |
@muxin When in iframe, |
Ok Updated PR now only addresses #5309 bug. Not the sidebar bug. |
Please PTAL 👀 |
elem = (ampdoc.getRootNode().getElementById(hash) || | ||
// Fallback to anchor[name] if element with id is not found. | ||
// Linking to an anchor element with name is obsolete in html5. | ||
ampdoc.getRootNode().querySelector(`a[name=${escapedHash}]`)); |
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 CSS selector here also needs "
I believe. Please add a test for search of an element with name with a space or such.
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.
Fixed
Done 👀 PTAL |
LGTM on my side. |
Fixes #5334
Updating tests. @dvoytenko let me know if this looks good.
Demo here try it on iOS simulator - second tab, open sidebar and click section 1 and 2 links to see it in effect, close the side bar notice everything works as expected.