Skip to content

Commit

Permalink
feat(tabs): update keyboard navigation to match W3C WAI standart (#1121)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek authored Nov 29, 2024
1 parent c1afcf3 commit 58583a3
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 20 deletions.
1 change: 1 addition & 0 deletions packages/docs/components/Tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ sidebarDepth: 2

| Prop name | Description | Type | Values | Default |
| ---------------- | --------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| activateOnFocus | Set the tab active on navigation focus | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| animateInitially | Apply animation on the initial render | boolean | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tabs: {<br>&nbsp;&nbsp;animateInitially: false<br>}</code> |
| animated | Tab will have an animation | boolean | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tabs: {<br>&nbsp;&nbsp;animated: true<br>}</code> |
| animation | Transition animation name | [string, string, string, string] \| [string, string] | `[next`, `prev]`, `[right`, `left`, `down`, `up]` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tabs: {<br>&nbsp;&nbsp;animation: [ "slide-next", "slide-prev", "slide-down", "slide-up",]<br>}</code> |
Expand Down
76 changes: 56 additions & 20 deletions packages/oruga/src/components/tabs/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const props = withDefaults(defineProps<TabsProps<T>>(), {
type: () => getDefault("tabs.type", "default"),
expanded: false,
destroyOnHide: false,
activateOnFocus: false,
animated: () => getDefault("tabs.animated", true),
animation: () =>
getDefault("tabs.animation", [
Expand Down Expand Up @@ -153,36 +154,67 @@ function tabClick(item: TabItem<T>): void {
}
/** Go to the next item or wrap around */
function next(): void {
const newIndex = mod(activeIndex.value + 1, items.value.length);
clickFirstViableChild(newIndex, true);
function next(event: KeyboardEvent, index: number): void {
if (
(props.vertical && event.key == "ArrowDown") ||
(!props.vertical && event.key == "ArrowRight")
) {
event.preventDefault(); // prevent default browser scrolling
const newIndex = mod(index + 1, items.value.length);
const item = getFirstViableItem(newIndex, true);
moveFocus(item);
}
}
/** Go to the previous item or wrap around */
function prev(): void {
const newIndex = mod(activeIndex.value - 1, items.value.length);
clickFirstViableChild(newIndex, false);
function prev(event: KeyboardEvent, index: number): void {
if (
(props.vertical && event.key == "ArrowUp") ||
(!props.vertical && event.key == "ArrowLeft")
) {
event.preventDefault(); // prevent default browser scrolling
const newIndex = mod(index - 1, items.value.length);
const item = getFirstViableItem(newIndex, false);
moveFocus(item);
}
}
/** Go to the first viable item */
function homePressed(): void {
if (items.value.length < 1) return;
clickFirstViableChild(0, true);
const item = getFirstViableItem(0, true);
moveFocus(item);
}
/** Go to the last viable item */
function endPressed(): void {
if (items.value.length < 1) return;
clickFirstViableChild(items.value.length - 1, false);
const item = getFirstViableItem(items.value.length - 1, false);
moveFocus(item);
}
/** Set focus on a tab item. */
function moveFocus(item: TabItem<T>): void {
if (props.activateOnFocus) {
tabClick(item);
} else {
const el = rootRef.value?.querySelector<HTMLElement>(
`#tab-${item.identifier} > *`,
);
el?.focus();
}
}
/**
* Select the first 'viable' child, starting at startingIndex and in the direction specified
* Get the first 'viable' child, starting at startingIndex and in the direction specified
* by the boolean parameter forward. In other words, first try to select the child at index
* startingIndex, and if it is not visible or it is disabled, then go to the index in the
* specified direction until either returning to startIndex or finding a viable child item.
*/
function clickFirstViableChild(startingIndex: number, forward: boolean): void {
function getFirstViableItem(
startingIndex: number,
forward: boolean,
): TabItem<T> {
const direction = forward ? 1 : -1;
let newIndex = startingIndex;
for (
Expand All @@ -194,7 +226,8 @@ function clickFirstViableChild(startingIndex: number, forward: boolean): void {
if (items.value[newIndex].visible && !items.value[newIndex].disabled)
break;
}
tabClick(items.value[newIndex]);
return items.value[newIndex];
}
/** Activate next child and deactivate prev child */
Expand Down Expand Up @@ -287,7 +320,10 @@ const contentClasses = defineClasses(
:class="childItem.navClasses"
role="tab"
:aria-controls="`tabpanel-${childItem.identifier}`"
:aria-selected="childItem.value === activeItem.value">
:aria-selected="childItem.value === activeItem.value"
:tabindex="
childItem.value === activeItem.value ? undefined : '-1'
">
<o-slot-component
v-if="childItem.$slots.header"
:component="childItem"
Expand All @@ -296,10 +332,10 @@ const contentClasses = defineClasses(
:class="childItem.classes"
@click="tabClick(childItem)"
@keydown.enter="tabClick(childItem)"
@keydown.left.prevent="prev"
@keydown.right.prevent="next"
@keydown.up.prevent="prev"
@keydown.down.prevent="next"
@keydown.left="prev($event, childItem.index)"
@keydown.right="next($event, childItem.index)"
@keydown.up="prev($event, childItem.index)"
@keydown.down="next($event, childItem.index)"
@keydown.home.prevent="homePressed"
@keydown.end.prevent="endPressed" />

Expand All @@ -311,10 +347,10 @@ const contentClasses = defineClasses(
:class="childItem.classes"
@click="tabClick(childItem)"
@keydown.enter="tabClick(childItem)"
@keydown.left.prevent="prev"
@keydown.right.prevent="next"
@keydown.up.prevent="prev"
@keydown.down.prevent="next"
@keydown.left="prev($event, childItem.index)"
@keydown.right="next($event, childItem.index)"
@keydown.up="prev($event, childItem.index)"
@keydown.down="next($event, childItem.index)"
@keydown.home.prevent="homePressed"
@keydown.end.prevent="endPressed">
<o-icon
Expand Down
2 changes: 2 additions & 0 deletions packages/oruga/src/components/tabs/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export type TabsProps<T> = {
expanded?: boolean;
/** Destroy tabItem on hide */
destroyOnHide?: boolean;
/** Set the tab active on navigation focus */
activateOnFocus?: boolean;
/** Tab will have an animation */
animated?: boolean;
/**
Expand Down

0 comments on commit 58583a3

Please sign in to comment.