Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
50bcc496
Commit
50bcc496
authored
Sep 14, 2015
by
Daniel Friedman
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #9751 from edx/dan-f/make-teams-tabs-accessible
Dan f/make teams tabs accessible
parents
4d0a1275
77384f1a
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
114 additions
and
42 deletions
+114
-42
common/test/acceptance/pages/lms/teams.py
+12
-7
lms/static/js/components/tabbed/views/tabbed_view.js
+55
-14
lms/static/js/spec/components/tabbed/tabbed_view_spec.js
+35
-15
lms/static/sass/views/_teams.scss
+5
-3
lms/templates/components/tabbed/tab.underscore
+1
-1
lms/templates/components/tabbed/tabbed_view.underscore
+3
-2
lms/templates/components/tabbed/tabpanel.underscore
+3
-0
No files found.
common/test/acceptance/pages/lms/teams.py
View file @
50bcc496
...
...
@@ -13,8 +13,8 @@ from .fields import FieldsMixin
TOPIC_CARD_CSS
=
'div.wrapper-card-core'
CARD_TITLE_CSS
=
'h3.card-title'
MY_TEAMS_BUTTON_CSS
=
'
a
.nav-item[data-index="0"]'
BROWSE_BUTTON_CSS
=
'
a
.nav-item[data-index="1"]'
MY_TEAMS_BUTTON_CSS
=
'.nav-item[data-index="0"]'
BROWSE_BUTTON_CSS
=
'.nav-item[data-index="1"]'
TEAMS_LINK_CSS
=
'.action-view'
TEAMS_HEADER_CSS
=
'.teams-header'
CREATE_TEAM_LINK_CSS
=
'.create-team'
...
...
@@ -23,24 +23,28 @@ CREATE_TEAM_LINK_CSS = '.create-team'
class
TeamCardsMixin
(
object
):
"""Provides common operations on the team card component."""
def
_bounded_selector
(
self
,
css
):
"""Bind the CSS to a particular tabpanel (e.g. My Teams or Browse)."""
return
'{tabpanel_id} {css}'
.
format
(
tabpanel_id
=
getattr
(
self
,
'tabpanel_id'
,
''
),
css
=
css
)
def
view_first_team
(
self
):
"""Click the 'view' button of the first team card on the page."""
self
.
q
(
css
=
'a.action-view'
)
.
first
.
click
()
self
.
q
(
css
=
self
.
_bounded_selector
(
'a.action-view'
)
)
.
first
.
click
()
@property
def
team_cards
(
self
):
"""Get all the team cards on the page."""
return
self
.
q
(
css
=
'.team-card'
)
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'.team-card'
)
)
@property
def
team_names
(
self
):
"""Return the names of each team on the page."""
return
self
.
q
(
css
=
'h3.card-title'
)
.
map
(
lambda
e
:
e
.
text
)
.
results
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'h3.card-title'
)
)
.
map
(
lambda
e
:
e
.
text
)
.
results
@property
def
team_descriptions
(
self
):
"""Return the names of each team on the page."""
return
self
.
q
(
css
=
'p.card-description'
)
.
map
(
lambda
e
:
e
.
text
)
.
results
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'p.card-description'
)
)
.
map
(
lambda
e
:
e
.
text
)
.
results
class
BreadcrumbsMixin
(
object
):
...
...
@@ -135,6 +139,7 @@ class MyTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin):
"""
url_path
=
"teams/#my-teams"
tabpanel_id
=
'#tabpanel-my-teams'
def
is_browser_on_page
(
self
):
"""Check if the "My Teams" tab is being viewed."""
...
...
@@ -166,7 +171,7 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
@property
def
topic_names
(
self
):
"""Return a list of the topic names present on the page."""
return
self
.
q
(
css
=
CARD_TITLE_CSS
)
.
map
(
lambda
e
:
e
.
text
)
.
results
return
self
.
q
(
css
=
'#tabpanel-browse '
+
CARD_TITLE_CSS
)
.
map
(
lambda
e
:
e
.
text
)
.
results
@property
def
topic_descriptions
(
self
):
...
...
lms/static/js/components/tabbed/views/tabbed_view.js
View file @
50bcc496
...
...
@@ -4,11 +4,37 @@
'underscore'
,
'jquery'
,
'text!templates/components/tabbed/tabbed_view.underscore'
,
'text!templates/components/tabbed/tab.underscore'
],
function
(
Backbone
,
_
,
$
,
tabbedViewTemplate
,
tabTemplate
)
{
'text!templates/components/tabbed/tab.underscore'
,
'text!templates/components/tabbed/tabpanel.underscore'
,
],
function
(
Backbone
,
_
,
$
,
tabbedViewTemplate
,
tabTemplate
,
tabPanelTemplate
)
{
var
getTabPanelId
=
function
(
id
)
{
return
'tabpanel-'
+
id
;
};
var
TabPanelView
=
Backbone
.
View
.
extend
({
template
:
_
.
template
(
tabPanelTemplate
),
initialize
:
function
(
options
)
{
this
.
url
=
options
.
url
;
this
.
view
=
options
.
view
;
},
render
:
function
()
{
var
tabPanelHtml
=
this
.
template
({
tabId
:
getTabPanelId
(
this
.
url
)});
this
.
setElement
(
$
(
tabPanelHtml
));
this
.
$el
.
append
(
this
.
view
.
render
().
el
);
return
this
;
}
});
var
TabbedView
=
Backbone
.
View
.
extend
({
events
:
{
'click .nav-item
[role="tab"]
'
:
'switchTab'
'click .nav-item
.tab
'
:
'switchTab'
},
template
:
_
.
template
(
tabbedViewTemplate
),
...
...
@@ -31,6 +57,10 @@
initialize
:
function
(
options
)
{
this
.
router
=
options
.
router
||
null
;
this
.
tabs
=
options
.
tabs
;
// Convert each view into a TabPanelView
_
.
each
(
this
.
tabs
,
function
(
tabInfo
)
{
tabInfo
.
view
=
new
TabPanelView
({
url
:
tabInfo
.
url
,
view
:
tabInfo
.
view
});
},
this
);
this
.
urlMap
=
_
.
reduce
(
this
.
tabs
,
function
(
map
,
value
)
{
map
[
value
.
url
]
=
value
;
return
map
;
...
...
@@ -42,12 +72,17 @@
this
.
$el
.
html
(
this
.
template
({}));
_
.
each
(
this
.
tabs
,
function
(
tabInfo
,
index
)
{
var
tabEl
=
$
(
_
.
template
(
tabTemplate
,
{
index
:
index
,
title
:
tabInfo
.
title
,
url
:
tabInfo
.
url
}));
index
:
index
,
title
:
tabInfo
.
title
,
url
:
tabInfo
.
url
,
tabPanelId
:
getTabPanelId
(
tabInfo
.
url
)
})),
tabContainerEl
=
this
.
$
(
'.tabs'
);
self
.
$
(
'.page-content-nav'
).
append
(
tabEl
);
});
// Render and append the current tab panel
tabContainerEl
.
append
(
tabInfo
.
view
.
render
().
$el
);
},
this
);
// Re-display the default (first) tab if the
// current route does not belong to one of the
// tabs. Otherwise continue displaying the tab
...
...
@@ -63,10 +98,16 @@
tab
=
tabMeta
.
tab
,
tabEl
=
tabMeta
.
element
,
view
=
tab
.
view
;
this
.
$
(
'a.is-active'
).
removeClass
(
'is-active'
).
attr
(
'aria-selected'
,
'false'
);
tabEl
.
addClass
(
'is-active'
).
attr
(
'aria-selected'
,
'true'
);
view
.
setElement
(
this
.
$
(
'.page-content-main'
)).
render
();
this
.
$
(
'.sr-is-focusable.sr-tab'
).
focus
();
// Hide old tab/tabpanel
this
.
$
(
'button.is-active'
).
removeClass
(
'is-active'
).
attr
(
'aria-expanded'
,
'false'
);
this
.
$
(
'.tabpanel[aria-expanded="true"]'
).
attr
(
'aria-expanded'
,
'false'
).
addClass
(
'is-hidden'
);
// Show new tab/tabpanel
tabEl
.
addClass
(
'is-active'
).
attr
(
'aria-expanded'
,
'true'
);
view
.
$el
.
attr
(
'aria-expanded'
,
'true'
).
removeClass
(
'is-hidden'
);
// This bizarre workaround makes focus work in Chrome.
_
.
defer
(
function
()
{
view
.
$
(
'.sr-is-focusable.'
+
getTabPanelId
(
tab
.
url
)).
focus
();
});
if
(
this
.
router
)
{
this
.
router
.
navigate
(
tab
.
url
,
{
replace
:
true
});
}
...
...
@@ -85,10 +126,10 @@
var
tab
,
element
;
if
(
typeof
tabNameOrIndex
===
'string'
)
{
tab
=
this
.
urlMap
[
tabNameOrIndex
];
element
=
this
.
$
(
'
a
[data-url='
+
tabNameOrIndex
+
']'
);
element
=
this
.
$
(
'
button
[data-url='
+
tabNameOrIndex
+
']'
);
}
else
{
tab
=
this
.
tabs
[
tabNameOrIndex
];
element
=
this
.
$
(
'
a
[data-index='
+
tabNameOrIndex
+
']'
);
element
=
this
.
$
(
'
button
[data-index='
+
tabNameOrIndex
+
']'
);
}
return
{
'tab'
:
tab
,
'element'
:
element
};
}
...
...
lms/static/js/spec/components/tabbed/tabbed_view_spec.js
View file @
50bcc496
...
...
@@ -15,20 +15,40 @@
render
:
function
()
{
this
.
$el
.
text
(
this
.
text
);
return
this
;
}
});
}),
activeTab
=
function
()
{
return
view
.
$
(
'.page-content-nav'
);
},
activeTabPanel
=
function
()
{
return
view
.
$
(
'.tabpanel[aria-expanded="true"]'
);
};
describe
(
'TabbedView component'
,
function
()
{
beforeEach
(
function
()
{
view
=
new
TabbedView
({
tabs
:
[{
title
:
'Test 1'
,
view
:
new
TestSubview
({
text
:
'this is test text'
})
view
:
new
TestSubview
({
text
:
'this is test text'
}),
url
:
'test-1'
},
{
title
:
'Test 2'
,
view
:
new
TestSubview
({
text
:
'other text'
})
view
:
new
TestSubview
({
text
:
'other text'
}),
url
:
'test-2'
}]
}).
render
();
// _.defer() is used to make calls to
// jQuery.focus() work in Chrome. _.defer()
// delays the execution of a function until the
// current call stack is clear. That behavior
// will cause tests to fail, so we'll instead
// make _.defer() immediately invoke its
// argument.
spyOn
(
_
,
'defer'
).
andCallFake
(
function
(
func
)
{
func
();
});
});
it
(
'can render itself'
,
function
()
{
...
...
@@ -36,33 +56,33 @@
});
it
(
'shows its first tab by default'
,
function
()
{
expect
(
view
.
$el
.
text
()).
toContain
(
'this is test text'
);
expect
(
view
.
$el
.
text
()).
not
.
toContain
(
'other text'
);
expect
(
activeTabPanel
()
.
text
()).
toContain
(
'this is test text'
);
expect
(
activeTabPanel
()
.
text
()).
not
.
toContain
(
'other text'
);
});
it
(
'displays titles for each tab'
,
function
()
{
expect
(
view
.
$el
.
text
()).
toContain
(
'Test 1'
);
expect
(
view
.
$el
.
text
()).
toContain
(
'Test 2'
);
expect
(
activeTab
()
.
text
()).
toContain
(
'Test 1'
);
expect
(
activeTab
()
.
text
()).
toContain
(
'Test 2'
);
});
it
(
'can switch tabs'
,
function
()
{
view
.
$
(
'.nav-item[data-index=1]'
).
click
();
expect
(
view
.
$el
.
text
()).
not
.
toContain
(
'this is test text'
);
expect
(
view
.
$el
.
text
()).
toContain
(
'other text'
);
expect
(
activeTabPanel
()
.
text
()).
not
.
toContain
(
'this is test text'
);
expect
(
activeTabPanel
()
.
text
()).
toContain
(
'other text'
);
});
it
(
'marks the active tab as selected using aria attributes'
,
function
()
{
expect
(
view
.
$
(
'.nav-item[data-index=0]'
)).
toHaveAttr
(
'aria-
select
ed'
,
'true'
);
expect
(
view
.
$
(
'.nav-item[data-index=1]'
)).
toHaveAttr
(
'aria-
select
ed'
,
'false'
);
expect
(
view
.
$
(
'.nav-item[data-index=0]'
)).
toHaveAttr
(
'aria-
expand
ed'
,
'true'
);
expect
(
view
.
$
(
'.nav-item[data-index=1]'
)).
toHaveAttr
(
'aria-
expand
ed'
,
'false'
);
view
.
$
(
'.nav-item[data-index=1]'
).
click
();
expect
(
view
.
$
(
'.nav-item[data-index=0]'
)).
toHaveAttr
(
'aria-
select
ed'
,
'false'
);
expect
(
view
.
$
(
'.nav-item[data-index=1]'
)).
toHaveAttr
(
'aria-
select
ed'
,
'true'
);
expect
(
view
.
$
(
'.nav-item[data-index=0]'
)).
toHaveAttr
(
'aria-
expand
ed'
,
'false'
);
expect
(
view
.
$
(
'.nav-item[data-index=1]'
)).
toHaveAttr
(
'aria-
expand
ed'
,
'true'
);
});
it
(
'sets focus for screen readers'
,
function
()
{
spyOn
(
$
.
fn
,
'focus'
);
view
.
$
(
'.nav-item[data-
index=1
]'
).
click
();
expect
(
view
.
$
(
'.sr-is-focusable.
sr-tab
'
).
focus
).
toHaveBeenCalled
();
view
.
$
(
'.nav-item[data-
url="test-2"
]'
).
click
();
expect
(
view
.
$
(
'.sr-is-focusable.
test-2
'
).
focus
).
toHaveBeenCalled
();
});
describe
(
'history'
,
function
()
{
...
...
lms/static/sass/views/_teams.scss
View file @
50bcc496
...
...
@@ -22,9 +22,12 @@
%button-reset
{
box-shadow
:
none
;
border
:
none
;
border-radius
:
0
;
background-image
:
none
;
background-color
:
transparent
;
font-weight
:
normal
;
font-family
:
inherit
;
font-size
:
inherit
;
font-weight
:
inherit
;
}
// layout
...
...
@@ -148,6 +151,7 @@
border-bottom
:
3px
solid
$gray-l5
;
.nav-item
{
@extend
%button-reset
;
display
:
inline-block
;
margin-bottom
:
-3px
;
// to match the border
border-bottom
:
3px
solid
$gray-l5
;
...
...
@@ -745,5 +749,3 @@
.create-team.form-actions
{
margin-top
:
$baseline
;
}
lms/templates/components/tabbed/tab.underscore
View file @
50bcc496
<
a class="nav-item" href="" data-url="<%= url %>" data-index="<%= index %>" role="tab" aria-selected="false"><%= title %></a
>
<
button class="nav-item tab" data-url="<%= url %>" data-index="<%= index %>" is-active="false" aria-expanded="false" aria-controls="<%= tabPanelId %>"><%= title %></button
>
lms/templates/components/tabbed/tabbed_view.underscore
View file @
50bcc496
<nav class="page-content-nav" aria-label="Teams"></nav>
<div class="sr-is-focusable sr-tab" tabindex="-1"></div>
<div class="page-content-main"></div>
<div class="page-content-main">
<div class="tabs"></div>
</div>
lms/templates/components/tabbed/tabpanel.underscore
0 → 100644
View file @
50bcc496
<div class="tabpanel is-hidden" id="<%= tabId %>" aria-expanded="false">
<div class="sr-is-focusable <%= tabId %>" tabindex="-1"></div>
</div>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment