CSS

21 mai, 2015

Normalizando bugs no flexbox em diversos navegadores

Publicidade

Update: como uma continuação para este artigo, eu criei o repositório Flexbugs no GitHub: uma lista com curadoria da comunidade que traz questões do flexbox em diversos navegadores e as suas soluções conhecidas. O objetivo é que se você estiver construindo um site usando flexbox, e algo não está funcionando do jeito que esperaria, você possa encontrar a solução lá.

De volta a setembro de 2013, enquanto testava meu projeto Solved by Flexbox, descobri um bug no Internet Explorer 10 e 11 que estava impedindo meu footer de realmente aderir à parte inferior da página. Gastei um tempo tentando contornar o problema, mas todas as minhas tentativas falharam.

No começo eu estava muito irritado. Antes do flexbox era impossível construir um layout do só com CSS, a menos que você soubesse as dimensões exatas do cabeçalho e do rodapé. [1] O Flexbox mudou isso – finalmente tivemos soluções CSS respeitáveis para os layouts responsivos modernos de hoje.

Depois da minha decepção inicial, acabei concluindo que isso não era uma grande coisa e, apesar do comportamento com falhas, lancei o projeto. Quer dizer, a partir de um ponto de vista de otimização progressiva, a minha solução ainda foi muito boa. Funcionou no Chrome, Firefox, Opera e Safari, e embora não estivesse perfeito no Internet Explorer, não estava completamente falho também. O conteúdo ainda estava acessível e isso só ficaria incorreto em páginas com conteúdo mínimo. Nas páginas mais longas, ficava igual a qualquer outro browser.

Um tempo atrás, eu recebi uma pull request no GitHub que corrigiu o problema sticky footer com uma regra @media para o IE apenas. Isso me fez pensar sobre o problema de novo, e eu estava determinado a encontrar uma solução que não exigisse qualquer hack específico de navegadores.

No final, descobri que existia uma solução, e ela era possível o tempo todo! Eu só não estava procurando o suficiente.

Neste artigo, vou explicar a solução, orientá-lo como cheguei até lá e falar sobre os bugs do navegador que descobri ao longo do caminho. Vou fazer também algumas recomendações sobre como escrever código flexbox mais compatível com cross-browser no futuro.

Então, quais são os bugs?

A especificação do flexbox ainda não está concluída, então vai, naturalmente, haver algum atraso entre as últimas versões e implementações do navegador. Este artigo não tem a intenção de apontar o dedo para as partes que estiverem atrasadas; em vez disso, é destinado a ajudar os desenvolvedores front-end a fazerem o que fazem melhor: gerenciar inconsistências do navegador.

A seguir, vou mostrar uma lista de erros que encontrei ao tentar fazer meu footer trabalhar em cross-browser:

  • No IE 10 -11, os itens do flex ignoram a altura de seu container principal se ela for definida por meio da propriedade min-height.
  • Chrome, Opera e Safari não conseguem honrar o dimensionamento do conteúdo mínimo padrão dos itens do flex.
  • IE 11/10 não permitem valores flex-basis sem unidade na abreviação flex.

O bug min-height

Nos Internet Explorer 10 e 11, a propriedade min-height pode ser usada para o tamanho de um container flex na direção da coluna, mas o elemento do container flex agirá como se eles não soubessem o tamanho de seu pai, como se nenhuma altura fosse fixada mesmo.

Isso é um problema para o meu footer, uma vez que os layouts dele exigem tradicionalmente uma declaração min-height de 100% (ou 100vh) para garantir que a área de conteúdo seja pelo menos tão alta quanto a janela do navegador.

Uma vez que min-height não estava dando conta do recado, eu precisava achar uma outra maneira.

Dimensionamento mínimo do conteúdo dos elementos flex

Quando os itens Flex são grandes demais para caber dentro de seu contêiner, eles são instruídos (pelo algoritmo de layout flexível) a encolher, proporcionalmente, de acordo com suas propriedades flex-shrink. Mas, ao contrário do que muitos browsers permitem, não é suposto a encolher indefinidamente. Eles devem ser sempre, pelo menos, tão grande quanto a sua altura mínima ou que as propriedades de largura declaram, e se não tiver propriedades mínimas de altura ou largura definidas, a sua dimensão mínima deve ser o tamanho mínimo padrão do seu conteúdo.

De acordo com a especificação do flexbox:

Por padrão, os itens do Flex não vão encolher abaixo do tamanho mínimo de conteúdo (o comprimento da palavra mais longa ou do elemento de tamanho fixo). Para mudar isso, defina o min-width ou a propriedade min-height.

Chrome, Opera e Safari atualmente ignoram essa instrução e permitem que os itens do Flex encolham a zero. Como resultado, você ganha a sobreposição de conteúdo.

