CSS

7 jul, 2015

Escreva media queries simples, elegantes e sustentáveis com Sass

Publicidade

Passei alguns meses experimentando abordagens diferentes para escrever media queries simples, elegantes e sustentáveis com Sass. Cada solução tinha algo de que eu realmente gostava, mas não conseguia encontrar uma que englobasse tudo que eu precisava fazer, então eu me aventurei em criar a minha própria: conheça include-media.

Comecei com esta lista básica de requisitos:

  • Declaração dinâmica de breakpoints: Eu não queria ter qualquer breakpoint hardcoded no meu mixin. Mesmo que eu possa usar telefone, tablet e desktop na maioria dos meus sites, talvez precise adicionar phablet, pequeno tablet ou grande-desktop à minha lista, dependendo do projeto. Como alternativa, eu poderia querer adotar uma abordagem diferente e ter os breakpoints orientados ao conteúdo, então eu poderia ter bp-small, bp-medium, bp-wide, e assim por diante. Para lidar com tudo isso corretamente, eu queria uma maneira simples de declarar os breakpoints que preciso em algum lugar do código (de preferência fora do arquivo mixin) e, em seguida, fazer referência a seus nomes para construir as media queries.
  • Sintaxe simples e natural: A minha sintaxe foi inspirada pela técnica de Dmitry Sheiko, que usa um mixin que recebe como argumentos os nomes dos breakpoints precedidos por um sinal de maior de ou menor (por exemplo, @include media(“>minWidth”)) para indicar que se trata de uma min-width ou max-width.
  • Combinar breakpoints com valores personalizados: Eu precisei por muitas vezes aplicar regras adicionais a um elemento em breakpoints intermediários. Em vez de poluir a minha lista global de breakpoints com os valores de cada caso específico, eu queria ser capaz de lançar os valores personalizados no mixin e combiná-los com os meus breakpoints (por exemplo, @include media(“>tablet”, “<1280px”)).
  • Breakpoints inclusivos e exclusivos: Na maioria das soluções que tentei, quando você faz o mesmo breakpoint do limite superior de uma media query e o limite inferior de outra, ambas as media queries serão acionadas quando a largura do viewport for exatamente a mesma do breakpoint, pois todos os intervalos são inclusivos. Isso, às vezes, pode levar a um comportamento inesperado, então eu queria ter um controle maior sobre essas situações com dois operadores adicionais: maior-ou-igual-a e menor do que-ou-igual-a.
  • Uma maneira elegante de incluir tipos de mídia e expressões complexas: Eu queria ser capaz de especificar os tipos de mídia, como a tela ou portáteis, mas como um argumento opcional, pois na maioria das vezes eu só vou omiti-lo. Além disso, eu queria o suporte adequado para as condições que continham um operador or (representada por uma vírgula em CSS), tais como a declaração retina de Chris. Praticamente todos os mixins que eu tentei não conseguiam lidar adequadamente com isso, porque quando você combina as expressões a com b ou c, geram (ab) ou c, em vez de (ab) ou (ac).

Então, basicamente, eu queria algo como isto:

// We'll define the breakpoints somehow. 
// For now, let's say tablet' is 768px and 'desktop' 1024px.

@include media(">=tablet", "<1280px") {

}

@include media("screen", ">tablet") {

}

@include media(">tablet", "retina2x") {

}

 

// Compiling to:

@media (min-width: 768px) and (max-width: 1279px) {

}

@media screen and (min-width: 769px) {

}

@media (min-width: 769px) and (-webkit-min-device-pixel-ratio: 2),
       (min-width: 769px) and (min-resolution: 192dpi) {

}

Vamos a ele então.

Parseando expressões

O primeiro passo é chegar a uma estrutura na qual podemos definir os nossos breakpoints e os nossos tipos de mídia e expressões. O Sass 3.3 adicionou suporte para o Maps, que são basicamente listas multidimensionais nas quais os valores podem ser associados de forma semelhante a JSON. Esta é a estrutura perfeita para acomodar os nossos breakpoints.

$breakpoints: (phone: 320px, tablet: 768px, desktop: 1024px) !default;

Isso é flexível o suficiente para armazenar qualquer número de breakpoints que desejarmos, e nós os classificamos automaticamente para o uso mais fácil. Isso porque nós provavelmente vamos aprimorar esses breakpoints em uma base de projeto, os declaramos como !default no mixin e, em seguida, substituímos a declaração sempre que necessário. Da mesma forma, podemos usar essa estrutura para armazenar nossos tipos e expressões de mídia.

$media-expressions: (screen: "screen", 
                    print: "print", 
                    handheld: "handheld",
                    retina2x: ("(-webkit-min-device-pixel-ratio: 2)", "(min-resolution: 192dpi)"), 
                    retina3x: ("(-webkit-min-device-pixel-ratio: 3)", "(min-resolution: 350dpi)")
                    ) !default;

Note que declarei as expressões contendo disjunções lógicas como listas aninhadas, porque nós vamos ter que lidar com as condições delas individualmente. Nós ainda podemos declará-las como strings separadas por uma vírgula, já que são naturalmente escritas em CSS, e depois separá-las, mas podemos muito bem dividi-las no início.

O próximo passo é parsear as strings que recebemos como argumentos e traduzi-las em expressões corretas. Em vez de tentar escrever um mixin que faz tudo isso de uma só vez, vamos para uma função menor que lida com um único argumento (por exemplo, >=tablet into min-width: 768px).

