app/scripts/ui-lib/widgets/accordions.coffee

Accordions

The Accordions class provides an accordions widget.

author: Julien Ramboz
version: 1.0
references: WAI-ARIA tab role, AOL's Tab Panel style guide
usage: Accordions examples requires: Collapse

AMD loader

Try loading as AMD module or fall back to default loading

((widget) -> if typeof define is "function" and define.amd define ["jslib", "core", "widget", "collapse"], widget else widget @$, _, _.AbstractWidget, _.Collapse ) accordions = ($, _, AbstractWidget, Collapse) -> "use strict"

Widget

The actual widget class

class Accordions extends AbstractWidget

Default options for the widget

@defaultOptions:
  • event: the event to react to
event: "click"
  • tabs: the tabs specified as a selector relative to the element
tabs: ""
  • panels: the panels specified as a selector relative to the element
panels: ""
  • position: where to put the tabs (top or bottom)
position: "top"

Initialisation

Initializer function.

initialize: (options) -> super options @tabs = @findTabs() @panels = @findPanels() @tabs.each (i, tab) => $tab = $(tab) $tab["#{_.namespace}collapse"] event: "" target: @panels.eq(@tabs.index($tab)) $tab.attr("tabindex", -1) $tab.find("*").attr("tabindex", -1) @tabs.eq(0).attr("tabindex", 0) .find("*").removeAttr("tabindex")

Accessibility markup

Add aria attributes

aria: () -> @element.attr "role": "tablist" "aria-multiselectable": true @tabs.attr "role", "tab" @panels.attr "role", "tabpanel"

Event handling

Attach evenets to the widget

bindEvents: () -> @handleKeys(@element)

Attach the event to the tabs

@element.on "#{@options.event}.#{_.namespace}accordions", "[role='tab']", (e) => e.preventDefault() return if e.target.getAttribute("aria-disabled") is "true" @selectTab($(e.currentTarget)) @currentTab["#{_.namespace}collapse"]("toggle")

Update tabindex properties on tabs when focused

@element.on "focus.#{_.namespace}accordions", "[role='tab']", (e) => $target = $(e.target) @currentTab = $target if @tabs.index($target) > -1

Activate the previous tab

selectPreviousTab: () -> @selectTab(@tabs.eq(@tabs.index(@currentTab) - 1))

Activate the next tab

selectNextTab: () -> @selectTab(@tabs.eq((@tabs.index(@currentTab) + 1) % @tabs.length))

Activate the specified tab

selectTab: ($tab) -> $tab = $tab or @tabs.first() return if @element.get(0).getAttribute("aria-disabled") is "true" or $tab.get(0).getAttribute("aria-disabled") is "true" @tabs.attr("tabindex", -1) @currentTab = $tab.attr("tabindex", 0)

Structure discovery

Find the widget structure using the specified configuration

Tabs

Find the tabs using:

  • elements with role="tab"
  • tabs option (selector relative to the element)
  • even elements if the tabs are on the top
  • odd elements if the tabs are on the bottom
findTabs: () -> $tabs = @element.find("[role='tab']") return $tabs if $tabs.length $tabs = $(@options.tabs, @element) if (@options.tabs) return $tabs if $tabs.length $tabs = @element.children().filter( (i) => i % 2 is if @options.position is "bottom" then 1 else 0) return $tabs if $tabs.length $()

Panels

Find the panels using:

  • elements with role="tab"
  • panels option (selector relative to the element)
  • odd elements if the tabs are on the top
  • even elements if the tabs are on the bottom
findPanels: () -> $panels = @element.find("[role='tabpanel']") return $panels if $panels.length $panels = $(@options.panels, @element) if (@options.panels) return $panels if $panels.length $panels = @element.children().filter( (i) => i % 2 is if @options.position is "bottom" then 0 else 1) return $panels if $panels.length $()

Key handling

Handle left key press

keyLeft: (e) -> $target = $(e.target) return if not (@currentTab.get(0) is e.target or @currentTab.find($target).length) e.preventDefault() if (e.ctrlKey or e.metaKey) @panels.each (i, p) => if $.contains(p, e.target) @tabs.eq(@panels.index(p)).focus() else @selectPreviousTab().focus()

Handle up key press

keyUp: (e) -> @keyLeft(e)

Handle right key press

keyRight: (e) -> $target = $(e.target) return if not (@currentTab.get(0) is e.target or @currentTab.find($target).length) e.preventDefault() @selectNextTab().focus()

Handle down key press

keyDown: (e) -> @keyRight(e)

Handle page up key press

keyPageUp: (e) -> e.preventDefault() if (e.ctrlKey or e.metaKey) @panels.each (i, p) => if $.contains(p, e.target) @tabs.eq(@panels.index(p) - 1).focus()

Handle page down key press

keyPageDown: (e) -> e.preventDefault() if (e.ctrlKey or e.metaKey) @panels.each (i, p) => if $.contains(p, e.target) @tabs.eq((@panels.index(p) + 1) % @panels.length).focus()

Cleanup

Cleanup the widget and remove remaining references

destructor: () -> @element.removeAttr("role aria-multiselectable") super @tabs["#{_.namespace}collapse"]("destroy") @tabs = null @panels.removeAttr("role tabindex") @panels = null

Installation

Install the widget into the JS library

Accordions.install("Accordions", () -> $("[data-widget='accordions']")["#{_.namespace}accordions"]() )