Skip to content

Commit 9343551

Browse files
committed
✨ add disableInitialFocus prop to prevent VS Code from stealing keyboard focus on slide entry
1 parent 5106ad9 commit 9343551

5 files changed

Lines changed: 82 additions & 31 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ livecode:
7272
zoom: 0.8
7373
```
7474
75+
## 🎯 Keyboard navigation guard
76+
77+
Use `disableInitialFocus` to prevent VS Code from stealing keyboard focus when you navigate to a slide — arrow keys keep working for Slidev navigation:
78+
79+
```md
80+
<Editor session="demo" disableInitialFocus />
81+
```
82+
83+
Focus is held on the slide for 5 seconds after VS Code loads, then released normally. The user can interact with VS Code freely after that.
84+
7585
## 🔒 Keep the session alive across navigation
7686

7787
By default, navigating away from a slide stops the session. Use `persist` to keep it running:
@@ -108,6 +118,7 @@ Per-component props override these values.
108118
| `defaultFolder` | `string` | project root | Workspace folder to open. Absolute or relative to the Slidev root. |
109119
| `colorScheme` | `'dark' \| 'light'` | auto | VS Code color theme. Defaults to Slidev's `colorSchema` if set, otherwise none. |
110120
| `fontSize` | `number` | — | Editor font size. Useful for visibility in large rooms. |
121+
| `disableInitialFocus` | `boolean` | `false` | Prevent VS Code from stealing keyboard focus on slide entry. |
111122
| `hideActivityBar` | `boolean` | `false` | Hide the VS Code activity bar (left icon sidebar). |
112123
| `hideMinimap` | `boolean` | `false` | Hide the editor minimap. |
113124
| `hideStatusBar` | `boolean` | `false` | Hide the VS Code status bar (bottom bar). |

components/Editor.vue

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, inject, onBeforeUnmount, shallowRef, watch } from 'vue'
2+
import { computed, inject, onBeforeUnmount, ref, shallowRef, useAttrs, useTemplateRef, watch } from 'vue'
33
import { useIsSlideActive, useNav, useSlideContext } from '@slidev/client'
44
55
import { requestStart, requestStop } from '../composables/useStartRequest'
@@ -8,17 +8,12 @@ import { REGISTRY_KEY } from '../setup/main'
88
import type { EditorDeckConfig, EditorProps, SessionEntry } from '../types'
99
1010
const props = withDefaults(defineProps<EditorProps>(), {
11-
colorScheme: undefined,
12-
defaultFolder: undefined,
13-
fontSize: undefined,
11+
disableInitialFocus: false,
1412
height: '100%',
1513
hideActivityBar: false,
1614
hideMinimap: false,
1715
hideStatusBar: false,
1816
persist: false,
19-
port: undefined,
20-
session: undefined,
21-
startTimeout: undefined,
2217
zoom: 1,
2318
})
2419
@@ -38,6 +33,11 @@ const { currentPage } = useNav()
3833
3934
const sessionId = props.session ?? `livecode-${currentPage.value}-${props.port ?? 'default'}`
4035
36+
const attrs = useAttrs()
37+
const isDisableInitialFocus = computed(() =>
38+
!!props.disableInitialFocus || 'disableInitialFocus' in attrs || 'disable-initial-focus' in attrs,
39+
)
40+
4141
const session = shallowRef<SessionEntry | null>(registry.get(sessionId) ?? null)
4242
4343
const isStarting = computed(
@@ -78,6 +78,7 @@ async function start(): Promise<void> {
7878
resolvedTimeout.value,
7979
)
8080
if (entry.state === 'DESTROYED') return
81+
if (isDisableInitialFocus.value) startGuard()
8182
registry!.setRunning(sessionId, url)
8283
} catch (err) {
8384
if (entry.state !== 'DESTROYED') registry!.setError(sessionId)
@@ -112,7 +113,40 @@ watch(
112113
{ flush: 'post', immediate: true },
113114
)
114115
116+
const containerRef = useTemplateRef<HTMLElement>('container')
117+
const isGuarding = ref(false)
118+
let guardTimer: ReturnType<typeof setTimeout> | null = null
119+
120+
function handleWindowBlur() {
121+
if (!isGuarding.value) return
122+
setTimeout(() => containerRef.value?.focus(), 0)
123+
}
124+
125+
function startGuard() {
126+
if (guardTimer) clearTimeout(guardTimer)
127+
isGuarding.value = true
128+
containerRef.value?.focus()
129+
window.addEventListener('blur', handleWindowBlur)
130+
guardTimer = setTimeout(() => {
131+
isGuarding.value = false
132+
window.removeEventListener('blur', handleWindowBlur)
133+
guardTimer = null
134+
}, 5000)
135+
}
136+
137+
function stopGuard() {
138+
window.removeEventListener('blur', handleWindowBlur)
139+
if (guardTimer) { clearTimeout(guardTimer); guardTimer = null }
140+
isGuarding.value = false
141+
}
142+
143+
watch(isActive, (active) => {
144+
if (!active || !isDisableInitialFocus.value) { stopGuard(); return }
145+
if (session.value?.state === 'RUNNING') startGuard()
146+
})
147+
115148
onBeforeUnmount(() => {
149+
stopGuard()
116150
if (!props.persist) {
117151
requestStop(sessionId)
118152
registry!.teardown(sessionId)
@@ -121,7 +155,7 @@ onBeforeUnmount(() => {
121155
</script>
122156

123157
<template>
124-
<div class="slidev-editor" :style="{ height }">
158+
<div ref="container" class="slidev-editor" :style="{ height }" tabindex="-1">
125159
<template v-if="!$slidev?.nav">
126160
<div class="slidev-editor-overlay">
127161
<div class="slidev-editor-overlay-title">IDE not available</div>

showcase/slides.md

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,54 +45,58 @@ livecode:
4545

4646
# 🚀 Basic usage
4747

48-
<div class="flex-1 rounded-xl overflow-hidden border border-gray-200">
49-
<Editor session="demo" />
50-
</div>
51-
52-
---
48+
Add `<Editor />` to any slide. Use `session` to identify it and `defaultFolder` to set the workspace.
5349

54-
# 📂 Specific workspace
55-
56-
<div class="flex-1 rounded-xl overflow-hidden border border-gray-200">
57-
<Editor session="my-project" defaultFolder=".." />
50+
<div class="flex-1 mt-2 rounded-xl overflow-hidden border border-gray-200">
51+
<Editor session="basic" />
5852
</div>
5953

6054
---
6155

6256
# 🔒 Persistent session
6357

64-
State is preserved when navigating away and back.
58+
Use `persist` to keep the session alive when navigating away — state is preserved when you come back.
6559

66-
<div class="flex-1 rounded-xl overflow-hidden border border-gray-200">
67-
<Editor session="demo" persist />
60+
<div class="flex-1 mt-2 rounded-xl overflow-hidden border border-gray-200">
61+
<Editor session="persistent" persist />
6862
</div>
6963

7064
---
7165

72-
# 📐 Custom height
73-
74-
<Editor session="small" height="300px" />
75-
76-
---
77-
7866
# 🔍 Zoom
7967

80-
<div class="flex-1 rounded-xl overflow-hidden border border-gray-200">
81-
<Editor session="zoomed" :zoom="0.5" />
68+
Use `:zoom` to scale down VS Code for a better fit. Can also be set globally in the frontmatter with `livecode.zoom`.
69+
70+
<div class="flex-1 mt-2 rounded-xl overflow-hidden border border-gray-200">
71+
<Editor session="zoomed" :zoom="0.7" />
8272
</div>
8373

8474
---
8575

8676
# 🌗 Color scheme
8777

88-
<div class="flex-1 rounded-xl overflow-hidden border border-gray-200">
89-
<Editor session="dark" colorScheme="dark" />
78+
Use `colorScheme` to force a dark or light theme. Follows Slidev's `colorSchema` automatically if not set.
79+
80+
<div class="flex-1 mt-2 rounded-xl overflow-hidden border border-gray-200">
81+
<Editor session="dark-theme" colorScheme="dark" />
9082
</div>
9183

9284
---
9385

9486
# 🔬 Presentation mode
9587

96-
<div class="flex-1 rounded-xl overflow-hidden border border-gray-200">
88+
Use `fontSize`, `hideMinimap`, `hideActivityBar` and `hideStatusBar` to clean up the UI for a focused demo.
89+
90+
<div class="flex-1 mt-2 rounded-xl overflow-hidden border border-gray-200">
9791
<Editor session="presentation" :fontSize="18" hideMinimap hideActivityBar hideStatusBar />
9892
</div>
93+
94+
---
95+
96+
# 🎯 Keyboard navigation
97+
98+
Use `disableInitialFocus` to prevent VS Code from stealing keyboard focus on slide entry — arrow keys keep working.
99+
100+
<div class="flex-1 mt-2 rounded-xl overflow-hidden border border-gray-200">
101+
<Editor session="focus-guard" disableInitialFocus />
102+
</div>

styles/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
background: var(--slidev-editor-bg);
1717
border: var(--slidev-editor-border);
1818
border-radius: var(--slidev-editor-border-radius);
19+
outline: none;
1920
overflow: hidden;
2021
position: relative;
2122
width: 100%;

types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type SessionState = 'IDLE' | 'STARTING' | 'RUNNING' | 'ERROR' | 'DESTROYE
33
export interface EditorProps {
44
colorScheme?: 'dark' | 'light'
55
defaultFolder?: string
6+
disableInitialFocus?: boolean
67
fontSize?: number
78
height?: string
89
hideActivityBar?: boolean

0 commit comments

Comments
 (0)