Mantener la posición de desplazamiento solo funciona cuando no está cerca de la parte inferior de los mensajes div

10

Estoy tratando de imitar otras aplicaciones de chat móviles donde, cuando seleccionas el send-messagecuadro de texto y se abre el teclado virtual, el mensaje más inferior todavía está a la vista. Parece que no hay una manera de hacer esto con CSS de manera sorprendente, por lo que JavaScript resize(única forma de averiguar cuándo se abre y cierra el teclado aparentemente) eventos y desplazamiento manual al rescate.

Alguien proporcionó esta solución y descubrí esta solución , que parecen funcionar.

Excepto en un caso. Por alguna razón, si está dentro de MOBILE_KEYBOARD_HEIGHT(250 píxeles en mi caso) píxeles de la parte inferior de la división de mensajes, cuando cierra el teclado del móvil, sucede algo extraño. Con la solución anterior, se desplaza hacia abajo. Y con la última solución, en su lugar, se desplaza hacia arriba MOBILE_KEYBOARD_HEIGHTpíxeles desde la parte inferior.

Si se desplaza por encima de esta altura, ambas soluciones proporcionadas anteriormente funcionan perfectamente. Es solo cuando estás cerca del fondo que tienen este problema menor.

Pensé que tal vez era solo mi programa lo que causaba esto con un código extraño, pero no, incluso reproduje un violín y tiene este problema exacto. Mis disculpas por hacer que esto sea tan difícil de depurar, pero si vas a https://jsfiddle.net/t596hy8d/6/show (el sufijo show proporciona un modo de pantalla completa) en tu teléfono, deberías poder ver el mismo comportamiento

Ese comportamiento es, si se desplaza hacia arriba lo suficiente, abrir y cerrar el teclado mantiene la posición. Sin embargo, si cierra el teclado dentro de los MOBILE_KEYBOARD_HEIGHTpíxeles de la parte inferior, encontrará que se desplaza hacia la parte inferior.

¿Qué está causando esto?

Reproducción de código aquí:

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
fuente
Reemplazaría los controladores de eventos con IntersectionObserver y ResizeObserver. Tienen una sobrecarga de CPU mucho menor que los controladores de eventos. Si está apuntando a navegadores antiguos, ambos tienen polyfills.
bigless
¿Has probado esto en Firefox para dispositivos móviles? No parece tener este problema. Sin embargo, intentar esto en Chrome causa el problema que mencionaste.
Richard
Bueno, tiene que funcionar en Chrome de todos modos. Sin embargo, eso es bueno, Firefox no tiene el problema.
Ryan Peschel
Mi mal por no transmitir mi punto correctamente. Si un navegador tiene un problema y otro no, esto, IMO, podría significar que es posible que deba tener una implementación ligeramente diferente para diferentes navegadores.
Richard
1
@halfer bien. Veo. Gracias por el recordatorio, lo tendré en cuenta la próxima vez que le pida a alguien que revise una respuesta.
Richard

Respuestas:

3

Finalmente encontré una solución que realmente funciona. Aunque puede no ser ideal, en realidad funciona en todos los casos. Aquí está el código:

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

Algunas epifanías que tuve en el camino:

  1. Al cerrar el teclado virtual, scrollse produce un evento instantáneamente antes del resizeevento. Esto parece suceder solo al cerrar el teclado, no al abrirlo. Esta es la razón por la que no puede usar el scrollevento para establecer pxFromBottom, porque si está cerca de la parte inferior, se establecerá en 0 en el scrollevento justo antes del resizeevento, lo que desordenará el cálculo.

  2. Otra razón por la cual todas las soluciones tuvieron dificultades cerca de la parte inferior del div de mensajes es un poco difícil de entender. Por ejemplo, en mi solución de cambio de tamaño solo agrego o resta 250 (altura del teclado móvil) scrollTopal abrir o cerrar el teclado virtual. Esto funciona perfectamente, excepto cerca del fondo. ¿Por qué? Porque digamos que estás a 50 píxeles de la parte inferior y cierras el teclado. Restará 250 de scrollTop(la altura del teclado), ¡pero solo debería restar 50! Por lo tanto, siempre se restablecerá a la posición fija incorrecta al cerrar el teclado cerca de la parte inferior.

  3. También creo que no puede usar onFocusy onBlureventos para esta solución, porque solo ocurren cuando se selecciona inicialmente el cuadro de texto para abrir el teclado. Es perfectamente capaz de abrir y cerrar el teclado móvil sin activar estos eventos, y como tal, no se pueden usar aquí.

Creo que los puntos anteriores son importantes para desarrollar una solución, porque al principio no son obvios, pero impiden que se desarrolle una solución sólida.

No me gusta esta solución (el intervalo es un poco ineficiente y propenso a las condiciones de carrera), pero no puedo encontrar nada mejor que siempre funcione.

Ryan Peschel
fuente
1