@function parse-expression($expression) {
  $operator: "";
  $value: "";
  $element: "";
  $result: "";
  $is-width: true;

  // Separating the operator from the rest of the expression
  @if (str-slice($expression, 2, 2) == "=") {
    $operator: str-slice($expression, 1, 2);
    $value: str-slice($expression, 3);
  } @else {
    $operator: str-slice($expression, 1, 1);
    $value: str-slice($expression, 2);
  }

  // Checking what type of expression we're dealing with
  @if map-has-key($breakpoints, $value) {
    $result: map-get($breakpoints, $value);
  } @else if map-has-key($media-expressions, $expression) {
    $result: map-get($media-expressions, $expression);
    $is-width: false;
  } @else {
    $result: to-number($value);
  }

  // If we're dealing with a width (breakpoint or custom value), 
  // we form the expression taking into account the operator.
  @if ($is-width) {
    @if ($operator == ">") {
      $element: "(min-width: #{$result + 1})";
    } @else if ($operator == "&lt;") {
      $element: "(max-width: #{$result - 1})";
    } @else if ($operator == ">=") {
      $element: "(min-width: #{$result})";
    } @else if ($operator == "&lt;=") {
      $element: "(max-width: #{$result})";
    }
  } @else {
    $element: $result;
  }

  @return $element;
}

Começamos detectando qual operador tem sido utilizado e, em seguida, combinamos com o resto da string contra o mapa dos breakpoints. Se ele corresponder a uma das chaves, vamos usar o seu valor. Se não, repetiremos o processo para o mapa das expressões de mídia. Finalmente, se nenhuma correspondência for encontrada, assumimos que é um valor personalizado, caso em que temos de lançar a string em um número que podemos usar para adicionar ou subtrair, conforme necessário, dependendo do operador. Eu usei to-number de SassyCast para fazer isso.

Lidando com Logical Disjunction

Devido à forma como or funciona em CSS, combinar as condições que contenham esse operador com os outras pode ser bastante complicado. Por exemplo, a expressão a (b or c) d e (f or g) vai gerar quatro branches de condições disjoint. Aqui está um processo que podemos usar para gerar todas as combinações possíveis:

  1. Pegue um “snapshot” da expressão, deixando intocados os singletons e tendo apenas o primeiro elemento de cada grupo or (um “snapshot” de uma expressão sem grupos é a expressão original).
  2. Encontre a próxima disjunction (grupo). Pegue as expressões já calculadas (resultado) e faça (N-1) de cópias, onde N é o número de elementos no grupo.
  3. Substitua todos os casos do primeiro elemento do grupo com todos os outros elementos do grupo e atualize o resultado.
  4. Repita o passo 2 até que não haja mais grupos.

A tabela a seguir ilustra o processo para a expressão mostrada acima.

Iteration Group Result
1 a b d e f (snapshot)
2 (b, c) a b d e f, a c d e f
3 (f, g) a b d e f, a c d e f, a b d e g, a c d e g

 

Veja como podemos escrever isso Sass:

@function get-query-branches($expressions) {
  $result: "";
  $has-groups: false;

  // Getting initial snapshot and looking for groups
  @each $expression in $expressions {
    @if (str-length($result) != 0) {
      $result: $result + " and ";
    }

    @if (type-of($expression) == "string") {
      $result: $result + $expression;
    } @else if (type-of($expression) == "list") {
      $result: $result + nth($expression, 1);
      $has-groups: true;
    }
  }

  // If we have groups, we have to create all possible combinations
  @if $has-groups {
    @each $expression in $expressions {
      @if (type-of($expression) == "list") {
        $first: nth($expression, 1);

        @each $member in $expression {
          @if ($member != $first) {
            @each $partial in $result {
              $result: join($result, str-replace-first($first, $member, $partial));
            }
          }
        }
      }
    }
  }

  @return $result;
}

O Sass não tem qualquer função de substituir uma string, então eu criei uma versão simples que substitui apenas a primeira ocorrência da string a ser substituída, que é o que precisamos:

@function str-replace-first($search, $replace, $subject) {
  $search-start: str-index($subject, $search);

  @if $search-start == null {
    @return $subject;
  }

  $result: str-slice($subject, 0, $search-start - 1);
  $result: $result + $replace;
  $result: $result + str-slice($subject, $search-start + str-length($search));

  @return $result;
}

Juntando tudo

Agora que nós construímos nossos pequenos helpers, finalmente podemos escrever o próprio mixin. Ele vai ter que pegar um número variável de strings, analisá-los, detectar as expressões de mídia e lidar com possíveis disjunctions e juntar tudo para formar a expressão de media query.

@mixin media($conditions...) {
  @for $i from 1 through length($conditions) {
    $conditions: set-nth($conditions, $i, parse-expression(nth($conditions, $i)));
  }

  $branches: get-query-branches($conditions);
  $query: "";

  @each $branch in $branches {
    @if (str-length($query) != 0) {
      $query: $query + ", ";
    }

    $query: $query + $branch;
  }

  @media #{$query} {
    @content;
  }
}

Finalizando

Esta implementação é um pouco complexa, e algumas pessoas podem argumentar que é muito código para escrever media queries. Pessoalmente, acho que é uma maneira fácil e confortável para escrever media queries poderosas e de fácil manutenção com uma sintaxe simples e confortável, apenas fazendo o download e importando um único arquivo SCSS. O repositório GitHub está lá para que as pessoas possam enviar suas perguntas e solicitações, e esperamos que isso vá manter o mixin atualizado com novas funcionalidades e melhorias. Quais são seus pensamentos?

***

Eduardo Bouças 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://davidwalsh.name/sass-media-query