mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-16 05:03:11 +08:00
More robust keybindings.
This commit is contained in:
parent
8b2928552d
commit
8724e7d97b
@ -22,6 +22,7 @@ import {
|
||||
isCloseType,
|
||||
} from './types';
|
||||
import { searchItem } from './utils';
|
||||
import { matchesKeyCode, matchesModifiers } from './keybinding';
|
||||
|
||||
const DEFAULT_MAX_SEARCH_RESULTS = 50;
|
||||
|
||||
@ -175,9 +176,13 @@ export const Search = React.memo<{
|
||||
|
||||
useEffect(() => {
|
||||
const focusSearch = (event: KeyboardEvent) => {
|
||||
if (!enableShortcuts || isLoading || !inputRef.current) return;
|
||||
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
||||
if (event.key === '/' && inputRef.current !== document.activeElement) {
|
||||
if (!enableShortcuts || isLoading || event.repeat) return;
|
||||
if (!inputRef.current || inputRef.current === document.activeElement) return;
|
||||
if (
|
||||
// Shift is required to type `/` on some keyboard layouts
|
||||
matchesModifiers({ ctrl: false, alt: false, meta: false }, event) &&
|
||||
matchesKeyCode('Slash', event)
|
||||
) {
|
||||
inputRef.current.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
SearchResult,
|
||||
} from './types';
|
||||
import { getLink } from './utils';
|
||||
import { matchesKeyCode, matchesModifiers } from './keybinding';
|
||||
|
||||
const ResultsList = styled.ol({
|
||||
listStyle: 'none',
|
||||
@ -188,9 +189,8 @@ export const SearchResults: FunctionComponent<{
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (!enableShortcuts || isLoading) return;
|
||||
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
||||
if (event.key === 'Escape') {
|
||||
if (!enableShortcuts || isLoading || event.repeat) return;
|
||||
if (matchesModifiers(false, event) && matchesKeyCode('Escape', event)) {
|
||||
const target = event.target as Element;
|
||||
if (target?.id === 'storybook-explorer-searchfield') return; // handled by downshift
|
||||
event.preventDefault();
|
||||
|
34
lib/ui/src/components/sidebar/keybinding.ts
Normal file
34
lib/ui/src/components/sidebar/keybinding.ts
Normal file
@ -0,0 +1,34 @@
|
||||
const codeToKeyMap = {
|
||||
// event.code => event.key
|
||||
Space: ' ',
|
||||
Slash: '/',
|
||||
ArrowLeft: 'ArrowLeft',
|
||||
ArrowUp: 'ArrowUp',
|
||||
ArrowRight: 'ArrowRight',
|
||||
ArrowDown: 'ArrowDown',
|
||||
Escape: 'Escape',
|
||||
Enter: 'Enter',
|
||||
};
|
||||
|
||||
interface Modifiers {
|
||||
alt?: boolean;
|
||||
ctrl?: boolean;
|
||||
meta?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
|
||||
const allFalse = { alt: false, ctrl: false, meta: false, shift: false };
|
||||
|
||||
export const matchesModifiers = (modifiers: Modifiers | false, event: KeyboardEvent) => {
|
||||
const { alt, ctrl, meta, shift } = modifiers === false ? allFalse : modifiers;
|
||||
if (typeof alt === 'boolean' && alt !== event.altKey) return false;
|
||||
if (typeof ctrl === 'boolean' && ctrl !== event.ctrlKey) return false;
|
||||
if (typeof meta === 'boolean' && meta !== event.metaKey) return false;
|
||||
if (typeof shift === 'boolean' && shift !== event.shiftKey) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const matchesKeyCode = (code: keyof typeof codeToKeyMap, event: KeyboardEvent) => {
|
||||
// event.code is preferable but not supported in IE
|
||||
return event.code ? event.code === code : event.key === codeToKeyMap[code];
|
||||
};
|
@ -2,6 +2,7 @@ import { StoriesHash } from '@storybook/api';
|
||||
import { document } from 'global';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React, { Dispatch, MutableRefObject, useCallback, useEffect, useReducer } from 'react';
|
||||
import { matchesKeyCode, matchesModifiers } from './keybinding';
|
||||
import { Highlight } from './types';
|
||||
|
||||
import { isAncestor, getAncestorIds, getDescendantIds, scrollIntoView } from './utils';
|
||||
@ -114,9 +115,14 @@ export const useExpanded = ({
|
||||
const navigateTree = throttle((event: KeyboardEvent) => {
|
||||
const highlightedItemId =
|
||||
highlightedRef.current?.refId === refId && highlightedRef.current?.itemId;
|
||||
if (!isBrowsing || !event.key || !containerRef.current || !highlightedItemId) return;
|
||||
if (event.repeat || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
||||
if (!['Enter', ' ', 'ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
||||
if (!isBrowsing || !containerRef.current || !highlightedItemId || event.repeat) return;
|
||||
if (!matchesModifiers(false, event)) return;
|
||||
|
||||
const isEnter = matchesKeyCode('Enter', event);
|
||||
const isSpace = matchesKeyCode('Space', event);
|
||||
const isArrowLeft = matchesKeyCode('ArrowLeft', event);
|
||||
const isArrowRight = matchesKeyCode('ArrowRight', event);
|
||||
if (!(isEnter || isSpace || isArrowLeft || isArrowRight)) return;
|
||||
|
||||
const highlightedElement = getElementByDataItemId(highlightedItemId);
|
||||
if (!highlightedElement || highlightedElement.getAttribute('data-ref-id') !== refId) return;
|
||||
@ -124,20 +130,20 @@ export const useExpanded = ({
|
||||
const target = event.target as Element;
|
||||
if (!isAncestor(menuElement, target) && !isAncestor(target, menuElement)) return;
|
||||
if (target.hasAttribute('data-action')) {
|
||||
if (['Enter', ' '].includes(event.key)) return;
|
||||
if (isEnter || isSpace) return;
|
||||
(target as HTMLButtonElement).blur();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const type = highlightedElement.getAttribute('data-nodetype');
|
||||
if (['Enter', ' '].includes(event.key) && ['component', 'story', 'document'].includes(type)) {
|
||||
if ((isEnter || isSpace) && ['component', 'story', 'document'].includes(type)) {
|
||||
onSelectStoryId(highlightedItemId);
|
||||
}
|
||||
|
||||
const isExpanded = highlightedElement.getAttribute('aria-expanded');
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
if (isArrowLeft) {
|
||||
if (isExpanded === 'true') {
|
||||
// The highlighted node is expanded, so we collapse it.
|
||||
setExpanded({ ids: [highlightedItemId], value: false });
|
||||
@ -158,7 +164,7 @@ export const useExpanded = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
if (isArrowRight) {
|
||||
if (isExpanded === 'false') {
|
||||
updateExpanded({ ids: [highlightedItemId], value: true });
|
||||
} else if (isExpanded === 'true') {
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { matchesKeyCode, matchesModifiers } from './keybinding';
|
||||
|
||||
import { CombinedDataset, Highlight, Selection } from './types';
|
||||
import { cycle, isAncestor, scrollIntoView } from './utils';
|
||||
@ -79,9 +80,12 @@ export const useHighlighted = ({
|
||||
|
||||
let lastRequestId: number;
|
||||
const navigateTree = (event: KeyboardEvent) => {
|
||||
if (isLoading || !isBrowsing || !event.key || !containerRef || !containerRef.current) return;
|
||||
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
||||
if (!['ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
||||
if (isLoading || !isBrowsing || !containerRef.current) return; // allow event.repeat
|
||||
if (!matchesModifiers(false, event)) return;
|
||||
|
||||
const isArrowUp = matchesKeyCode('ArrowUp', event);
|
||||
const isArrowDown = matchesKeyCode('ArrowDown', event);
|
||||
if (!(isArrowUp || isArrowDown)) return;
|
||||
event.preventDefault();
|
||||
|
||||
const requestId = window.requestAnimationFrame(() => {
|
||||
@ -100,10 +104,8 @@ export const useHighlighted = ({
|
||||
el.getAttribute('data-item-id') === highlightedRef.current?.itemId &&
|
||||
el.getAttribute('data-ref-id') === highlightedRef.current?.refId
|
||||
);
|
||||
const nextIndex = cycle(highlightable, currentIndex, event.key === 'ArrowUp' ? -1 : 1);
|
||||
const didRunAround =
|
||||
(event.key === 'ArrowDown' && nextIndex === 0) ||
|
||||
(event.key === 'ArrowUp' && nextIndex === highlightable.length - 1);
|
||||
const nextIndex = cycle(highlightable, currentIndex, isArrowUp ? -1 : 1);
|
||||
const didRunAround = isArrowUp ? nextIndex === highlightable.length - 1 : nextIndex === 0;
|
||||
highlightElement(highlightable[nextIndex], didRunAround);
|
||||
});
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user