Flex-basis sem unidade

Antes do lançamento do IE 10, a especificação do flexbox na época afirmou que tamanho preferido de um item do flexbox requeria uma unidade ao usar a abreviação flex:

Se <preferred-size> for ‘0’, ele deve ser especificado com uma unidade (como “0px ‘) para evitar ambiguidades; zero sem uma unidade será interpretado como uma das flexibilidades, ou um erro de sintaxe.

Isso não é mais verdade na especificação, mas IE 10-11 ainda agem assim. Se você usar a declaração flex: 1 0 0 em um desses navegadores, será um erro e toda a regra (incluindo todas as propriedades de flexibilidade) será ignorada.

Encontrando uma solução alternativa para o footer

Eu escolhi o layout do footer como o exemplo principal para este artigo porque eu achei cada um desses bugs enquanto tentava encontrar uma solução cross-browser para ele. Mas, antes de entrar em muitos detalhes, vamos ter certeza de que estamos todos na mesma página.

Aqui está a marcação que estou usando para o meu layout do footer:

<body class="Site">
  <header class="Site-header">…</header>
  <main class="Site-content">…</main>
  <footer class="Site-footer">…</footer>
</body>

E aqui está o CSS que vai fazer o footer colar em qualquer navegador compatível com a especificação:

.Site {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
.Site-content {
  flex: 1;
}

Agora, como eu já mencionei, esse CSS deveria funcionar, mas, devido aos erros existentes e do fato de que nem todos os navegadores são totalmente compatíveis com especificação, isso não acontece.

Se você é um desenvolvedor front-end experiente, sabe que qualquer solução para um problema de cross-browser não só precisa funcionar hoje, mas precisa continuar funcionando muito depois de você terminar um projeto.

Uma solução que depende de um comportamento errático não é solução alguma.

Com base em tudo o que eu disse até agora, aqui estão as minhas necessidades pessoais para qualquer solução alternativa aceitável para o problema de layout do footer. Ela deve:

  • funcionar em todos os navegadores [2]
  • continuar funcionando enquanto navegadores corrigem o comportamento quebrado
  • não confiar em qualquer hack específico para navegadores

Usando height em vez de min-height

Se você está no mercado há algum tempo, deve se lembrar de que o Internet Explorer 6 nunca suportou a propriedade min-height (ou largura). Ele, no entanto, trata height:100% da mesma forma que outros navegadores trataram min-height:100%, por isso todas as soluções originais do footer recomendariam definir height:100% em um estilo apenas para IE 6.

Sabendo disso, usar height:100vh em vez de min-height:100vh foi uma das primeiras soluções alternativas que eu tentei. Isso realmente funcionou no IE, mas não no Chrome, então eu imediatamente tirei.

Como se constata, eu deveria ter lido melhor a especificação em vez de simplesmente assumir que o Chrome estava certo e o IE, errado.

Em CSS, você normalmente opta por usar min-height em vez de height para se proteger do terrível overflow. Quando houver muito conteúdo, uma altura explícita vai dizer que há clipping, overlapping ou uma barra de rolagem. E em muitas situações, são todos ruins. No entanto, quando você estiver lidando com o elemento body (já que você está em um layout com um sticky footer), uma barra de rolagem não é grande coisa. É realmente o que você quer. Portanto, se você definir uma altura explícita de 100vh sobre o body e tiver muito conteúdo, o resultado final deverá ser o mesmo.

Então a pergunta é: por que isso não funcionou no Chrome?

Bugs de dimensionamento mínimo

Anteriormente, eu mencionei que alguns navegadores equivocadamente permitem que os itens do Flex diminuam para menos do que seu tamanho de conteúdo mínimo padrão, resultando em sobreposição de conteúdo. É por isso que trocar min-height por height não funcionou quando eu testei no Chrome.

O que deveria acontecer é que o cabeçalho, o rodapé, e os elementos de conteúdo todos deveriam encolher para o seu tamanho do conteúdo mínimo padrão (mas não menos). Se esses elementos (combinados) têm mais conteúdo do que pode caber na tela, o elemento body deve transbordar de uma barra de rolagem como faz normalmente. Os elementos de conteúdo, cabeçalho e devem renderizar tudo normalmente, um em cima do outro, sem sobreposições.

Em vez isso, o que estava acontecendo é que o Chrome estava permitindo que o cabeçalho, o rodapé e os elementos de conteúdo encolhessem a um tamanho menor do que seus tamanhos mínimos de conteúdo padrão. Como resultado, em vez de o transbordamento acontecer no elemento body, ele estava acontecendo no cabeçalho, no rodapé e nos próprios elementos de conteúdo. E como o valor padrão em excesso desses elementos é visible, o seu conteúdo foi sobreposto uns sobre os outros. O rodapé foi fixado ao fundo da página, e o conteúdo da página foi transbordando por ela.

Felizmente, existe uma solução fácil para esse problema.

A especificação do flexbox define um valor incial flex-shrink de 1, mas diz que os itens não devem encolher abaixo do seu tamanho de conteúdo mínimo padrão. Você pode obter praticamente esse mesmo comportamento usando um valor de flex-shrink de 0 em vez do padrão 1 [3]. Se o seu elemento já estiver sendo dimensionado com base em seus filhos, e ainda não tiver definido uma width, height, ou valor flex-basis, então configurar flex-shrink:0 irá renderizar da mesma maneira, mas vai evitar esse bug.

Evitando flex-basis sem unidade

O bug do flex-basis sem unidade flex-basis é de longe o mais fácil dos três para se contornar, mas é sem dúvida o mais difícil de rastrear quando encontrados em estado selvagem.

Minha solução original para o problema do sticky footer aplicada uma declaração de flex:1 para o principal elemento de conteúdo. Uma vez que um valor flex 1 é uma abreviação para 1 1 0px [4], e desde que eu soube que não queria qualquer encolhimento acontecendo, eu decidi usar 1 0 0px ao invés.

Isso funcionou muito bem até que eu testei no IE.

O problema acabou sendo uma combinação desse isso bug do IE e meu minificador CSS: o minificador estava convertendo 1 0 0px 1 0 0 (que tem um valor flex-basis sem unidade), então IE 10-11 ignorou, inteiramente, essa declaração.

Uma vez que eu finalmente descobri a raiz do problema, a correção era trivial. Ou definir um valor flex-base explícito ou usar 0% no flex shorthand. Note, usar 0% é melhor do que 0px já que a maioria minificadores não vai tocar valores percentuais por outras razões.

Juntando tudo

O que se segue é um resumo rápido dos erros discutidos neste artigo e suas respectivas soluções:

  • min-height em um container de coluna flex não se aplica aos seus itens filhos flex no IE 10-11. Se for possível, use height.
  • Chrome, Opera e Safari não honram o tamanho padrão mínimo de conteúdo dos itens flex. Defina flex-shrink a 0 (em vez do padrão 1) para evitar o encolhimento indesejado.
  • Não use valores flex-basis sem unidade no shorthand flex porque dará erro no IE 10-11. Use, também, 0% em vez de 0px, uma vez que minificadores muitas vezes converterão 0px para 0 (o que é sem unidade e terá o mesmo problema).

Com todos esses erros e soluções alternativas em mente, aqui está a solução final proposta por mim. Pode não ser tão limpa ou intuitiva como a maneira que eu promovi originalmente, mas satisfaz todos os meus requisitos para uma solução alternativa:

  • Funciona em todos os navegadores.
  • É compatível com a especificação, por isso deve continuar a funcionar assim que os bugs forem corrigidos.
  • Ele não usa nenhum hack específico de navegadores.

Eu adicionei comentários para o CSS para esclarecer quais partes são soluções alternativas:

/**
 * 1. Avoid the IE 10-11 `min-height` bug.
 * 2. Set `flex-shrink` to `0` to prevent Chrome, Opera, and Safari from
 *    letting these items shrink to smaller than their content's default
 *    minimum size.
 */
.Site {
  display: flex;
  flex-direction: column;
  height: 100vh; /* 1 */
}
.Site-header,
.Site-footer {
  flex-shrink: 0; /* 2 */
}
.Site-content {
  flex: 1 0 auto; /* 2 */
}

Para ver esta nova solução em ação, confira o demo da atualização Resolvida por Flexbox sticky footer.

Notas de rodapé:

[1] Tecnicamente, se você não precisa suportar o IE 7 e as versões mais antigas, você pode construir um layout do footer usando display:table, que permite alturas de cabeçalho/rodapé desconhecidos. Apenas para deixar claro que isso é apenas um hack para fazer funcionar em cross-browser, não uma solução real e moderna.

[2] Quando eu digo “todos os navegadores”, quero dizer todos os navegadores que implementam uma versão da especificação flexbox datada de março de 2012 ou mais recente. Em outras palavras, ela deve funcionar em todos os navegadores que pretendem suportar o flexbox moderno.

[3] Utilizar flex-basis:0 resolve a grande maioria dos problemas associados a esse bug, mas não todos eles. Se você quiser que seus itens do flex encolham, mas que o encolhimento não ocorra além do limite do conteúdo padrão, essa solução não funcionará.

[4] A atualização de março de 2014 para especificação flexbox mudou o significado de do shorthand flex: 1 1 0px para 1 1 0%.

***

Philip Walton faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://philipwalton.com/articles/normalizing-cross-browser-flexbox-bugs/