From 01e3ab561bcc1c12a8319702e3d55604764db735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yi=C4=9Fit=20FINDIKLI?= Date: Thu, 30 Nov 2023 00:06:15 +0300 Subject: [PATCH] #5429 for TabView --- components/doc/tabview/accessibilitydoc.js | 141 +++++++++++---------- components/lib/tabview/TabView.js | 137 ++++++++++++++++++-- 2 files changed, 200 insertions(+), 78 deletions(-) diff --git a/components/doc/tabview/accessibilitydoc.js b/components/doc/tabview/accessibilitydoc.js index aa135b2b77..376393cf09 100644 --- a/components/doc/tabview/accessibilitydoc.js +++ b/components/doc/tabview/accessibilitydoc.js @@ -1,73 +1,82 @@ -import { DevelopmentSection } from '@/components/doc/common/developmentsection'; import { DocSectionText } from '@/components/doc/common/docsectiontext'; export function AccessibilityDoc() { return ( - - -

Screen Reader

-

- TabView container is defined with the tablist role, as any attribute is passed to the container element aria-labelledby can be optionally used to specify an element to describe the TabView. Each tab header has a{' '} - tab role along with aria-selected state attribute and aria-controls to refer to the corresponding tab content element. The content element of each tab has tabpanel role, an id to match the - aria-controls of the header and aria-labelledby reference to the header as the accessible name. -

+ +

Screen Reader

+

+ TabView container is defined with the tablist role, as any attribute is passed to the container element aria-labelledby can be optionally used to specify an element to describe the TabView. Each tab header has a{' '} + tab role along with aria-selected state attribute and aria-controls to refer to the corresponding tab content element. The content element of each tab has tabpanel role, an id to match the + aria-controls of the header and aria-labelledby reference to the header as the accessible name. +

-

Tab Header Keyboard Support

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyFunction
- tab - Moves focus through the header.
- enter - Activates the focused tab header.
- space - Activates the focused tab header.
- right arrow - Moves focus to the next header.
- left arrow - Moves focus to the previous header.
- home - Moves focus to the last header.
- end - Moves focus to the first header.
-
-
-
+

