Implement fancy breadcrumbs in new files

closes: CNVS-15249 CNVS-15372

test plan:
go to new files and make sure the breadcrumbs look like blakes mockups
especially try it when you have a bunch of crumbs and a small screen
it should have a '...' that opens in a drop down.

Change-Id: I632e4278aec4ee2d7373a6864d7d7c50fd067abc
Reviewed-on: https://gerrit.instructure.com/41052
Reviewed-by: Clay Diffrient <cdiffrient@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Product-Review: Ryan Shaw <ryan@instructure.com>
QA-Review: Ryan Shaw <ryan@instructure.com>
This commit is contained in:
Ryan Shaw 2014-09-17 13:31:41 -06:00
parent 2f4acbb850
commit f44dcfe1c2
7 changed files with 224 additions and 44 deletions

View File

@ -0,0 +1,52 @@
define [
'jquery'
'react'
'react-router'
'compiled/react/shared/utils/withReactDOM',
], ($, React, {Link}, withReactDOM) ->
BreadcrumbCollapsedContainer = React.createClass
propTypes:
foldersToContain: React.PropTypes.array.isRequired
getInitialState: ->
open: false
open: ->
clearTimeout @timeout
@setState open: true
close: ->
@timeout = setTimeout =>
@setState open: false
, 100
render: withReactDOM ->
li {
href: '#'
onMouseEnter: @open
onMouseLeave: @close
onFocus: @open
onBlur: @close
style: {position: 'relative'}
},
a href: '#',
'',
div className: "popover bottom ef-breadcrumb-popover #{'open' if @state.open}",
div({className: 'arrow'}),
div className: 'popover-content',
ul {},
@props.foldersToContain.map (folder) =>
li {},
Link {
to: (if folder.urlPath() then 'folder' else 'rootFolder')
contextType: @props.contextType
contextId: @props.contextId
splat: folder.urlPath()
activeClassName: 'active'
className: 'ellipsis'
},
i({className: 'ef-big-icon icon-folder'}),
span {}, folder.get('name')

View File

@ -1,15 +1,120 @@
define [
'jquery'
'underscore'
'react'
'react-router'
'./BreadcrumbCollapsedContainer'
'compiled/react/shared/utils/withReactDOM'
], (React, ReactRouter, withReactDOM) ->
], ($, _, React, {Link}, BreadcrumbCollapsedContainer, withReactDOM ) ->
Breadcrumbs = React.createClass
propTypes:
rootTillCurrentFolder: React.PropTypes.array.isRequired
contextType: React.PropTypes.oneOf(['users', 'groups', 'accounts', 'courses']).isRequired
contextId: React.PropTypes.string.isRequired
getInitialState: ->
{
minCrumbWidth: 40
maxCrumbWidth: 500
availableWidth: 200000
}
componentWillMount: ->
# Get the existing Canvas breadcrumbs, store them, and remove them
@fixOldCrumbs()
componentDidMount: ->
# Attach the resize listener to dynamically change the components
# involved in the breadcrumb trail.
$(window).on('resize', @handleResize)
@handleResize()
componentWillUnmount: ->
$(window).off('resize', @handleResize)
fixOldCrumbs: ->
$oldCrumbs = $('#breadcrumbs')
heightOfOneBreadcrumb = $oldCrumbs.find('li:visible:first').height() * 1.5
homeName = $oldCrumbs.find('.home').text()
$a = $oldCrumbs.find('li').eq(1).find('a')
contextUrl = $a.attr('href')
contextName = $a.text()
$oldCrumbs.remove()
@setState({homeName, contextUrl, contextName, heightOfOneBreadcrumb})
handleResize: ->
@startRecalculating(window.innerWidth)
startRecalculating: (newAvailableWidth) ->
@setState({
availableWidth: newAvailableWidth
maxCrumbWidth: 500
}, @checkIfCrumbsFit)
componentWillReceiveProps: -> setTimeout(@startRecalculating)
checkIfCrumbsFit: ->
return unless @state.heightOfOneBreadcrumb
breadcrumbHeight = $(@refs.breadcrumbs.getDOMNode()).height()
if (breadcrumbHeight > @state.heightOfOneBreadcrumb) and (@state.maxCrumbWidth > @state.minCrumbWidth)
maxCrumbWidth = Math.max(@state.minCrumbWidth, @state.maxCrumbWidth - 20)
@setState({maxCrumbWidth}, @checkIfCrumbsFit)
renderSingleCrumb: (folder, isLastCrumb) ->
li {},
Link {
to: (if folder.urlPath() then 'folder' else 'rootFolder')
contextType: @props.contextType
contextId: @props.contextId
splat: folder.urlPath()
activeClassName: 'active'
title: folder.get('name')
},
span {
className: 'ellipsis'
style:
maxWidth: (@state.maxCrumbWidth if isLastCrumb)
},
folder.get('name')
renderDynamicCrumbs: ->
return [] unless @props.rootTillCurrentFolder?.length
[foldersInMiddle..., lastFolder] = @props.rootTillCurrentFolder
if @state.maxCrumbWidth > @state.minCrumbWidth
@props.rootTillCurrentFolder.map (folder) => @renderSingleCrumb(folder, folder isnt lastFolder)
else
[
BreadcrumbCollapsedContainer({
foldersToContain: foldersInMiddle
contextType: @props.contextType,
contextId: @props.contextId
}),
@renderSingleCrumb(lastFolder, false)
]
render: withReactDOM ->
nav 'aria-label':'breadcrumbs', role:'navigation', className:'ef-breadcrumbs',
@props.rootTillCurrentFolder.map (folder) =>
ReactRouter.Link to: (if folder.urlPath() then 'folder' else 'rootFolder'), contextType: @props.contextType, contextId: @props.contextId, splat: folder.urlPath(), activeClassName: 'active',
span className:'ellipsible', folder.get('name')
nav {
'aria-label':'breadcrumbs'
role: 'navigation'
id: 'breadcrumbs'
ref: 'breadcrumbs'
},
ul {},
# The first link (house icon)
li className: 'home',
a href: '/',
i className: 'icon-home standalone-icon', title: @state.homeName,
span className: 'screenreader-only',
@state.homeName
# Context link
li {},
a href: @state.contextUrl,
span className: 'ellipsible',
@state.contextName
@renderDynamicCrumbs()...

View File

