Skip to content

Commit d94ccec

Browse files
committed
fix: reset pressed hotkeys on key up
1 parent 5fb8082 commit d94ccec

File tree

17 files changed

+138
-68
lines changed

17 files changed

+138
-68
lines changed

.changeset/good-bees-pretend.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@saas-ui/use-hotkeys': patch
3+
'@saas-ui/hotkeys': patch
4+
'@saas-ui/core': patch
5+
'@saas-ui/react': patch
6+
---
7+
8+
Fixed issue where long pressed hotkeys would not reset and prevent other keys from triggering

.changeset/quick-brooms-perform.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@saas-ui/core': patch
3+
'@saas-ui/react': patch
4+
---
5+
6+
Fixed aria role of PropertyList

packages/saas-ui-core/src/link/link.test.tsx

+4-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22

3-
import { render, testStories } from '@saas-ui/test-utils'
3+
import { render } from '@saas-ui/test-utils'
44

55
import { SaasProvider } from '../provider'
66

@@ -14,17 +14,9 @@ interface LinkComponentProps {
1414
const LinkComponent: React.FC<LinkComponentProps> = (props) => {
1515
const { children, href, ...rest } = props
1616
return (
17-
<>
18-
{React.Children.map(children, (child) => {
19-
if (React.isValidElement<any>(child)) {
20-
return React.cloneElement(child, {
21-
href,
22-
className: 'saas-ui-link',
23-
...rest,
24-
})
25-
}
26-
})}
27-
</>
17+
<a href={href} {...rest} className="saas-ui-link">
18+
{children}
19+
</a>
2820
)
2921
}
3022

packages/saas-ui-core/src/navbar/navbar.stories.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,16 @@ const WithMenuTemplate = (args: NavbarProps) => {
181181
<NavbarItem>
182182
<Button variant="primary">Sign Up</Button>
183183
</NavbarItem>
184-
<Button
185-
aria-label={mobileNav.isOpen ? 'Close menu' : 'Open menu'}
186-
display={{ base: 'inline-flex', sm: 'none' }}
187-
onClick={mobileNav.onToggle}
188-
variant="ghost"
189-
>
190-
{mobileNav.isOpen ? <FiX /> : <FiMenu />}
191-
</Button>
184+
<NavbarItem>
185+
<Button
186+
aria-label={mobileNav.isOpen ? 'Close menu' : 'Open menu'}
187+
display={{ base: 'inline-flex', sm: 'none' }}
188+
onClick={mobileNav.onToggle}
189+
variant="ghost"
190+
>
191+
{mobileNav.isOpen ? <FiX /> : <FiMenu />}
192+
</Button>
193+
</NavbarItem>
192194
</NavbarContent>
193195
<Drawer {...mobileNav}>
194196
<DrawerOverlay />

packages/saas-ui-core/src/navbar/navbar.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@ describe('Navbar', () => {
4444

4545
const navbarContent = wrapper.getByTestId('navbar-content-test')
4646

47-
expect(navbarContent.children.length).toBe(5)
47+
expect(navbarContent.children.length).toBe(3)
4848
})
4949
})

packages/saas-ui-core/src/property/property.stories.tsx

+29-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import * as React from 'react'
2-
import { Story, Meta } from '@storybook/react'
2+
import { StoryObj, Meta } from '@storybook/react'
33

44
import { Container, Avatar } from '@chakra-ui/react'
55

6-
import { PropertyList, Property, PropertyLabel, PropertyValue } from '.'
6+
import {
7+
PropertyList,
8+
Property,
9+
PropertyLabel,
10+
PropertyValue,
11+
PropertyProps,
12+
} from '.'
713

814
import { Persona } from '../persona'
9-
import { Select } from '@saas-ui/forms'
15+
import { Select, SelectButton, SelectList } from '@saas-ui/forms'
1016

1117
export default {
1218
title: 'Components/Data Display/Property',
@@ -20,27 +26,36 @@ export default {
2026
],
2127
} as Meta
2228

23-
const Template: Story = (args) => <Property {...args} />
29+
type Story = StoryObj<PropertyProps>
2430

