8
votes

Comment faire défiler une page React lors de son chargement?

Je veux passer une valeur comme hachage dans l'url (myapp.com # somevalue) et faire défiler la page jusqu'à cet élément lorsque la page se charge - exactement le comportement de l'utilisation d'un fragment de hachage depuis le début d'Internet. J'ai essayé d'utiliser scrollIntoView mais cela échoue sur iOS. Ensuite, j'ai juste essayé de désactiver / paramétrer window.location.hash mais il semble y avoir une condition de concurrence. Cela ne fonctionne que lorsque le délai est supérieur à 600 ms.

Je voudrais une solution plus solide et je ne veux pas introduire de retard inutile. Lorsque le délai est trop court, il semble défiler jusqu'à l'élément souhaité, puis défile vers le haut de la page. Vous ne verrez pas l'effet sur cette démo, mais cela se produit dans mon application actuelle https://codesandbox.io/s/ pjok544nrx line 75

(anonymous) @   VM38319:1
setValueForStyles   @   react-dom.development.js:6426
updateDOMProperties @   react-dom.development.js:7587
updateProperties    @   react-dom.development.js:7953
commitUpdate    @   react-dom.development.js:8797
commitWork  @   react-dom.development.js:17915
commitAllHostEffects    @   react-dom.development.js:18634
callCallback    @   react-dom.development.js:149
invokeGuardedCallbackDev    @   react-dom.development.js:199
invokeGuardedCallback   @   react-dom.development.js:256
commitRoot  @   react-dom.development.js:18867
(anonymous) @   react-dom.development.js:20372
unstable_runWithPriority    @   scheduler.development.js:255
completeRoot    @   react-dom.development.js:20371
performWorkOnRoot   @   react-dom.development.js:20300
performWork @   react-dom.development.js:20208
performSyncWork @   react-dom.development.js:20182
requestWork @   react-dom.development.js:20051
scheduleWork    @   react-dom.development.js:19865
scheduleRootUpdate  @   react-dom.development.js:20526
updateContainerAtExpirationTime @   react-dom.development.js:20554
updateContainer @   react-dom.development.js:20611
ReactRoot.render    @   react-dom.development.js:20907
(anonymous) @   react-dom.development.js:21044
unbatchedUpdates    @   react-dom.development.js:20413
legacyRenderSubtreeIntoContainer    @   react-dom.development.js:21040
render  @   react-dom.development.js:21109
(anonymous) @   questionsPageIndex.jsx:10
./src/client/questionsPageIndex.jsx @   index.html:673
__webpack_require__ @   index.html:32
(anonymous) @   index.html:96
(anonymous) @   index.html:99

EDIT: J'ai suivi la ligne de React qui restitue la page et par conséquent réinitialise la position de défilement après avoir déjà défilé là où je l'ai dit pour aller dans componentDidMount (). C'est celui-ci . style [styleName] = styleValue; Au moment du rendu de la page, il définit la propriété de style width du composant inputbox du composant react-select box. La trace de pile juste avant cela ressemble à

  componentDidMount() {
    let self = this;

    let updateSearchControl = hash => {
      let selectedOption = self.state.searchOptions.filter(
        option => option.value === hash
      )[0];
      if (selectedOption) {
        // this doesn't work with Safari on iOS
        // document.getElementById(hash).scrollIntoView(true);

        // this works if delay is 900 but not 500 ms
        setTimeout(() => {
          // unset and set the hash to trigger scrolling to target
          window.location.hash = null;
          window.location.hash = hash;
          // scroll back by the height of the Search box
          window.scrollBy(
            0,
            -document.getElementsByClassName("heading")[0].clientHeight
          );
        }, 900);
      } else if (hash) {
        this.searchRef.current.select.focus();
      }
    };

    // Get the hash
    // I want this to work as a Google Apps Script too which runs in
    // an iframe and has a special way to get the hash
    if (!!window["google"]) {
      let updateHash = location => {
        updateSearchControl(location.hash);
      };
      eval("google.script.url.getLocation(updateHash)");
    } else {
      let hash = window.location.hash.slice(1);
      updateSearchControl(hash);
    }
  }

Je ne sais pas où déplacer mes instructions pour faire défiler la page. Cela doit se produire après la définition de ces styles, ce qui se produit après componentDidMount ().

EDIT: J'ai besoin de mieux clarifier exactement ce que j'ai besoin de faire. Toutes mes excuses pour ne pas l'avoir fait auparavant.

Doit avoir

Cela doit fonctionner sur tous les ordinateurs de bureau et appareils mobiles courants.

Lorsque la page se charge, il peut y en avoir trois situations en fonction de la requête fournie dans l'url après le #:

  • 1) aucune requête n'a été fournie - rien ne se passe
  • 2) une requête valide a été fournie - la page défile instantanément jusqu'à l'ancre souhaitée et le titre de la page se transforme en libellé de l'option
  • 3) une requête non valide a été fournie - la requête est saisie dans la zone de recherche, la zone de recherche est focalisée et le menu s'ouvre avec des options limitées par la requête fournie comme si elles avaient été saisies. Le curseur se trouve dans le zone de recherche à la fin de la requête afin que l'utilisateur puisse modifier la requête.

Une requête valide est celle qui correspond à la valeur de l'une des options. Une requête non valide en est une qui ne l'est pas.

Les boutons d'arrière-plan du navigateur doivent déplacer la position de défilement en conséquence.

Chaque fois qu'une option est choisie dans la zone de recherche, la page faites défiler instantanément jusqu'à cette ancre, la requête doit s'effacer en retournant à l'espace réservé "Rechercher ...", l'url doit se mettre à jour et le titre du document doit devenir le libellé de l'option.

Facultatif mais ce serait génial

Après avoir fait une sélection, le menu se ferme et la requête revient à "Rechercher ...", et soit

  • la zone de recherche conserve le focus afin que l'utilisateur puisse commencer à taper une autre requête. La prochaine fois que l'on clique sur , le menu s'ouvre et la requête de la boîte de recherche avant la sélection, le filtre du menu et la position de défilement du menu sont restaurés (de préférence), ou
  • la zone de recherche perd le focus. La prochaine fois qu'il est concentré , le menu s'ouvre et la requête du champ de recherche avant la sélection, le filtre du menu et la position de défilement du menu sont restaurés (préféré), ou
  • la zone de recherche conserve le focus afin que l'utilisateur puisse commencer à taper une autre requête. La requête précédente et la position de défilement du menu sont perdues.


2 commentaires

solution possible - stackoverflow .com / questions / 3163615 /…


peut-être que useEffect vaut la peine d'essayer. si je comprends bien, useEffect fonctionnera après le rendu soumis à l'écran et à ce moment l'élément est prêt à


4 Réponses :


-1
votes

Vous pouvez utiliser https://www.npmjs.com/package/element. scrollintoviewifneeded-polyfill .

Dès que possible dans votre application, importez la bibliothèque

    ref.current.scrollIntoViewIfNeeded();

Ensuite, vous pouvez utiliser la méthode scrollIntoViewIfNeeded (); sur tous les nœuds dom. La bibliothèque polyfill la méthode afin que vous puissiez l'utiliser sur tous les navigateurs.

Je vous recommande de l'utiliser avec un react ref afin que vous n'ayez pas à appeler getElementById ou autre document afin de récupérer le nœud dom.

const ref = React.createRef();
<div id="somevalue" ref={ref} />

Dans votre componentDidMount , vous pouvez utiliser la même méthode que vous décrivez pour récupérer l'id du div que vous voulez faire défiler, obtenir la référence associée et appeler

import 'element.scrollintoviewifneeded-polyfill';

Lorsque la référence est la bonne référence de div, vous pouvez évidemment stocker toutes les références dans une carte et récupérer dans vos méthodes de composants.


1 commentaires

Merci. Malheureusement, il le fait toujours. Il saute au bon endroit, puis revient en haut. github.com/dancancro/ questions / blob / nodiv / src / client / compone‌ nts /…



2
votes

Probablement plus facile à faire avec la bibliothèque react-scrollable-anchor :

import React from "react";
import ReactDOM from "react-dom";
import ScrollableAnchor from "react-scrollable-anchor";

