|
|
@@ -91,6 +91,15 @@ let internalUpdatePending = false
|
|
|
|
|
|
const blocks = ref<EditorBlockData[]>([])
|
|
|
|
|
|
+const liveAnnouncement = ref("")
|
|
|
+
|
|
|
+let announceTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
+function announce(msg: string) {
|
|
|
+ liveAnnouncement.value = msg
|
|
|
+ if (announceTimer) clearTimeout(announceTimer)
|
|
|
+ announceTimer = setTimeout(() => { liveAnnouncement.value = "" }, 2000)
|
|
|
+}
|
|
|
+
|
|
|
const registry = useFocusRegistry()
|
|
|
|
|
|
const log = createLogger("Editor", props.debug)
|
|
|
@@ -445,6 +454,7 @@ function onSplit(index: number, before: string, after: string) {
|
|
|
generateBlockId,
|
|
|
)
|
|
|
if (!result.newBlock) return
|
|
|
+ announce("Block split")
|
|
|
blocks.value = result.blocks
|
|
|
onBlockContentChange()
|
|
|
|
|
|
@@ -466,6 +476,7 @@ function onSplit(index: number, before: string, after: string) {
|
|
|
function onMergePrevious(index: number) {
|
|
|
const result = opMergePrevious(blocks.value, index)
|
|
|
if (result.blocks.length >= blocks.value.length) return
|
|
|
+ announce("Merged with previous block")
|
|
|
|
|
|
const current = blocks.value[index]
|
|
|
const prev = blocks.value[index - 1]
|
|
|
@@ -508,6 +519,7 @@ function onDeleteIfEmpty(index: number) {
|
|
|
if (!result.nextId || result.blocks.length >= blocks.value.length) return
|
|
|
const removedBlock = blocks.value[index]
|
|
|
if (!removedBlock) return
|
|
|
+ announce("Block deleted")
|
|
|
|
|
|
blocks.value = result.blocks
|
|
|
onBlockContentChange()
|
|
|
@@ -672,6 +684,7 @@ function onZoneActivate(index: number, content: string) {
|
|
|
canRedo.value = history.canRedo
|
|
|
const lastInsertedId = insertedId
|
|
|
if (lastInsertedId) {
|
|
|
+ announce("Inserted block")
|
|
|
nextTick(() => registry.focus(lastInsertedId, "end"))
|
|
|
}
|
|
|
} else {
|
|
|
@@ -723,6 +736,11 @@ defineExpose({
|
|
|
|
|
|
<template>
|
|
|
<div class="editor-shell flex flex-col gap-4" :style="themeCSSVars">
|
|
|
+ <div
|
|
|
+ aria-live="polite"
|
|
|
+ aria-atomic="true"
|
|
|
+ class="sr-only"
|
|
|
+ >{{ liveAnnouncement }}</div>
|
|
|
<div v-if="$slots.toolbar" class="editor-toolbar-wrapper">
|
|
|
<slot
|
|
|
name="toolbar"
|