メモを揉め

お勉強の覚書。

width、heightがautoの要素にtransitionを適用する

CSS3のtransition。

一般的なウェブアプリのUIで動きが必要になる時って、始点と終点がある程度決まっていて、animation@keyframeを駆使するような動きは限定的なことが多いと思うのだけど。

このtransitionを使っていていつも悩ましいのが、widthheightautoに設定されている要素にtransitionを適用したい時です。

transitionを有効にする場合、始点と終点の値を100pxのように絶対値で指定するか、50%(もちろんheightなどで親のautoを引継いでしまっている場合はダメ)のように相対値で指定することはできるけど、autoだとtransitionが効かない。

// Stylus

.foo
  width 200px
  height 0
  transition height .2s
  &.opened
    height auto

上の例の場合、

// JavaScript
document.querySelector('.foo').classList.add('opened');

のようにclass="opened"になるとheightの値は0からautoに変わるが、transitionによる効果は適用されない。

同様に、

// JavaScript
document.querySelector('.foo').classList.remove('opened');

のようにopenedを取り去ると、heightの値はautoから0に変わるが、この場合もtransitionの効果は発生しない。

ドロップダウンメニューや開閉可能なペインのように、コンテンツの内容に応じてリサイズしたい場合に困る。

これを解決する良い方法はまだよく分からないのですが、実際に試してみた幾つかのアプローチをメモしておきます。

基本方針

JavaScriptによる操作は最低限にしたい。
classの付け外しなどにとどめ、インラインスタイルの値を直接いじるのは最終手段にしたい。

autoを使わない

いきなり解決して無いですが、サイズを明示的に指定できる場合は極力指定する、という方法。

例えば、(折り返しを許可しない)一行だけのインラインが含まれるような要素の場合line-height+padding-top+padding-bottomheightを指定できる。
インラインが取り得る文字数の範囲が予測できる場合は、widthの値も明示できる可能性がある。

// Stylus

.foo
  line-height 16px
  padding 6px 0
  width 200px
  height 0
  transition height .2s
  &.opened
    height @line-height+@padding-top*2

20pxのような絶対値の指定でなくても、80vhcalc(50% + 20px)のような指定であればtransitionは効くので、それらで解決できないかも検討してみる。

max-heightmax-widthを使用する

heightwidthの代わりにmax-heightmax-widthを使用する方法。
transition-propertyにはmax-heightmax-widthを指定する。

heightの場合

以下のようにmax-height100vhのように指定しておくと、コンテンツの高さに合わせて.fooの高さが決まる(ただしコンテンツが100vh以上の場合は100vhになる)。

// Stylus

.foo
  width 200px
  max-height 0
  transition max-height .2s
  &.opened
    max-height 100vh

widthの場合

display: blockの場合、widthは親要素いっぱいに広がろうとするので、コンテンツのサイズに合わせたい場合はdisplay: inline-blockdisplay: flexにする必要がある。

// Stylus

.foo
  display inline-block
  max-width 0
  height 500px
  transition max-width .2s
  &.opened
    max-width 100%

ただし、以下のようなデメリットがある為、きめ細く動きを調整したい場合には向いていない。

  • transitionの始点、終点はmax-heightmax-widthの値で計算される
    • そのため、実際の高さ、幅とmax-heightmax-widthの差が大きいほどtransitionの効果は不自然になる
      • 基本的に設定したtransition-durationより短くなる(早くtransitionが終了する)
      • transition-timing-functionheightwidthの始点、終点とズレるので綺麗に適用されない
      • サイズが小さくなる動きの場合、クリックなどのユーザアクションに対して反応が遅れる

簡単なので間に合わせでとりあえず動きを付けたいという時は良いかも知れない。

JavaScriptでコンテンツサイズを調べる

getComputedStylegetBoudingClientRectなどでコンテンツのサイズを調べる方法。

ここではgetBoundingClientRectを使うようにする。(getComputedStyleだとautoの値がうまく取れなかった事があったはずだけどうまく再現できず)

ラッパー要素を用意する

コンテンツ全体を包む要素を用意し、position: absoluteを適用する。
この要素のheightwidthautoを指定して、ドロップボックスがエキスパンドした状態などを維持させておき、常にコンテンツサイズに合わせておく。
このラッパー要素からgetBoundingClientRectでサイズを取得してリサイズする要素に適用する。

今のところこの方法が一番良さそうに感じる。

autoの値を正しく得るために、外側のheightwidthが固定値の場合はそれをラッパー要素にも適用する。

// Stylus

.foo
  width 200px
  height 0
  transition height .2s
  .bar
    position absolute
    width @width
    height auto

