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

Fix internal/local navigation links scrolling #5381

Merged
merged 12 commits into from Oct 5, 2016

Conversation

mkhatib
Copy link
Contributor

@mkhatib mkhatib commented Oct 3, 2016

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.

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 3, 2016

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please swap this conditional.

Copy link
Contributor Author

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}`);
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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');
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points. Will fix.

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 3, 2016

PTAL 👀

function removeAttrsWithMatchingHash_(ampdoc, hash) {
const restoreElementsAttrs = [];
const targetElements = ampdoc.getRootNode().querySelectorAll(
`#${hash},a[name=${hash}]`) || [];
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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 = {};
Copy link
Contributor

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,
};

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dvoytenko can you clarify?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both:

  1. It's enough to just have RestoreElementAttributesDef be {element, attrName, attrValue} and thus you can just take care of everything with a single loop.
  2. 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 :)

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 3, 2016

PTAL 👀

@@ -14,11 +14,14 @@
* limitations under the License.
*/

import {closestByTag} from './dom';
import {
closestByTag,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-2sp

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 4, 2016

So @dvoytenko found few problems with this solution,

  • Breaking CSS styling (because of removing of ID attr)
  • Causing relayout with ID changes

I am exploring other solutions, one being calling scrollIntoView twice, once normally and once in a 0-timeout callback - the first is basically to override the browsers scroll right away and the timed out call to actually do the actual scroll into view.

viewport.scrollIntoView(elem);
setTimeout(() => viewport.scrollIntoView(elem), 0);

@muxin
Copy link
Contributor

muxin commented Oct 4, 2016

Why can't we use replaceState again? I saw a post about disabling default page scrolling by using replaceState

viewport./*OK*/scrollIntoView(elem);
setTimeout(() => viewport./*OK*/scrollIntoView(elem), 0);
Copy link
Contributor Author

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.

Copy link
Contributor

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)?

Copy link
Contributor

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 || '#'}`);
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 4, 2016

@dvoytenko PTAL 👀

Please ignore tests - outdated.

@dvoytenko
Copy link
Contributor

@muxin When in iframe, replaceState may break parent history stack and definitely breaks :target selector.

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 4, 2016

Ok Updated PR now only addresses #5309 bug. Not the sidebar bug.

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 4, 2016

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}]`));
Copy link
Contributor

@dvoytenko dvoytenko Oct 4, 2016

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@mkhatib
Copy link
Contributor Author

mkhatib commented Oct 4, 2016

Done 👀 PTAL

@dvoytenko
Copy link
Contributor

LGTM on my side.

@mkhatib mkhatib merged commit afbf063 into ampproject:master Oct 5, 2016
@mkhatib mkhatib deleted the debug-doclick branch October 5, 2016 20:01
dreamofabear pushed a commit to dreamofabear/amphtml that referenced this pull request Oct 6, 2016
dreamofabear pushed a commit to dreamofabear/amphtml that referenced this pull request Oct 12, 2016
mityaha pushed a commit to ooyala/amphtml that referenced this pull request Nov 30, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fragment link navigation is broken on iOS10
5 participants