class App extends React.Component {
  render() {
    return (
      <React.Fragment>
        <div style={{ marginBottom: 7000 }}>nothing to see here</div>
        <div style={{ marginBottom: 7000 }}>never mind me</div>
        <ScrollableAnchor id={"doge"}>
          <div>bork</div>
        </ScrollableAnchor>
      </React.Fragment>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Vous pouvez le voir fonctionner en allant ici: https://zwoj0xw503.codesandbox.io/#doge


1 commentaires

En interne, ScrollableAnchors effectue le défilement en définissant window.location.hash, de sorte qu'il a la même réponse à la question Quelle et ne répond pas à la question Quand. Si c'est effectivement une question pour TC39 comme il semble, et étant donné que je n'ai pas immédiatement reçu de réponses instantanées que la capture de "l'événement prêt à être défilé" est impossible, je dois violer un principe de base de la conception Web sur mon premier projet, mais c'est vraiment difficile à accepter. Cela semble être une exigence éminemment raisonnable. github.com/gabergg/ react-scrollable-anchor / blob / master / src /…



2
votes

Vous pouvez simplifier votre code en utilisant HashRouter avec le ref.current.scrollIntoView () ou window.scrollTo (ref .currrent.offsetTop) .

Actuellement testé et fonctionne sur Chrome (bureau / Android), Firefox (bureau), Silk Browser (Android), Saumsung Internet (Android) et Safari (iOS - avec une mise en garde en ce sens qu'il passe instantanément à la sélection) . Bien que j'ai pu tester et confirmer qu'il fonctionne dans l ' iframe de l'environnement sandbox, vous devrez l'adapter / le tester dans un iframe autonome.

Je tiens également à souligner que votre approche ressemble beaucoup à jQuery et que vous devez éviter de tenter directement d'utiliser ou de manipuler le DOM et / ou le fenêtre . De plus, vous semblez utiliser beaucoup de variables let dans tout votre code, mais elles restent inchangées. Au lieu de cela, ils devraient être une variable const car React évalue les variables à chaque fois qu'il effectue un nouveau rendu, donc à moins que vous ne prévoyiez de changer cette variable pendant le processus de rendu, il n'est pas nécessaire d'utiliser let .

Exemple de travail : https://v6y5211n4l.codesandbox.io a>

 Modifier ScrollIntoView on Selection


components/ScrollBar

import React from "react";
import { render } from "react-dom";
import { HashRouter } from "react-router-dom";
import Helmet from "react-helmet";
import ScrollBar from "../src/components/ScrollBar";

const config = {
  htmlAttributes: { lang: "en" },
  title: "My App",
  titleTemplate: "Scroll Search - %s",
  meta: [
    {
      name: "description",
      content: "The best app in the world."
    }
  ]
};

const searchOptions = [
  {
    label: "apple",
    value: "apple"
  },
  {
    label: "orange",
    value: "orange"
  },
  {
    label: "banana",
    value: "banana"
  },
  {
    label: "strawberry",
    value: "strawberry"
  }
];

const styles = {
  menu: base => ({ ...base, width: "100%", height: "75vh" }),
  menuList: base => ({ ...base, minHeight: "75vh" }),
  option: base => ({ ...base, color: "blue" })
};

const App = () => (
  <HashRouter>
    <Helmet {...config} />
    <ScrollBar searchOptions={searchOptions} styles={styles} />
  </HashRouter>
);

render(<App />, document.getElementById("root"));

index.js

import React, { Component } from "react";
import PropTypes from "prop-types";
import Helmet from "react-helmet";
import Select from "react-select";
import { withRouter } from "react-router-dom";
import "./styles.css";

const scrollOpts = {
  behavior: "smooth",
  block: "start"
};

class ScrollBar extends Component {
  constructor(props) {
    super(props);

    const titleRefs = props.searchOptions.map(
      ({ label }) => (this[label] = React.createRef())
    ); // map over options and use "label" as a ref name; ex: this.orange
    this.topWindow = React.createRef();
    this.searchRef = React.createRef();
    const pathname = props.history.location.pathname.replace(/\//g, ""); // set current pathname without the "/"

    this.state = {
      titleRefs,
      menuIsOpen: false,
      isValid: props.searchOptions.some(({ label }) => label === pathname), // check if the current pathname matches any of the labels
      pathname
    };
  }

  componentDidMount() {
    const { pathname, isValid } = this.state;
    if (isValid) this.scrollToElement(); // if theres a pathname and it matches a label, scroll to it
    if (!isValid && pathname) this.searchRef.current.select.focus(); // if there's a pathname but it's invalid, focus on the search bar
  }

  // allows us to update state based upon prop updates (in this case
  // we're looking for changes in the history.location.pathname)
  static getDerivedStateFromProps(props, state) {
    const nextPath = props.history.location.pathname.replace(/\//g, "");
    const isValid = props.searchOptions.some(({ label }) => label === nextPath);
    return state.pathname !== nextPath ? { pathname: nextPath, isValid } : null; // if the path has changed, update the pathname and whether or not it is valid
  }

  // allows us to compare new state to old state (in this case
  // we're looking for changes in the pathname)
  componentDidUpdate(prevProps, prevState) {
    if (this.state.pathname !== prevState.pathname) {
      this.scrollToElement();
    }
  }

  scrollToElement = () => {
    const { isValid, pathname } = this.state;
    const elem = isValid ? pathname : "topWindow";
    setTimeout(() => {
     window.scrollTo({
        behavior: "smooth",
        top: this[elem].current.offsetTop - 45
      });
    }, 100);
  };

  onInputChange = (options, { action }) => {
    if (action === "menu-close") {
      this.setState({ menuIsOpen: false }, () =>
        this.searchRef.current.select.blur()
      );
    } else {
      this.setState({ menuIsOpen: true });
    }
  };

  onChange = ({ value }) => {
    const { history } = this.props;
    if (history.location.pathname.replace(/\//g, "") !== value) { // if the select value is different than the previous selected value, update the url -- required, otherwise, react-router will complain about pushing the same pathname/value that is current in the url
      history.push(value);
    }
    this.searchRef.current.select.blur();
  };

  onFocus = () => this.setState({ menuIsOpen: true });

  render() {
    const { pathname, menuIsOpen, titleRefs } = this.state;
    const { searchOptions, styles } = this.props;

    return (
      <div className="container">
        <Helmet title={this.state.pathname || "Select an option"} />
        <div className="heading">
          <Select
            placeholder="Search..."
            isClearable={true}
            isSearchable={true}
            options={searchOptions}
            defaultInputValue={pathname}
            onFocus={this.onFocus}
            onInputChange={this.onInputChange}
            onChange={this.onChange}
            menuIsOpen={menuIsOpen}
            ref={this.searchRef}
            value={null}
            styles={styles || {}}
          />
        </div>
        <div className="fruits-list">
          <div ref={this.topWindow} />
          {searchOptions.map(({ label, value }, key) => (
            <div key={value} id={value}>
              <p ref={titleRefs[key]}>{label}</p>
              {[1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14].map(num => (
                <p key={num}>{num}</p>
              ))}
            </div>
          ))}
        </div>
      </div>
    );
  }
}

ScrollBar.propTypes = {
  searchOptions: PropTypes.arrayOf(
    PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  styles: PropTypes.objectOf(PropTypes.func)
};

export default withRouter(ScrollBar);


5 commentaires

C'est génial merci. Il gère bien le cas où une valeur valide est fournie, mais je veux également gérer le cas où une valeur invalide l'est. J'ai clarifié les détails ci-dessus et commencé à travailler sur une modification de votre solution ici codesandbox.io/s/xl46pnmr8p


Réponse mise à jour ci-dessus. De plus, le setTimeout est requis pour les navigateurs mobiles. Sinon, il ne défilera pas.


Juste fini. Cela fonctionne sur mon ordinateur mais cela ne fonctionne pas dans Safari sur mon iPhone. Il défilera jusqu'au bon endroit lors du chargement, mais la zone de recherche disparaît. Si vous faites glisser manuellement tout le chemin vers le haut, puis balayez encore une fois, la boîte revient. La zone de recherche reste en place si vous faites défiler manuellement mais que scrollIntoView () ne fonctionne pas sur cet appareil. Veuillez essayer ceci sur mobile: v6y5211n4l.codesandbox.io/#/orange


Réponse mise à jour. C'est un problème avec le mobile qui n'aime pas overflow: hidden; sur le body et position: absolute; sur le Select composant. Au lieu de cela, définissez .heading comme position: fixed; width: 100%; et supprimez le overflow: hidden; . En tant que tel, vous devrez utiliser un window.scrollTo () avec un calcul offsetTop , qui défilera jusqu'à l'emplacement correct (puisqu'une position fixe flotte au-dessus, vous 'devra le compenser, sinon il atterrira directement au-dessus de la ref ). De plus, un autre inconvénient est que lorsque vous utilisez le bouton retour , il saute immédiatement au dernier emplacement sélectionné.


Quelle que soit la direction que vous prenez, ref.scrollIntoView () ou window.scrollTo () vous allez rencontrer des limitations de navigateur.



1
votes

Je mets ceci juste après les importations dans le fichier de mon composant:

  componentDidMount() {
    const { pathname, isValid } = this.state;
    if(!isValid && pathname) {               // if there's a pathname but it's invalid,
      this.searchRef.current.select.focus(); // focus on the search bar
    }
    initialValidSlug = pathname;  // sets the value to be used in the $.ready() above
  }

et l'ai pour mon componentDidMount():

let initialValidSlug = '';

window.addEventListener('load', () => {
  window.location.hash = '';
  window.location.hash = (initialValidSlug ? initialValidSlug : '');
  window.scrollBy(0, -document.getElementsByClassName("heading")[0].clientHeight);
  console.log('window.addEventListener')
});


0 commentaires