Skip to content

Commit

Permalink
#5429 for TabView
Browse files Browse the repository at this point in the history
  • Loading branch information
yigitfindikli committed Nov 29, 2023
1 parent c19962a commit 01e3ab5
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 78 deletions.
141 changes: 75 additions & 66 deletions components/doc/tabview/accessibilitydoc.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,82 @@
import { DevelopmentSection } from '@/components/doc/common/developmentsection';
import { DocSectionText } from '@/components/doc/common/docsectiontext';

export function AccessibilityDoc() {
return (
<DevelopmentSection>
<DocSectionText id="accessibility" label="Accessibility">
<h3>Screen Reader</h3>
<p>
TabView container is defined with the <i>tablist</i> role, as any attribute is passed to the container element <i>aria-labelledby</i> can be optionally used to specify an element to describe the TabView. Each tab header has a{' '}
<i>tab</i> role along with <i>aria-selected</i> state attribute and <i>aria-controls</i> to refer to the corresponding tab content element. The content element of each tab has <i>tabpanel</i> role, an id to match the
<i>aria-controls</i> of the header and <i>aria-labelledby</i> reference to the header as the accessible name.
</p>
<DocSectionText id="accessibility" label="Accessibility">
<h3>Screen Reader</h3>
<p>
TabView container is defined with the <i>tablist</i> role, as any attribute is passed to the container element <i>aria-labelledby</i> can be optionally used to specify an element to describe the TabView. Each tab header has a{' '}
<i>tab</i> role along with <i>aria-selected</i> state attribute and <i>aria-controls</i> to refer to the corresponding tab content element. The content element of each tab has <i>tabpanel</i> role, an id to match the
<i>aria-controls</i> of the header and <i>aria-labelledby</i> reference to the header as the accessible name.
</p>

<h3>Tab Header Keyboard Support</h3>
<div className="doc-tablewrapper">
<table className="doc-table">
<thead>
<tr>
<th>Key</th>
<th>Function</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<i>tab</i>
</td>
<td>Moves focus through the header.</td>
</tr>
<tr>
<td>
<i>enter</i>
</td>
<td>Activates the focused tab header.</td>
</tr>
<tr>
<td>
<i>space</i>
</td>
<td>Activates the focused tab header.</td>
</tr>
<tr>
<td>
<i>right arrow</i>
</td>
<td>Moves focus to the next header.</td>
</tr>
<tr>
<td>
<i>left arrow</i>
</td>
<td>Moves focus to the previous header.</td>
</tr>
<tr>
<td>
<i>home</i>
</td>
<td>Moves focus to the last header.</td>
</tr>
<tr>
<td>
<i>end</i>
</td>
<td>Moves focus to the first header.</td>
</tr>
</tbody>
</table>
</div>
</DocSectionText>
</DevelopmentSection>
<h3>Tab Header Keyboard Support</h3>
<div className="doc-tablewrapper">
<table className="doc-table">
<thead>
<tr>
<th>Key</th>
<th>Function</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<i>tab</i>
</td>
<td>Moves focus through the header.</td>
</tr>
<tr>
<td>
<i>enter</i>
</td>
<td>Activates the focused tab header.</td>
</tr>
<tr>
<td>
<i>space</i>
</td>
<td>Activates the focused tab header.</td>
</tr>
<tr>
<td>
<i>right arrow</i>
</td>
<td>Moves focus to the next header.</td>
</tr>
<tr>
<td>
<i>left arrow</i>
</td>
<td>Moves focus to the previous header.</td>
</tr>
<tr>
<td>
<i>home</i>
</td>
<td>Moves focus to the last header.</td>
</tr>
<tr>
<td>
<i>end</i>
</td>
<td>Moves focus to the first header.</td>
</tr>
<tr>
<td>
<i>pageUp</i>
</td>
<td>Moves scroll position to first header.</td>
</tr>
<tr>
<td>
<i>pageDown</i>
</td>
<td>Moves scroll position to last header.</td>
</tr>
</tbody>
</table>
</div>
</DocSectionText>
);
}
137 changes: 125 additions & 12 deletions components/lib/tabview/TabView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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 });
}
};

Expand All @@ -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' });
Expand Down Expand Up @@ -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]);

Expand All @@ -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(
{
Expand All @@ -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)
},
Expand Down Expand Up @@ -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')
Expand All @@ -336,16 +445,15 @@ 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(
{
id: contentId,
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'),
Expand All @@ -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 || <ChevronLeftIcon {...prevIconProps} />;
const leftIcon = IconUtils.getJSXIcon(icon, { ...prevIconProps }, { props });
const prevButtonProps = mergeProps(
Expand Down

0 comments on commit 01e3ab5

Please sign in to comment.