Creo que lo que quieres es overflow-anchor

El soporte está aumentando, pero no es total, pero https://caniuse.com/#feat=css-overflow-anchor

De un artículo de CSS-Tricks sobre él:

El anclaje de desplazamiento evita esa experiencia de "salto" al bloquear la posición del usuario en la página mientras se realizan cambios en el DOM sobre la ubicación actual. Esto permite al usuario permanecer anclado donde se encuentra en la página, incluso cuando se cargan nuevos elementos en el DOM.

La propiedad de desbordamiento de anclaje nos permite inhabilitar la función de anclaje de desplazamiento en caso de que se prefiera permitir que el contenido se vuelva a fluir a medida que se cargan los elementos.

Aquí hay una versión ligeramente modificada de uno de sus ejemplos:

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>

Abra esto en el móvil: https://cdpn.io/chasebank/debug/PowxdOR

Lo que está haciendo es básicamente deshabilitar cualquier anclaje predeterminado de los nuevos elementos del mensaje, con #scroller * { overflow-anchor: none }

Y en cambio, anclar un elemento vacío #anchor { overflow-anchor: auto }que siempre vendrá después de esos nuevos mensajes, ya que los nuevos mensajes se están insertando antes .

Tiene que haber un desplazamiento para notar un cambio en el anclaje, lo que creo que generalmente es una buena experiencia de usuario. Pero de cualquier manera, la posición de desplazamiento actual debe mantenerse cuando se abre el teclado.

Persecución
fuente
0

Mi solución es la misma que su solución propuesta con una adición de verificación condicional. Aquí hay una descripción de mi solución:

  • Grabar la última posición de desplazamiento scrollTopy último clientHeightde .messagesa oldScrollTopy oldHeightrespectivamente
  • Actualizar oldScrollTopy oldHeightcada vez que resizesucede un windowy actualizar oldScrollTopcada vez que scrollocurre un.messages
  • Cuando windowse reduce (cuando se muestra el teclado virtual), la altura de .messagesse retraerá automáticamente. El comportamiento previsto es hacer que el contenido más inferior de .messagestodavía sea visible incluso cuando.messages la altura se retraiga. Esto requiere que ajustemos manualmente la posición scrollTopde desplazamiento de .messages.
  • Cuando se muestra el teclado virtual, actualización scrollTopde.messages asegurarse de que la parte más inferior de .messagesantes de que ocurra su retracción altura es aún visible
  • Cuando las pieles teclado virtual, la actualización scrollTopde .messagespara asegurarse de que la parte más inferior de los .messagesrestos de la parte más inferior de .messagesdespués de la expansión de altura (a menos que la expansión no puede pasar hacia arriba, lo que pasa cuando estás casi en la parte superior de.messages )

¿Qué causó el problema?

Mi pensamiento lógico (inicial posiblemente defectuoso) es: resizesucede, .messages'la altura cambia, la actualización .messages scrollTopocurre dentro de nuestro resizecontrolador de eventos. Sin embargo, en .messagesla expansión de altura, scrollcuriosamente un evento ocurre antes de a resize! Y aún más curioso, el scrollevento solo ocurre cuando ocultamos el teclado cuando nos hemos desplazado por encima del scrollTopvalor máximo de cuando .messagesno está retraído. En mi caso, esto significa que cuando me desplazo hacia abajo 270.334px(el máximo scrollTopantes .messagesse retrae) y oculto el teclado, ese extraño evento scrollantes de resizeque suceda y lo desplaza .messagesexactamente 270.334px. Obviamente, esto arruina nuestra solución anterior.

Afortunadamente, podemos solucionar esto. Mi deducción personal de por qué esto scrollantes del resizeevento ocurre porque .messagesno puede mantener su scrollTopposición de arriba 270.334pxcuando se expande en altura (es por eso que mencioné que mi pensamiento lógico inicial es defectuoso; simplemente porque no hay forma de .messagesmantener su scrollTopposición por encima de su máximo valor) . Por lo tanto, establece inmediatamente su scrollTopvalor máximo que puede dar (que es, como era de esperar,270.334px ).

¿Qué podemos hacer?

Debido a que solo actualizamos oldHeighten el cambio de tamaño, podemos verificar si este desplazamiento forzado (o más correctamente resize) ocurre y, si es así, no actualice oldScrollTop(¡porque ya lo hemos manejado resize!) Simplemente necesitamos comparar oldHeighty la altura actual en scrollpara ver si ocurre este desplazamiento forzado. Esto funciona porque la condición de oldHeightno ser igual a la altura actual scrollsolo será verdadera cuandoresize suceda (que es coincidencia cuando ocurre ese desplazamiento forzado).

Aquí está el código (en JSFiddle) a continuación:

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>

Probado en Firefox y Chrome para dispositivos móviles y funciona para ambos navegadores.

Ricardo
fuente