@ -28,18 +28,18 @@ define [
render: withReactDOM ->
div null,
Breadcrumbs({
rootTillCurrentFolder: @state.rootTillCurrentFolder,
contextType: @props.params.contextType,
contextId: @props.params.contextId
})
Toolbar({
currentFolder: @state.currentFolder,
query: @props.query,
params: @props.params
selectedItems: @state.selectedItems
})
if @state.rootTillCurrentFolder
Breadcrumbs({
rootTillCurrentFolder: @state.rootTillCurrentFolder,
contextType: @props.params.contextType,
contextId: @props.params.contextId
})
div className: 'ef-main',
aside className: 'visible-desktop ef-folder-content',
if @state.rootTillCurrentFolder

View File

@ -319,9 +319,7 @@ class FilesController < ApplicationController
def ember_app
raise ActiveRecord::RecordNotFound unless tab_enabled?(@context.class::TAB_FILES) && @context.feature_enabled?(:better_file_browsing)
clear_crumbs
@padless = true
@body_classes << 'full-width'
@body_classes << 'full-width padless-content'
js_bundle :react_files
jammit_css :ember_files
render :text => "".html_safe, :layout => true

View File

@ -6,11 +6,12 @@
list-style: none;
> li {
display: inline-block;
white-space: nowrap;
+ li {
&:before {
@extend i[class*=icon-]:before;
@extend .icon-arrow-open-right:before;
font-size: 10px;
font-size: 10px !important;
color: hsl(0,0,61%);
padding: 0 5px;
}
@ -20,6 +21,16 @@
}
}
}
// This is kinda hacky, but the label for the crumbs is in a
// <span class="ellipsis"> so we have to give it a display other than inline
// for it to actually do ellipsis. but when we do that, it is positioned weird;
// hence the position, top and margin-top, to get it back to where it should be.
.ellipsis {
display: inline-block;
position: relative;
top: 6px;
margin-top: -6px;
}
.icon-home:before {
font-size: 10px;
color: $base-font-color--subdued;

View File

@ -291,6 +291,30 @@ i[class*=current_uploads__instructions__icon-upload] {
font-weight: bold;
}
.ef-breadcrumb-popover {
display: block;
left: -9999px;
top: 15px;
opacity: 0;
transition: opacity .2s;
width: auto;
&.open {
left: -23px;
opacity: 1;
}
&.popover > .arrow {
left: 53px;
}
ul {
list-style: none;
margin: 0;
}
}
// Hack, Hack, Hack!
// to make sure that there is space for the ItemCog menu to appear below the bottom thing in the list of files

View File

@ -3,38 +3,28 @@ define [
'jquery'
'compiled/react_files/components/Breadcrumbs'
'compiled/models/Folder'
'react-router'
], (React, $, Breadcrumbs, Folder, ReactRouter) ->
'compiled/react_files/routes'
], (React, $, Breadcrumbs, Folder, routes) ->
Simulate = React.addons.TestUtils.Simulate
module 'FolderChild',
setup: ->
React.addons.TestUtils.renderIntoDocument(routes)
# Need to pass in setup objects but doing the same test
breadcrumbTest = (routerObject, test) =>
sinon.stub(ReactRouter, 'Link').returns("/some_url")
@breadcrumbs = React.renderComponent(Breadcrumbs(rootTillCurrentFolder: routerObject), $('<div>').appendTo('body')[0])
sampleProps =
rootTillCurrentFolder: [new Folder(), new Folder({name: 'test_folder_name', full_name: 'course_files/test_folder_name'})]
contextId: 'sample_course_id'
contextType: 'courses'
test()
@component = React.renderComponent(Breadcrumbs(sampleProps), $('<div>').appendTo('body')[0])
ReactRouter.Link.restore()
React.unmountComponentAtNode(@breadcrumbs.getDOMNode().parentNode)
teardown: ->
React.unmountComponentAtNode(@component.getDOMNode().parentNode)
module 'Breadcrumbs#render',
test 'generates the rootFolder link', ->
routerObject =
[
new Folder(name: 'folder')
]
breadcrumbTest routerObject, ->
ok ReactRouter.Link.calledWith(to: 'rootFolder', contextType: undefined, contextId: undefined, splat: "", activeClassName: 'active'), 'called with correct parameters for rootFolder'
test 'generates a folder link', ->
folder = new Folder(name: 'folder')
folder.urlPath = -> "somePath"
routerObject =
[
folder
]
breadcrumbTest routerObject, ->
ok ReactRouter.Link.calledWith(to: 'folder', contextType: undefined, contextId: undefined, splat: "somePath", activeClassName: 'active'), 'called with correct parameters for a folder link'
test 'generates the home, rootFolder, and other links', ->
$breadcrumbs = $(this.component.getDOMNode())
equal $breadcrumbs.find('.home a').attr('href'), '/', 'correct home url'
equal $breadcrumbs.find('li:nth-child(3) a').attr('href'), '/courses/sample_course_id/files', 'rootFolder link has correct url'
equal $breadcrumbs.find('li:nth-child(4) a').attr('href'), '/courses/sample_course_id/files/folder/test_folder_name', 'correct url for child'
equal $breadcrumbs.find('li:nth-child(4) a').text(), 'test_folder_name', 'shows folder names'