Skip to content

Commit

Permalink
Fix focus navigation and focus lock
Browse files Browse the repository at this point in the history
  • Loading branch information
lxsmnsyc committed Jul 20, 2022
1 parent f41d028 commit 983581b
Show file tree
Hide file tree
Showing 22 changed files with 353 additions and 439 deletions.
252 changes: 109 additions & 143 deletions packages/solid-headless/dist/cjs/development/index.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/solid-headless/dist/cjs/development/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/solid-headless/dist/cjs/production/index.js

Large diffs are not rendered by default.

252 changes: 109 additions & 143 deletions packages/solid-headless/dist/esm/development/index.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/solid-headless/dist/esm/development/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/solid-headless/dist/esm/production/index.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export declare function focusNextContinuous<T extends ValidConstructor>(nodes: H
export declare function focusPrevContinuous<T extends ValidConstructor>(nodes: HTMLElement[] | NodeListOf<HTMLElement>, targetNode: DynamicNode<T>): void;
export declare function focusNext<T extends ValidConstructor>(nodes: HTMLElement[] | NodeListOf<HTMLElement>, targetNode: DynamicNode<T>): void;
export declare function focusPrev<T extends ValidConstructor>(nodes: HTMLElement[] | NodeListOf<HTMLElement>, targetNode: DynamicNode<T>): void;
export declare function focusFirst(nodes: HTMLElement[] | NodeListOf<HTMLElement>): void;
export declare function focusLast(nodes: HTMLElement[] | NodeListOf<HTMLElement>): void;
export declare function focusFirst(nodes: HTMLElement[] | NodeListOf<HTMLElement>): boolean;
export declare function focusLast(nodes: HTMLElement[] | NodeListOf<HTMLElement>): boolean;
export declare function focusMatch(nodes: HTMLElement[] | NodeListOf<HTMLElement>, character: string): void;
export declare function lockFocus(ref: HTMLElement, reverse: boolean): void;
1 change: 1 addition & 0 deletions packages/solid-headless/dist/types/utils/focus-query.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function getFocusableElements(node: HTMLElement, filter?: HTMLElement): HTMLElement[];
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export declare function isFocusable<T extends HTMLElement>(element: T): boolean;
export default function getFocusableElements(node: HTMLElement, filter?: HTMLElement): HTMLElement[];
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {
HeadlessPropsWithRef,
DynamicProps,
} from '../../utils/dynamic-prop';
import { focusNext, focusPrev } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/get-focusable-elements';
import { focusFirst, lockFocus } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/focus-query';
import {
useAlertDialogContext,
} from './AlertDialogContext';
Expand All @@ -43,28 +43,14 @@ export function AlertDialogPanel<T extends ValidConstructor = 'div'>(
const ref = internalRef();
if (ref instanceof HTMLElement) {
if (properties.isOpen()) {
const initialNodes = getFocusableElements(ref);
if (initialNodes.length) {
initialNodes[0].focus();
}
focusFirst(getFocusableElements(ref));

const onKeyDown = (e: KeyboardEvent) => {
if (!props.disabled) {
if (e.key === 'Tab') {
e.preventDefault();

const nodes = getFocusableElements(ref);
if (e.shiftKey) {
if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[nodes.length - 1].focus();
} else {
focusPrev(nodes, document.activeElement);
}
} else if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[0].focus();
} else {
focusNext(nodes, document.activeElement);
}
lockFocus(ref, e.shiftKey);
} else if (e.key === 'Escape') {
properties.setState(false);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/solid-headless/src/components/button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export function Button<T extends ValidConstructor = 'button'>(
() => props.as ?? ('button' as T),
mergeProps(
{
tabindex: 0,
get tabindex() {
return props.disabled ? -1 : 0;
},
role: 'button',
},
createDisabled(() => props.disabled),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {
HeadlessPropsWithRef,
ValidConstructor,
} from '../../utils/dynamic-prop';
import { focusNext, focusPrev } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/get-focusable-elements';
import { focusFirst, lockFocus } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/focus-query';
import {
useCommandBarContext,
} from './CommandBarContext';
Expand All @@ -43,28 +43,14 @@ export function CommandBarPanel<T extends ValidConstructor = 'div'>(
const ref = internalRef();
if (ref instanceof HTMLElement) {
if (properties.isOpen()) {
const initialNodes = getFocusableElements(ref);
if (initialNodes.length) {
initialNodes[0].focus();
}
focusFirst(getFocusableElements(ref));

const onKeyDown = (e: KeyboardEvent) => {
if (!props.disabled) {
if (e.key === 'Tab') {
e.preventDefault();

const nodes = getFocusableElements(ref);
if (e.shiftKey) {
if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[nodes.length - 1].focus();
} else {
focusPrev(nodes, document.activeElement);
}
} else if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[0].focus();
} else {
focusNext(nodes, document.activeElement);
}
lockFocus(ref, e.shiftKey);
} else if (e.key === 'Escape') {
properties.setState(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ import {
HeadlessPropsWithRef,
ValidConstructor,
} from '../../utils/dynamic-prop';
import {
focusNext,
focusPrev,
} from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/get-focusable-elements';
import { focusFirst, lockFocus } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/focus-query';
import {
createUnmountable,
UnmountableProps,
Expand All @@ -50,23 +47,14 @@ export function ContextMenuPanel<T extends ValidConstructor = 'div'>(
const ref = internalRef();
if (ref instanceof HTMLElement) {
if (properties.isOpen()) {
focusFirst(getFocusableElements(ref));

const onKeyDown = (e: KeyboardEvent) => {
if (!props.disabled) {
if (e.key === 'Tab') {
e.preventDefault();

const nodes = getFocusableElements(ref);
if (e.shiftKey) {
if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[nodes.length - 1].focus();
} else {
focusPrev(nodes, document.activeElement);
}
} else if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[0].focus();
} else {
focusNext(nodes, document.activeElement);
}
lockFocus(ref, e.shiftKey);
} else if (e.key === 'Escape') {
properties.setState(false);
}
Expand Down
22 changes: 4 additions & 18 deletions packages/solid-headless/src/components/dialog/DialogPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {
HeadlessPropsWithRef,
ValidConstructor,
} from '../../utils/dynamic-prop';
import { focusNext, focusPrev } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/get-focusable-elements';
import { focusFirst, lockFocus } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/focus-query';
import {
useDialogContext,
} from './DialogContext';
Expand All @@ -43,28 +43,14 @@ export function DialogPanel<T extends ValidConstructor = 'div'>(
const ref = internalRef();
if (ref instanceof HTMLElement) {
if (properties.isOpen()) {
const initialNodes = getFocusableElements(ref);
if (initialNodes.length) {
initialNodes[0].focus();
}
focusFirst(getFocusableElements(ref));

const onKeyDown = (e: KeyboardEvent) => {
if (!props.disabled) {
if (e.key === 'Tab') {
e.preventDefault();

const nodes = getFocusableElements(ref);
if (e.shiftKey) {
if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[nodes.length - 1].focus();
} else {
focusPrev(nodes, document.activeElement);
}
} else if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[0].focus();
} else {
focusNext(nodes, document.activeElement);
}
lockFocus(ref, e.shiftKey);
} else if (e.key === 'Escape') {
properties.setState(false);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/solid-headless/src/components/feed/Feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
ValidConstructor,
} from '../../utils/dynamic-prop';
import { focusNext, focusPrev } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/get-focusable-elements';
import getFocusableElements from '../../utils/focus-query';
import {
FeedContext,
} from './FeedContext';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,18 @@ export function ListboxOptions<V, T extends ValidConstructor = 'ul'>(
role: 'listbox',
'aria-multiselectable': context.multiple,
'aria-labelledby': context.buttonID,
tabindex: 0,
ref: createRef(props, (e) => {
setInternalRef(() => e);
controller.setRef(e);
}),
get 'aria-orientation'() {
return context.horizontal ? 'horizontal' : 'vertical';
},
get tabindex() {
const internalDisabled = properties.disabled();
const granularDisabled = props.disabled;
return (internalDisabled || granularDisabled) ? -1 : 0;
},
},
createDisabled(() => {
const internalDisabled = properties.disabled();
Expand Down
39 changes: 4 additions & 35 deletions packages/solid-headless/src/components/popover/PopoverPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
HeadlessPropsWithRef,
ValidConstructor,
} from '../../utils/dynamic-prop';
import getFocusableElements from '../../utils/get-focusable-elements';
import { focusFirst, lockFocus } from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/focus-query';
import {
createUnmountable,
UnmountableProps,
Expand All @@ -46,46 +47,14 @@ export function PopoverPanel<T extends ValidConstructor = 'div'>(
const ref = internalRef();
if (ref instanceof HTMLElement) {
if (properties.isOpen()) {
const initialNodes = getFocusableElements(ref);
if (initialNodes.length) {
initialNodes[0].focus();
}
focusFirst(getFocusableElements(ref));

const onKeyDown = (e: KeyboardEvent) => {
if (!props.disabled) {
if (e.key === 'Tab') {
e.preventDefault();

const nodes = getFocusableElements(ref);
if (e.shiftKey) {
if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[nodes.length - 1].focus();
} else {
for (let i = 0, len = nodes.length; i < len; i += 1) {
if (document.activeElement === nodes[i]) {
if (i === 0) {
nodes[len - 1].focus();
} else {
nodes[i - 1].focus();
}
break;
}
}
}
} else if (!document.activeElement || !ref.contains(document.activeElement)) {
nodes[0].focus();
} else {
for (let i = 0, len = nodes.length; i < len; i += 1) {
if (document.activeElement === nodes[i]) {
if (i === len - 1) {
nodes[0].focus();
} else {
nodes[i + 1].focus();
}
break;
}
}
}
lockFocus(ref, e.shiftKey);
} else if (e.key === 'Escape') {
properties.setState(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ export function RadioGroupOption<V, T extends ValidConstructor = 'div'>(
setInternalRef(() => e);
}),
get tabindex() {
return properties.isSelected(props.value) ? 0 : -1;
const selected = properties.isSelected(props.value);
return (!isDisabled() || selected) ? 0 : -1;
},
},
createDisabled(isDisabled),
Expand Down
7 changes: 4 additions & 3 deletions packages/solid-headless/src/components/tabs/Tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,16 @@ export function Tab<V, T extends ValidConstructor = 'div'>(
ref: createRef(props, (e) => {
setInternalRef(() => e);
}),
get tabindex() {
return properties.isSelected(props.value) ? 0 : -1;
},
get id() {
return rootContext.getId('tab', props.value);
},
get 'aria-controls'() {
return rootContext.getId('tab-panel', props.value);
},
get tabindex() {
const selected = properties.isSelected(props.value);
return (!isDisabled() || selected) ? 0 : -1;
},
},
createDisabled(isDisabled),
createSelected(() => properties.isSelected(props.value)),
Expand Down
17 changes: 6 additions & 11 deletions packages/solid-headless/src/components/toolbar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import {
} from '../../utils/dynamic-prop';
import {
focusFirst,
focusLast,
focusNext,
focusPrev,
} from '../../utils/focus-navigation';
import getFocusableElements from '../../utils/get-focusable-elements';
import getFocusableElements from '../../utils/focus-query';
import { createTag } from '../../utils/namespace';

const TOOLBAR_TAG = createTag('toolbar');
Expand Down Expand Up @@ -86,21 +87,15 @@ export function Toolbar<T extends ValidConstructor = 'div'>(
getNextFocusable();
}
break;
case 'Home': {
const nodes = getFocusableElements(ref);
if (nodes.length) {
case 'Home':
if (focusFirst(getFocusableElements(ref))) {
e.preventDefault();
nodes[0].focus();
}
}
break;
case 'End': {
const nodes = getFocusableElements(ref);
if (nodes.length) {
case 'End':
if (focusLast(getFocusableElements(ref))) {
e.preventDefault();
nodes[nodes.length - 1].focus();
}
}
break;
default:
break;
Expand Down
Loading

0 comments on commit 983581b

Please sign in to comment.