// JavaScript
const foo = document.querySelector('.foo');
const bar = document.querySelector('.bar');

document.documentElement.addEventListener('click', e => {
  if(foo.classList.contains('opened')) {
    foo.classList.remove('opened');
    foo.style.height = '';
  } else {
    const contentHeight = bar.getBoundingClientRect().height;

    foo.style.height = `${contentHeight}px`;
    foo.classList.add('opened');
  }
});

唯一の欠点は、ドロップダウンメニューなどが開閉する際、コンテンツ自体はサイズが伸び縮みしないので、もしそういう風に動きを付けたい場合は、もう一手間必要になる、という点。
個人的な感覚では、コンテンツのサイズを伸び縮みさせたくないことのほうが多い(トランジションの途中で文字の折り返し部分が変わっていったりするとキモい)ので、あまり困らないかなと思う。

transitionを一旦無効にしてサイズを調べる方法

やってみたけど、ボツにした方法。

あえて利点を挙げるとすれば、コンテンツがテキストノードだけの場合でも使えるので、ラッパー要素を用意する必要が無い。
ブラウザ間の誤差を気持ち悪いハックで誤魔化しているので、いつ動かなくなっても驚かない。
使わないほうが良い。

  1. インラインスタイルでtransitionnoneを設定
  2. heightwidthautoを適用
  3. getBoundingClientRectheightwidthを調べる
  4. heightwidthを元の値に戻す
  5. transitionを元の値に戻す
  6. setTimeoutで非同期にheightwidthに適用する

FireFoxChromeではsetTimeoutで非同期に適用しないとtransitionが効かない、またChromeでは第二引数を0にすると効かないことがある。

Safariでは同期処理中にもチラつきがある。
また、元の値を0以外にすると、autoの値を適用した時に、一度0になってしまう。

// Stylus

.foo
  width 200px
  height 0
  transition height .2s

// JavaScript

const foo = document.querySelector('.foo');

document.documentElement.addEventListener('click', e => {
  if(foo.classList.contains('opened')) {
    foo.classList.remove('opened');
    foo.style.height = '';
  } else {
    foo.style.transition = 'none';
    foo.style.height = 'auto';

    const contentHeight = foo.getBoundingClientRect().height;

    foo.style.height = '';
    foo.style.transition = '';

    setTimeout(() => {
      foo.style.height = `${contentHeight}px`;
      foo.classList.add('opened');
    }, 30);
  }
});

今日の結論

heightwidthautoが設定してある要素にtransitionを適用したい場合は、ラッパー要素を用意してJavaScriptでサイズを取得するのが良いと思う。
CSSだけだと厳しそうなので、JavaScriptで少しカバーしてあげるとバランスが良い。

Stylusのあまり知られていない機能

というより、自分が最近知った機能。

ハッシュをCSSのプロパティ名と値に展開できる

Stylusではハッシュが使えます。
シンタックスはJSのオブジェクトリテラルとほぼ同じですが、Stylusのnodeをそのまま値に使用できる点が違います。
以下のように、値に10pxと書いてもvalidです。

foo = {
  width: 10px
}

// JSと同じようにアクセスできる
bar = foo.width
bar = foo['width']

// 代入もできる
foo.height = 20px
foo['height'] = 20px

ハッシュを{foo}のようにブレースで囲むと、ハッシュのキーバリューがそのままStylusとして解釈され、CSSのプロパティと値に展開されます。

ハッシュがネストになっている部分は、Stylusのネスト(インデント)として解釈されるので、以下のように&は親セレクタとして展開されます。

foo = {
  width: 10px,
  height: 20px,
  '&:hover': {
    color: crimson
  }
}

.bar
  {foo}

// こう解釈される
// .bar
//   width 10px
//   height 20px
//   &:hover
//     color crimson

// => .bar {
//      width: 10px;
//      height: 20px;
//    }
//    .bar:hover {
//      color crimson
//    }

注意点としてネストされたハッシュや、関数が返すハッシュをそのまま展開しようとするとエラーになります。
いったん変数に代入するとうまくいきます。

foo = {
  bar: {
    opacity: 1
  }
}

.baz
  {foo.bar} // エラー

.baz
  {foo['bar']} // エラー

qux = foo.bar

.baz
  {qux}

// => .baz {
//      opacity: 1;
//    }

JSONを読み込んで色々できる

jsonというビルトイン関数を使用して、JSONを読み込むことができます。

キーバリューがそのまま変数名と値として展開され、変数が定義されます。
ネストされている場合は、各階層のキーが-で連結された物が変数名になります。

