Le maintien de la position de défilement ne fonctionne que lorsqu'il n'est pas près du bas des messages div

10

J'essaie d'imiter d'autres applications de chat mobile où lorsque vous sélectionnez la send-messagezone de texte et qu'il ouvre le clavier virtuel, le message le plus bas est toujours en vue. Il ne semble pas y avoir un moyen incroyable de le faire avec CSS, donc JavaScript resize(seul moyen de savoir quand le clavier est ouvert et fermé apparemment) et le défilement manuel à la rescousse.

Quelqu'un a fourni cette solution et j'ai découvert cette solution , qui semblent fonctionner toutes les deux.

Sauf dans un cas. Pour une raison quelconque, si vous êtes à moins de MOBILE_KEYBOARD_HEIGHT(250 pixels dans mon cas) pixels du bas de la div des messages, lorsque vous fermez le clavier mobile, quelque chose d'étrange se produit. Avec l'ancienne solution, il défile vers le bas. Et avec cette dernière solution, il fait défiler les MOBILE_KEYBOARD_HEIGHTpixels à partir du bas.

Si vous faites défiler au-dessus de cette hauteur, les deux solutions fournies ci-dessus fonctionnent parfaitement. Ce n'est que lorsque vous êtes près du fond qu'ils ont ce problème mineur.

Je pensais que c'était peut-être juste mon programme qui causait cela avec du code errant étrange, mais non, j'ai même reproduit un violon et il a ce problème exact. Mes excuses pour avoir rendu cela si difficile à déboguer, mais si vous allez sur https://jsfiddle.net/t596hy8d/6/show (le suffixe d'émission fournit un mode plein écran) sur votre téléphone, vous devriez pouvoir voir le même comportement.

Ce comportement étant, si vous faites défiler suffisamment vers le haut, l'ouverture et la fermeture du clavier maintient la position. Cependant, si vous fermez le clavier à quelques MOBILE_KEYBOARD_HEIGHTpixels du bas, vous constaterez qu'il défile vers le bas à la place.

Quelle est la cause de cela?

Reproduction du code ici:

window.onload = function(e){ 
  document.querySelector(".messages").scrollTop = 10000;
  
  bottomScroller(document.querySelector(".messages"));
}
  

