Skip to content

Commit 8f194c1

Browse files
committed
feat: add NcFormBoxButton
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
1 parent 4c2098d commit 8f194c1

File tree

6 files changed

+593
-0
lines changed

6 files changed

+593
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { Slot } from 'vue'
8+
import type { VueClassType } from '../../utils/VueTypes.ts'
9+
10+
import { useNcFormBox } from '../../components/NcFormBox/useNcFormBox.ts'
11+
import { createElementId } from '../../utils/createElementId.ts'
12+
import { isLegacy } from '../../utils/legacy.ts'
13+
14+
defineOptions({ inheritAttrs: false })
15+
16+
const {
17+
tag,
18+
label = undefined,
19+
description = undefined,
20+
invertedAccent = false,
21+
class: rootClasses = undefined,
22+
itemClasses = undefined,
23+
} = defineProps<{
24+
/** Interactive item element's tag */
25+
tag: string
26+
/** Main Label */
27+
label?: string
28+
/** Optional description below the label, also used for the aria-describedby */
29+
description?: string
30+
/** Accent on the description instead of the label */
31+
invertedAccent?: boolean
32+
/** Root element classes */
33+
class?: VueClassType
34+
/** Interactive item classes */
35+
itemClasses?: VueClassType
36+
}>()
37+
38+
defineEmits<{
39+
/** Click on the item */
40+
click: [event: MouseEvent]
41+
}>()
42+
43+
const slots = defineSlots<{
44+
/** Item's label custom content */
45+
default?: Slot<{
46+
/** IDRef of the description element if present */
47+
descriptionId?: string
48+
}>
49+
/** Custom description content */
50+
description?: Slot<{
51+
/** IDRef of the description element if present */
52+
descriptionId?: string
53+
}>
54+
/** Icon content */
55+
icon?: Slot
56+
}>()
57+
58+
const { formBoxItemClass } = useNcFormBox()
59+
60+
const descriptionId = createElementId()
61+
const hasDescription = () => !!description || !!slots.description
62+
</script>
63+
64+
<template>
65+
<div
66+
:class="[
67+
rootClasses,
68+
$style.formBoxItem,
69+
formBoxItemClass,
70+
{
71+
[$style.formBoxItem_inverted]: invertedAccent && hasDescription(),
72+
[$style.formBoxItem_legacy]: isLegacy,
73+
},
74+
]">
75+
<span :class="$style.formBoxItem__content">
76+
<component
77+
:is="tag"
78+
:class="[$style.formBoxItem__element, itemClasses]"
79+
v-bind="$attrs"
80+
@click="$emit('click', $event)">
81+
<slot :description-id>
82+
{{ label || '⚠️ Label is missing' }}
83+
</slot>
84+
</component>
85+
<span v-if="hasDescription()" :id="descriptionId" :class="$style.formBoxItem__description">
86+
<slot name="description">
87+
{{ description }}
88+
</slot>
89+
</span>
90+
</span>
91+
<span :class="$style.formBoxItem__icon">
92+
<slot name="icon" :description-id>
93+
⚠️ Icon is missing
94+
</slot>
95+
</span>
96+
</div>
97+
</template>
98+
99+
<style lang="scss" module>
100+
.formBoxItem {
101+
--nc-form-box-item-border-width: 1px;
102+
--nc-form-box-item-min-height: 40px; // Special size defined by the design
103+
--form-element-label-offset: calc(var(--border-radius-element) + var(--default-grid-baseline));
104+
--form-element-label-padding: calc(var(--form-element-label-offset) - var(--nc-form-box-item-border-width));
105+
// New colors we don't yet have in theming
106+
// TODO: add new colors to the theming
107+
--color-primary-element-extra-light: hsl(from var(--color-primary-element-light) h s calc(l * 1.045));
108+
--color-primary-element-extra-light-hover: hsl(from var(--color-primary-element-light-hover) h s calc(l * 1.045));
109+
position: relative;
110+
display: flex;
111+
align-items: center;
112+
gap: calc(2 * var(--default-grid-baseline));
113+
min-height: var(--nc-form-box-item-min-height);
114+
padding-inline: var(--form-element-label-padding);
115+
border: 1px solid var(--color-primary-element-extra-light-hover);
116+
border-bottom-width: 2px;
117+
border-radius: var(--border-radius-element);
118+
background-color: var(--color-primary-element-extra-light);
119+
color: var(--color-primary-element-light-text);
120+
transition-property: color, border-color, background-color;
121+
transition-duration: var(--animation-quick);
122+
transition-timing-function: linear;
123+
user-select: none;
124+
cursor: pointer;
125+
126+
* {
127+
cursor: inherit;
128+
}
129+
130+
&:has(:disabled) {
131+
cursor: default;
132+
opacity: 0.5;
133+
}
134+
135+
&:hover:not(:has(:disabled)) {
136+
color: var(--color-primary-element-light-text);
137+
background-color: var(--color-primary-element-extra-light-hover);
138+
}
139+
140+
&:has(:focus-visible) {
141+
outline: 2px solid var(--color-main-text);
142+
box-shadow: 0 0 0 4px var(--color-main-background);
143+
}
144+
145+
&.formBoxItem_legacy {
146+
--nc-form-box-item-border-width: 0px;
147+
border: none;
148+
}
149+
150+
&.formBoxItem_inverted {
151+
.formBoxItem__element {
152+
color: var(--color-text-maxcontrast);
153+
}
154+
155+
.formBoxItem__description {
156+
color: inherit;
157+
}
158+
}
159+
}
160+
161+
.formBoxItem__content {
162+
flex: 1;
163+
display: flex;
164+
flex-direction: column;
165+
padding-block: calc(2 * var(--default-grid-baseline));
166+
overflow-wrap: anywhere;
167+
}
168+
169+
// A trick for accessibility:
170+
// make entire component clickable while internally splitting the interactive item and the description
171+
.formBoxItem__element::after {
172+
content: '';
173+
position: absolute;
174+
inset: 0;
175+
}
176+
177+
.formBoxItem__description {
178+
color: var(--color-text-maxcontrast);
179+
}
180+
181+
.formBoxItem__icon {
182+
display: flex;
183+
align-items: center;
184+
justify-content: flex-end;
185+
}
186+
</style>
187+
188+
<docs>
189+
An internal component
190+
</docs>

0 commit comments

Comments
 (0)