Skip to content

Commit 88d894e

Browse files
mainframevHotell
andauthored
feat(react-keytips): add invokeEvent prop (#251)
Co-authored-by: Martin Hochel <[email protected]>
1 parent d047927 commit 88d894e

File tree

7 files changed

+99
-19
lines changed

7 files changed

+99
-19
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: add invokeEvent prop",
4+
"packageName": "@fluentui-contrib/react-keytips",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/react-keytips/src/components/Keytips/Keytips.component-browser-spec.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,26 @@ test.describe('keytip and dynamic content update', () => {
128128
).toBeVisible();
129129
});
130130
});
131+
132+
test.describe('keytips should have invoking events', () => {
133+
test('it should invoke on keyup event', async ({ mount, page }) => {
134+
await mount(<KeytipsBasicExample invokeEvent="keyup" />);
135+
const tooltip = page.getByRole('tooltip');
136+
await page.keyboard.down('Alt');
137+
await page.keyboard.down('Meta');
138+
await expect(tooltip).toBeHidden();
139+
await page.keyboard.press('Alt+Meta');
140+
await expect(tooltip).toBeVisible();
141+
});
142+
143+
test('it should invoke on keydown event', async ({ mount, page }) => {
144+
await mount(<KeytipsBasicExample />);
145+
const tooltip = page.getByRole('tooltip');
146+
await page.keyboard.up('Alt');
147+
await page.keyboard.up('Meta');
148+
await expect(tooltip).toBeHidden();
149+
await page.keyboard.down('Alt');
150+
await page.keyboard.down('Meta');
151+
await expect(tooltip).toBeVisible();
152+
});
153+
});

packages/react-keytips/src/components/Keytips/Keytips.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,40 @@ describe('Keytips', () => {
116116
expect(screen.queryAllByRole('tooltip')).toHaveLength(0);
117117
expect(onExitKeytipsMode).toHaveBeenCalledTimes(1);
118118
});
119+
120+
it('should support different invoking event', async () => {
121+
const addEventListener = jest.spyOn(document, 'addEventListener');
122+
const removeEventListener = jest.spyOn(document, 'removeEventListener');
123+
124+
const Component = () => {
125+
const ref = useKeytipRef({
126+
keySequences: ['a'],
127+
content: 'A',
128+
});
129+
130+
return (
131+
<>
132+
{/* default is keydown */}
133+
<Keytips invokeEvent="keyup" />
134+
<Button ref={ref}>keytip A</Button>
135+
</>
136+
);
137+
};
138+
139+
const { unmount } = render(<Component />);
140+
141+
await act(async () => await userEvent.keyboard('{Alt>}{Meta}'));
142+
143+
expect(addEventListener).toHaveBeenCalledWith(
144+
'keyup',
145+
expect.any(Function)
146+
);
147+
148+
unmount();
149+
150+
expect(removeEventListener).toHaveBeenCalledWith(
151+
'keyup',
152+
expect.any(Function)
153+
);
154+
});
119155
});

packages/react-keytips/src/components/Keytips/Keytips.types.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,32 @@ export type KeytipsProps = ComponentProps<KeytipsSlots> &
2525
content?: string;
2626
/**
2727
* Key sequence that will start keytips mode
28-
* @default 'alt+meta'
28+
* @default 'alt+meta'.
2929
*/
3030
startSequence?: string;
3131
/**
3232
* Key sequences that execute the return functionality in keytips
33-
* (going back to the previous level of keytips)
33+
* (going back to the previous level of keytips).
3434
* @default 'escape'
3535
*/
3636
returnSequence?: string;
3737
/**
38-
* Key sequences that will exit keytips mode
38+
* Key sequences that will exit keytips mode.
3939
*/
4040
exitSequence?: string;
4141
/**
4242
* Callback function triggered when keytip mode is exited.
4343
*/
4444
onExitKeytipsMode?: EventHandler<OnExitKeytipsModeData>;
4545
/**
46-
* Callback function triggered when keytip mode is entered
46+
* Callback function triggered when keytip mode is entered.
4747
*/
4848
onEnterKeytipsMode?: EventHandler<OnEnterKeytipsModeData>;
49+
/**
50+
* Event responsible for invoking keytips.
51+
* @default 'keydown'
52+
* */
53+
invokeEvent?: 'keydown' | 'keyup';
4954
};
5055

