diff --git a/aria-practices.html b/aria-practices.html index 8f556ea2b5..9c567a2b24 100644 --- a/aria-practices.html +++ b/aria-practices.html @@ -663,11 +663,11 @@

Checkbox

Examples

@@ -1846,7 +1846,7 @@

Keyboard Interaction

  • Control + A (Optional): Selects all options in the list. Optionally, if all options are selected, it may also unselect all options.
  • -
  • Alternative selection model -- moving focus without holding a Shift or Control modifier unselects all selected nodes except the focused node: +
  • Alternative selection model -- moving focus without holding a Shift or Control modifier unselects all selected options except the focused option: -

    - If aria-owns is set on the tree container to include elements that are not DOM children of the container, - those elements will appear in the reading order in the sequence they are referenced and after any items that are DOM children. - Scripts that manage focus need to ensure the visual focus order matches this assistive technology reading order. -

    +
      +
    1. Some factors to consider when choosing whether to indicate selection with aria-selected or aria-checked are: +
        +
      • + Some design systems use aria-selected for single-select widgets and aria-checked for multi-select widgets. + In the absence of factors that would make an alternative convention more appropriate, this is a recommended convention. +
      • +
      • + The language of instructions and the appearance of the interface might suggest one attribute is more appropriate than the other. + For instance, do instructions say to select items? Or, is the visual indicator of selection a check mark? +
      • +
      • It is important to adopt a consistent convention for selection models across a site or app.
      • +
      +
    2. +
    3. + Conditions that would permit a tree to include both aria-selected and aria-checked are extremely rare. + It is strongly recommended to avoid designing a tree widget that would have the need for more than one type of state. + If both states were to be used within a tree, all the following conditions need to be satisfied: +
        +
      • The meaning and purpose of aria-selected is different from the meaning and purpose of aria-checked in the user interface.
      • +
      • The user interface makes the meaning and purpose of each state apparent.
      • +
      • The user interface provides a separate method for controlling each state.
      • +
      +
    4. +
    5. + If aria-owns is set on the tree container to include elements that are not DOM children of the container, + those elements will appear in the reading order in the sequence they are referenced and after any items that are DOM children. + Scripts that manage focus need to ensure the visual focus order matches this assistive technology reading order. +
    6. +
    diff --git a/examples/checkbox/checkbox-mixed.html b/examples/checkbox/checkbox-mixed.html index 925b12e5de..118df68f91 100644 --- a/examples/checkbox/checkbox-mixed.html +++ b/examples/checkbox/checkbox-mixed.html @@ -34,7 +34,7 @@

    Checkbox Example (Mixed-State)

    Similar examples include:

    diff --git a/examples/checkbox/checkbox.html b/examples/checkbox/checkbox.html index c264a125e5..6f7fb7774a 100644 --- a/examples/checkbox/checkbox.html +++ b/examples/checkbox/checkbox.html @@ -29,7 +29,7 @@

    Checkbox Example (Two State)

    Similar examples include:

      -
    • Checkbox (Mixed-State): Mixed state checkbox controlling standard input checkboxes.
    • +
    • Checkbox (Mixed-State): Demonstrates a checkbox that uses the mixed value for aria-checked to reflect and control checked states within a group of two-state HTML checkboxes contained in an HTML fieldset.
    diff --git a/examples/combobox/combobox-autocomplete-list.html b/examples/combobox/combobox-autocomplete-list.html index 3263a5faf5..8fc38e76b9 100644 --- a/examples/combobox/combobox-autocomplete-list.html +++ b/examples/combobox/combobox-autocomplete-list.html @@ -203,7 +203,7 @@

    Textbox

    Enter - Closes the listbox. + Closes the listbox if it is displayed. Escape diff --git a/examples/combobox/combobox-autocomplete-none.html b/examples/combobox/combobox-autocomplete-none.html index 21718615e4..a38ba49d2e 100644 --- a/examples/combobox/combobox-autocomplete-none.html +++ b/examples/combobox/combobox-autocomplete-none.html @@ -156,12 +156,7 @@

    Textbox

    Enter - -
      -
    • Sets the textbox value to the content of the selected option.
    • -
    • Closes the listbox if it is displayed.
    • -
    - + Closes the listbox if it is displayed. Standard single line text editing keys diff --git a/examples/combobox/grid-combo.html b/examples/combobox/grid-combo.html index ec3138e52f..1c43d05dce 100644 --- a/examples/combobox/grid-combo.html +++ b/examples/combobox/grid-combo.html @@ -370,7 +370,7 @@

    Grid Popup

    div
      -
    • Specified on a row in the grid when it is visually indicated as selected.
    • +
    • Specified on a cell when the row containing the cell is visually indicated as selected.
    • Occurs only when a cell in the grid is referenced by aria-activedescendant.
    diff --git a/examples/dialog-modal/alertdialog.html b/examples/dialog-modal/alertdialog.html index 52931bb8cc..4aab7e6612 100644 --- a/examples/dialog-modal/alertdialog.html +++ b/examples/dialog-modal/alertdialog.html @@ -35,15 +35,14 @@

    Alert Dialog Example

    To use this example:

    • - Activate the "discard" button to trigger a confirmation dialog. + Activate the "discard" button to trigger a confirmation dialog that has the alertdialog role.
        -
      • Activating the "yes" button removes the contents of both the "Notes" text area and local storage of the notes.
      • +
      • Activating the "yes" button in the confirmation dialog removes the contents of both the "Notes" text area and local storage of the notes.
      • Activating the "no" button or pressing escape closes the dialog.
      • The "discard" button is disabled if the notes text area does not contain any text.
    • -
    • - Activate the "save" button to trigger an alert when it saves the contents of the "Notes" text area to local storage. +
    • Activate the "save" button to trigger an alert when the contents of the "Notes" text area is saved to local storage.
      • A successful save triggers a short alert to notify the user that the notes have been saved.
      • The "save" button is disabled if the user's local storage value is the same as the "Notes" field.
      • @@ -68,9 +67,17 @@

        Example

        - - - + + +
        - +
        @@ -88,14 +95,18 @@

        Confirmation

    Accessibility Features

    -
      -
    1. The accessible label for the alert dialog is set to its heading ("Confirmation").
    2. +
        +
      • The accessible name of the alert dialog is set to its heading ("Confirmation").
      • The dialog's prompt ("Are you sure...?") is referenced via aria-describedby to ensure that the user is immediately aware of the prompt.
      • Focus is automatically set to the first focusable element inside the dialog, which is the "No" button. This is the least destructive action, so focusing "No" helps prevent users from accidentally confirming the destructive "Discard" action, which cannot be undone.
      • -
    +
  • + When the buttons are disabled, aria-disabled is used instead of the HTML disabled attribute so the buttons will remain in the page Tab sequence. + This makes it easier for screen reader users to discover the buttons and discern how the interface works. +
  • +

    Keyboard Support

    @@ -162,7 +173,7 @@

    Role, Property, State, and Tabindex Attributes

    - aria-labelledby=IDREF + aria-labelledby="ID_REFERENCE" div Gives the alert dialog an accessible name by referring to the element that provides the alert dialog title. @@ -170,7 +181,7 @@

    Role, Property, State, and Tabindex Attributes

    - aria-describedby=IDREF + aria-describedby="ID_REFERENCE" div Gives the alert dialog an accessible description by referring to the alert dialog content that describes the primary message or purpose of the alert dialog. @@ -178,7 +189,7 @@

    Role, Property, State, and Tabindex Attributes

    - aria-modal=true + aria-modal="true" div Tells assistive technologies that the windows underneath the current alert dialog are not available for interaction (inert). @@ -192,26 +203,31 @@

    Role, Property, State, and Tabindex Attributes

    Identifies the element that serves as the alert notification. + + + aria-disabled="true" + button + Tells assistive technology users the button cannot be activated. +

    Notes on aria-modal and aria-hidden

    -
      +
      • The aria-modal property was introduced in ARIA 1.1. - As a new property, screen reader users may experience varying degrees of support for it. + As a relatively new property, screen reader users may experience varying degrees of support for it.
      • - Applying the aria-modal property to the dialog element - replaces the technique of using aria-hidden on the background for informing assistive technologies that content outside a dialog is inert. + Applying the aria-modal property to the dialog element replaces the technique of using aria-hidden on the background for informing assistive technologies that content outside a dialog is inert.
      • In legacy dialog implementations where aria-hidden is used to make content outside a dialog inert for assistive technology users, it is important that: -
          +
          • aria-hidden is set to true on each element containing a portion of the inert layer.
          • The dialog element is not a descendant of any element that has aria-hidden set to true.
          • -
        +
      -
    +

    Javascript and CSS Source Code

    diff --git a/examples/dialog-modal/css/dialog.css b/examples/dialog-modal/css/dialog.css index bf079d8a1a..3275283510 100644 --- a/examples/dialog-modal/css/dialog.css +++ b/examples/dialog-modal/css/dialog.css @@ -146,20 +146,48 @@ width: 33%; } -.toast { - background-color: rgb(0 0 0 / 90%); - color: #fff; - padding: 1rem; - border: none; - border-radius: 0.25rem; - box-shadow: 0 3px 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%); - position: fixed; - top: 1rem; - right: 1rem; - transform: translateY(-150%); - transition: transform 225ms cubic-bezier(0.4, 0, 0.2, 1); +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: auto; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} + +#notes_save { + display: inline-flex; + align-items: center; + gap: 0.5rem; } -.toast.active { - transform: translateY(0); +#notes_save svg { + display: block; + width: 0.75rem; +} + +#notes_save .icon { + display: none; +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +#notes_save.loading .spinner { + display: block; + animation: rotate 2s linear infinite; +} + +#notes_save.saved .check { + display: block; } diff --git a/examples/dialog-modal/js/alertdialog.js b/examples/dialog-modal/js/alertdialog.js index 42723f3970..72f3e752f5 100644 --- a/examples/dialog-modal/js/alertdialog.js +++ b/examples/dialog-modal/js/alertdialog.js @@ -7,43 +7,45 @@ var aria = aria || {}; aria.Utils = aria.Utils || {}; aria.Utils.disableCtrl = function (ctrl) { - ctrl.setAttribute('disabled', true); + ctrl.setAttribute('aria-disabled', 'true'); }; aria.Utils.enableCtrl = function (ctrl) { - ctrl.removeAttribute('disabled'); + ctrl.removeAttribute('aria-disabled'); }; -aria.Utils.triggerAlert = function (alertEl, content) { - return new Promise(function (resolve, reject) { - try { - alertEl.textContent = content || null; - alertEl.classList.remove('hidden'); - alertEl.addEventListener( - 'transitionend', - function () { - if (!this.classList.contains('active')) { - this.classList.add('hidden'); - } - }, - true - ); - setTimeout(function () { - alertEl.classList.add('active'); - }, 1); - setTimeout(function () { - alertEl.classList.remove('active'); - resolve(); - }, 3000); - } catch (err) { - reject(err); - } - }); +aria.Utils.setLoading = function (saveBtn, saveStatusView) { + saveBtn.classList.add('loading'); + this.disableCtrl(saveBtn); + + // use a timeout for the loading message + // if the saved state happens very quickly, + // we don't need to explicitly announce the intermediate loading state + const loadingTimeout = window.setTimeout(() => { + saveStatusView.textContent = 'Loading'; + }, 200); + + // set timeout for saved state, to mimic loading + const fakeLoadingTimeout = Math.random() * 2000; + window.setTimeout(() => { + saveBtn.classList.remove('loading'); + saveBtn.classList.add('saved'); + + window.clearTimeout(loadingTimeout); + saveStatusView.textContent = 'Saved successfully'; + }, fakeLoadingTimeout); }; -aria.Notes = function Notes(notesId, saveId, discardId, localStorageKey) { +aria.Notes = function Notes( + notesId, + saveId, + saveStatusId, + discardId, + localStorageKey +) { this.notesInput = document.getElementById(notesId); this.saveBtn = document.getElementById(saveId); + this.saveStatusView = document.getElementById(saveStatusId); this.discardBtn = document.getElementById(discardId); this.localStorageKey = localStorageKey || 'alertdialog-notes'; this.initialized = false; @@ -51,7 +53,7 @@ aria.Notes = function Notes(notesId, saveId, discardId, localStorageKey) { Object.defineProperty(this, 'controls', { get: function () { return document.querySelectorAll( - '[aria-controls=' + this.notesInput.id + ']' + '[data-textbox=' + this.notesInput.id + ']' ); }, }); @@ -91,14 +93,16 @@ aria.Notes = function Notes(notesId, saveId, discardId, localStorageKey) { }; aria.Notes.prototype.save = function (val) { - if (this.alert && !this.isCurrent) { - aria.Utils.triggerAlert(this.alert, 'Saved'); + const isDisabled = this.saveBtn.getAttribute('aria-disabled') === 'true'; + if (isDisabled) { + return; } localStorage.setItem( this.localStorageKey, JSON.stringify(val || this.notesInput.value) ); aria.Utils.disableCtrl(this.saveBtn); + aria.Utils.setLoading(this.saveBtn, this.saveStatusView); }; aria.Notes.prototype.loadSaved = function () { @@ -107,10 +111,19 @@ aria.Notes.prototype.loadSaved = function () { } }; +aria.Notes.prototype.restoreSaveBtn = function () { + this.saveBtn.classList.remove('loading'); + this.saveBtn.classList.remove('saved'); + this.saveBtn.removeAttribute('aria-disabled'); + + this.saveStatusView.textContent = ''; +}; + aria.Notes.prototype.discard = function () { localStorage.clear(); this.notesInput.value = ''; this.toggleControls(); + this.restoreSaveBtn(); }; aria.Notes.prototype.disableControls = function () { @@ -133,6 +146,7 @@ aria.Notes.prototype.toggleCurrent = function () { if (!this.isCurrent) { this.notesInput.classList.remove('can-save'); aria.Utils.enableCtrl(this.saveBtn); + this.restoreSaveBtn(); } else { this.notesInput.classList.add('can-save'); aria.Utils.disableCtrl(this.saveBtn); @@ -162,8 +176,12 @@ aria.Notes.prototype.init = function () { /** initialization */ document.addEventListener('DOMContentLoaded', function initAlertDialog() { - var notes = new aria.Notes('notes', 'notes_save', 'notes_confirm'); - notes.alert = document.getElementById('alert_toast'); + var notes = new aria.Notes( + 'notes', + 'notes_save', + 'notes_save_status', + 'notes_confirm' + ); window.discardInput = function (closeBtn) { notes.discard.call(notes); @@ -171,8 +189,13 @@ document.addEventListener('DOMContentLoaded', function initAlertDialog() { }; window.openAlertDialog = function (dialogId, triggerBtn, focusFirst) { + // do not proceed if the trigger button is disabled + if (triggerBtn.getAttribute('aria-disabled') === 'true') { + return; + } + var target = document.getElementById( - triggerBtn.getAttribute('aria-controls') + triggerBtn.getAttribute('data-textbox') ); var dialog = document.getElementById(dialogId); var desc = document.getElementById(dialog.getAttribute('aria-describedby')); diff --git a/examples/index.html b/examples/index.html index 6ee05ca274..89aa931fdd 100644 --- a/examples/index.html +++ b/examples/index.html @@ -567,6 +567,7 @@

    Examples By Properties and States

    aria-disabled @@ -622,6 +623,7 @@

    Examples By Properties and States

    diff --git a/examples/js/notice.html b/examples/js/notice.html index a53d688efb..749cd58e8a 100644 --- a/examples/js/notice.html +++ b/examples/js/notice.html @@ -15,8 +15,8 @@ Testing code based on this example with assistive technologies is essential before considering use in production systems.
  • - The ARIA-AT project - plans to provide measurements of this example's assistive technology support by the end of 2020. + The ARIA and Assistive Technologies Project + is developing measurements of assistive technology support for APG examples.
  • Robust accessibility can be further optimized by choosing implementation patterns that diff --git a/examples/menubar/css/menubar-editor.css b/examples/menubar/css/menubar-editor.css index 0827c62439..dd3662832e 100644 --- a/examples/menubar/css/menubar-editor.css +++ b/examples/menubar/css/menubar-editor.css @@ -1,3 +1,5 @@ +@charset "utf-8"; + .menubar-editor { margin: 0; padding: 2px; @@ -43,20 +45,18 @@ left: 1px; } -.menubar-editor [role="menubar"] > li > [role="menuitem"]::after { - content: url("../images/down-arrow.svg"); - padding-left: 0.25em; -} - -.menubar-editor [role="menubar"] > li > [role="menuitem"]:focus::after { - content: url("../images/down-arrow-focus.svg"); +.menubar-editor [role="menubar"] [role="menuitem"] > [aria-hidden]::before { + content: "▼"; + padding-left: 0.25rem; + font-size: 11px; + vertical-align: middle; } .menubar-editor [role="menubar"] - > li - > [role="menuitem"][aria-expanded="true"]::after { - content: url("../images/up-arrow-focus.svg"); + [role="menuitem"][aria-expanded="true"] + > [aria-hidden]::before { + content: "▲"; } .menubar-editor [role="menubar"] [role="menu"] { @@ -80,7 +80,8 @@ .menubar-editor [role="menubar"] [role="separator"] { padding-top: 3px; - background-image: url("../images/separator.svg"); + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cline x1='0' y1='6' x2='12' y2='6' style='stroke:black;stroke-width:1' /%3E%3C/svg%3E%0A"); + background-size: 10px 10px; background-position: center; background-repeat: repeat-x; } @@ -119,16 +120,20 @@ .menubar-editor [role="menubar"] - [role="menuitemradio"][aria-checked="true"]::before { - content: url("../images/radio-checked.svg"); - padding-right: 3px; + [role="menuitemradio"][aria-checked="true"] + > [aria-hidden]::before { + content: "●"; + display: inline-block; + width: 18px; } .menubar-editor [role="menubar"] - [role="menuitemcheckbox"][aria-checked="true"]::before { - content: url("../images/checkbox-checked.svg"); - padding-right: 3px; + [role="menuitemcheckbox"][aria-checked="true"] + > [aria-hidden]::before { + content: "✓"; + display: inline-block; + width: 18px; } /* focus and hover styling */ @@ -145,16 +150,13 @@ .menubar-editor [role="menubar"] - [role="menuitemradio"][aria-checked="true"]:focus::before { - content: url("../images/radio-checked-focus.svg"); - padding-right: 3px; -} - + [role="menuitemradio"][aria-checked="true"]:focus + > [aria-hidden]::before, .menubar-editor [role="menubar"] - [role="menuitemcheckbox"][aria-checked="true"]:focus::before { - content: url("../images/checkbox-checked-focus.svg"); - padding-right: 3px; + [role="menuitemcheckbox"][aria-checked="true"]:focus + > [aria-hidden]::before { + margin-left: -2px; } .menubar-editor [role="menubar"] [role="menuitem"]:hover { diff --git a/examples/menubar/css/menubar-navigation.css b/examples/menubar/css/menubar-navigation.css index f7c89d16f5..f66ef5d95d 100644 --- a/examples/menubar/css/menubar-navigation.css +++ b/examples/menubar/css/menubar-navigation.css @@ -1,3 +1,5 @@ +@charset "utf-8"; + .page header { border: #005a9c solid 2px; background: #005a9c; @@ -74,6 +76,7 @@ .menubar-navigation [role="separator"] { padding-top: 3px; background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cline x1='0' y1='6' x2='12' y2='6' style='stroke:black;stroke-width:1' /%3E%3C/svg%3E%0A"); + background-size: 10px 10px; background-position: center; background-repeat: repeat-x; } diff --git a/examples/menubar/images/checkbox-checked-focus.svg b/examples/menubar/images/checkbox-checked-focus.svg deleted file mode 100644 index 47f273cec4..0000000000 --- a/examples/menubar/images/checkbox-checked-focus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/checkbox-checked.svg b/examples/menubar/images/checkbox-checked.svg deleted file mode 100644 index 4a68a8a972..0000000000 --- a/examples/menubar/images/checkbox-checked.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/down-arrow-focus.svg b/examples/menubar/images/down-arrow-focus.svg deleted file mode 100644 index 8f6ea66370..0000000000 --- a/examples/menubar/images/down-arrow-focus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/down-arrow.svg b/examples/menubar/images/down-arrow.svg deleted file mode 100644 index a0a2556a5e..0000000000 --- a/examples/menubar/images/down-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/radio-checked-focus.svg b/examples/menubar/images/radio-checked-focus.svg deleted file mode 100644 index 04c1a03d0f..0000000000 --- a/examples/menubar/images/radio-checked-focus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/radio-checked.svg b/examples/menubar/images/radio-checked.svg deleted file mode 100644 index f34a549a37..0000000000 --- a/examples/menubar/images/radio-checked.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/separator.svg b/examples/menubar/images/separator.svg deleted file mode 100644 index b4b39760b2..0000000000 --- a/examples/menubar/images/separator.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/up-arrow-focus.svg b/examples/menubar/images/up-arrow-focus.svg deleted file mode 100644 index ede8a5d71e..0000000000 --- a/examples/menubar/images/up-arrow-focus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/images/up-arrow.svg b/examples/menubar/images/up-arrow.svg deleted file mode 100644 index d034dad152..0000000000 --- a/examples/menubar/images/up-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/menubar/js/menubar-editor.js b/examples/menubar/js/menubar-editor.js index 7a7c9d70dc..e1fd29e022 100644 --- a/examples/menubar/js/menubar-editor.js +++ b/examples/menubar/js/menubar-editor.js @@ -11,694 +11,682 @@ 'use strict'; -var MenubarEditor = function (domNode) { - this.domNode = domNode; - this.menubarNode = domNode.querySelector('[role=menubar]'); - this.textareaNode = domNode.querySelector('textarea'); - this.actionManager = new StyleManager(this.textareaNode); - - this.popups = []; - this.menuitemGroups = {}; - this.menuOrientation = {}; - this.isPopup = {}; - - this.firstChars = {}; // see Menubar init method - this.firstMenuitem = {}; // see Menubar init method - this.lastMenuitem = {}; // see Menubar init method - - this.initMenu(this.menubarNode); - this.domNode.addEventListener('focusin', this.handleFocusin.bind(this)); - this.domNode.addEventListener('focusout', this.handleFocusout.bind(this)); - - window.addEventListener( - 'mousedown', - this.handleBackgroundMousedown.bind(this), - true - ); -}; - -MenubarEditor.prototype.getMenuitems = function (domNode) { - var nodes = []; - - var initMenu = this.initMenu.bind(this); - var getGroupId = this.getGroupId.bind(this); - var menuitemGroups = this.menuitemGroups; - var popups = this.popups; - - function findMenuitems(node, group) { - var role, flag, groupId; - - while (node) { - flag = true; - role = node.getAttribute('role'); +class MenubarEditor { + constructor(domNode) { + this.domNode = domNode; + this.menubarNode = domNode.querySelector('[role=menubar]'); + this.textareaNode = domNode.querySelector('textarea'); + this.actionManager = new StyleManager(this.textareaNode); + + this.popups = []; + this.menuitemGroups = {}; + this.menuOrientation = {}; + this.isPopup = {}; + + this.firstChars = {}; // see Menubar init method + this.firstMenuitem = {}; // see Menubar init method + this.lastMenuitem = {}; // see Menubar init method + + this.initMenu(this.menubarNode); + this.domNode.addEventListener('focusin', this.onFocusin.bind(this)); + this.domNode.addEventListener('focusout', this.onFocusout.bind(this)); + + window.addEventListener( + 'pointerdown', + this.onBackgroundPointerdown.bind(this), + true + ); + } - switch (role) { - case 'menu': - node.tabIndex = -1; - initMenu(node); - flag = false; - break; + getMenuitems(domNode) { + var nodes = []; - case 'group': - groupId = getGroupId(node); - menuitemGroups[groupId] = []; - break; + var initMenu = this.initMenu.bind(this); + var getGroupId = this.getGroupId.bind(this); + var menuitemGroups = this.menuitemGroups; + var popups = this.popups; - case 'menuitem': - case 'menuitemradio': - case 'menuitemcheckbox': - if (node.getAttribute('aria-haspopup') === 'true') { - popups.push(node); - } - nodes.push(node); - if (group) { - group.push(node); - } - break; + function findMenuitems(node, group) { + var role, flag, groupId; - default: - break; - } + while (node) { + flag = true; + role = node.getAttribute('role'); - if (flag && node.firstElementChild) { - findMenuitems(node.firstElementChild, menuitemGroups[groupId]); - } + switch (role) { + case 'menu': + node.tabIndex = -1; + initMenu(node); + flag = false; + break; + + case 'group': + groupId = getGroupId(node); + menuitemGroups[groupId] = []; + break; + + case 'menuitem': + case 'menuitemradio': + case 'menuitemcheckbox': + if (node.getAttribute('aria-haspopup') === 'true') { + popups.push(node); + } + nodes.push(node); + if (group) { + group.push(node); + } + break; + + default: + break; + } + + if (flag && node.firstElementChild) { + findMenuitems(node.firstElementChild, menuitemGroups[groupId]); + } - node = node.nextElementSibling; + node = node.nextElementSibling; + } } - } - findMenuitems(domNode.firstElementChild, false); + findMenuitems(domNode.firstElementChild, false); - return nodes; -}; + return nodes; + } -MenubarEditor.prototype.initMenu = function (menu) { - var i, menuitems, menuitem, role; + initMenu(menu) { + var i, menuitems, menuitem, role; - var menuId = this.getMenuId(menu); + var menuId = this.getMenuId(menu); - menuitems = this.getMenuitems(menu); - this.menuOrientation[menuId] = this.getMenuOrientation(menu); - this.isPopup[menuId] = menu.getAttribute('role') === 'menu'; + menuitems = this.getMenuitems(menu); + this.menuOrientation[menuId] = this.getMenuOrientation(menu); + this.isPopup[menuId] = menu.getAttribute('role') === 'menu'; - this.menuitemGroups[menuId] = []; - this.firstChars[menuId] = []; - this.firstMenuitem[menuId] = null; - this.lastMenuitem[menuId] = null; + this.menuitemGroups[menuId] = []; + this.firstChars[menuId] = []; + this.firstMenuitem[menuId] = null; + this.lastMenuitem[menuId] = null; - for (i = 0; i < menuitems.length; i++) { - menuitem = menuitems[i]; - role = menuitem.getAttribute('role'); + for (i = 0; i < menuitems.length; i++) { + menuitem = menuitems[i]; + role = menuitem.getAttribute('role'); - if (role.indexOf('menuitem') < 0) { - continue; - } + if (role.indexOf('menuitem') < 0) { + continue; + } - menuitem.tabIndex = -1; - this.menuitemGroups[menuId].push(menuitem); - this.firstChars[menuId].push(menuitem.textContent[0].toLowerCase()); + menuitem.tabIndex = -1; + this.menuitemGroups[menuId].push(menuitem); + this.firstChars[menuId].push(menuitem.textContent[0].toLowerCase()); - menuitem.addEventListener('keydown', this.handleKeydown.bind(this)); - menuitem.addEventListener('click', this.handleMenuitemClick.bind(this)); + menuitem.addEventListener('keydown', this.onKeydown.bind(this)); + menuitem.addEventListener('click', this.onMenuitemClick.bind(this)); - menuitem.addEventListener( - 'mouseover', - this.handleMenuitemMouseover.bind(this) - ); + menuitem.addEventListener( + 'pointerover', + this.onMenuitemPointerover.bind(this) + ); - if (!this.firstMenuitem[menuId]) { - if (this.hasPopup(menuitem)) { - menuitem.tabIndex = 0; + if (!this.firstMenuitem[menuId]) { + if (this.hasPopup(menuitem)) { + menuitem.tabIndex = 0; + } + this.firstMenuitem[menuId] = menuitem; } - this.firstMenuitem[menuId] = menuitem; + this.lastMenuitem[menuId] = menuitem; } - this.lastMenuitem[menuId] = menuitem; } -}; -/* MenubarEditor FOCUS MANAGEMENT METHODS */ + /* MenubarEditor FOCUS MANAGEMENT METHODS */ -MenubarEditor.prototype.setFocusToMenuitem = function (menuId, newMenuitem) { - var isAnyPopupOpen = this.isAnyPopupOpen(); + setFocusToMenuitem(menuId, newMenuitem) { + var isAnyPopupOpen = this.isAnyPopupOpen(); - this.closePopupAll(newMenuitem); + this.closePopupAll(newMenuitem); - if (this.hasPopup(newMenuitem)) { - if (isAnyPopupOpen) { - this.openPopup(newMenuitem); + if (this.hasPopup(newMenuitem)) { + if (isAnyPopupOpen) { + this.openPopup(newMenuitem); + } + } else { + var menu = this.getMenu(newMenuitem); + var cmi = menu.previousElementSibling; + if (!this.isOpen(cmi)) { + this.openPopup(cmi); + } } - } else { - var menu = this.getMenu(newMenuitem); - var cmi = menu.previousElementSibling; - if (!this.isOpen(cmi)) { - this.openPopup(cmi); + + if (this.hasPopup(newMenuitem)) { + if (this.menuitemGroups[menuId]) { + this.menuitemGroups[menuId].forEach(function (item) { + item.tabIndex = -1; + }); + } + newMenuitem.tabIndex = 0; } + + newMenuitem.focus(); } - if (this.hasPopup(newMenuitem)) { - if (this.menuitemGroups[menuId]) { - this.menuitemGroups[menuId].forEach(function (item) { - item.tabIndex = -1; - }); - } - newMenuitem.tabIndex = 0; + setFocusToFirstMenuitem(menuId) { + this.setFocusToMenuitem(menuId, this.firstMenuitem[menuId]); } - newMenuitem.focus(); -}; + setFocusToLastMenuitem(menuId) { + this.setFocusToMenuitem(menuId, this.lastMenuitem[menuId]); + } -MenubarEditor.prototype.setFocusToFirstMenuitem = function (menuId) { - this.setFocusToMenuitem(menuId, this.firstMenuitem[menuId]); -}; + setFocusToPreviousMenuitem(menuId, currentMenuitem) { + var newMenuitem, index; -MenubarEditor.prototype.setFocusToLastMenuitem = function (menuId) { - this.setFocusToMenuitem(menuId, this.lastMenuitem[menuId]); -}; + if (currentMenuitem === this.firstMenuitem[menuId]) { + newMenuitem = this.lastMenuitem[menuId]; + } else { + index = this.menuitemGroups[menuId].indexOf(currentMenuitem); + newMenuitem = this.menuitemGroups[menuId][index - 1]; + } -MenubarEditor.prototype.setFocusToPreviousMenuitem = function ( - menuId, - currentMenuitem -) { - var newMenuitem, index; + this.setFocusToMenuitem(menuId, newMenuitem); - if (currentMenuitem === this.firstMenuitem[menuId]) { - newMenuitem = this.lastMenuitem[menuId]; - } else { - index = this.menuitemGroups[menuId].indexOf(currentMenuitem); - newMenuitem = this.menuitemGroups[menuId][index - 1]; + return newMenuitem; } - this.setFocusToMenuitem(menuId, newMenuitem); - - return newMenuitem; -}; + setFocusToNextMenuitem(menuId, currentMenuitem) { + var newMenuitem, index; -MenubarEditor.prototype.setFocusToNextMenuitem = function ( - menuId, - currentMenuitem -) { - var newMenuitem, index; + if (currentMenuitem === this.lastMenuitem[menuId]) { + newMenuitem = this.firstMenuitem[menuId]; + } else { + index = this.menuitemGroups[menuId].indexOf(currentMenuitem); + newMenuitem = this.menuitemGroups[menuId][index + 1]; + } + this.setFocusToMenuitem(menuId, newMenuitem); - if (currentMenuitem === this.lastMenuitem[menuId]) { - newMenuitem = this.firstMenuitem[menuId]; - } else { - index = this.menuitemGroups[menuId].indexOf(currentMenuitem); - newMenuitem = this.menuitemGroups[menuId][index + 1]; + return newMenuitem; } - this.setFocusToMenuitem(menuId, newMenuitem); - return newMenuitem; -}; + setFocusByFirstCharacter(menuId, currentMenuitem, char) { + var start, index; -MenubarEditor.prototype.setFocusByFirstCharacter = function ( - menuId, - currentMenuitem, - char -) { - var start, index; + char = char.toLowerCase(); - char = char.toLowerCase(); - - // Get start index for search based on position of currentItem - start = this.menuitemGroups[menuId].indexOf(currentMenuitem) + 1; - if (start >= this.menuitemGroups[menuId].length) { - start = 0; - } + // Get start index for search based on position of currentItem + start = this.menuitemGroups[menuId].indexOf(currentMenuitem) + 1; + if (start >= this.menuitemGroups[menuId].length) { + start = 0; + } - // Check remaining slots in the menu - index = this.getIndexFirstChars(menuId, start, char); + // Check remaining slots in the menu + index = this.getIndexFirstChars(menuId, start, char); - // If not found in remaining slots, check from beginning - if (index === -1) { - index = this.getIndexFirstChars(menuId, 0, char); - } + // If not found in remaining slots, check from beginning + if (index === -1) { + index = this.getIndexFirstChars(menuId, 0, char); + } - // If match was found... - if (index > -1) { - this.setFocusToMenuitem(menuId, this.menuitemGroups[menuId][index]); + // If match was found... + if (index > -1) { + this.setFocusToMenuitem(menuId, this.menuitemGroups[menuId][index]); + } } -}; -// Utilities + // Utilities -MenubarEditor.prototype.getIndexFirstChars = function ( - menuId, - startIndex, - char -) { - for (var i = startIndex; i < this.firstChars[menuId].length; i++) { - if (char === this.firstChars[menuId][i]) { - return i; + getIndexFirstChars(menuId, startIndex, char) { + for (var i = startIndex; i < this.firstChars[menuId].length; i++) { + if (char === this.firstChars[menuId][i]) { + return i; + } } + return -1; } - return -1; -}; -MenubarEditor.prototype.isPrintableCharacter = function (str) { - return str.length === 1 && str.match(/\S/); -}; + isPrintableCharacter(str) { + return str.length === 1 && str.match(/\S/); + } -MenubarEditor.prototype.getIdFromAriaLabel = function (node) { - var id = node.getAttribute('aria-label'); - if (id) { - id = id.trim().toLowerCase().replace(' ', '-').replace('/', '-'); + getIdFromAriaLabel(node) { + var id = node.getAttribute('aria-label'); + if (id) { + id = id.trim().toLowerCase().replace(' ', '-').replace('/', '-'); + } + return id; } - return id; -}; -MenubarEditor.prototype.getMenuOrientation = function (node) { - var orientation = node.getAttribute('aria-orientation'); + getMenuOrientation(node) { + var orientation = node.getAttribute('aria-orientation'); - if (!orientation) { - var role = node.getAttribute('role'); + if (!orientation) { + var role = node.getAttribute('role'); - switch (role) { - case 'menubar': - orientation = 'horizontal'; - break; + switch (role) { + case 'menubar': + orientation = 'horizontal'; + break; - case 'menu': - orientation = 'vertical'; - break; + case 'menu': + orientation = 'vertical'; + break; - default: - break; + default: + break; + } } - } - return orientation; -}; + return orientation; + } -MenubarEditor.prototype.getDataOption = function (node) { - var option = false; - var hasOption = node.hasAttribute('data-option'); - var role = node.hasAttribute('role'); + getDataOption(node) { + var option = false; + var hasOption = node.hasAttribute('data-option'); + var role = node.hasAttribute('role'); - if (!hasOption) { - while (node && !hasOption && role !== 'menu' && role !== 'menubar') { - node = node.parentNode; - if (node) { - role = node.getAttribute('role'); - hasOption = node.hasAttribute('data-option'); + if (!hasOption) { + while (node && !hasOption && role !== 'menu' && role !== 'menubar') { + node = node.parentNode; + if (node) { + role = node.getAttribute('role'); + hasOption = node.hasAttribute('data-option'); + } } } - } - if (node) { - option = node.getAttribute('data-option'); + if (node) { + option = node.getAttribute('data-option'); + } + + return option; } - return option; -}; + getGroupId(node) { + var id = false; + var role = node.getAttribute('role'); -MenubarEditor.prototype.getGroupId = function (node) { - var id = false; - var role = node.getAttribute('role'); + while (node && role !== 'group' && role !== 'menu' && role !== 'menubar') { + node = node.parentNode; + if (node) { + role = node.getAttribute('role'); + } + } - while (node && role !== 'group' && role !== 'menu' && role !== 'menubar') { - node = node.parentNode; if (node) { - role = node.getAttribute('role'); + id = role + '-' + this.getIdFromAriaLabel(node); } - } - if (node) { - id = role + '-' + this.getIdFromAriaLabel(node); + return id; } - return id; -}; + getMenuId(node) { + var id = false; + var role = node.getAttribute('role'); -MenubarEditor.prototype.getMenuId = function (node) { - var id = false; - var role = node.getAttribute('role'); + while (node && role !== 'menu' && role !== 'menubar') { + node = node.parentNode; + if (node) { + role = node.getAttribute('role'); + } + } - while (node && role !== 'menu' && role !== 'menubar') { - node = node.parentNode; if (node) { - role = node.getAttribute('role'); + id = role + '-' + this.getIdFromAriaLabel(node); } - } - if (node) { - id = role + '-' + this.getIdFromAriaLabel(node); + return id; } - return id; -}; + getMenu(menuitem) { + var menu = menuitem; + var role = menuitem.getAttribute('role'); -MenubarEditor.prototype.getMenu = function (menuitem) { - var menu = menuitem; - var role = menuitem.getAttribute('role'); - - while (menu && role !== 'menu' && role !== 'menubar') { - menu = menu.parentNode; - if (menu) { - role = menu.getAttribute('role'); + while (menu && role !== 'menu' && role !== 'menubar') { + menu = menu.parentNode; + if (menu) { + role = menu.getAttribute('role'); + } } - } - - return menu; -}; -MenubarEditor.prototype.toggleCheckbox = function (menuitem) { - if (menuitem.getAttribute('aria-checked') === 'true') { - menuitem.setAttribute('aria-checked', 'false'); - return false; + return menu; } - menuitem.setAttribute('aria-checked', 'true'); - return true; -}; - -MenubarEditor.prototype.setRadioButton = function (menuitem) { - var groupId = this.getGroupId(menuitem); - var radiogroupItems = this.menuitemGroups[groupId]; - radiogroupItems.forEach(function (item) { - item.setAttribute('aria-checked', 'false'); - }); - menuitem.setAttribute('aria-checked', 'true'); - return menuitem.textContent; -}; - -MenubarEditor.prototype.updateFontSizeMenu = function (menuId) { - var fontSizeMenuitems = this.menuitemGroups[menuId]; - var currentValue = this.actionManager.getFontSize(); - - for (var i = 0; i < fontSizeMenuitems.length; i++) { - var mi = fontSizeMenuitems[i]; - var dataOption = mi.getAttribute('data-option'); - var value = mi.textContent.trim().toLowerCase(); - - switch (dataOption) { - case 'font-smaller': - if (currentValue === 'x-small') { - mi.setAttribute('aria-disabled', 'true'); - } else { - mi.removeAttribute('aria-disabled'); - } - break; - case 'font-larger': - if (currentValue === 'x-large') { - mi.setAttribute('aria-disabled', 'true'); - } else { - mi.removeAttribute('aria-disabled'); - } - break; + toggleCheckbox(menuitem) { + if (menuitem.getAttribute('aria-checked') === 'true') { + menuitem.setAttribute('aria-checked', 'false'); + return false; + } + menuitem.setAttribute('aria-checked', 'true'); + return true; + } + + setRadioButton(menuitem) { + var groupId = this.getGroupId(menuitem); + var radiogroupItems = this.menuitemGroups[groupId]; + radiogroupItems.forEach(function (item) { + item.setAttribute('aria-checked', 'false'); + }); + menuitem.setAttribute('aria-checked', 'true'); + return menuitem.textContent; + } + + updateFontSizeMenu(menuId) { + var fontSizeMenuitems = this.menuitemGroups[menuId]; + var currentValue = this.actionManager.getFontSize(); + + for (var i = 0; i < fontSizeMenuitems.length; i++) { + var mi = fontSizeMenuitems[i]; + var dataOption = mi.getAttribute('data-option'); + var value = mi.textContent.trim().toLowerCase(); + + switch (dataOption) { + case 'font-smaller': + if (currentValue === 'x-small') { + mi.setAttribute('aria-disabled', 'true'); + } else { + mi.removeAttribute('aria-disabled'); + } + break; - default: - if (currentValue === value) { - mi.setAttribute('aria-checked', 'true'); - } else { - mi.setAttribute('aria-checked', 'false'); - } - break; + case 'font-larger': + if (currentValue === 'x-large') { + mi.setAttribute('aria-disabled', 'true'); + } else { + mi.removeAttribute('aria-disabled'); + } + break; + + default: + if (currentValue === value) { + mi.setAttribute('aria-checked', 'true'); + } else { + mi.setAttribute('aria-checked', 'false'); + } + break; + } } } -}; -// Popup menu methods + // Popup menu methods -MenubarEditor.prototype.isAnyPopupOpen = function () { - for (var i = 0; i < this.popups.length; i++) { - if (this.popups[i].getAttribute('aria-expanded') === 'true') { - return true; + isAnyPopupOpen() { + for (var i = 0; i < this.popups.length; i++) { + if (this.popups[i].getAttribute('aria-expanded') === 'true') { + return true; + } } + return false; } - return false; -}; -MenubarEditor.prototype.openPopup = function (menuitem) { - // set aria-expanded attribute - var popupMenu = menuitem.nextElementSibling; + openPopup(menuitem) { + // set aria-expanded attribute + var popupMenu = menuitem.nextElementSibling; - var rect = menuitem.getBoundingClientRect(); + var rect = menuitem.getBoundingClientRect(); - // set CSS properties - popupMenu.style.position = 'absolute'; - popupMenu.style.top = rect.height - 3 + 'px'; - popupMenu.style.left = '0px'; - popupMenu.style.zIndex = 100; - popupMenu.style.display = 'block'; + // set CSS properties + popupMenu.style.position = 'absolute'; + popupMenu.style.top = rect.height - 3 + 'px'; + popupMenu.style.left = '0px'; + popupMenu.style.zIndex = 100; + popupMenu.style.display = 'block'; - menuitem.setAttribute('aria-expanded', 'true'); + menuitem.setAttribute('aria-expanded', 'true'); - return this.getMenuId(popupMenu); -}; + return this.getMenuId(popupMenu); + } -MenubarEditor.prototype.closePopup = function (menuitem) { - var menu, cmi; + closePopup(menuitem) { + var menu, cmi; - if (this.hasPopup(menuitem)) { - if (this.isOpen(menuitem)) { - menuitem.setAttribute('aria-expanded', 'false'); - menuitem.nextElementSibling.style.display = 'none'; - menuitem.nextElementSibling.style.zIndex = 0; + if (this.hasPopup(menuitem)) { + if (this.isOpen(menuitem)) { + menuitem.setAttribute('aria-expanded', 'false'); + menuitem.nextElementSibling.style.display = 'none'; + menuitem.nextElementSibling.style.zIndex = 0; + } + } else { + menu = this.getMenu(menuitem); + cmi = menu.previousElementSibling; + cmi.setAttribute('aria-expanded', 'false'); + cmi.focus(); + menu.style.display = 'none'; + menu.style.zIndex = 0; } - } else { - menu = this.getMenu(menuitem); - cmi = menu.previousElementSibling; - cmi.setAttribute('aria-expanded', 'false'); - cmi.focus(); - menu.style.display = 'none'; - menu.style.zIndex = 0; + return cmi; } - return cmi; -}; -MenubarEditor.prototype.doesNotContain = function (popup, menuitem) { - if (menuitem) { - return !popup.nextElementSibling.contains(menuitem); + doesNotContain(popup, menuitem) { + if (menuitem) { + return !popup.nextElementSibling.contains(menuitem); + } + return true; } - return true; -}; -MenubarEditor.prototype.closePopupAll = function (menuitem) { - if (typeof menuitem !== 'object') { - menuitem = false; - } + closePopupAll(menuitem) { + if (typeof menuitem !== 'object') { + menuitem = false; + } - for (var i = 0; i < this.popups.length; i++) { - var popup = this.popups[i]; - if (this.isOpen(popup) && this.doesNotContain(popup, menuitem)) { - this.closePopup(popup); + for (var i = 0; i < this.popups.length; i++) { + var popup = this.popups[i]; + if (this.isOpen(popup) && this.doesNotContain(popup, menuitem)) { + this.closePopup(popup); + } } } -}; - -MenubarEditor.prototype.hasPopup = function (menuitem) { - return menuitem.getAttribute('aria-haspopup') === 'true'; -}; - -MenubarEditor.prototype.isOpen = function (menuitem) { - return menuitem.getAttribute('aria-expanded') === 'true'; -}; - -// Menu event handlers - -MenubarEditor.prototype.handleFocusin = function () { - this.domNode.classList.add('focus'); -}; - -MenubarEditor.prototype.handleFocusout = function () { - this.domNode.classList.remove('focus'); -}; - -MenubarEditor.prototype.handleBackgroundMousedown = function (event) { - if (!this.menubarNode.contains(event.target)) { - this.closePopupAll(); - } -}; - -MenubarEditor.prototype.handleKeydown = function (event) { - var tgt = event.currentTarget, - key = event.key, - flag = false, - menuId = this.getMenuId(tgt), - id, - popupMenuId, - mi, - role, - option, - value; - - switch (key) { - case ' ': - case 'Enter': - if (this.hasPopup(tgt)) { - popupMenuId = this.openPopup(tgt); - this.setFocusToFirstMenuitem(popupMenuId); - } else { - role = tgt.getAttribute('role'); - option = this.getDataOption(tgt); - switch (role) { - case 'menuitem': - this.actionManager.setOption(option, tgt.textContent); - break; - case 'menuitemcheckbox': - value = this.toggleCheckbox(tgt); - this.actionManager.setOption(option, value); - break; + hasPopup(menuitem) { + return menuitem.getAttribute('aria-haspopup') === 'true'; + } - case 'menuitemradio': - value = this.setRadioButton(tgt); - this.actionManager.setOption(option, value); - break; + isOpen(menuitem) { + return menuitem.getAttribute('aria-expanded') === 'true'; + } - default: - break; - } + // Menu event handlers - if (this.getMenuId(tgt) === 'menu-size') { - this.updateFontSizeMenu('menu-size'); - } - this.closePopup(tgt); - } - flag = true; - break; + onFocusin() { + this.domNode.classList.add('focus'); + } - case 'ArrowDown': - case 'Down': - if (this.menuOrientation[menuId] === 'vertical') { - this.setFocusToNextMenuitem(menuId, tgt); - flag = true; - } else { + onFocusout() { + this.domNode.classList.remove('focus'); + } + + onBackgroundPointerdown(event) { + if (!this.menubarNode.contains(event.target)) { + this.closePopupAll(); + } + } + + onKeydown(event) { + var tgt = event.currentTarget, + key = event.key, + flag = false, + menuId = this.getMenuId(tgt), + id, + popupMenuId, + mi, + role, + option, + value; + + switch (key) { + case ' ': + case 'Enter': if (this.hasPopup(tgt)) { popupMenuId = this.openPopup(tgt); this.setFocusToFirstMenuitem(popupMenuId); - flag = true; - } - } - break; - - case 'Esc': - case 'Escape': - this.closePopup(tgt); - flag = true; - break; + } else { + role = tgt.getAttribute('role'); + option = this.getDataOption(tgt); + switch (role) { + case 'menuitem': + this.actionManager.setOption(option, tgt.textContent); + break; + + case 'menuitemcheckbox': + value = this.toggleCheckbox(tgt); + this.actionManager.setOption(option, value); + break; + + case 'menuitemradio': + value = this.setRadioButton(tgt); + this.actionManager.setOption(option, value); + break; + + default: + break; + } - case 'Left': - case 'ArrowLeft': - if (this.menuOrientation[menuId] === 'horizontal') { - this.setFocusToPreviousMenuitem(menuId, tgt); + if (this.getMenuId(tgt) === 'menu-size') { + this.updateFontSizeMenu('menu-size'); + } + this.closePopup(tgt); + } flag = true; - } else { - mi = this.closePopup(tgt); - id = this.getMenuId(mi); - mi = this.setFocusToPreviousMenuitem(id, mi); - this.openPopup(mi); - } - break; + break; - case 'Right': - case 'ArrowRight': - if (this.menuOrientation[menuId] === 'horizontal') { - this.setFocusToNextMenuitem(menuId, tgt); - flag = true; - } else { - mi = this.closePopup(tgt); - id = this.getMenuId(mi); - mi = this.setFocusToNextMenuitem(id, mi); - this.openPopup(mi); - } - break; + case 'ArrowDown': + case 'Down': + if (this.menuOrientation[menuId] === 'vertical') { + this.setFocusToNextMenuitem(menuId, tgt); + flag = true; + } else { + if (this.hasPopup(tgt)) { + popupMenuId = this.openPopup(tgt); + this.setFocusToFirstMenuitem(popupMenuId); + flag = true; + } + } + break; - case 'Up': - case 'ArrowUp': - if (this.menuOrientation[menuId] === 'vertical') { - this.setFocusToPreviousMenuitem(menuId, tgt); + case 'Esc': + case 'Escape': + this.closePopup(tgt); flag = true; - } else { - if (this.hasPopup(tgt)) { - popupMenuId = this.openPopup(tgt); - this.setFocusToLastMenuitem(popupMenuId); + break; + + case 'Left': + case 'ArrowLeft': + if (this.menuOrientation[menuId] === 'horizontal') { + this.setFocusToPreviousMenuitem(menuId, tgt); flag = true; + } else { + mi = this.closePopup(tgt); + id = this.getMenuId(mi); + mi = this.setFocusToPreviousMenuitem(id, mi); + this.openPopup(mi); } - } - break; - - case 'Home': - case 'PageUp': - this.setFocusToFirstMenuitem(menuId, tgt); - flag = true; - break; + break; - case 'End': - case 'PageDown': - this.setFocusToLastMenuitem(menuId, tgt); - flag = true; - break; + case 'Right': + case 'ArrowRight': + if (this.menuOrientation[menuId] === 'horizontal') { + this.setFocusToNextMenuitem(menuId, tgt); + flag = true; + } else { + mi = this.closePopup(tgt); + id = this.getMenuId(mi); + mi = this.setFocusToNextMenuitem(id, mi); + this.openPopup(mi); + } + break; - case 'Tab': - this.closePopup(tgt); - break; + case 'Up': + case 'ArrowUp': + if (this.menuOrientation[menuId] === 'vertical') { + this.setFocusToPreviousMenuitem(menuId, tgt); + flag = true; + } else { + if (this.hasPopup(tgt)) { + popupMenuId = this.openPopup(tgt); + this.setFocusToLastMenuitem(popupMenuId); + flag = true; + } + } + break; - default: - if (this.isPrintableCharacter(key)) { - this.setFocusByFirstCharacter(menuId, tgt, key); + case 'Home': + case 'PageUp': + this.setFocusToFirstMenuitem(menuId, tgt); flag = true; - } - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } -}; - -MenubarEditor.prototype.handleMenuitemClick = function (event) { - var tgt = event.currentTarget; - var value; - - if (this.hasPopup(tgt)) { - if (this.isOpen(tgt)) { - this.closePopup(tgt); - } else { - var menuId = this.openPopup(tgt); - this.setFocusToMenuitem(menuId, tgt); - } - } else { - var role = tgt.getAttribute('role'); - var option = this.getDataOption(tgt); - switch (role) { - case 'menuitem': - this.actionManager.setOption(option, tgt.textContent); break; - case 'menuitemcheckbox': - value = this.toggleCheckbox(tgt); - this.actionManager.setOption(option, value); + case 'End': + case 'PageDown': + this.setFocusToLastMenuitem(menuId, tgt); + flag = true; break; - case 'menuitemradio': - value = this.setRadioButton(tgt); - this.actionManager.setOption(option, value); + case 'Tab': + this.closePopup(tgt); break; default: + if (this.isPrintableCharacter(key)) { + this.setFocusByFirstCharacter(menuId, tgt, key); + flag = true; + } break; } - if (this.getMenuId(tgt) === 'menu-size') { - this.updateFontSizeMenu('menu-size'); + if (flag) { + event.stopPropagation(); + event.preventDefault(); } - this.closePopup(tgt); } - event.stopPropagation(); - event.preventDefault(); -}; + onMenuitemClick(event) { + var tgt = event.currentTarget; + var value; -MenubarEditor.prototype.handleMenuitemMouseover = function (event) { - var tgt = event.currentTarget; + if (this.hasPopup(tgt)) { + if (this.isOpen(tgt)) { + this.closePopup(tgt); + } else { + var menuId = this.openPopup(tgt); + this.setFocusToMenuitem(menuId, tgt); + } + } else { + var role = tgt.getAttribute('role'); + var option = this.getDataOption(tgt); + switch (role) { + case 'menuitem': + this.actionManager.setOption(option, tgt.textContent); + break; + + case 'menuitemcheckbox': + value = this.toggleCheckbox(tgt); + this.actionManager.setOption(option, value); + break; + + case 'menuitemradio': + value = this.setRadioButton(tgt); + this.actionManager.setOption(option, value); + break; - if (this.isAnyPopupOpen() && this.getMenu(tgt)) { - this.setFocusToMenuitem(this.getMenu(tgt), tgt); + default: + break; + } + + if (this.getMenuId(tgt) === 'menu-size') { + this.updateFontSizeMenu('menu-size'); + } + this.closePopup(tgt); + } + + event.stopPropagation(); + event.preventDefault(); + } + + onMenuitemPointerover(event) { + var tgt = event.currentTarget; + + if (this.isAnyPopupOpen() && this.getMenu(tgt)) { + this.setFocusToMenuitem(this.getMenu(tgt), tgt); + } } -}; +} // Initialize menubar editor diff --git a/examples/menubar/js/menubar-navigation.js b/examples/menubar/js/menubar-navigation.js index 858871e134..6f42fea2ab 100644 --- a/examples/menubar/js/menubar-navigation.js +++ b/examples/menubar/js/menubar-navigation.js @@ -54,12 +54,12 @@ class MenubarNavigation { this.initMenu(domNode, 0); - domNode.addEventListener('focusin', this.handleMenubarFocusin.bind(this)); - domNode.addEventListener('focusout', this.handleMenubarFocusout.bind(this)); + domNode.addEventListener('focusin', this.onMenubarFocusin.bind(this)); + domNode.addEventListener('focusout', this.onMenubarFocusout.bind(this)); window.addEventListener( - 'mousedown', - this.handleBackgroundMousedown.bind(this), + 'pointerdown', + this.onBackgroundPointerdown.bind(this), true ); @@ -233,14 +233,14 @@ class MenubarNavigation { menuitem.textContent.trim().toLowerCase()[0] ); - menuitem.addEventListener('keydown', this.handleKeydown.bind(this)); - menuitem.addEventListener('click', this.handleMenuitemClick.bind(this), { + menuitem.addEventListener('keydown', this.onKeydown.bind(this)); + menuitem.addEventListener('click', this.onMenuitemClick.bind(this), { capture: true, }); menuitem.addEventListener( - 'mouseover', - this.handleMenuitemMouseover.bind(this) + 'pointerover', + this.onMenuitemPointerover.bind(this) ); if (!this.firstMenuitem[menuId]) { @@ -539,17 +539,17 @@ class MenubarNavigation { // Menu event handlers - handleMenubarFocusin() { + onMenubarFocusin() { // if the menubar or any of its menus has focus, add styling hook for hover this.domNode.classList.add('focus'); } - handleMenubarFocusout() { + onMenubarFocusout() { // remove styling hook for hover on menubar item this.domNode.classList.remove('focus'); } - handleKeydown(event) { + onKeydown(event) { var tgt = event.currentTarget, key = event.key, flag = false, @@ -687,7 +687,7 @@ class MenubarNavigation { } } - handleMenuitemClick(event) { + onMenuitemClick(event) { var tgt = event.currentTarget; var menuId = this.getMenuId(tgt); @@ -706,7 +706,7 @@ class MenubarNavigation { event.preventDefault(); } - handleMenuitemMouseover(event) { + onMenuitemPointerover(event) { var tgt = event.currentTarget; var menuId = this.getMenuId(tgt); @@ -722,7 +722,7 @@ class MenubarNavigation { } } - handleBackgroundMousedown(event) { + onBackgroundPointerdown(event) { if (!this.domNode.contains(event.target)) { this.closePopupAll(); } diff --git a/examples/menubar/js/style-manager.js b/examples/menubar/js/style-manager.js index 06e35d7c4b..93cdb6624f 100644 --- a/examples/menubar/js/style-manager.js +++ b/examples/menubar/js/style-manager.js @@ -9,150 +9,154 @@ 'use strict'; -var StyleManager = function (node) { - this.node = node; - this.fontSize = 'medium'; -}; - -StyleManager.prototype.setFontFamily = function (value) { - this.node.style.fontFamily = value; -}; - -StyleManager.prototype.setTextDecoration = function (value) { - this.node.style.textDecoration = value; -}; - -StyleManager.prototype.setTextAlign = function (value) { - this.node.style.textAlign = value; -}; - -StyleManager.prototype.setFontSize = function (value) { - this.fontSize = value; - this.node.style.fontSize = value; -}; - -StyleManager.prototype.setColor = function (value) { - this.node.style.color = value; -}; - -StyleManager.prototype.setBold = function (flag) { - if (flag) { - this.node.style.fontWeight = 'bold'; - } else { - this.node.style.fontWeight = 'normal'; +/* exported StyleManager */ + +class StyleManager { + constructor(node) { + this.node = node; + this.fontSize = 'medium'; + } + + setFontFamily(value) { + this.node.style.fontFamily = value; + } + + setTextDecoration(value) { + this.node.style.textDecoration = value; + } + + setTextAlign(value) { + this.node.style.textAlign = value; + } + + setFontSize(value) { + this.fontSize = value; + this.node.style.fontSize = value; + } + + setColor(value) { + this.node.style.color = value; + } + + setBold(flag) { + if (flag) { + this.node.style.fontWeight = 'bold'; + } else { + this.node.style.fontWeight = 'normal'; + } + } + + setItalic(flag) { + if (flag) { + this.node.style.fontStyle = 'italic'; + } else { + this.node.style.fontStyle = 'normal'; + } + } + + fontSmaller() { + switch (this.fontSize) { + case 'small': + this.setFontSize('x-small'); + break; + + case 'medium': + this.setFontSize('small'); + break; + + case 'large': + this.setFontSize('medium'); + break; + + case 'x-large': + this.setFontSize('large'); + break; + + default: + break; + } // end switch + } + + fontLarger() { + switch (this.fontSize) { + case 'x-small': + this.setFontSize('small'); + break; + + case 'small': + this.setFontSize('medium'); + break; + + case 'medium': + this.setFontSize('large'); + break; + + case 'large': + this.setFontSize('x-large'); + break; + + default: + break; + } // end switch + } + + isMinFontSize() { + return this.fontSize === 'x-small'; } -}; -StyleManager.prototype.setItalic = function (flag) { - if (flag) { - this.node.style.fontStyle = 'italic'; - } else { - this.node.style.fontStyle = 'normal'; + isMaxFontSize() { + return this.fontSize === 'x-large'; } -}; - -StyleManager.prototype.fontSmaller = function () { - switch (this.fontSize) { - case 'small': - this.setFontSize('x-small'); - break; - - case 'medium': - this.setFontSize('small'); - break; - - case 'large': - this.setFontSize('medium'); - break; - - case 'x-large': - this.setFontSize('large'); - break; - - default: - break; - } // end switch -}; - -StyleManager.prototype.fontLarger = function () { - switch (this.fontSize) { - case 'x-small': - this.setFontSize('small'); - break; - - case 'small': - this.setFontSize('medium'); - break; - - case 'medium': - this.setFontSize('large'); - break; - - case 'large': - this.setFontSize('x-large'); - break; - - default: - break; - } // end switch -}; - -StyleManager.prototype.isMinFontSize = function () { - return this.fontSize === 'x-small'; -}; - -StyleManager.prototype.isMaxFontSize = function () { - return this.fontSize === 'x-large'; -}; - -StyleManager.prototype.getFontSize = function () { - return this.fontSize; -}; - -StyleManager.prototype.setOption = function (option, value) { - option = option.toLowerCase(); - if (typeof value === 'string') { - value = value.toLowerCase(); + + getFontSize() { + return this.fontSize; } - switch (option) { - case 'font-bold': - this.setBold(value); - break; + setOption(option, value) { + option = option.toLowerCase(); + if (typeof value === 'string') { + value = value.toLowerCase(); + } - case 'font-color': - this.setColor(value); - break; + switch (option) { + case 'font-bold': + this.setBold(value); + break; - case 'font-family': - this.setFontFamily(value); - break; + case 'font-color': + this.setColor(value); + break; - case 'font-smaller': - this.fontSmaller(); - break; + case 'font-family': + this.setFontFamily(value); + break; - case 'font-larger': - this.fontLarger(); - break; + case 'font-smaller': + this.fontSmaller(); + break; - case 'font-size': - this.setFontSize(value); - break; + case 'font-larger': + this.fontLarger(); + break; - case 'font-italic': - this.setItalic(value); - break; + case 'font-size': + this.setFontSize(value); + break; - case 'text-align': - this.setTextAlign(value); - break; + case 'font-italic': + this.setItalic(value); + break; - case 'text-decoration': - this.setTextDecoration(value); - break; + case 'text-align': + this.setTextAlign(value); + break; - default: - break; - } // end switch -}; + case 'text-decoration': + this.setTextDecoration(value); + break; + + default: + break; + } // end switch + } +} diff --git a/examples/menubar/menubar-editor.html b/examples/menubar/menubar-editor.html index f47c2ac2f6..0dd6eb4729 100644 --- a/examples/menubar/menubar-editor.html +++ b/examples/menubar/menubar-editor.html @@ -16,15 +16,6 @@ - - - - - - - - -
    -

    Editor Menubar Example

    -

    - The following example demonstrates using the - menubar design pattern - to provide access to sets of actions. - Each item in the below menubar identifies a category of text formatting actions that can be executed from its submenu. - The submenus also demonstrate menuitemradio and menuitemcheckbox elements. -

    -

    Similar examples include:

    - +

    Editor Menubar Example

    +

    + The following example demonstrates using the + menubar design pattern + to provide access to editing actions for a text area. + Each item in the menubar identifies a category of text formatting actions that can be executed from its submenu. + The submenus demonstrate menuitemradio and menuitemcheckbox elements. +

    +

    Similar examples include:

    +
    -

    Example

    +
    +

    Example

    +
    @@ -415,7 +413,7 @@

    Menubar

    - tabindex="-1" + tabindex="-1" span @@ -428,7 +426,7 @@

    Menubar

    - tabindex="0" + tabindex="0" span @@ -440,10 +438,10 @@

    Menubar

    part of the tab sequence of the page.
  • - Only one menuitem in the menubar has tabindex="0". + Only one menuitem in the menubar has tabindex="0".
  • - When the page loads, the first item in the menubar has tabindex="0". + When the page loads, the first item in the menubar has tabindex="0".
  • Focus is managed using roving tabindex. @@ -454,7 +452,7 @@

    Menubar

    - aria-haspopup="true" + aria-haspopup="true" span @@ -466,299 +464,349 @@

    Menubar

    - aria-expanded="true" + aria-expanded="true" span - Indicates the menu is open. + + + - aria-expanded="false" + aria-expanded="false" + + + span + + + + + + + + + aria-hidden="true" span - Indicates the submenu is closed. + + Removes the character entities used to represent the down arrow icons for parent menu items from the accessibility tree to prevent them from being included in the accessible name of the menu item. +

    Submenu

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    - menu - - ul - -
      -
    • Identifies the element as a menu container for a set of menu items.
    • -
    • Is not focusable because focus is managed using roving tabindex.
    • -
    -
    - aria-label="string" - - ul - - Defines an accessible name for the menu. -
    - menuitem - - li - -
      -
    • Identifies the element as an item in the submenu.
    • -
    • Accessible name comes from the text content.
    • -
    -
    - tabindex="-1" - - li - - Makes the item focusable but not part of the page tab sequence. -
    - aria-disabled="false" - - li - - Used on the font size "Smaller" and "Larger" options to indicate they are active. -
    - aria-disabled="true" - - li - - Used on the font size "Smaller" and "Larger" options to indicate one of the options is not active because the largest or smallest font has been selected. -
    - menuitemcheckbox - - li - -
      -
    • Identifies the element as a menuitemcheckbox.
    • -
    • Accessible name comes from the text content.
    • -
    -
    - tabindex="-1" - - li - - Makes the menuitemcheckbox focusable but not part of the page tab sequence. -
    - aria-checked="true" - - li - -
      -
    • - Indicates that the menuitemcheckbox is checked. -
    • -
    • - The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors. -
    • -
    -
    - aria-checked="false" - - li - -
      -
    • - Indicates that the menuitemcheckbox is NOT checked. -
    • -
    • - The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors. -
    • -
    -
    - separator - - li - -
      -
    • Identifies the element as a visual separator between groups of items within a menu, such as groups of menuitemradio or menuitemcheckbox elements.
    • -
    • Is not focusable but may be perceivable by a screen reader user when using a reading cursor that does not depend on focus.
    • -
    -
    - group - - ul - -
      -
    • - Identifies the element as a container for a set of menuitemradio elements. -
    • -
    • - Enables browsers to compute values of aria-setsize and aria-posinset. -
    • -
    -
    - aria-label="string" - - ul - - Provides an accessible name for the group of menu items. -
    - menuitemradio - - li - -
      -
    • - Identifies the element as a menuitemradio element. -
    • -
    • - When all items in a submenu are members of the same radio group, - the group is defined by the menu element; a group element is not necessary. -
    • -
    • - Accessible name is computed from the text content. -
    • -
    -
    - tabindex="-1" - - li - - Makes the menuitemradio focusable but not part of the page tab sequence. -
    - aria-checked="true" - - li - -
      -
    • - Indicates the menuitemradio is checked. -
    • -
    • - The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors. -
    • -
    -
    - aria-checked="false" - - li - -
      -
    • - Indicates that the menuitemradio is NOT checked. -
    • -
    • - The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors. -
    • -
    -
    RoleAttributeElementUsage
    + menu + + ul + +
      +
    • Identifies the element as a menu container for a set of menu items.
    • +
    • Is not focusable because focus is managed using roving tabindex.
    • +
    +
    + aria-label="string" + + ul + + Defines an accessible name for the menu. +
    + menuitem + + li + +
      +
    • Identifies the element as an item in the submenu.
    • +
    • Accessible name comes from the text content.
    • +
    +
    + tabindex="-1" + + li + + Makes the item focusable but not part of the page tab sequence. +
    + aria-disabled="false" + + li + + Used on the font size "Smaller" and "Larger" options to indicate they are active. +
    + aria-disabled="true" + + li + + Used on the font size "Smaller" and "Larger" options to indicate one of the options is not active because the largest or smallest font has been selected. +
    + menuitemcheckbox + + li + +
      +
    • Identifies the element as a menuitemcheckbox.
    • +
    • Accessible name comes from the text content.
    • +
    +
    + tabindex="-1" + + li + + Makes the menuitemcheckbox focusable but not part of the page tab sequence. +
    + aria-checked="true" + + li + +
      +
    • + Indicates that the menuitemcheckbox is checked. +
    • +
    • + The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors and hidden from screen readers with aria-hidden="true". +
    • +
    +
    + aria-checked="false" + + li + +
      +
    • + Indicates that the menuitemcheckbox is NOT checked. +
    • +
    • + The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors and hidden from screen readers with aria-hidden="true". +
    • +
    +
    + aria-hidden="true" + + span + + Removes the character entities that visually represent the checked state of menuitemcheckbox elements from the accessibility tree to prevent them from being included in the accessible name of the menu item. +
    + separator + + li + +
      +
    • Identifies the element as a visual separator between groups of items within a menu, such as groups of menuitemradio or menuitemcheckbox elements.
    • +
    • Is not focusable but may be perceivable by a screen reader user when using a reading cursor that does not depend on focus.
    • +
    +
    + group + + ul + +
      +
    • + Identifies the element as a container for a set of menuitemradio elements. +
    • +
    • + Enables browsers to compute values of aria-setsize and aria-posinset. +
    • +
    +
    + aria-label="string" + + ul + + Provides an accessible name for the group of menu items. +
    + menuitemradio + + li + +
      +
    • + Identifies the element as a menuitemradio element. +
    • +
    • + When all items in a submenu are members of the same radio group, + the group is defined by the menu element; a group element is not necessary. +
    • +
    • + Accessible name is computed from the text content. +
    • +
    +
    + tabindex="-1" + + li + + Makes the menuitemradio focusable but not part of the page tab sequence. +
    + aria-checked="true" + + li + +
      +
    • + Indicates the menuitemradio is checked. +
    • +
    • + The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors and hidden from screen readers with aria-hidden="true". +
    • +
    +
    + aria-checked="false" + + li + +
      +
    • + Indicates that the menuitemradio is NOT checked. +
    • +
    • + The visual appearance of the selected state is synchronized with the aria-checked value using CSS attribute selectors and hidden from screen readers with aria-hidden="true". +
    • +
    +
    + aria-hidden="true" + + span + + Removes the character entities that visually represent the checked state of menuitemradio elements from the accessibility tree to prevent them from being included in the accessible name of the menu item. +

    Textarea

    @@ -774,7 +822,7 @@

    Textarea

    - + @@ -406,7 +423,7 @@

    Role, Property, State, and Tabindex Attributes

    - + - + - + - + + + + + + + + + + + + + @@ -464,15 +503,15 @@

    Role, Property, State, and Tabindex Attributes

  • Identifies the ul element as a container of treeitem elements that form a branch of the tree.
  • The group is contained in the element that serves as the parent treeitem.
  • Browsers use the grouping to compute aria-level, aria-setsize and aria-posinset values for the nodes contained in the branch.
  • +
  • The grouping also prevents browsers from including the content of the nodes in the group in the accessible name for the parent node.
  • - aria-label="string" + aria-label="string" textarea @@ -789,7 +837,7 @@

    Textarea

    Javascript and CSS Source Code

    -
      +
      • CSS: menubar-editor.css @@ -809,12 +857,9 @@

        HTML Source Code

        - - + sourceCode.add('sc1', 'ex1', 'ex1_label', 'css_js_files'); + sourceCode.make(); +
    aria-labelledby="IDREF"aria-labelledby="ID_REFERENCE" ul Refers to the heading element that contains the label that identifies the purpose of the tree.
    tabindex="-1"tabindex="-1" li
      @@ -417,12 +434,12 @@

      Role, Property, State, and Tabindex Attributes

    tabindex="0"tabindex="0" li
    • Includes the treeitem element in the tab sequence.
    • -
    • Only one treeitem in the tree has tabindex="0".
    • +
    • Only one treeitem in the tree has tabindex="0".
    • In this implementation, the first treeitem in the tree is included in the tab sequence when the page loads.
    • When the user moves focus in the tree, the element included in the tab sequence changes to the element with focus as described in the section on @@ -433,7 +450,7 @@

      Role, Property, State, and Tabindex Attributes

    aria-expanded="false"aria-expanded="false" li
      @@ -445,7 +462,7 @@

      Role, Property, State, and Tabindex Attributes

    aria-expanded="true"aria-expanded="true" li
      @@ -455,6 +472,28 @@

      Role, Property, State, and Tabindex Attributes

    aria-selected="false"li +
      +
    • Applied to treeitem elements.
    • +
    • Indicates the file or folder for the item is not currently selected.
    • +
    +
    aria-selected="true"li +
      +
    • Applied to treeitem elements.
    • +
    • Indicates the file or folder for the item is currently selected.
    • +
    +
    group
    - -
    +

    Javascript and CSS Source Code

    • diff --git a/examples/treeview/treeview-1/treeview-1b.html b/examples/treeview/treeview-1/treeview-1b.html index 4cc681a4fd..36e2b55fcc 100644 --- a/examples/treeview/treeview-1/treeview-1b.html +++ b/examples/treeview/treeview-1/treeview-1b.html @@ -12,6 +12,9 @@ + + @@ -28,15 +31,16 @@

      File Directory Treeview Example Using Declared Properties

      - The below example implements the - Treeview Design Pattern - to simulate a file selector. - When users activate an item that represents a file name in the below tree, the name of the selected file appears in the read-only edit field next to the tree. + The following example implementation of the + Tree View Design Pattern + simulates a widget for selecting a file or folder from within a hierarchical file system for viewing in a file viewer. + In the My Documents tree, each parent node represents a folder and each end node represents a file. + Activating a node selects the node and puts the name of the folder or file in the read-only edit field that represents the file viewer.

      The code in this example explicitly declares values for aria-setsize, aria-posinset and aria-level, which overrides browser computation of values for these properties. - The ARIA 1.0 specification for these properties states that browsers can, but are not required to, compute these values. -

      + The ARIA specification for these properties states that browsers can, but are not required to, compute these values. +

      Similar examples include:

      • File Directory Treeview using computed properties
      • @@ -48,369 +52,414 @@

        Example

        -

        File Viewer

        -
          - - - -
        -

        +

        My Documents

        +
          + + + +
        +

    @@ -418,7 +467,7 @@

    File Viewer

    Accessibility Features

    - To make the focus indicator easier to see, nodes in the tree have a custom focus and hover styling created using CSS focus and hover pseudo-classes. + To make the focus indicator easier to see, nodes in the tree have custom focus and hover styling created using CSS focus and hover pseudo-classes.

    @@ -435,6 +484,11 @@

    Terms Used to Describe Trees

    Keyboard Support

    +

    + Note that in this example, selection and focus are distinct; moving focus does not change which node is selected. + Because selection does not follow focus, keyboard and screen reader users can navigate and explore the tree without changing the content of the file viewer. + To learn more about this aspect of the design, read the guidance section about Deciding When to Make Selection Automatically Follow Focus. +

    @@ -443,14 +497,13 @@

    Keyboard Support

    - - - + + + + + + + @@ -552,7 +605,7 @@

    Role, Property, State, and Tabindex Attributes

    - + @@ -564,7 +617,7 @@

    Role, Property, State, and Tabindex Attributes

    - + - + - + - + + + + + + + + + + + + + - + - + - + @@ -650,16 +725,16 @@

    Role, Property, State, and Tabindex Attributes

    • Identifies the ul element as a container of treeitem elements that form a branch of the tree.
    • The group is contained in the element that serves as the parent treeitem.
    • -
    • Browsers use the grouping to compute aria-level, aria-setsize and aria-posinset values for the nodes contained in the branch.
    • +
    • Browsers use the grouping to compute aria-level, aria-setsize and aria-posinset values for the nodes contained in the branch if those properties are not specified explicitly in the code.
    • +
    • The grouping also prevents browsers from including the content of the nodes in the group in the accessible name for the parent node.
    Enter
    or Space
    -
      -
    • Performs the default action (e.g. onclick event) for the focused node.
    • -
    • In this example, the default action is to update the File or Folder Selected textbox.
    • -
    -
    EnterPerforms the default action, which is to select the node, causing the name of the node to appear in the File or Folder Selected textbox.
    SpacePerforms the default action, which is to select the node, causing the name of the node to appear in the File or Folder Selected textbox.
    Down arrow
    aria-labelledby="IDREF"aria-labelledby="ID_REFERENCE" ul Refers to the heading element that contains the label that identifies the purpose of the tree.
    tabindex="-1"tabindex="-1" li
      @@ -575,12 +628,12 @@

      Role, Property, State, and Tabindex Attributes

    tabindex="0"tabindex="0" li
    • Includes the treeitem element in the tab sequence.
    • -
    • Only one treeitem in the tree has tabindex="0".
    • +
    • Only one treeitem in the tree has tabindex="0".
    • In this implementation, the first treeitem in the tree is included in the tab sequence when the page loads.
    • When the user moves focus in the tree, the element included in the tab sequence changes to the element with focus as described in the section on @@ -591,7 +644,7 @@

      Role, Property, State, and Tabindex Attributes

    aria-expanded="false"aria-expanded="false" li
      @@ -603,7 +656,7 @@

      Role, Property, State, and Tabindex Attributes

    aria-expanded="true"aria-expanded="true" li
      @@ -613,15 +666,37 @@

      Role, Property, State, and Tabindex Attributes

    aria-selected="false"li +
      +
    • Applied to treeitem elements.
    • +
    • Indicates the file or folder for the item is not currently selected.
    • +
    +
    aria-selected="true"li +
      +
    • Applied to treeitem elements.
    • +
    • Indicates the file or folder for the item is currently selected.
    • +
    +
    aria-setsize="number"aria-setsize="number" li Defines the number of treeitem elements in the set of treeitem elements that are in the same branch and at the same level within the hierarchy.
    aria-posinset="number"aria-posinset="number" li
      @@ -632,13 +707,13 @@

      Role, Property, State, and Tabindex Attributes

    aria-level="number"aria-level="number" li
    • Defines the level of the treeitem in the hierarchical tree structure.
    • Counting is one-based.
    • -
    • Root treeitem elements have aria-level=1.
    • +
    • Root treeitem elements have aria-level="1".
    -
    -
    +

    Javascript and CSS Source Code

    • @@ -686,7 +761,6 @@

      HTML Source Code

      -