25-
export const Basic = Template.bind({})
26-
Basic.args = {
27-
value: 'Name',
28-
label: 'Elliot Alderson',
31+
export const Basic = {
32+
args: {
33+
value: 'Name',
34+
label: 'Elliot Alderson',
35+
},
2936
}
3037

31-
export const LabelWidth = Template.bind({})
32-
LabelWidth.args = {
33-
value: 'Name',
34-
label: 'Elliot Alderson',
35-
labelWidth: '60px',
38+
export const LabelWidth = {
39+
args: {
40+
value: 'Name',
41+
label: 'Elliot Alderson',
42+
labelWidth: '60px',
43+
},
3644
}
3745

3846
export const Composed = () => {
3947
return (
4048
<Property>
4149
<PropertyLabel justifyContent="flex-end">Status</PropertyLabel>
4250
<PropertyValue>
43-
<Select value="New" options={[{ value: 'New' }, { value: 'Open' }]} />
51+
<Select
52+
name="status"
53+
value="New"
54+
options={[{ value: 'New' }, { value: 'Open' }]}
55+
>
56+
<SelectButton />
57+
<SelectList />
58+
</Select>
4459
</PropertyValue>
4560
</Property>
4661
)

packages/saas-ui-core/src/property/property.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -159,19 +159,15 @@ export interface PropertyListProps extends Omit<ListProps, 'items'> {}
159159
export const PropertyList: React.FC<PropertyListProps> = (props) => {
160160
const { children, ...rest } = props
161161
return (
162-
<List
163-
as="dl"
164-
{...rest}
165-
className={cx('sui-property-list', props.className)}
166-
>
162+
<chakra.dl {...rest} className={cx('sui-property-list', props.className)}>
167163
{React.Children.map(children, (child) =>
168164
React.isValidElement<PropertyProps>(child)
169165
? React.cloneElement(child, {
170166
as: 'div',
171167
})
172168
: child
173169
)}
174-
</List>
170+
</chakra.dl>
175171
)
176172
}
177173

packages/saas-ui-core/src/structured-list/list.stories.tsx

+2-11
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ export const StickyHeaders: Story = {
582582
const inProgress = issues.filter(({ status }) => status === 'in-progress')
583583
const todo = issues.filter(({ status }) => status === 'todo')
584584

585-
const renderIssue = (issue) => {
585+
const renderIssue = (issue: any) => {
586586
return (
587587
<StructuredListItem
588588
href="#"
@@ -593,21 +593,12 @@ export const StickyHeaders: Story = {
593593
borderColor: 'whiteAlpha.100',
594594
}}
595595
>
596-
<StructuredListCell width="4" role="group" px="0">
597-
<Checkbox
598-
opacity="0"
599-
_checked={{ opacity: 1 }}
600-
_groupHover={{ opacity: 1 }}
601-
size="md"
602-
rounded="sm"
603-
/>
604-
</StructuredListCell>
605596
<StructuredListCell color="muted">{issue.id}</StructuredListCell>
606597
<StructuredListCell flex="1">
607598
<Text noOfLines={1}>{issue.title}</Text>
608599
</StructuredListCell>
609600
<StructuredListCell color="muted" as={HStack}>
610-
{issue.labels.map((label) => (
601+
{issue.labels.map((label: any) => (
611602
<Tag
612603
key={label}
613604
bg="none"

packages/saas-ui-core/src/structured-list/list.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import * as React from 'react'
33
import { render, testStories } from '@saas-ui/test-utils'
44
import * as stories from './list.stories'
55

6-
const { CustomStyles, Nested, ...rest } = stories
6+
const { CustomStyles, WithCheckbox, WithRadio, Nested, ...rest } = stories
77

88
testStories<typeof rest>(rest)

packages/saas-ui-forms/src/number-input/number-input.test.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ import * as React from 'react'
33
import { render, testStories } from '@saas-ui/test-utils'
44
import * as stories from './number-input.stories'
55

6-
testStories<typeof stories>(stories)
6+
testStories<typeof stories>(stories, {
7+
a11y: false,
8+
})
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as React from 'react'
22

3-
import { render, testStories } from '@saas-ui/test-utils'
3+
import { testStories } from '@saas-ui/test-utils'
44
import * as stories from './select.stories'
55

66
const { MaxHeight, ...rest } = stories
77

8-
testStories<typeof rest>(rest)
8+
testStories(rest, {
9+
a11y: false,
10+
})

packages/saas-ui-hotkeys/stories/hotkeys.stories.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,25 @@ export const PressAndHold = () => {
312312
return <Box>{presses} presses</Box>
313313
}
314314

315+
export const PressAndHoldRelease = () => {
316+
const [presses, setPresses] = React.useState(0)
317+
useHotkeys(
318+
'Cmd+Z',
319+
() => {
320+
setPresses((prev) => prev + 1)
321+
},
322+
{
323+
preventDefault: true,
324+
}
325+
)
326+
327+
useHotkeys('ArrowUp', () => {
328+
setPresses(0)
329+
})
330+
331+
return <Box>{presses} presses (Arrow up to reset)</Box>
332+
}
333+
315334
export const WithHotkey = () => {
316335
const searchRef = React.useRef<HTMLInputElement>(null)
317336

packages/saas-ui-hotkeys/test/hotkeys.test.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react'
22
import { render } from '@saas-ui/test-utils'
3+
import { vi } from 'vitest'
34

45
import {
56
useHotkeys,
@@ -36,7 +37,7 @@ const renderModal = (ui: React.ReactNode) => {
3637
}
3738

3839
test('should trigger hotkey shortcuts.', async () => {
39-
const action = jest.fn()
40+
const action = vi.fn()
4041
const TestComponent = () => {
4142
const command = useHotkeysShortcut('general.compose', action)
4243

@@ -54,7 +55,7 @@ test('should trigger hotkey shortcuts.', async () => {
5455
})
5556

5657
test('should trigger shifted keys.', async () => {
57-
const action = jest.fn()
58+
const action = vi.fn()
5859
const TestComponent = () => {
5960
useHotkeysShortcut('general.help', action)
6061
return null
@@ -68,7 +69,7 @@ test('should trigger shifted keys.', async () => {
6869
})
6970

7071
test('should trigger key combinations.', async () => {
71-
const action = jest.fn()
72+
const action = vi.fn()
7273
const TestComponent = () => {
7374
useHotkeysShortcut('general.logout', action)
7475
return null
@@ -82,7 +83,7 @@ test('should trigger key combinations.', async () => {
8283
})
8384

8485
test('should trigger key sequences.', async () => {
85-
const action = jest.fn()
86+
const action = vi.fn()
8687
const TestComponent = () => {
8788
useHotkeysShortcut('general.dashboard', action)
8889
return null
@@ -97,7 +98,7 @@ test('should trigger key sequences.', async () => {
9798
})
9899

99100
test('should trigger custom hotkeys.', async () => {
100-
const action = jest.fn()
101+
const action = vi.fn()
101102
const TestComponent = () => {
102103
useHotkeys('c', action)
103104
return null
@@ -111,7 +112,7 @@ test('should trigger custom hotkeys.', async () => {
111112
})
112113

113114
test('should support multiple key combinations.', async () => {
114-
const action = jest.fn()
115+
const action = vi.fn()
115116
const TestComponent = () => {
116117
useHotkeys(['c', 'G then C'], action)
117118
return null

packages/saas-ui-use-hotkeys/src/use-hotkeys.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const keysMatch = (
103103

104104
const getKeyFromEvent = (event: KeyboardEvent): string => {
105105
const key = event.key.toLowerCase()
106+
106107
if (shiftedKeys[key]) {
107108
return shiftedKeys[key]
108109
}
@@ -193,6 +194,7 @@ export const useHotkeys = (
193194
}
194195

195196
function onKeyDown(event: Event) {
197+
console.log(event)
196198
if (isInputEvent(event as KeyboardEvent)) {
197199
return
198200
}
@@ -206,6 +208,7 @@ export const useHotkeys = (
206208
clearTimeout(bufferTimeout.current)
207209
bufferTimeout.current = null
208210
}
211+
209212
bufferTimeout.current = setTimeout(() => {
210213
bufferKeys.clear()
211214
}, 400)
@@ -224,11 +227,8 @@ export const useHotkeys = (
224227
}
225228

226229
function onKeyUp(event: Event) {
227-
if (isInputEvent(event as KeyboardEvent)) {
228-
pressedKeys.clear()
229-
return
230-
}
231-
pressedKeys.delete(getKeyFromEvent(event as KeyboardEvent))
230+
// reset all keys on keyup
231+
pressedKeys.clear()
232232
}
233233

234234
function onWindowBlur() {

packages/saas-ui-use-hotkeys/tests/hotkeys.test.tsx renamed to packages/saas-ui-use-hotkeys/tests/use-hotkeys.test.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,24 @@ test('should support multiple key combinations.', async () => {
132132
expect(action).toBeCalledTimes(2)
133133
})
134134

135+
test('should reset after modifier keys.', async () => {
136+
const action = vi.fn()
137+
const action2 = vi.fn()
138+
const TestComponent = () => {
139+
useHotkeys('ctrl+z', action)
140+
useHotkeys('ArrowUp', action2)
141+
return null
142+
}
143+
144+
const { user } = renderModal(<TestComponent />)
145+
146+
await user.keyboard('{Control>}z')
147+
148+
await user.keyboard('[ArrowUp]')
149+
150+
expect(action2).toBeCalled()
151+
})
152+
135153
test('Hotkey should trigger hotkey shortcuts.', async () => {
136154
const action = vi.fn()
137155
const TestComponent = () => {

vitest.config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { defineConfig } from 'vitest/config'
22

33
export default defineConfig({
44
test: {
5-
include: ['packages/saas-ui-*/tests/**/*.test.{ts,tsx}'],
5+
setupFiles: 'vitest.setup.ts',
6+
include: ['packages/saas-ui-*/**/*.test.{ts,tsx}'],
67
globals: true,
78
environment: 'jsdom',
89
},

0 commit comments

Comments
 (0)