Skip to content

Commit 2d5198b

Browse files
Feature/kbar (#1161)
* Added CommandBar component for kbar * Added Kbar results section * Kbar init integration * Kbar clean up * Kbar clean up * Kbar clean up * More Kbar clean up - use internal_shortcut to show we're not using kbar shortcut integration * Kbar clean up, tried to optimize kbar results * clean up * little refactor * Added test for platform-key util * Added test for use-modes * pnpm * Fixing tests * Fixing tests * oof * Add some test clean up * Clean up * Added some comments, and added back to default mode text * added changeset * Update .changeset/long-suns-smoke.md Co-authored-by: Ryan Roemer <ryan.roemer@formidable.com> Co-authored-by: Ryan Roemer <ryan.roemer@formidable.com>
1 parent d9e35e7 commit 2d5198b

File tree

21 files changed

+737
-122
lines changed

21 files changed

+737
-122
lines changed

.changeset/long-suns-smoke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'spectacle': minor
3+
---
4+
5+
Utilize `Kbar` to allow users to quickly search and use the current commands Spectacle supports within presentations. Fixes #1115

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
strict-peer-dependencies=false
12
prefer-workspace-packages=true

packages/spectacle/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"broadcastchannel-polyfill": "^1.0.0",
4141
"dedent": "^0.7.0",
4242
"history": "^4.9.0",
43+
"kbar": "0.1.0-beta.36",
4344
"mdast-builder": "^1.1.1",
4445
"mdast-zone": "^4.0.0",
4546
"merge-anything": "^3.0.3",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
KEYBOARD_SHORTCUTS_IDS,
3+
SpectacleMode,
4+
SPECTACLE_MODES
5+
} from '../../utils/constants';
6+
import useModes from '../../hooks/use-modes';
7+
8+
/**
9+
* Kbar default actions, those that do not depend on dynamic logic, can be added here.
10+
* To register actions dynamically use 'useRegisterActions' and make sure the action
11+
* is registed within the KBarProvider.
12+
* @see https://kbar.vercel.app/docs/concepts/actions
13+
* Kbar action shortcuts dont seem to support all keybindings. If you need to utilize
14+
* keybindings that are not supported you'll have to implement the keybinding seperately.
15+
* @see useMousetrap
16+
* To display keybindings that are not supported in the Kbar results, please use
17+
* KEYBOARD_SHORTCUTS instead of Kbar actions 'shortcut' property.
18+
* @see CommandBarResults getShortcutKeys
19+
*/
20+
21+
const spectacleModeDisplay = {
22+
[SPECTACLE_MODES.DEFAULT_MODE]: 'Default Mode',
23+
[SPECTACLE_MODES.PRESENTER_MODE]: 'Presenter Mode',
24+
[SPECTACLE_MODES.OVERVIEW_MODE]: 'Overview Mode',
25+
[SPECTACLE_MODES.PRINT_MODE]: 'Print Mode',
26+
[SPECTACLE_MODES.EXPORT_MODE]: 'Export Mode'
27+
};
28+
29+
const getName = (currentMode: string, mode: SpectacleMode) => {
30+
const defaultMode = SPECTACLE_MODES.DEFAULT_MODE;
31+
32+
return currentMode === mode
33+
? `← Back to ${spectacleModeDisplay[defaultMode]}`
34+
: spectacleModeDisplay[mode];
35+
};
36+
37+
const useCommandBarActions = () => {
38+
const { toggleMode, getCurrentMode } = useModes();
39+
const currentMode = getCurrentMode();
40+
return [
41+
{
42+
id: KEYBOARD_SHORTCUTS_IDS.PRESENTER_MODE,
43+
name: getName(currentMode, SPECTACLE_MODES.PRESENTER_MODE),
44+
keywords: 'presenter',
45+
perform: () => toggleMode({ newMode: SPECTACLE_MODES.PRESENTER_MODE }),
46+
section: 'Mode'
47+
},
48+
{
49+
id: KEYBOARD_SHORTCUTS_IDS.OVERVIEW_MODE,
50+
name: getName(currentMode, SPECTACLE_MODES.OVERVIEW_MODE),
51+
keywords: 'overview',
52+
perform: () => toggleMode({ newMode: SPECTACLE_MODES.OVERVIEW_MODE }),
53+
section: 'Mode'
54+
},
55+
{
56+
id: KEYBOARD_SHORTCUTS_IDS.PRINT_MODE,
57+
name: getName(currentMode, SPECTACLE_MODES.PRINT_MODE),
58+
keywords: 'export',
59+
perform: () => toggleMode({ newMode: SPECTACLE_MODES.PRINT_MODE }),
60+
section: 'Mode'
61+
},
62+
{
63+
id: KEYBOARD_SHORTCUTS_IDS.EXPORT_MODE,
64+
name: getName(currentMode, SPECTACLE_MODES.EXPORT_MODE),
65+
keywords: 'export',
66+
perform: () => toggleMode({ newMode: SPECTACLE_MODES.EXPORT_MODE }),
67+
section: 'Mode'
68+
}
69+
];
70+
};
71+
72+
export default useCommandBarActions;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ReactNode } from 'react';
2+
import { KBarProvider } from 'kbar';
3+
import useCommandBarActions from './command-bar-actions';
4+
import CommandBarSearch from './search';
5+
6+
const CommandBar = ({ children }: CommandBarProps): JSX.Element => {
7+
const actions = useCommandBarActions();
8+
return (
9+
<KBarProvider actions={actions}>
10+
<CommandBarSearch />
11+
{children}
12+
</KBarProvider>
13+
);
14+
};
15+
16+
export type CommandBarProps = {
17+
children: ReactNode;
18+
};
19+
20+
export default CommandBar;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import styled from 'styled-components';
2+
import { ActionImpl, KBarResults, useMatches } from 'kbar';
3+
import { prettifyShortcut } from '../../../utils/platform-keys';
4+
import {
5+
KeyboardShortcutTypes,
6+
KEYBOARD_SHORTCUTS,
7+
SYSTEM_FONT
8+
} from '../../../utils/constants';
9+
import { Text } from '../../typography';
10+
11+
type RenderParams = {
12+
item: ActionImpl | string;
13+
active: boolean;
14+
};
15+
16+
function getShortcutKeys({ id, shortcut = [] }: ActionImpl): string[] {
17+
if (id in KEYBOARD_SHORTCUTS && !shortcut?.length) {
18+
const _id = id as KeyboardShortcutTypes;
19+
return prettifyShortcut(KEYBOARD_SHORTCUTS[_id]);
20+
}
21+
return prettifyShortcut(shortcut);
22+
}
23+
24+
const ResultCommand = styled.div<Partial<RenderParams>>`
25+
display: flex;
26+
justify-content: space-between;
27+
align-items: center;
28+
background-color: ${(p) => (p.active ? 'lightsteelblue' : 'transparent')};
29+
padding: 0.5rem 1rem;
30+
cursor: pointer;
31+
height: 30px;
32+
`;
33+
34+
const ResultSectionHeader = styled(Text)`
35+
background-color: white;
36+
color: gray;
37+
margin: 0 2rem;
38+
padding: 0.5rem 0;
39+
font-size: small;
40+
font-weight: bold;
41+
font-family: ${SYSTEM_FONT};
42+
`;
43+
44+
const ResultShortcut = styled.span`
45+
display: flex;
46+
gap: 5px;
47+
`;
48+
49+
const ResultShortcutKey = styled.kbd`
50+
display: flex;
51+
justify-content: center;
52+
align-items: center;
53+
background-color: #eee;
54+
border-radius: 5px;
55+
border: 1px solid #b4b4b4;
56+
padding: 5px 10px;
57+
min-width: 20px;
58+
height: 25px;
59+
white-space: nowrap;
60+
font-family: ${SYSTEM_FONT};
61+
`;
62+
63+
function onRender({ item, active }: RenderParams) {
64+
if (typeof item === 'string') {
65+
return <ResultSectionHeader>{item}</ResultSectionHeader>;
66+
} else {
67+
return (
68+
<ResultCommand active={active}>
69+
<Text fontFamily={SYSTEM_FONT}>{item.name}</Text>
70+
<ResultShortcut>
71+
{getShortcutKeys(item).map(
72+
(key) =>
73+
key && (
74+
<ResultShortcutKey key={`${item.id}-${key}`}>
75+
{key}
76+
</ResultShortcutKey>
77+
)
78+
)}
79+
</ResultShortcut>
80+
</ResultCommand>
81+
);
82+
}
83+
}
84+
85+
const CommandBarResults = () => {
86+
const { results } = useMatches();
87+
return <KBarResults items={results} onRender={onRender} />;
88+
};
89+
90+
export default CommandBarResults;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import styled from 'styled-components';
2+
import { KBarPortal, KBarPositioner, KBarAnimator, KBarSearch } from 'kbar';
3+
import CommandBarResults from '../results';
4+
5+
const KBarSearchStyled = styled(KBarSearch)`
6+
padding: 12px 16px;
7+
font-size: 16px;
8+
width: 100%;
9+
box-sizing: border-box;
10+
outline: none;
11+
border: none;
12+
`;
13+
14+
const KBarAnimatorStyled = styled(KBarAnimator)`
15+
max-width: 600px;
16+
width: 100%;
17+
background: white;
18+
border-radius: 8px;
19+
overflow: hidden;
20+
box-shadow: rgb(0 0 0 / 50%) 0px 16px 70px;
21+
`;
22+
23+
const CommandBarSearch = () => {
24+
return (
25+
<KBarPortal>
26+
<KBarPositioner>
27+
<KBarAnimatorStyled>
28+
<KBarSearchStyled />
29+
<CommandBarResults />
30+
</KBarAnimatorStyled>
31+
</KBarPositioner>
32+
</KBarPortal>
33+
);
34+
};
35+
36+
export default CommandBarSearch;