5156
/**

packages/react-keytips/src/components/Keytips/useKeytips.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const useKeytips_unstable = (props: KeytipsProps): KeytipsState => {
2929
returnSequence = 'escape',
3030
onEnterKeytipsMode,
3131
onExitKeytipsMode,
32+
invokeEvent = 'keydown',
3233
} = props;
3334
const { subscribe, reset } = useEventService();
3435
const [state, dispatch] = useKeytipsState();
@@ -92,13 +93,16 @@ export const useKeytips_unstable = (props: KeytipsProps): KeytipsState => {
9293
[state.inKeytipMode]
9394
);
9495

95-
useHotkeys([
96-
[startSequence, handleEnterKeytipMode],
97-
[returnSequence, handleReturnSequence],
98-
...[exitSequence, 'tab', 'enter', 'space'].map(
99-
(key) => [key, handleExitKeytipMode] as Hotkey
100-
),
101-
]);
96+
useHotkeys(
97+
[
98+
[startSequence, handleEnterKeytipMode],
99+
[returnSequence, handleReturnSequence],
100+
...[exitSequence, 'tab', 'enter', 'space'].map(
101+
(key) => [key, handleExitKeytipMode] as Hotkey
102+
),
103+
],
104+
invokeEvent
105+
);
102106

103107
React.useEffect(() => {
104108
const handleKeytipAdded = (keytip: KeytipWithId) => {
@@ -197,7 +201,7 @@ export const useKeytips_unstable = (props: KeytipsProps): KeytipsState => {
197201
React.useEffect(() => {
198202
if (!targetDocument) return;
199203

200-
const handleKeyDown = (ev: KeyboardEvent) => {
204+
const handleInvokeEvent = (ev: KeyboardEvent) => {
201205
ev.preventDefault();
202206
ev.stopPropagation();
203207

@@ -215,9 +219,9 @@ export const useKeytips_unstable = (props: KeytipsProps): KeytipsState => {
215219
handlePartiallyMatchedNodes(currSeq);
216220
};
217221

218-
targetDocument?.addEventListener('keydown', handleKeyDown);
222+
targetDocument?.addEventListener(invokeEvent, handleInvokeEvent);
219223
return () => {
220-
targetDocument?.removeEventListener('keydown', handleKeyDown);
224+
targetDocument?.removeEventListener(invokeEvent, handleInvokeEvent);
221225
};
222226
}, [
223227
state.inKeytipMode,

packages/react-keytips/src/docs/MIGRATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This Migration guide is a work in progress and is not yet ready for use.
1111
- `keytipExitSequence` -> renamed to `exitSequence`, instead of `IKeytipTransitionKey[]`
1212
accepts a string value. Can be a single key or a combination of keys separated by "+".
1313
- `keytipStartSequence` -> renamed to `startSequence`, instead of `IKeytipTransitionKey[]`,
14-
accepts a string value (default: "alt+control"). Can be a single key or a combination of keys separated by "+".
14+
accepts a string value (default: "alt+meta (alt+win on Windows)"). Can be a single key or a combination of keys separated by "+".
1515
- `keytipReturnSequence` -> renamed to `returnSequence`, instead of `IKeytipTransitionKey[]`,
1616
accepts a string value. Can be a single key or a combination of keys separated by "+".
1717
- `styles` - Not supported.
@@ -22,6 +22,6 @@ This Migration guide is a work in progress and is not yet ready for use.
2222
- `styles` - Not supported.
2323
- `theme` - Not supported.
2424
- `disabled` - Not supported. `Keytip` won't appear for disabled target.
25-
`callOutProps` -> Not supported. Instead there are multiple props available, that has to be used individually: `positioning`, `appearance`, `visible`, `content`.
25+
`callOutProps` -> Not supported. Instead there are multiple props available, that has to be used individually: `positioning`, `visible`, `content`.
2626
- `hasDynamicChildren` and `hasMenu` - merged into `dynamic` prop. If `Keytip` triggers dynamic content: Menu, Modal, Tabs or any other item that
2727
that has keytips, set `dynamic` to `true`.

packages/react-keytips/src/hooks/useHotkeys.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
useIsomorphicLayoutEffect,
33
useFluent,
44
} from '@fluentui/react-components';
5+
import { KeytipsProps } from '../components/Keytips/Keytips.types';
56

67
type Options = { preventDefault?: boolean; stopPropagation?: boolean };
78
export type Hotkey = [string, (ev: KeyboardEvent) => void, Options?];
@@ -54,7 +55,11 @@ const isKeyMatchingKeyboardEvent = (
5455
return false;
5556
};
5657

57-
export const useHotkeys = (hotkeys: Hotkey[], target?: Document) => {
58+
export const useHotkeys = (
59+
hotkeys: Hotkey[],
60+
invokeEvent: KeytipsProps['invokeEvent'] = 'keydown',
61+
target?: Document
62+
) => {
5863
const { targetDocument } = useFluent();
5964
const doc = target ?? targetDocument;
6065

@@ -85,9 +90,9 @@ export const useHotkeys = (hotkeys: Hotkey[], target?: Document) => {
8590
);
8691
};
8792

88-
doc?.addEventListener('keydown', listener);
93+
doc?.addEventListener(invokeEvent, listener);
8994
return () => {
90-
doc?.removeEventListener('keydown', listener);
95+
doc?.removeEventListener(invokeEvent, listener);
9196
};
9297
}, [hotkeys, doc]);
9398
};

0 commit comments

Comments
 (0)