//- Only truly works in Chrome, but that's
//- the use case I need. Firefox has a rerender
//- that throws off the positioning.
//- This methed is not very good. One should 
//- measure the and finished-but-hidden element
//- and tween to that rather than readjust the
//- button as it travels.
div#app
View Compiled
$color-neutral-light: #b0b5b8
$color-neutral-medium: #545e61


html
  box-sizing: border-box
  background-color: #2d3437
  font-size: 62.5%
  font-family: Roboto, Helvetica, sans-serif
  overflow: hidden
  -webkit-font-smoothing: antialiased
  -moz-osx-font-smoothing: grayscale


  
*, *::before, *::after
  box-sizing: inherit


html, body
  height: 100%

  
.sr-only
  position: absolute
  width: 1px
  height: 1px
  padding: 0
  margin: -1px
  overflow: hidden
  clip: rect(0, 0, 0, 0)
  border: 0
  
  
#app
  display: flex
  flex: 1 1 auto
  flex-direction: row
  flex-wrap: wrap
  justify-content: space-between
  align-items: center
  
  
.morph-button-to-modal-wrapper
  position: relative
  overflow: visible
  top: 80vh
  left: 10vw
  width: 3.2rem
  height: 3.2rem
  

.morph-button-to-modal
  position: relative
  top: 0
  bottom: 0
  overflow: hidden
  min-width: 3.2rem
  min-height: 3.2rem
  width: 3.2rem
  //max-width: 3.2rem
  max-height: 3.2rem
  border: 0.2rem solid #fff
  border-radius: 50%
  background: transparent
  color: #fff
  will-change: content, width, max-width, max-height, transform
  transition-property: width, max-width, max-height, border-color, border-radius, background-color
  transition-duration: 350ms, 350ms, 350ms, 200ms, 200ms, 400ms
  transition-timing-function: cubic-bezier(0.46, 0.03, 0.52, 0.96)
  transition-delay: 100ms, 100ms, 200ms, 400ms, 400ms, 200ms
  //transition-delay: 200ms
    
  &.open
    z-index: 1000
    width: 90rem
    max-width: calc(100vw - 2rem)
    max-height: calc(100vh - 2rem)
    border-color: transparent
    border-radius: 1rem
    background-color: #394144
    transition-delay: 200ms, 200ms, 200ms, 200ms, 200ms, 200ms
    
  button
    appearence: none
    background: none
    border: none
    padding: 0
    
  .morph-button-enter
    opacity: 0.01
    
    &.morph-button-enter-active
      opacity: 1
      transition-property: opacity
      transition-duration: 300ms
      transition-timing-function: ease-out
      transition-delay: 400ms
      
  .morph-button-leave
    opacity: 1
  
    &.morph-button-leave-active
      opacity: 0
      transition: opacity 200ms ease-out
     
  .morph-modal-enter
    opacity: 0.01
    
    &.morph-modal-enter-active
      opacity: 1
      transition-property: opacity 320ms ease-out 500ms
      
  .morph-modal-leave
    opacity: 1
  
    &.morph-modal-leave-active
      opacity: 0
      transition: opacity 200ms ease-out
    
  .morph-button
    position: absolute
    width: 100%
    height: 100%

  .morph-modal-container
    display: flex
    flex-flow: column nowrap
    align-items: stretch
    position: relative
    padding: 2rem
    
  .morph-modal-title
    flex: 0 0 auto
    overflow: hidden
    padding-bottom: 2rem
    border-bottom: 0.1rem solid $color-neutral-medium
    font-size: 2rem
    font-weight: 500
    white-space: nowrap
    text-overflow: ellipsis
    
  .btn-close
    position: absolute
    top: 1.4rem
    right: 2rem
    color: $color-neutral-light
    font-size: 2.4rem
    
  .morph-modal-body
    flex: 0 1 auto
    overflow-y: auto
    max-height: calc(100vh - 14rem)
    font-size: 1.5rem
    
    
    
    