packages/spectacle/src/components/deck/deck.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { defaultTransition, SlideTransition } from '../transitions';
3838
import { SwipeEventData } from 'react-swipeable';
3939
import { MarkdownComponentMap } from '../../utils/mdx-component-mapper';
4040
import TemplateWrapper from '../template-wrapper';
41+
import { useRegisterActions } from 'kbar';
42+
import { KEYBOARD_SHORTCUTS_IDS } from '../../utils/constants';
4143

4244
export type DeckContextType = {
4345
deckId: string | number;
@@ -66,6 +68,7 @@ export type DeckContextType = {
6668
};
6769
skipTo(options: { slideIndex: number; stepIndex: number }): void;
6870
stepForward(): void;
71+
stepBackward(): void;
6972
advanceSlide(): void;
7073
regressSlide(): void;
7174
commitTransition(newView?: { stepIndex: number }): void;
@@ -217,6 +220,37 @@ export const DeckInternal = forwardRef<DeckRef, DeckInternalProps>(
217220
]
218221
);
219222

223+
useRegisterActions(
224+
!disableInteractivity
225+
? [
226+
{
227+
id: KEYBOARD_SHORTCUTS_IDS.NEXT_SLIDE,
228+
name: 'Next Slide',
229+
keywords: 'next',
230+
perform: () => stepForward(),
231+
section: 'Slide'
232+
},
233+
{
234+
id: KEYBOARD_SHORTCUTS_IDS.PREVIOUS_SLIDE,
235+
name: 'Previous Slide',
236+
keywords: 'previous',
237+
perform: () => stepBackward(),
238+
section: 'Slide'
239+
},
240+
{
241+
id: 'Restart Presentation',
242+
name: 'Restart Presentation',
243+
keywords: 'restart',
244+
perform: () =>
245+
skipTo({
246+
slideIndex: 0,
247+
stepIndex: 0
248+
}),
249+
section: 'Slide'
250+
}
251+
]
252+
: []
253+
);
220254
useMousetrap(
221255
disableInteractivity
222256
? {}
@@ -441,6 +475,7 @@ export const DeckInternal = forwardRef<DeckRef, DeckInternalProps>(
441475
},
442476
skipTo,
443477
stepForward,
478+
stepBackward,
444479
advanceSlide,
445480
regressSlide,
446481
commitTransition,

packages/spectacle/src/components/deck/default-deck.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import useMousetrap from '../../hooks/use-mousetrap';
55
import {
66
KEYBOARD_SHORTCUTS,
77
SPECTACLE_MODES,
8-
SpectacleMode
8+
ToggleModeParams
99
} from '../../utils/constants';
1010

1111
/**
@@ -51,7 +51,9 @@ const DefaultDeck = (props: DefaultDeckProps): JSX.Element => {
5151
stepIndex: 0
5252
}),
5353
[KEYBOARD_SHORTCUTS.SELECT_SLIDE_OVERVIEW_MODE]: (e) =>
54-
toggleMode(e, SPECTACLE_MODES.DEFAULT_MODE)
54+
toggleMode({
55+
newMode: SPECTACLE_MODES.DEFAULT_MODE
56+
})
5557
}
5658
: {},
5759
[]
@@ -62,7 +64,11 @@ const DefaultDeck = (props: DefaultDeckProps): JSX.Element => {
6264
>(
6365
(e, slideIndex) => {
6466
if (overviewMode) {
65-
toggleMode(e, SPECTACLE_MODES.DEFAULT_MODE, +slideIndex);
67+
toggleMode({
68+
e,
69+
newMode: SPECTACLE_MODES.DEFAULT_MODE,
70+
senderSlideIndex: +slideIndex
71+
});
6672
}
6773
},
6874
[overviewMode, toggleMode]
@@ -98,11 +104,7 @@ const DefaultDeck = (props: DefaultDeckProps): JSX.Element => {
98104
export default DefaultDeck;
99105

100106
type DefaultDeckProps = DeckProps & {
101-
toggleMode(
102-
e: unknown,
103-
newMode: SpectacleMode,
104-
senderSlideIndex?: number
105-
): void;
107+
toggleMode(args: ToggleModeParams): void;
106108
overviewMode?: boolean;
107109
printMode?: boolean;
108110
exportMode?: boolean;

0 commit comments

Comments
 (0)