Skip to content

Commit 18cbe47

Browse files
committed
cmk-frontend-vue: extract a generic InlineEditPill component
Pull the pill chrome, the collapsed/expanded toggle, the optional close button and the click-outside/Escape lifecycle out of AttributeFilterPill into a reusable, slot-based InlineEditPill, and migrate AttributeFilterPill onto it. The shell stays agnostic of what it edits: content comes through the read-only and edit slots, validation vetoing is delegated via a canLeave guard, and the focus-navigation marker attributes are configurable, so the attribute-filter behavior is preserved unchanged. Change-Id: Iee5b58377a930756f7e253a026ddfeb7e60ed2e2
1 parent 99b7b14 commit 18cbe47

3 files changed

Lines changed: 402 additions & 166 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<!--
2+
Copyright (C) 2026 Checkmk GmbH - License: Checkmk Enterprise License
3+
This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
4+
conditions defined in the file COPYING, which is part of this source code package.
5+
-->
6+
7+
<script setup lang="ts">
8+
import { nextTick, useTemplateRef, watch } from 'vue'
9+
10+
import CmkIconButton from '@/components/CmkIconButton.vue'
11+
12+
import useInlineEdit, { type InlineEditLeaveReason } from './useInlineEdit'
13+
14+
// A pill toggling between a collapsed read-only summary and inline edit controls, both supplied via the `read-only` and `edit` slots.
15+
const props = withDefaults(
16+
defineProps<{
17+
editing?: boolean
18+
removable?: boolean
19+
tabFocusable?: boolean
20+
ariaLabel?: string | undefined
21+
title?: string | undefined
22+
editAriaLabel?: string | undefined
23+
removeLabel?: string | undefined
24+
/** Veto leaving edit mode; returning `false` keeps the pill open. */
25+
canLeave?: (reason: InlineEditLeaveReason) => boolean
26+
/** Attribute marking the edit pane as a focus-navigation scope. */
27+
scopeMarkerAttr: string
28+
/** Attribute marking focus-navigation participants within that scope. */
29+
itemMarkerAttr: string
30+
}>(),
31+
{
32+
editing: false,
33+
removable: false,
34+
tabFocusable: true,
35+
ariaLabel: undefined,
36+
title: undefined,
37+
editAriaLabel: undefined,
38+
removeLabel: undefined,
39+
canLeave: () => true
40+
}
41+
)
42+
43+
const emit = defineEmits<{
44+
(e: 'edit'): void
45+
(e: 'remove'): void
46+
(e: 'done', reason: InlineEditLeaveReason): void
47+
}>()
48+
49+
const closedPillRef = useTemplateRef<HTMLElement>('closedPillRef')
50+
const editPaneRef = useTemplateRef<HTMLElement>('editPaneRef')
51+
52+
// Escape returns focus to the collapsed pill; click-outside commits without moving focus.
53+
let returnFocusToClosedPill = false
54+
55+
function onLeave(reason: InlineEditLeaveReason): void {
56+
if (!props.canLeave(reason)) {
57+
return
58+
}
59+
if (reason === 'escape') {
60+
returnFocusToClosedPill = true
61+
}
62+
emit('done', reason)
63+
}
64+
65+
const { vClickOutside, onOutsideClick, onEscapeCapture, onEscape } = useInlineEdit({
66+
isOpen: () => props.editing,
67+
paneRef: editPaneRef,
68+
onLeave
69+
})
70+
71+
watch(
72+
() => props.editing,
73+
(now) => {
74+
if (!now && returnFocusToClosedPill) {
75+
returnFocusToClosedPill = false
76+
void nextTick(() => closedPillRef.value?.focus())
77+
}
78+
}
79+
)
80+
81+
defineExpose({
82+
focus: () => {
83+
closedPillRef.value?.focus()
84+
}
85+
})
86+
</script>
87+
88+
<template>
89+
<span
90+
class="metric-backend-inline-edit-pill"
91+
:class="{ 'metric-backend-inline-edit-pill--editing': editing }"
92+
:aria-label="ariaLabel"
93+
role="group"
94+
>
95+
<span
96+
v-if="editing"
97+
ref="editPaneRef"
98+
v-click-outside="onOutsideClick"
99+
class="metric-backend-inline-edit-pill__edit"
100+
:[scopeMarkerAttr]="''"
101+
:title="title"
102+
@keydown.tab.capture.stop
103+
@keydown.esc.capture="onEscapeCapture"
104+
@keydown.esc.stop="onEscape"
105+
>
106+
<slot name="edit" />
107+
<CmkIconButton
108+
v-if="removable"
109+
:[itemMarkerAttr]="''"
110+
class="metric-backend-inline-edit-pill__remove"
111+
name="close"
112+
size="small"
113+
:title="removeLabel"
114+
:aria-label="removeLabel"
115+
@mousedown.prevent
116+
@click.stop="emit('remove')"
117+
/>
118+
</span>
119+
<span
120+
v-else
121+
ref="closedPillRef"
122+
:[itemMarkerAttr]="''"
123+
class="metric-backend-inline-edit-pill__closed"
124+
:tabindex="tabFocusable ? 0 : -1"
125+
@keydown.enter.prevent="emit('edit')"
126+
@keydown.space.prevent="emit('edit')"
127+
@keydown.delete.prevent="emit('remove')"
128+
>
129+
<button
130+
type="button"
131+
class="metric-backend-inline-edit-pill__main"
132+
tabindex="-1"
133+
:title="title"
134+
:aria-label="editAriaLabel"
135+
@mousedown.prevent
136+
@click.stop="emit('edit')"
137+
@keydown.delete.prevent="emit('remove')"
138+
>
139+
<slot name="read-only" />
140+
</button>
141+
<CmkIconButton
142+
v-if="removable"
143+
class="metric-backend-inline-edit-pill__remove"
144+
name="close"
145+
size="small"
146+
tabindex="-1"
147+
:title="removeLabel"
148+
:aria-label="removeLabel"
149+
@mousedown.prevent
150+
@click.stop="emit('remove')"
151+
/>
152+
</span>
153+
</span>
154+
</template>
155+
156+
<style scoped>
157+
.metric-backend-inline-edit-pill {
158+
display: inline-flex;
159+
align-items: stretch;
160+
background: var(--default-form-element-bg-color);
161+
border: 1px solid var(--ux-theme-4);
162+
padding-right: var(--dimension-3);
163+
white-space: nowrap;
164+
}
165+
166+
.metric-backend-inline-edit-pill:not(.metric-backend-inline-edit-pill--editing):hover {
167+
background-color: var(--input-hover-bg-color);
168+
}
169+
170+
.metric-backend-inline-edit-pill--editing {
171+
background: var(--ux-theme-3);
172+
}
173+
174+
.metric-backend-inline-edit-pill__edit {
175+
display: inline-flex;
176+
}
177+
178+
.metric-backend-inline-edit-pill__closed {
179+
display: inline-flex;
180+
align-items: stretch;
181+
}
182+
183+
.metric-backend-inline-edit-pill__closed:focus-visible {
184+
outline: revert;
185+
}
186+
187+
.metric-backend-inline-edit-pill__main {
188+
display: inline-flex;
189+
background: transparent;
190+
border: none;
191+
padding: 0;
192+
margin: 0;
193+
font: inherit;
194+
color: inherit;
195+
cursor: pointer;
196+
}
197+
198+
.metric-backend-inline-edit-pill__main:focus-visible {
199+
outline: revert;
200+
}
201+
202+
.metric-backend-inline-edit-pill__remove {
203+
display: inline-flex;
204+
align-items: center;
205+
padding: 0 var(--dimension-2);
206+
}
207+
208+
.metric-backend-inline-edit-pill--editing .metric-backend-inline-edit-pill__remove:hover {
209+
background-color: var(--default-form-element-bg-color);
210+
}
211+
</style>

0 commit comments

Comments
 (0)