@nexxtmove/ui 1.51.0 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.vue.d.ts +44 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +583 -504
- package/dist/nuxt.js +1 -0
- package/package.json +1 -1
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.vue +213 -0
- package/src/components.json +1 -0
- package/src/index.ts +1 -0
package/dist/nuxt.js
CHANGED
|
@@ -94,6 +94,7 @@ var s = {
|
|
|
94
94
|
NexxtEditorLineHeight: "components/editor/molecules/EditorLineHeight/EditorLineHeight.vue",
|
|
95
95
|
NexxtEditorLineStyle: "components/editor/molecules/EditorLineStyle/EditorLineStyle.vue",
|
|
96
96
|
NexxtEditorMaxWidth: "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
|
|
97
|
+
NexxtEditorOverrideStatus: "components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.vue",
|
|
97
98
|
NexxtEditorPadding: "components/editor/molecules/EditorPadding/EditorPadding.vue",
|
|
98
99
|
NexxtEditorRadius: "components/editor/molecules/EditorRadius/EditorRadius.vue",
|
|
99
100
|
NexxtEditorShadow: "components/editor/molecules/EditorShadow/EditorShadow.vue",
|
package/package.json
CHANGED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface NexxtEditorOverrideStatusProps {
|
|
3
|
+
/** Whether the setting differs from where its value comes from without an override. */
|
|
4
|
+
overridden: boolean
|
|
5
|
+
/**
|
|
6
|
+
* The status while the setting is not overridden, such as `'Volgt huisstijl'`
|
|
7
|
+
* or `'Grootte 100% (origineel)'`. With `hideWhenDefault` it is not shown,
|
|
8
|
+
* but it is still announced when a reset brings the setting back.
|
|
9
|
+
*/
|
|
10
|
+
label: string
|
|
11
|
+
/**
|
|
12
|
+
* The status while the setting is overridden, shown next to a dot. Defaults
|
|
13
|
+
* to the Dutch `'Aangepast'`; a value such as `'Grootte 150%'` works too.
|
|
14
|
+
*/
|
|
15
|
+
overriddenLabel?: string
|
|
16
|
+
/**
|
|
17
|
+
* Text of the reset button, shown only while overridden, such as
|
|
18
|
+
* `'Terug naar huisstijl'`.
|
|
19
|
+
*/
|
|
20
|
+
resetLabel: string
|
|
21
|
+
/** Render no status line at all while the setting is not overridden. */
|
|
22
|
+
hideWhenDefault?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* A selector, matched inside the default slot, for the element that takes the
|
|
25
|
+
* focus after a reset. Defaults to the first focusable element in the slot.
|
|
26
|
+
* When it matches nothing focusable, the first focusable element is used, and
|
|
27
|
+
* without one the component itself takes the focus.
|
|
28
|
+
*/
|
|
29
|
+
focusTarget?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Everything that can take the focus; `tabIndex` and state are checked per element. */
|
|
33
|
+
const FOCUSABLE =
|
|
34
|
+
'a[href], button, input, select, textarea, [tabindex], [contenteditable="true"], [contenteditable=""]'
|
|
35
|
+
|
|
36
|
+
/** How long an announcement stays in the live region before it is cleared again. */
|
|
37
|
+
const ANNOUNCEMENT_MS = 1000
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script lang="ts" setup>
|
|
41
|
+
import { computed, nextTick, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
|
|
42
|
+
|
|
43
|
+
defineOptions({ name: 'NexxtEditorOverrideStatus' })
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
overridden,
|
|
47
|
+
label,
|
|
48
|
+
overriddenLabel = 'Aangepast',
|
|
49
|
+
resetLabel,
|
|
50
|
+
hideWhenDefault = false,
|
|
51
|
+
focusTarget,
|
|
52
|
+
} = defineProps<NexxtEditorOverrideStatusProps>()
|
|
53
|
+
|
|
54
|
+
const emit = defineEmits<{
|
|
55
|
+
/** The reset button was pressed. The parent clears the override. */
|
|
56
|
+
reset: []
|
|
57
|
+
}>()
|
|
58
|
+
|
|
59
|
+
defineSlots<{
|
|
60
|
+
/** The control the status belongs to, rendered above the status line. */
|
|
61
|
+
default?: () => unknown
|
|
62
|
+
}>()
|
|
63
|
+
|
|
64
|
+
const root = useTemplateRef<HTMLElement>('root')
|
|
65
|
+
const row = useTemplateRef<HTMLElement>('row')
|
|
66
|
+
|
|
67
|
+
const status = computed(() => (overridden ? overriddenLabel : label))
|
|
68
|
+
|
|
69
|
+
const showRow = computed(() => overridden || !hideWhenDefault)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether an element can take the focus right now. `tabIndex` is -1 for the
|
|
73
|
+
* cells a roving tabindex skips, so only the tab stop of such a group counts.
|
|
74
|
+
*/
|
|
75
|
+
const canFocus = (el: Element): el is HTMLElement =>
|
|
76
|
+
el instanceof HTMLElement &&
|
|
77
|
+
el.tabIndex >= 0 &&
|
|
78
|
+
!el.matches(':disabled') &&
|
|
79
|
+
!el.closest('[inert]') &&
|
|
80
|
+
// Not in jsdom; a real browser also skips elements that are not rendered.
|
|
81
|
+
(typeof el.checkVisibility !== 'function' || el.checkVisibility())
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Candidates in the slot only: the status line itself is about to go. An
|
|
85
|
+
* invalid selector matches nothing rather than throwing, so a typo in
|
|
86
|
+
* `focusTarget` cannot stop the reset.
|
|
87
|
+
*/
|
|
88
|
+
const inSlot = (selector: string) => {
|
|
89
|
+
let matches: Element[]
|
|
90
|
+
try {
|
|
91
|
+
matches = [...(root.value?.querySelectorAll(selector) ?? [])]
|
|
92
|
+
} catch {
|
|
93
|
+
// `process.env.NODE_ENV` stays in the library build, so the consumer's
|
|
94
|
+
// bundler decides whether this runs (as in NexxtTabs).
|
|
95
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
96
|
+
console.warn(
|
|
97
|
+
`[NexxtEditorOverrideStatus] \`focusTarget\` is not a valid selector: "${selector}". The focus goes to the first focusable element instead.`,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
return matches.filter((el): el is HTMLElement => !row.value?.contains(el) && canFocus(el))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The last resort: the component itself, focusable for this one move only. */
|
|
106
|
+
const focusRoot = () => {
|
|
107
|
+
const el = root.value
|
|
108
|
+
if (!el) return
|
|
109
|
+
const release = () => el.removeAttribute('tabindex')
|
|
110
|
+
el.setAttribute('tabindex', '-1')
|
|
111
|
+
el.addEventListener('blur', release, { once: true })
|
|
112
|
+
el.focus()
|
|
113
|
+
// Without the focus there is no blur to wait for: take the tabindex off now.
|
|
114
|
+
if (document.activeElement !== el) {
|
|
115
|
+
el.removeEventListener('blur', release)
|
|
116
|
+
release()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const focusControl = () => {
|
|
121
|
+
const target = (focusTarget ? inSlot(focusTarget)[0] : undefined) ?? inSlot(FOCUSABLE)[0]
|
|
122
|
+
target?.focus()
|
|
123
|
+
if (!target || document.activeElement !== target) focusRoot()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const focusIsLost = () => {
|
|
127
|
+
const active = document.activeElement
|
|
128
|
+
return !active || active === document.body || !root.value?.contains(active)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The reset button goes with the override, which would drop the focus on
|
|
133
|
+
* `body`. The focus moves to the control first, while the button still
|
|
134
|
+
* exists; after the update it is checked again, in case the reset replaced the
|
|
135
|
+
* control it landed on. The reset is emitted whatever the focus move does.
|
|
136
|
+
*/
|
|
137
|
+
const onReset = async () => {
|
|
138
|
+
try {
|
|
139
|
+
focusControl()
|
|
140
|
+
} finally {
|
|
141
|
+
emit('reset')
|
|
142
|
+
}
|
|
143
|
+
await nextTick()
|
|
144
|
+
if (focusIsLost()) focusControl()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Only the switch between overridden and not is announced, with the status
|
|
149
|
+
* that belongs to the new state. A new `overriddenLabel` within the override,
|
|
150
|
+
* such as "Grootte 110%" on every arrow key, stays silent: the control already
|
|
151
|
+
* announces its own value. And only while the focus is inside: then the switch
|
|
152
|
+
* follows from what the user just did (a reset, a change in the control).
|
|
153
|
+
* Switching to another element in the editor changes every status at once,
|
|
154
|
+
* and that stays silent. Cleared afterwards, so the copy is not read twice
|
|
155
|
+
* when browsing.
|
|
156
|
+
*/
|
|
157
|
+
const announcement = ref('')
|
|
158
|
+
let clearTimer: ReturnType<typeof setTimeout> | undefined
|
|
159
|
+
|
|
160
|
+
watch(
|
|
161
|
+
() => overridden,
|
|
162
|
+
() => {
|
|
163
|
+
if (!root.value?.contains(document.activeElement)) return
|
|
164
|
+
announcement.value = status.value
|
|
165
|
+
clearTimeout(clearTimer)
|
|
166
|
+
clearTimer = setTimeout(() => (announcement.value = ''), ANNOUNCEMENT_MS)
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
onBeforeUnmount(() => clearTimeout(clearTimer))
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<template>
|
|
174
|
+
<div
|
|
175
|
+
ref="root"
|
|
176
|
+
class="rounded-sm outline-cornflower-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
177
|
+
>
|
|
178
|
+
<!--
|
|
179
|
+
`rounded-sm` and the outline only show when the root itself holds the
|
|
180
|
+
focus: the fallback after a reset when the slot has nothing focusable.
|
|
181
|
+
-->
|
|
182
|
+
<slot />
|
|
183
|
+
|
|
184
|
+
<div
|
|
185
|
+
v-if="showRow"
|
|
186
|
+
ref="row"
|
|
187
|
+
:class="['flex items-center justify-between gap-3', $slots.default && 'pt-2']"
|
|
188
|
+
>
|
|
189
|
+
<span
|
|
190
|
+
v-if="overridden"
|
|
191
|
+
class="flex items-center gap-1.5 extra-small-normal text-cornflower-blue-700"
|
|
192
|
+
>
|
|
193
|
+
<span aria-hidden="true" class="size-1.5 shrink-0 rounded-full bg-cornflower-blue-500" />
|
|
194
|
+
{{ status }}
|
|
195
|
+
</span>
|
|
196
|
+
<span v-else class="extra-small-normal text-gray-700">{{ status }}</span>
|
|
197
|
+
|
|
198
|
+
<!-- No `focus:outline-none`: in Tailwind v4 it hides the focus-visible ring. -->
|
|
199
|
+
<button
|
|
200
|
+
v-if="overridden"
|
|
201
|
+
type="button"
|
|
202
|
+
class="shrink-0 cursor-pointer rounded-sm extra-small-link text-gray-800 hover:text-gray-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cornflower-blue-600"
|
|
203
|
+
@click="onReset"
|
|
204
|
+
>
|
|
205
|
+
{{ resetLabel }}
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
|
|
210
|
+
{{ announcement }}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</template>
|
package/src/components.json
CHANGED
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"NexxtEditorLineHeight": "components/editor/molecules/EditorLineHeight/EditorLineHeight.vue",
|
|
92
92
|
"NexxtEditorLineStyle": "components/editor/molecules/EditorLineStyle/EditorLineStyle.vue",
|
|
93
93
|
"NexxtEditorMaxWidth": "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
|
|
94
|
+
"NexxtEditorOverrideStatus": "components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.vue",
|
|
94
95
|
"NexxtEditorPadding": "components/editor/molecules/EditorPadding/EditorPadding.vue",
|
|
95
96
|
"NexxtEditorRadius": "components/editor/molecules/EditorRadius/EditorRadius.vue",
|
|
96
97
|
"NexxtEditorShadow": "components/editor/molecules/EditorShadow/EditorShadow.vue",
|
package/src/index.ts
CHANGED
|
@@ -226,6 +226,7 @@ export { default as NexxtEditorLetterSpacing } from './components/editor/molecul
|
|
|
226
226
|
export { default as NexxtEditorLineHeight } from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
|
|
227
227
|
export { default as NexxtEditorLineStyle } from './components/editor/molecules/EditorLineStyle/EditorLineStyle.vue'
|
|
228
228
|
export { default as NexxtEditorMaxWidth } from './components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue'
|
|
229
|
+
export { default as NexxtEditorOverrideStatus } from './components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.vue'
|
|
229
230
|
export { default as NexxtEditorPadding } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
|
|
230
231
|
export { default as NexxtEditorRadius } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
|
|
231
232
|
export { default as NexxtEditorSettingsBlock } from './components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|