Tab Header Keyboard Support

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyFunction
+ tab + Moves focus through the header.
+ enter + Activates the focused tab header.
+ space + Activates the focused tab header.
+ right arrow + Moves focus to the next header.
+ left arrow + Moves focus to the previous header.
+ home + Moves focus to the last header.
+ end + Moves focus to the first header.
+ pageUp + Moves scroll position to first header.
+ pageDown + Moves scroll position to last header.
+
+ ); } diff --git a/components/lib/tabview/TabView.js b/components/lib/tabview/TabView.js index 69490a8d12..5f367f0940 100644 --- a/components/lib/tabview/TabView.js +++ b/components/lib/tabview/TabView.js @@ -89,6 +89,10 @@ export const TabView = React.forwardRef((inProps, ref) => { }; const onTabHeaderClick = (event, tab, index) => { + changeActiveIndex(event, tab, index); + }; + + const changeActiveIndex = (event, tab, index) => { if (event) { event.preventDefault(); } @@ -103,12 +107,114 @@ export const TabView = React.forwardRef((inProps, ref) => { else setActiveIndexState(index); } - updateScrollBar(index); + updateScrollBar({ index }); }; const onKeyDown = (event, tab, index) => { - if (event.key === 'Enter') { - onTabHeaderClick(event, tab, index); + switch (event.code) { + case 'ArrowLeft': + onTabArrowLeftKey(event); + break; + + case 'ArrowRight': + onTabArrowRightKey(event); + break; + + case 'Home': + onTabHomeKey(event); + break; + + case 'End': + onTabEndKey(event); + break; + + case 'PageDown': + onPageDownKey(event); + break; + + case 'PageUp': + onPageUpKey(event); + break; + + case 'Enter': + case 'Space': + onTabEnterKey(event, tab, index); + break; + + default: + break; + } + }; + + const onTabArrowRightKey = (event) => { + const nextHeaderAction = findNextHeaderAction(event.target.parentElement); + nextHeaderAction ? changeFocusedTab(nextHeaderAction) : onTabHomeKey(event); + event.preventDefault(); + }; + + const onTabArrowLeftKey = (event) => { + const prevHeaderAction = findPrevHeaderAction(event.target.parentElement); + prevHeaderAction ? changeFocusedTab(prevHeaderAction) : onTabEndKey(event); + event.preventDefault(); + }; + + const onTabHomeKey = (event) => { + const firstHeaderAction = findFirstHeaderAction(); + changeFocusedTab(firstHeaderAction); + event.preventDefault(); + }; + + const onTabEndKey = (event) => { + const lastHeaderAction = findLastHeaderAction(); + changeFocusedTab(lastHeaderAction); + event.preventDefault(); + }; + + const onPageDownKey = (event) => { + updateScrollBar({ index: React.Children.count(props.children) - 1 }); + event.preventDefault(); + }; + + const onPageUpKey = (event) => { + updateScrollBar({ index: 0 }); + event.preventDefault(); + }; + + const onTabEnterKey = (event, tab, index) => { + changeActiveIndex(event, tab, index); + event.preventDefault(); + }; + + const findNextHeaderAction = (tabElement, selfCheck = false) => { + const headerElement = selfCheck ? tabElement : tabElement.nextElementSibling; + return headerElement + ? DomHandler.getAttribute(headerElement, 'data-p-disabled') || DomHandler.getAttribute(headerElement, 'data-pc-section') === 'inkbar' + ? findNextHeaderAction(headerElement) + : DomHandler.findSingle(headerElement, '[data-pc-section="headeraction"]') + : null; + }; + + const findPrevHeaderAction = (tabElement, selfCheck = false) => { + const headerElement = selfCheck ? tabElement : tabElement.previousElementSibling; + return headerElement + ? DomHandler.getAttribute(headerElement, 'data-p-disabled') || DomHandler.getAttribute(headerElement, 'data-pc-section') === 'inkbar' + ? findPrevHeaderAction(headerElement) + : DomHandler.findSingle(headerElement, '[data-pc-section="headeraction"]') + : null; + }; + + const findFirstHeaderAction = () => { + return findNextHeaderAction(navRef.current.firstElementChild, true); + }; + + const findLastHeaderAction = () => { + return findPrevHeaderAction(navRef.current.lastElementChild, true); + }; + + const changeFocusedTab = (element) => { + if (element) { + DomHandler.focus(element); + updateScrollBar({ element }); } }; @@ -119,8 +225,8 @@ export const TabView = React.forwardRef((inProps, ref) => { inkbarRef.current.style.left = DomHandler.getOffset(tabHeader).left - DomHandler.getOffset(navRef.current).left + 'px'; }; - const updateScrollBar = (index) => { - let tabHeader = tabsRef.current[`tab_${index}`]; + const updateScrollBar = ({ index, element }) => { + let tabHeader = element || tabsRef.current[`tab_${index}`]; if (tabHeader && tabHeader.scrollIntoView) { tabHeader.scrollIntoView({ block: 'nearest' }); @@ -188,7 +294,7 @@ export const TabView = React.forwardRef((inProps, ref) => { useUpdateEffect(() => { if (props.activeIndex !== activeIndexState) { - updateScrollBar(props.activeIndex); + updateScrollBar({ index: props.activeIndex }); } }, [props.activeIndex]); @@ -202,8 +308,8 @@ export const TabView = React.forwardRef((inProps, ref) => { const selected = isSelected(index); const { headerStyle, headerClassName, style: _style, className: _className, disabled, leftIcon, rightIcon, header, headerTemplate, closable, closeIcon } = TabPanelBase.getCProps(tab); const headerId = idState + '_header_' + index; - const ariaControls = idState + '_content_' + index; - const tabIndex = disabled ? null : 0; + const ariaControls = idState + index + '_content'; + const tabIndex = disabled || !selected ? -1 : 0; const leftIconElement = leftIcon && IconUtils.getJSXIcon(leftIcon, undefined, { props }); const headerTitleProps = mergeProps( { @@ -225,6 +331,7 @@ export const TabView = React.forwardRef((inProps, ref) => { tabIndex, 'aria-controls': ariaControls, 'aria-selected': selected, + 'aria-disabled': disabled, onClick: (e) => onTabHeaderClick(e, tab, index), onKeyDown: (e) => onKeyDown(e, tab, index) }, @@ -310,6 +417,8 @@ export const TabView = React.forwardRef((inProps, ref) => { const inkbarProps = mergeProps( { ref: inkbarRef, + 'aria-hidden': 'true', + role: 'presentation', className: cx('inkbar') }, ptm('inkbar') @@ -336,7 +445,7 @@ export const TabView = React.forwardRef((inProps, ref) => { const contents = React.Children.map(props.children, (tab, index) => { if (shouldUseTab(tab) && (!props.renderActiveOnly || isSelected(index))) { const selected = isSelected(index); - const contentId = idState + '_content_' + index; + const contentId = idState + index + '_content'; const ariaLabelledBy = idState + '_header_' + index; const contentProps = mergeProps( { @@ -344,8 +453,7 @@ export const TabView = React.forwardRef((inProps, ref) => { className: cx('tab.content', { props, selected, getTabProp, tab, isSelected, shouldUseTab, index }), style: sx('tab.content', { props, getTabProp, tab, isSelected, shouldUseTab, index }), role: 'tabpanel', - 'aria-labelledby': ariaLabelledBy, - 'aria-hidden': !selected + 'aria-labelledby': ariaLabelledBy }, TabPanelBase.getCOtherProps(tab), getTabPT(tab, 'root'), @@ -360,7 +468,12 @@ export const TabView = React.forwardRef((inProps, ref) => { }; const createPrevButton = () => { - const prevIconProps = mergeProps(ptm('previcon')); + const prevIconProps = mergeProps( + { + 'aria-hidden': 'true' + }, + ptm('previcon') + ); const icon = props.prevButton || ; const leftIcon = IconUtils.getJSXIcon(icon, { ...prevIconProps }, { props }); const prevButtonProps = mergeProps(