function bottomScroller(scroller) {
  let scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;

  scroller.addEventListener('scroll', () => { 
  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });   

  window.addEventListener('resize', () => { 
  scroller.scrollTop = scroller.scrollHeight - scrollBottom - scroller.clientHeight;

  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
  <div class="message">hello 1</div>
  <div class="message">hello 2</div>
  <div class="message">hello 3</div>
  <div class="message">hello 4</div>
  <div class="message">hello 5</div>
  <div class="message">hello 6 </div>
  <div class="message">hello 7</div>
  <div class="message">hello 8</div>
  <div class="message">hello 9</div>
  <div class="message">hello 10</div>
  <div class="message">hello 11</div>
  <div class="message">hello 12</div>
  <div class="message">hello 13</div>
  <div class="message">hello 14</div>
  <div class="message">hello 15</div>
  <div class="message">hello 16</div>
  <div class="message">hello 17</div>
  <div class="message">hello 18</div>
  <div class="message">hello 19</div>
  <div class="message">hello 20</div>
  <div class="message">hello 21</div>
  <div class="message">hello 22</div>
  <div class="message">hello 23</div>
  <div class="message">hello 24</div>
  <div class="message">hello 25</div>
  <div class="message">hello 26</div>
  <div class="message">hello 27</div>
  <div class="message">hello 28</div>
  <div class="message">hello 29</div>
  <div class="message">hello 30</div>
  <div class="message">hello 31</div>
  <div class="message">hello 32</div>
  <div class="message">hello 33</div>
  <div class="message">hello 34</div>
  <div class="message">hello 35</div>
  <div class="message">hello 36</div>
  <div class="message">hello 37</div>
  <div class="message">hello 38</div>
  <div class="message">hello 39</div>
  </div>
  <div class="send-message">
	<input />
  </div>
</div>

Ryan Peschel
la source
Je remplacerais les gestionnaires d'événements par IntersectionObserver et ResizeObserver. Ils ont un temps système beaucoup plus faible que les gestionnaires d'événements. Si vous ciblez des navigateurs plus anciens, les deux ont des polyfills.
bigless
Avez-vous essayé cela sur Firefox pour les appareils mobiles? Il ne semble pas avoir ce problème. Cependant, essayer ceci sur Chrome provoque le problème que vous avez mentionné.
Richard
Eh bien, cela doit fonctionner sur Chrome de toute façon. C'est bien que Firefox n'ait pas le problème.
Ryan Peschel
Mon mauvais pour ne pas transmettre mon point correctement. Si un navigateur a un problème et pas un autre, cela, l'OMI, peut signifier que vous devrez peut- être avoir une implémentation légèrement différente pour différents navigateurs.
Richard
1
@halfer Très bien. Je vois. Merci pour le rappel, j'en tiendrai compte la prochaine fois que je demanderai à quelqu'un de revoir sa réponse.
Richard

Réponses:

3

J'ai enfin trouvé une solution qui fonctionne réellement . Bien qu'il ne soit pas idéal, il fonctionne en fait dans tous les cas. Voici le code:

bottomScroller(document.querySelector(".messages"));

bottomScroller = scroller => {
  let pxFromBottom = 0;

  let calcPxFromBottom = () => pxFromBottom = scroller.scrollHeight - (scroller.scrollTop + scroller.clientHeight);

  setInterval(calcPxFromBottom, 500);

  window.addEventListener('resize', () => { 
    scroller.scrollTop = scroller.scrollHeight - pxFromBottom - scroller.clientHeight;
  });
}

Quelques épiphanies que j'ai eues en cours de route:

  1. Lors de la fermeture du clavier virtuel, un scrollévénement se produit instantanément avant l' resizeévénement. Cela semble se produire uniquement lors de la fermeture du clavier, pas de son ouverture. C'est la raison pour laquelle vous ne pouvez pas utiliser l' scrollévénement pour définir pxFromBottom, car si vous êtes près du bas, il se mettra à 0 dans l' scrollévénement juste avant l' resizeévénement, gâchant le calcul.

  2. Une autre raison pour laquelle toutes les solutions ont rencontré des difficultés près du bas des messages div est un peu difficile à comprendre. Par exemple, dans ma solution de redimensionnement, j'ajoute ou soustrais simplement 250 (hauteur du clavier mobile) scrollToplors de l'ouverture ou de la fermeture du clavier virtuel. Cela fonctionne parfaitement sauf près du bas. Pourquoi? Parce que disons que vous êtes à 50 pixels du bas et fermez le clavier. Il soustrait 250 de scrollTop(la hauteur du clavier), mais il ne devrait soustraire que 50! Ainsi, il sera toujours réinitialisé à la mauvaise position fixe lors de la fermeture du clavier vers le bas.

  3. Je crois également que vous ne pouvez pas utiliser onFocuset les onBlurévénements pour cette solution, car ceux-ci ne se produisent que lors de la sélection initiale de la zone de texte pour ouvrir le clavier. Vous pouvez parfaitement ouvrir et fermer le clavier mobile sans activer ces événements, et en tant que tels, ils ne peuvent pas être utilisés ici.

Je crois que les points ci-dessus sont importants pour développer une solution, car ils ne sont pas évidents au début, mais empêchent le développement d'une solution robuste.

Je n'aime pas cette solution (l'intervalle est un peu inefficace et sujet aux conditions de course), mais je ne trouve rien de mieux qui fonctionne toujours.

Ryan Peschel
la source
1

Je pense que ce que tu veux c'est overflow-anchor

Le support est en augmentation, mais pas total, pourtant https://caniuse.com/#feat=css-overflow-anchor

À partir d'un article CSS-Tricks à ce sujet:

L'ancrage de défilement empêche cette expérience de "saut" en verrouillant la position de l'utilisateur sur la page pendant que des modifications ont lieu dans le DOM au-dessus de l'emplacement actuel. Cela permet à l'utilisateur de rester ancré là où il se trouve sur la page même lorsque de nouveaux éléments sont chargés dans le DOM.

La propriété overflow-anchor nous permet de désactiver la fonction d'ancrage de défilement dans le cas où il est préférable de permettre au contenu d'être redirigé lorsque les éléments sont chargés.

Voici une version légèrement modifiée d'un de leurs exemples:

let scroller = document.querySelector('#scroller');
let anchor = document.querySelector('#anchor');

// https://ajaydsouza.com/42-phrases-a-lexophile-would-love/
let messages = [
  'I wondered why the baseball was getting bigger. Then it hit me.',
  'Police were called to a day care, where a three-year-old was resisting a rest.',
  'Did you hear about the guy whose whole left side was cut off? He’s all right now.',
  'The roundest knight at King Arthur’s round table was Sir Cumference.',
  'To write with a broken pencil is pointless.',
  'When fish are in schools they sometimes take debate.',
  'The short fortune teller who escaped from prison was a small medium at large.',
  'A thief who stole a calendar… got twelve months.',
  'A thief fell and broke his leg in wet cement. He became a hardened criminal.',
  'Thieves who steal corn from a garden could be charged with stalking.',
  'When the smog lifts in Los Angeles , U. C. L. A.',
  'The math professor went crazy with the blackboard. He did a number on it.',
  'The professor discovered that his theory of earthquakes was on shaky ground.',
  'The dead batteries were given out free of charge.',
  'If you take a laptop computer for a run you could jog your memory.',
  'A dentist and a manicurist fought tooth and nail.',
  'A bicycle can’t stand alone; it is two tired.',
  'A will is a dead giveaway.',
  'Time flies like an arrow; fruit flies like a banana.',
  'A backward poet writes inverse.',
  'In a democracy it’s your vote that counts; in feudalism, it’s your Count that votes.',
  'A chicken crossing the road: poultry in motion.',
  'If you don’t pay your exorcist you can get repossessed.',
  'With her marriage she got a new name and a dress.',
  'Show me a piano falling down a mine shaft and I’ll show you A-flat miner.',
  'When a clock is hungry it goes back four seconds.',
  'The guy who fell onto an upholstery machine was fully recovered.',
  'A grenade fell onto a kitchen floor in France and resulted in Linoleum Blownapart.',
  'You are stuck with your debt if you can’t budge it.',
  'Local Area Network in Australia : The LAN down under.',
  'He broke into song because he couldn’t find the key.',
  'A calendar’s days are numbered.',
];

function randomMessage() {
  return messages[(Math.random() * messages.length) | 0];
}

function appendChild() {
  let msg = document.createElement('div');
  msg.className = 'message';
  msg.innerText = randomMessage();
  scroller.insertBefore(msg, anchor);
}
setInterval(appendChild, 1000);
html {
  height: 100%;
  display: flex;
}

body {
  min-height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  padding: 0;
}

#scroller {
  flex: 2;
}

#scroller * {
  overflow-anchor: none;
}

.new-message {
  position: sticky;
  bottom: 0;
  background-color: blue;
  padding: .2rem;
}

#anchor {
  overflow-anchor: auto;
  height: 1px;
}

body {
  background-color: #7FDBFF;
}

.message {
  padding: 0.5em;
  border-radius: 1em;
  margin: 0.5em;
  background-color: white;
}
<div id="scroller">
  <div id="anchor"></div>
</div>

<div class="new-message">
  <input type="text" placeholder="New Message">
</div>

Ouvrez ceci sur mobile: https://cdpn.io/chasebank/debug/PowxdOR

Cela revient à désactiver fondamentalement tout ancrage par défaut des nouveaux éléments de message, avec #scroller * { overflow-anchor: none }

Et à la place, ancrer un élément vide #anchor { overflow-anchor: auto }qui viendra toujours après ces nouveaux messages, car les nouveaux messages sont insérés avant lui.

Il doit y avoir un défilement pour remarquer un changement d'ancrage, ce qui est généralement une bonne UX. Mais de toute façon, la position de défilement actuelle doit être maintenue lorsque le clavier s'ouvre.

Chasse
la source
0

Ma solution est la même que votre solution proposée avec un ajout de vérification conditionnelle. Voici une description de ma solution:

  • Enregistrez la dernière position de défilement scrollTopet dernière clientHeightde .messagesà oldScrollTopet oldHeightrespectivement
  • Mettre à jour oldScrollTopet oldHeightchaque fois qu'un resizese produit windowet mettre à jour oldScrollTopchaque fois qu'un scrollse produit.messages
  • Lorsque windowest rétréci (lorsque le clavier virtuel s'affiche), la hauteur de .messagesse rétracte automatiquement. Le comportement prévu est de rendre le contenu le plus bas .messagesencore visible même lorsque .messagesla hauteur se rétracte. Cela nous oblige à ajuster manuellement la position scrollTopde défilement de .messages.
  • Lorsque le clavier virtuel montre, mise à jour scrollTopde .messagesfaire en sorte que la partie de plus basse .messagesavant sa rétraction de la hauteur se produit est encore visible
  • Lorsque les peaux de clavier virtuel, mise à jour scrollTopde .messagespour vous assurer que la partie de plus basse .messagesreste la partie de plus basse .messagesaprès l' expansion de la hauteur ( à moins que l' expansion ne peut se faire vers le haut, ce qui arrive quand vous êtes presque au sommet de .messages)

Qu'est-ce qui a causé le problème?

Ma pensée logique (initiale peut-être imparfaite) est: resizese produit, .messages'les changements de hauteur, la mise à jour .messages scrollTopse produit à l'intérieur de notre resizegestionnaire d'événements. Cependant, lors .messagesde l'expansion en hauteur, un scrollévénement se produit curieusement avant un resize! Et encore plus curieux, l' scrollévénement ne se produit que lorsque nous masquons le clavier lorsque nous avons fait défiler au-dessus de la scrollTopvaleur maximale de quand .messagesn'est pas rétracté. Dans mon cas, cela signifie que lorsque je défile en dessous 270.334px(le maximum scrollTopavant .messagesest rétracté) et que je masque le clavier, cet événement étrange scrollavant resizese produit et vous fait défiler .messagesexactement 270.334px. Cela perturbe évidemment notre solution ci-dessus.

Heureusement, nous pouvons contourner ce problème. Ma déduction personnelle de la raison pour laquelle cela scrollavant que l' resizeévénement se produise est parce que .messagesne peut pas maintenir sa scrollTopposition au-dessus 270.334pxquand il se dilate en hauteur (c'est pourquoi j'ai mentionné que ma pensée logique initiale est défectueuse; simplement parce qu'il n'y a aucun moyen .messagesde maintenir sa scrollTopposition au-dessus de son maximum valeur) . Par conséquent, il définit immédiatement son scrollTopà la valeur maximale qu'il peut donner (ce qui n'est pas surprenant 270.334px).

Que pouvons-nous faire?

Parce que nous ne mettons à jour que oldHeightlors du redimensionnement, nous pouvons vérifier si ce défilement forcé (ou plus correctement, resize) se produit et s'il le fait, ne pas mettre à jour oldScrollTop(car nous l'avons déjà géré resize!) Nous avons simplement besoin de comparer oldHeightet la hauteur actuelle sur scrollpour voir si ce défilement forcé se produit. Cela fonctionne parce que la condition de oldHeightne pas être égale à la hauteur actuelle scrollne sera vraie que lorsque resizecela se produit (ce qui est une coïncidence lorsque ce défilement forcé se produit).

Voici le code (dans JSFiddle) ci-dessous:

window.onload = function(e) {
  let messages = document.querySelector('.messages')
  messages.scrollTop = messages.scrollHeight - messages.clientHeight
  bottomScroller(messages);
}


function bottomScroller(scroller) {
  let oldScrollTop = scroller.scrollTop
  let oldHeight = scroller.clientHeight

  scroller.addEventListener('scroll', e => {
    console.log(`Scroll detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${scroller.scrollTop}`)
    if (oldHeight === scroller.clientHeight)
      oldScrollTop = scroller.scrollTop
  });

  window.addEventListener('resize', e => {
    let newScrollTop = oldScrollTop + oldHeight - scroller.clientHeight

    console.log(`Resize detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${newScrollTop}`)
    scroller.scrollTop = newScrollTop
    oldScrollTop = newScrollTop
    oldHeight = scroller.clientHeight
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
    <div class="message">hello 1</div>
    <div class="message">hello 2</div>
    <div class="message">hello 3</div>
    <div class="message">hello 4</div>
    <div class="message">hello 5</div>
    <div class="message">hello 6 </div>
    <div class="message">hello 7</div>
    <div class="message">hello 8</div>
    <div class="message">hello 9</div>
    <div class="message">hello 10</div>
    <div class="message">hello 11</div>
    <div class="message">hello 12</div>
    <div class="message">hello 13</div>
    <div class="message">hello 14</div>
    <div class="message">hello 15</div>
    <div class="message">hello 16</div>
    <div class="message">hello 17</div>
    <div class="message">hello 18</div>
    <div class="message">hello 19</div>
    <div class="message">hello 20</div>
    <div class="message">hello 21</div>
    <div class="message">hello 22</div>
    <div class="message">hello 23</div>
    <div class="message">hello 24</div>
    <div class="message">hello 25</div>
    <div class="message">hello 26</div>
    <div class="message">hello 27</div>
    <div class="message">hello 28</div>
    <div class="message">hello 29</div>
    <div class="message">hello 30</div>
    <div class="message">hello 31</div>
    <div class="message">hello 32</div>
    <div class="message">hello 33</div>
    <div class="message">hello 34</div>
    <div class="message">hello 35</div>
    <div class="message">hello 36</div>
    <div class="message">hello 37</div>
    <div class="message">hello 38</div>
    <div class="message">hello 39</div>
  </div>
  <div class="send-message">
    <input />
  </div>
</div>

Testé sur Firefox et Chrome pour mobile et il fonctionne pour les deux navigateurs.

Richard
la source