Skip to content

Commit

Permalink
feat: added focus control inside the active modal window
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandrshy committed Jul 10, 2020
1 parent d19d039 commit cc7dcf1
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 3 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ Keukenhof.init();

## Roadmap

* Add optional focus control inside the modal window
* Add accessibility support
* Write documentation
* ~~Add optional focus control inside the modal window~~
* ~~Add accessibility support~~
* ~~Add callbacks `onOpen`, `onClose`, `beforeOpen`, `beforeClose`~~
* ~~Add support for CSS animations~~
* ~~Add optional scroll lock~~
Expand Down
1 change: 1 addition & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const CLASS_NAMES = {
export const KEY = {
ESC: 'Esc',
ESCAPE: 'Escape',
TAB: 'Tab',
} as const;

export const FOCUSING_ELEMENTS = [
Expand Down
47 changes: 46 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const Keukenhof = ((): KeukenhofType => {
openClass: string;
hasAnimation: boolean;
isAssignFocus: boolean;
isFocusInside: boolean;
scrollBehavior: {
isDisabled: boolean;
container: string;
Expand All @@ -35,6 +36,7 @@ export const Keukenhof = ((): KeukenhofType => {
openClass = CLASS_NAMES.IS_OPEN,
hasAnimation = false,
isAssignFocus = true,
isFocusInside = true,
scrollBehavior = {},
onOpen = () => {},
onClose = () => {},
Expand All @@ -53,6 +55,7 @@ export const Keukenhof = ((): KeukenhofType => {
this.openClass = openClass;
this.hasAnimation = hasAnimation;
this.isAssignFocus = isAssignFocus;
this.isFocusInside = isFocusInside;
this.scrollBehavior = {
isDisabled: true,
container: 'body',
Expand All @@ -64,6 +67,7 @@ export const Keukenhof = ((): KeukenhofType => {

this.onClick = this.onClick.bind(this);
this.onKeyup = this.onKeyup.bind(this);
this.onKeydown = this.onKeydown.bind(this);
}

/**
Expand Down Expand Up @@ -172,21 +176,31 @@ export const Keukenhof = ((): KeukenhofType => {
}

/**
* Keyboard press handler
* Event keyup handler
*
* @param {KeyboardEvent} event - Event data
*/
onKeyup(event: KeyboardEvent) {
if (event.key === KEY.ESCAPE || event.key === KEY.ESC) this.close(event);
}

/**
* Event keydown handler
*
* @param {KeyboardEvent} event - Event data
*/
onKeydown(event: KeyboardEvent) {
if (event.key === KEY.TAB) this.controlFocus(event);
}

/**
* Add event listeners for an open modal
*/
addEventListeners() {
this.$modal?.addEventListener('touchstart', this.onClick);
this.$modal?.addEventListener('click', this.onClick);
document.addEventListener('keyup', this.onKeyup);
if (this.isFocusInside) document.addEventListener('keydown', this.onKeydown);
}

/**
Expand All @@ -196,6 +210,7 @@ export const Keukenhof = ((): KeukenhofType => {
this.$modal?.removeEventListener('touchstart', this.onClick);
this.$modal?.removeEventListener('click', this.onClick);
document.removeEventListener('keyup', this.onKeyup);
if (this.isFocusInside) document.removeEventListener('keydown', this.onKeydown);
}

/**
Expand Down Expand Up @@ -243,6 +258,36 @@ export const Keukenhof = ((): KeukenhofType => {
);
(filteredNodesList.length ? filteredNodesList[0] : nodesList[0]).focus();
}

/**
* Leaves focus control inside a modal
*
* @param {KeyboardEvent} event - Event data
*/
controlFocus(event: KeyboardEvent) {
const nodesList = this.getFocusNodesList();
if (!nodesList.length) return;
const filteredNodesList = nodesList.filter(({offsetParent}) => offsetParent !== null);
if (!this.$modal?.contains(document.activeElement)) {
filteredNodesList[0].focus();
} else {
const index = filteredNodesList.indexOf(document.activeElement as HTMLElement);

if (event.shiftKey && index === 0) {
filteredNodesList[filteredNodesList.length - 1].focus();
event.preventDefault();
}

if (
!event.shiftKey &&
filteredNodesList.length &&
index === filteredNodesList.length - 1
) {
filteredNodesList[0].focus();
event.preventDefault();
}
}
}
}

let modal: ModalType;
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type ConfigType = {
openClass?: string;
hasAnimation?: boolean;
isAssignFocus?: boolean;
isFocusInside?: boolean;
scrollBehavior?: {
isDisabled?: boolean;
container?: string;
Expand All @@ -27,9 +28,11 @@ export type ModalType = {
closeBySelector: (selector: string) => void;
onClick: (event: Event) => void;
onKeyup: (event: KeyboardEvent) => void;
onKeydown: (event: KeyboardEvent) => void;
addEventListeners: () => void;
removeEventListeners: () => void;
changeScrollBehavior: (value: 'disable' | 'enable') => void;
controlFocus: (event: KeyboardEvent) => void;
};

export type KeukenhofType = {
Expand Down

0 comments on commit cc7dcf1

Please sign in to comment.