Implement Tabs with Automatic Activation pattern for EmojiPicker header
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/21833/head
							parent
							
								
									457f4c82db
								
							
						
					
					
						commit
						6677a3f20e
					
				|  | @ -147,8 +147,12 @@ class EmojiPicker extends React.Component { | |||
|             // We update this here instead of through React to avoid re-render on scroll.
 | ||||
|             if (cat.visible) { | ||||
|                 cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible"); | ||||
|                 cat.ref.current.setAttribute("aria-selected", true); | ||||
|                 cat.ref.current.setAttribute("tabindex", 0); | ||||
|             } else { | ||||
|                 cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible"); | ||||
|                 cat.ref.current.setAttribute("aria-selected", false); | ||||
|                 cat.ref.current.setAttribute("tabindex", -1); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -18,20 +18,74 @@ import React from 'react'; | |||
| import PropTypes from 'prop-types'; | ||||
| import classNames from "classnames"; | ||||
| 
 | ||||
| import {_t} from "../../../languageHandler"; | ||||
| import {Key} from "../../../Keyboard"; | ||||
| 
 | ||||
| class Header extends React.PureComponent { | ||||
|     static propTypes = { | ||||
|         categories: PropTypes.arrayOf(PropTypes.object).isRequired, | ||||
|         onAnchorClick: PropTypes.func.isRequired, | ||||
|         refs: PropTypes.object, | ||||
|     }; | ||||
| 
 | ||||
|     findNearestEnabled(index, delta) { | ||||
|         index += this.props.categories.length; | ||||
|         const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories]; | ||||
| 
 | ||||
|         while (index < cats.length && index >= 0) { | ||||
|             if (cats[index].enabled) return index % this.props.categories.length; | ||||
|             index += delta > 0 ? 1 : -1; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     changeCategoryRelative(delta) { | ||||
|         const current = this.props.categories.findIndex(c => c.visible); | ||||
|         this.changeCategoryAbsolute(current + delta, delta); | ||||
|     } | ||||
| 
 | ||||
|     changeCategoryAbsolute(index, delta=1) { | ||||
|         const category = this.props.categories[this.findNearestEnabled(index, delta)]; | ||||
|         if (category) { | ||||
|             this.props.onAnchorClick(category.id); | ||||
|             category.ref.current.focus(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Implements ARIA Tabs with Automatic Activation pattern
 | ||||
|     // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
 | ||||
|     onKeyDown = (ev) => { | ||||
|         let handled = true; | ||||
|         switch (ev.key) { | ||||
|             case Key.ARROW_LEFT: | ||||
|                 this.changeCategoryRelative(-1); | ||||
|                 break; | ||||
|             case Key.ARROW_RIGHT: | ||||
|                 this.changeCategoryRelative(1); | ||||
|                 break; | ||||
| 
 | ||||
|             case Key.HOME: | ||||
|                 this.changeCategoryAbsolute(0); | ||||
|                 break; | ||||
|             case Key.END: | ||||
|                 this.changeCategoryAbsolute(this.props.categories.length - 1, -1); | ||||
|                 break; | ||||
|             default: | ||||
|                 handled = false; | ||||
|         } | ||||
| 
 | ||||
|         if (handled) { | ||||
|             ev.preventDefault(); | ||||
|             ev.stopPropagation(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <nav className="mx_EmojiPicker_header" role="tablist"> | ||||
|             <nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}> | ||||
|                 {this.props.categories.map(category => { | ||||
|                     const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, { | ||||
|                         mx_EmojiPicker_anchor_visible: category.visible, | ||||
|                     }); | ||||
|                     // Properties of this button are also modified by EmojiPicker's updateVisibility in DOM.
 | ||||
|                     return <button | ||||
|                         disabled={!category.enabled} | ||||
|                         key={category.id} | ||||
|  | @ -40,6 +94,7 @@ class Header extends React.PureComponent { | |||
|                         onClick={() => this.props.onAnchorClick(category.id)} | ||||
|                         title={category.name} | ||||
|                         role="tab" | ||||
|                         tabIndex={category.visible ? 0 : -1} // roving
 | ||||
|                         aria-selected={category.visible} | ||||
|                         aria-controls={`mx_EmojiPicker_category_${category.id}`} | ||||
|                     />; | ||||
|  |  | |||
|  | @ -1408,6 +1408,7 @@ | |||
|     "Objects": "Objects", | ||||
|     "Symbols": "Symbols", | ||||
|     "Flags": "Flags", | ||||
|     "Categories": "Categories", | ||||
|     "Quick Reactions": "Quick Reactions", | ||||
|     "Cancel search": "Cancel search", | ||||
|     "Unknown Address": "Unknown Address", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski