読者です 読者をやめる 読者になる 読者になる

undefined

bokuweb.me

ぽよんと表示されるmodalコンポーネントを作った


f:id:bokuweb:20160318085435g:plain

吹き出しコンポーネントを作った時から、SVGで面白い動きのコンポーネントが作ってみたいと思っていて、その習作としてSVGで描画したぽよんと表示されるmodalコンポーネントを作ってみた。

blog.bokuweb.me

作ったもの

github.com

デモ

React-elastic-modal example

使い方

インストール

npm i react-elastic-modal

サンプル

以下のように使用する。極力react-modalに似せたつもり。

<Modal
  isOpen={ this.state.isOpen }
  onRequestClose={ () => this.setState({ isOpen: false }) }
  modal={{
    width: '50%',
    height: '360px',
    backgroundColor: '#fff',
    opacity: 0.5,
  }}
  overlay={{
    background: 'rgba(0, 0, 0, 0.4)',
  }}
>
  <div>modal example</div>
</Modal>

このコンポーネントについて

使用例

@59nagaさんが以下のサイトで使ってくれている。ありがとうござます。

https://cdn.berabou.me/

SVGまわり

以下のように書いて、rafでアニメーションしてる。なんかあまりいい方法とは思えない。また、親から例えばサイズを100px×100pxでもらった場合、伸縮を表現するためにSVG領域は110px×110px確保している。アニメーション完了後いサイズを戻せばいいんだろうけど、今は放置している。SVGの機能についても、もっと理解を深める必要がありそう。

modalのコンテンツはSVGとは別レイヤーにしてz-indexで被せている。この辺りも正攻法を把握してない。

<svg
  width={`${100 + svgMarginRatio * 200}%`}
  height={`${100 + svgMarginRatio * 200}%`}
  style={{
    position: 'absolute',
    top: `-${100 * svgMarginRatio}%`,
    left: `-${100 * svgMarginRatio}%`,
    transform: `scale3d(${this.state.scale}, ${this.state.scale}, 1)`,
    opacity: this.props.modal.opacity || 1,
  }}
>
  <path d={ `M ${x0} ${y0}
             Q ${cx} ${top} ${x1} ${y0}
             Q ${right} ${cy} ${x1} ${y1}
             Q ${cx} ${bottom} ${x0} ${y1}
             Q ${left} ${cy} ${x0} ${y0}` }
    fill={ this.props.modal.backgroundColor }
  />
</svg>

中央寄せの話し

modalを中央寄せする中で最近は以下の方法がかなりお気に入りで使用していたんだけど、要素のwidth/heightが奇数の場合、transform: translate(-50%, -50%)で座標が小数点になって表示がぼやけるってことに遭遇した。これだったらボックスのサイズを知る必要がなく便利なんだけど、結局古のネガティブマージンで対応することにした。css難しい。

.outer {
  position: relative;
}
.inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

テストまわり

最近はenzymeを使っているんだけどまだ理解できていないところが多い。基本shallowを使ってテスト(この場合はmockは不要?)して、必要に応じてmountするという方法をとってるんだけどそういうスタンスでいいんだろうか。

たとえば、defaultPropsの値を確認したかったり、componentDidMountを発火させたかったり、element.clientHeightを取りたいような場合はどうしいてもmountする必要がある。その場合は結局mockeryとかproxyquireとかでmockに置き換える必要があると思う(今回は単一のコンポーネントなので必要ないけど)んだけどそうのようなスタンスでいいのか不明だ。

もう一点ハマった点は、modalclinetHeight/Widthが常に0が取れてきていて、なんだろうと思っていたんだけどマウントの仕方が悪かったよう。どうやら以下のようにしているとサイズが出ないようで、

const modal = mount(
  <Modal ..... />
);

正しくは以下のようにdivにアタッチする必要があるよう。

const modal = mount(
  <Modal ..... />, { attachTo: div }
);

そのため、beforeとafterで以下のようにしている。

describe('Modal test', () => {
  let div;
  beforeEach(() => {
    div = document.createElement('div');
    document.body.appendChild(div);
  });

  afterEach(() => {
    document.body.innerHTML = '';
  });

 ...

さいご

SVGは楽しいんだけど、もう少し、SVG自身の機能や作法を学ぶ必要がありそう。またモーフィング周りにいいライブラリがあるともっと楽しめそうなんだが、sebmarkbage/artこのあたりとか使えるんだろうか。