Skip to content

Commit

Permalink
feat: Listbox: auto activate first item; onActiveItemChange; events o…
Browse files Browse the repository at this point in the history
…n element
  • Loading branch information
josemarluedke committed Mar 9, 2024
1 parent b22e515 commit 8ee0ddd
Show file tree
Hide file tree
Showing 7 changed files with 3,651 additions and 9,891 deletions.
1 change: 1 addition & 0 deletions packages/collections/src/components/dropdown.gts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ class Menu extends Component<MenuSignature> {
@selectedKeys={{@selectedKeys}}
@selectionMode={{if @selectionMode @selectionMode "none"}}
@type="menu"
@autoActivateFirstItem={{false}}
...attributes
as |l|
>
Expand Down
132 changes: 100 additions & 32 deletions packages/collections/src/components/listbox/listbox.gts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Component from '@glimmer/component';
import { hash } from '@ember/helper';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import { useStyles } from '@frontile/theme';
import {
Expand All @@ -10,6 +9,7 @@ import {
type ListItem
} from '../../utils/listManager';
import { ListboxItem, type ListboxItemSignature } from './item';
import { modifier } from 'ember-modifier';
import type { WithBoundArgs } from '@glint/template';

type ItemCompBounded = WithBoundArgs<typeof ListboxItem, 'manager'>;
Expand All @@ -28,8 +28,21 @@ interface ListboxSignature<T> {
class?: string;
isKeyboardEventsEnabled?: boolean;

/** The element to add keyboard events to.
*
* This does not respect the option `iskeyboardEventsEnabled`.
* @defaultValue null
*/
elementToAddKeyboardEvents?: HTMLElement;

/**
* @edefaultValue true
*/
autoActivateFirstItem?: boolean;

onAction?: (key: string) => void;
onSelectionChange?: (key: string[]) => void;
onActiveItemChange?: (key?: string) => void;

/**
* The appearance of each item
Expand All @@ -50,62 +63,113 @@ interface ListboxSignature<T> {
};
}

const isInputElement = (
target: EventTarget | null
): target is HTMLInputElement => {
return target instanceof HTMLInputElement;
};

const isUndefined = (a: unknown) => typeof a === 'undefined';

class Listbox<T = unknown> extends Component<ListboxSignature<T>> {
listManager = new ListManager({
selectionMode: this.args.selectionMode,
selectedKeys: this.args.selectedKeys,
disabledKeys: this.args.disabledKeys,
allowEmpty: this.args.allowEmpty,
onSelectionChange: this.args.onSelectionChange,
onAction: this.args.onAction
onAction: this.args.onAction,
onActiveItemChange: this.args.onActiveItemChange,
autoActivateFirstItem: isUndefined(this.args.autoActivateFirstItem)
? true
: this.args.autoActivateFirstItem
});

@action
handleKeyPress(event: KeyboardEvent) {
if (this.args.isKeyboardEventsEnabled) {
handleKeyPress = (event: KeyboardEvent) => {
if (isInputElement(event.target)) {
if (event.key === 'Enter') {
this.listManager.selectActiveItem();
return;
}
} else {
if (
event.key === 'Enter' ||
((event.key === 'Space' || event.key === ' ') &&
this.listManager.searchKeys == '')
['Enter', ' ', 'Space'].includes(event.key) &&
this.listManager.searchKeys == ''
) {
this.listManager.selectActiveItem();
event.preventDefault();
event.stopPropagation();
return;
} else if (event.key.length === 1) {
this.listManager.search(event.key);
return;
}
}
}
};

@action
handleKeyDown(event: KeyboardEvent) {
if (!this.args.isKeyboardEventsEnabled) {
return;
}
handleKeyDown = (event: KeyboardEvent) => {
if (
['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End'].includes(
event.key
)
) {
event.preventDefault();

if (event.key === 'ArrowDown') {
this.listManager.setNextOptionActive();
} else if (event.key === 'ArrowUp') {
this.listManager.setPreviousOptionActive();
} else if (event.key === 'Home' || event.key === 'PageUp') {
this.listManager.setFirstOptionActive();
} else if (event.key === 'End' || event.key === 'PageDown') {
this.listManager.setLastOptionActive();
}
}
}
};

@action
handleKeyUp(event: KeyboardEvent) {
if (!this.args.isKeyboardEventsEnabled) {
return;
onKeyPress = (event: KeyboardEvent) => {
if (this.args.isKeyboardEventsEnabled) {
this.handleKeyPress(event);
}
if (event.key === 'ArrowDown') {
this.listManager.setNextOptionActive();
} else if (event.key === 'ArrowUp') {
this.listManager.setPreviousOptionActive();
} else if (event.key === 'Home' || event.key === 'PageUp') {
this.listManager.setFirstOptionActive();
} else if (event.key === 'End' || event.key === 'PageDown') {
this.listManager.setLastOptionActive();
};

onKeyDown = (event: KeyboardEvent) => {
if (this.args.isKeyboardEventsEnabled) {
this.handleKeyDown(event);
}
}
};

setupEvents = modifier(
(
_el: HTMLElement,
_: unknown[],
args: { elementToAddKeyboardEvents?: HTMLElement }
) => {
if (args.elementToAddKeyboardEvents) {
args.elementToAddKeyboardEvents.addEventListener(
'keydown',
this.handleKeyDown
);
args.elementToAddKeyboardEvents.addEventListener(
'keypress',
this.handleKeyPress
);
}

return () => {
if (args.elementToAddKeyboardEvents) {
args.elementToAddKeyboardEvents.removeEventListener(
'keydown',
this.handleKeyDown
);
args.elementToAddKeyboardEvents.removeEventListener(
'keypress',
this.handleKeyPress
);
}
};
}
);

get classNames() {
const { listbox } = useStyles();
Expand All @@ -128,11 +192,15 @@ class Listbox<T = unknown> extends Component<ListboxSignature<T>> {
disabledKeys=@disabledKeys
selectionMode=@selectionMode
allowEmpty=@allowEmpty
isKeyboardEventsEnabled=true
autoActivateFirstItem=(if
(isUndefined @autoActivateFirstItem) true @autoActivateFirstItem
)
}}
{{on "keypress" this.onKeyPress}}
{{on "keydown" this.onKeyDown}}
{{this.setupEvents
elementToAddKeyboardEvents=@elementToAddKeyboardEvents
}}
{{on "keypress" this.handleKeyPress}}
{{on "keydown" this.handleKeyDown}}
{{on "keyup" this.handleKeyUp}}
data-test-id="listbox"
data-component="listbox"
class={{this.classNames}}
Expand Down
Loading

0 comments on commit 8ee0ddd

Please sign in to comment.