.device-entry-info-modal
  .morph-modal-enter
    .device-details-info
      > *
        border-color: transparent
      
        > *
          opacity: 0.01
    
    &.morph-modal-enter-active
      .device-details-info
        > *
          border-left-color: $color-neutral-medium
          transition: border-color 250ms ease-in 500ms
        
          > *
            opacity: 1
            transition: opacity 300ms ease-in 420ms
            
            @for $i from 1 through 24
              &:nth-child(#{$i})
                transition-delay: ($i * 32) + 420ms
        
    //.morph-modal-leave
    //  &.morph-leave-active
      
  .modal-button
    font-size: 3.2rem
  
  .info-key-value
    display: flex
    justify-content: space-between
    white-space: nowrap
    line-height: 2.5
    
    &, .info-key, .info-value
      margin: 0
    
    .info-key
      flex: 0 1 auto
      overflow: hidden
      padding-right: 1em
      color: #fff
      font-weight: 600
      text-overflow: ellipsis
    
    .info-value
      flex: 0 0 auto
      text-align: right
      color: $color-neutral-light
      
  .device-details-info
    display: flex
    flex-flow: row nowrap
    justify-content: space-between
    width: 100%
    max-width: 100%
    margin: 3rem 0
    
    > *
      flex: 0 1 33.33333%
      max-width: 33.33333%
      padding: 0 3rem
      border-left: 0.1rem solid $color-neutral-medium
    
      &:first-of-type
        border: none
        padding-left: 0
    
      &:last-of-type
        padding-right: 0
    
  .sub.info-key::before
    content: "\21B3"
    margin-right: 0.5em
    font-family: sans-serif
    color: $color-neutral-light
    
  .device-details-info-header
    display: flex
    flex-flow: row nowrap
    align-content: center
    margin-bottom: 4rem
    padding: 2.4rem 0 1.8rem
    border-bottom: 0.1rem solid $color-neutral-medium
    
    .device-image
      margin-right: 2.8rem
      color: #36d3b4
      font-size: 6.4rem
      
    .device-name, .device-online-status
      display: block
      
    .device-name
      font-size: 2.4rem
      font-weight: 500
      
    .device-online-status
      font-style: italic
      
      &::before
        content: "("
      
      &::after
        content: ")"
  
  .device-volume
    display: inline-block
    width: 100%
    break-inside: avoid
      
  // to save time color these
  .device-os-and-connection
    .fa-check
      color: #24E873
      
    .fa-times
      color: #FF005A
      
      
View Compiled
{PropTypes} = React
{button, dd, div, dt, dl, i, li, span, ul} = React.DOM
{CSSTransitionGroup} = React.addons


# Loosely Definining Types:
#
# type alias Volume =
#   { name : String
#   , subdirs : Maybe (List String)  // but not actually wrapped in Maybe
#   }
#
# type alias Coord =
#   { x : Number
#   , y : Number
#   }

# availableVolums : List Volume
availableVolumes = 
  [
    {name: "iPhoto"}
    {name: "LightRoom"}
    {name: "Macintosh HD", subdirs: ["Users/lucius"]}
    {name: "Lucius's Biig Hard Drive Name"}
    {name: "MOVIES"}
    {name: "Music"}
    {name: "OnePlus Two"}
    {name: "NSFW", subdirs: ["latina", "teen", "twins", "snakes"]}
    {name: "Lucius Thumb"}
    {name: "Snakes"}
    {name: "Animu"}
  ]
  

# Button & Title are passed in as a prop, children ar the modal
#
# MorphButtonToModal : ReactClass
MorphButtonToModal = React.createClass {
  name: "MorphButtonToModal"
  
  propTypes: {
    buttonDom: PropTypes.node
    titleDom: PropTypes.node
  }
  
  getInitialState: :getInitialState ->
    {
      isOpen: false
      mainTranslateCoord: {x: 0, y: 0}
    }
    
  #componentWillMount: :componentWillMount ->
  #  return
    
  componentWillUnmount: :componentWillUnmount ->
    window.cancelAnimationFrame @mainTranslateAnim
    window.removeEventListener "resize", @boundResizeHandler, false
    document.removeEventListener "click", @clickOutClose, false
    return
  
  #mainTranslateDuration : Int
  mainTranslateDuration: 600
  
  # coordToTranslate : Coord -> String
  coordToTranslate: :coordToTranslate({x, y}) ->
    "translate(" + x + "px, " + y + "px)"
  
  # quadInOut : Number -> Number -> Number -> Number -> Number
  quadInOut: :quadInOut(time, begin, end, duration) ->
    if (time = time / (duration / 2)) < 1
      (end - begin) / 2 * time * time + begin
    else
      (end - begin) * -1 / 2 * ((time -= 1) * (time - 2) - 1) + begin
     
  mainTranslateBack: :mainTranslateBack ->
    startTime = window.performance.now!
    startX = @state.mainTranslateCoord.x
    startY = @state.mainTranslateCoord.y
    duration = @mainTranslateDuration
    translate = (t) ~>
      deltaTime = window.performance.now! - startTime
      newCoord = {
        x: @quadInOut deltaTime, startX, 0, duration
        y: @quadInOut deltaTime, startY, 0, duration
      }
      @setState {mainTranslateCoord: newCoord}
      if deltaTime < duration
        @mainTranslateAnim = window.requestAnimationFrame translate
    translate!
    return
        
  mainTranslateToCenter: :mainTranslateToCenter ->
    {morphMain} = @refs
    startTime = window.performance.now!
    rect = morphMain.getBoundingClientRect!
    startX = rect.left
    startY = rect.top
    duration = @mainTranslateDuration
    translate = (t) ~>
      deltaTime = window.performance.now! - startTime
      {width, height} = morphMain.getBoundingClientRect!
      newCoord = {
        x: @quadInOut deltaTime, 0, ((window.innerWidth - width) * 0.5) - startX, duration
        y: @quadInOut deltaTime, 0, ((window.innerHeight - height) * 0.5) - startY, duration
      }
      @setState {mainTranslateCoord: newCoord}
      if deltaTime < duration
         @mainTranslateAnim = window.requestAnimationFrame translate
    translate!
    return
    
  resizeHandler: :resizeHandler(event) ->
    {top, left} = @refs.morphWrapper.getBoundingClientRect!
    {width, height} = @refs.morphMain.getBoundingClientRect!
    newCoord = {
      x: ((window.innerWidth - width) * 0.5) - left
      y: ((window.innerHeight - height) * 0.5) - top
    }
    @setState {mainTranslateCoord: newCoord}
    return
    
  toggleClickOutClose: :toggleClickOutClose ->
    if @state.isOpen
      @boundResizeHandler = @resizeHandler.bind @
      @clickOutClose = (event) ~> 
        if not event.target.closest ".morph-modal-container"
          @closeModal!
      window.addEventListener "resize", @boundResizeHandler, false
      document.addEventListener "click", @clickOutClose, false
    else
      window.removeEventListener "resize", @boundResizeHandler, false
      document.removeEventListener "click", @clickOutClose, false
    return
    
  openModal: :openModal(event) ->
    @mainTranslateToCenter!
    @setState {isOpen: true}, @toggleClickOutClose
    return
    
  closeModal: :closeModal(event) ->
    @mainTranslateBack!
    @setState {isOpen: false}, @toggleClickOutClose
    return
    
  # renderButton : () -> React.Element
  renderButton: :renderButton ->
    div {ref: "morphButtonContainer", className: "morph-button-container"},
      button {ref: "morphButton", className: "morph-button", onClick: @openModal}, @props.buttonDom
  
  # renderModal : () -> React.Element
  renderModal: :renderModal ->
    div {ref: "morphModalContainer", className: "morph-modal-container"},
      div {ref: "morphModalTitle", className: "morph-modal-title"},
        @props.titleDom
        button {ref: "morphCloseButton", className: "btn-close", onClick: @closeModal},
          i {ref: "morphCloseButtonIcon", className: "fa fa-times"}
          span {ref: "morphCloseButtonLabel", className: "sr-only"}, "Close"
      div {ref: "morphModalBody", className: "morph-modal-body"},
        @props.children
  
  # render : () -> React.Element
  render: :render ->
    classNames = ["morph-button-to-modal", (if @state.isOpen then "open" else "closed"), @props.className].join " " .trim!
    wrapperClassNames = classNames.split " " .map ((s) -> s + "-wrapper") .join " "
    div {ref: "morphWrapper", className: wrapperClassNames},
      div {ref: "morphMain", key: "morphButtonToModal", className: classNames, style: {transform: @coordToTranslate @state.mainTranslateCoord}},
        React.createElement CSSTransitionGroup, 
          { key                    : "morphButtonTrans"
          , transitionName         : "morph-button"
          , transitionEnterTimeout : 700
          , transitionLeaveTimeout : 200
          },
          if @state.isOpen then null else @renderButton!,
        React.createElement CSSTransitionGroup, 
          { key                    : "morphModalTrans"
          , transitionName         : "morph-modal"
          , transitionEnterTimeout : 1200
          , transitionLeaveTimeout : 200
          },
          if @state.isOpen then @renderModal! else null
}


DeviceInfoButtonModal = React.createClass {
  name: "DeviceInfoButtonModal"
  
  propTypes: {
    volumes: PropTypes.arrayOf PropTypes.shape {
      name: PropTypes.string.isRequired
      subdirs: PropTypes.arrayOf PropTypes.string
    }
  }

  # renderVolume : Int -> Volume -> Int -> React.Element
  renderVolume: :renderVolume(colIdx, vol, idx=0) ->
    div {key: "vol" + colIdx + "." + idx, className: "device-volume"},
      [ dl {key: colIdx + "." + idx, className: "info-key-value"},
        dt {key: "key", className: "info-key"}, vol.name
        dd {key: "value", className: "info-value"}, "Available"
      ].concat if not vol.subdirs or not vol.subdirs.length then [] else vol.subdirs.map (sub, idx) ~>
        dl {key: idx, className: "info-key-value"}, [
          dt {key: "subkey" + idx, className: "sub info-key"}, sub
          dd {key: "subvalue" + idx, className: "sub info-value"}, "Available"
        ]

  # renderVolumes : List Volume -> Int -> React.Element
  renderVolumes: :renderVolumes(volumes, count) ->
    count = count or @this.props.volumes.reduce (acc, vol) ->
      acc + 1 + (if vol.subdirs then vol.subdirs.length else 0)
    , 0
    if count < 7
      div {key: "vol-list-0", className: "device-volume-list", style: {flexBasis: "50%", maxWidth: "50%", paddingLeft: "9rem"}},
        volumes.map @renderVolume.bind @, 0
    else
      splitAt = Math.ceil count * 0.5
      splitVolumes = ([head, ...tail], count_=0, acc=[[], []]) ->
        acc[if count_ < splitAt then 0 else 1].push head
        if tail.length === 0 then acc else splitVolumes tail, count_ + 1 + (if head.subdirs then head.subdirs.length else 0), acc
      splitVolumes(volumes).map (volColumn, idx) ~>
        div {key: "vol-list-" + idx, className: "device-volume-list"},
          volColumn.map @renderVolume.bind @, idx


  # renderDeviceDetailsInfo : () -> React.Element
  renderDeviceDetailsInfo: :renderDeviceDetailsInfo ->
    volCount = @props.volumes.reduce (acc, vol) ->
      acc + 1 + (if vol.subdirs then vol.subdirs.length else 0)
    , 0
    div {className: "device-details-info"}, [
      div {key: "os-conn-0", className: "device-os-and-connection", style: if volCount < 7 then {flexBasis: "50%", maxWidth: "50%", paddingRight: "9rem"} else {}},
        dl {className: "info-key-value"},
          dt {className: "info-key"}, "Operating System"
          dd {className: "info-value"}, "OS X"
        dl {className: "info-key-value"},
          dt {className: "info-key"},
            i {className: "fa fa-check"}, null
            span null, " LAN"
          dd {className: "info-value"}, "192.168.1.1:8111"
        dl {className: "info-key-value"},
          dt {className: "info-key"},
            i {className: "fa fa-times"}, null
            span null, " P2P"
          dd {className: "info-value"}, "none"
        dl {className: "info-key-value"},
          dt {className: "info-key"},
            i {className: "fa fa-check"}, null
            span null, " Relay"
          dd {className: "info-value"}, "toast.al"
      ].concat @renderVolumes availableVolumes, volCount
  
  # renderButtonDom : React.Element
  renderButtonDom: [
    i {key: "buttonIcon", className: "info-icon fa fa-info"}, null
    span {key: "buttonLabel", className: "info-label sr-only"}, "Device Info"
  ]
    
  # renderTitleDom : React.Element
  renderTitleDom: [
    i {key: "titleIcon", className: "title-icon fa fa-desktop", style: {paddingRight: "0.5em"}}, null
    span {key: "titleLabel", className: "title-label"}, "Devices Info"
  ]

  # deviceInfoButtonModal : () -> React.Element
  render: :render ->
    React.createElement MorphButtonToModal, {
      key: "deviceEntryInfoModal"
      className: "device-entry-info-modal"
      buttonDom: @renderButtonDom
      titleDom: @renderTitleDom
    }, div {key: "deviceDetailsInfoContainer", className: "device-details-info-container"},
      div {className: "device-details-info-header"},
        i {className: "device-image fa fa-desktop"}, null
        div {className: "device-label"},
          span {className: "device-name"}, "Lucius Desktop"
          span {className: "device-online-status"}, "Online"
      @renderDeviceDetailsInfo!
}
  
    

# render the guy
ReactDOM.render (React.createElement DeviceInfoButtonModal, {volumes: availableVolumes}), document.getElementById "app"
View Compiled
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-with-addons.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js