{
  "foo": "10px",
  "bar": {
    "baz": "20px",
    "qux": "'Helvetica Neue'"
  }
}

上のJSONを読み込むと以下のように変数定義が行われます。
注意点として、ダブルクォートはトリムされるので文字列を値にしたい場合は内側でさらにシングルクォートで囲うなどする必要があります。

json('./vars.json')

// foo = 10px
// bar-baz = 20px
// bar-qux = 'Helvetica Neue'

.title
  font-family bar-qux

// => .title {
//      font-family: 'Helvetica Neue';
//    }

json関数には以下のオプションが用意されています。

  • hash
  • leave-strings
  • optional

hash

JSONをそのままハッシュとして読み込みます。

vars = json('./vars.json', { hash: true })

p(vars.foo) // => inspect: 10px
p(vars.bar.qux) // => inspect: 'Helvetica Neue'

leave-strings

ダブルクォートをトリムしないようにする、読み込まれる値が全て文字列になります。
leave-stringsは実際にはhashオプション前提のようで、hashをつけなくてもハッシュとして読み込まれます。

vars = json('./vars.json', { hash: true, leave-strings: true })

p(vars.foo) // => inspect: '10px'
p(vars.bar.qux) // => inspect: ''Helvetica Neue''

optional

jsonファイルが存在しなくてもエラーにならない。
こちらもhashオプション前提で、hashをつけなくてもハッシュとして読み込まれます。

vars = json('./nothing.json', { hash: true, optional: true })

p(vars) // => inspect: null

JavaScriptの関数を呼べる

こうしてStylusの中でハッシュも配列も関数もあって、文字列操作もそれなりに用意されていると、だんだん複雑な関数を作り始めたりします。
とはいえ、JSで書けたら楽なのになーという場面も結構あります。

そんな時にもStylusにはJavaScriptを呼び出すためのAPIが用意されています。

JavaScript側では以下のようにプラグインを作っておき、Stylus側ではuse関数を使って読み込みます。

// add.js
module.exports = () => stylus => {
  stylus.define('add', (a, b) => a.operate('+', b));
};

use('path/to/add.js')

res = add(10px, 20px)

// => 30px

CLIの場合でも-uオプションで同じように使用できます。

$ stylus -u ./path/to/add.js src/index.styl -o dist

Stylusのノードを作って返すようなこともできるみたいです。
この辺ドキュメントがあまり詳しく書かれてないのでよく分かってないです、またいずれ調べてみます。

nibはおそらくこんな感じでJavaScriptAPIからnode-canvasを呼び出してgradientをpng画像に変換したりしてるんだと思います。

他にも公式ページを見ていると色々知らない機能が出てくるので面白いです。

stylus-lang.com

button要素にはflexboxを使わないほうが良さそうです

<button>要素にflexboxを適用したところ、ChromeSafariFireFoxでそれぞれ表示が違った。
IEではまだ試していない。

調べてみるとどうやらこういう事らしい。

stackoverflow: Flexbox not working on <button> element in some browsers

The problem is that the <button> element cannot be a flex container (in some browsers).

Certain HTML elements, by design, do not accept display changes. Three of these elements are:

  • <button>
  • <fieldset>
  • <legend>

The idea is to maintain a level of common sense when styling HTML elements. For instance, the rule prevents authors from turning a fieldset into a table.

button要素はflex containerにはなれないと。
上記三つの要素はCertain HTML elements(用途が明らかな要素?)なので、displayの値は変えられないよ、というような事が書いてあるような気がします。

デモ

Chromeだと他の通常のブロック要素と同じようにflexboxが適用される。
Safariだとdisplay: flexflex-flow: row nowrapは効いてるっぽいが、justify-contentが効かない。
FireFoxだとそもそもdisplay: flexが効かない。

looks_one looks_two

// jade
button
  i.material-icons looks_one
  i.material-icons looks_two
.button
  i.material-icons looks_one
  i.material-icons looks_two

// stylus
@import 'nib'

button,
.button
  size 200px 50px
  padding 10px
  margin 20px
  text-align center
  box-sizing border-box
  display flex // FireFoxで効かない
  flex-flow row nowrap
  justify-content space-around // Safariで効かない
  outline none
  border none
  background-color #3399aa
  .material-icons
    size 30px auto
    line-height 30px
    background-color #bbb

各ブラウザのスクリーンショットこんな感じです。

Chrome
f:id:alluser:20160724074231p:plain

Safari
f:id:alluser:20160724074236p:plain

FireFox
f:id:alluser:20160724074224p:plain