Skip to content

Commit e14b022

Browse files
authored
Merge pull request #7690 from nextcloud-libraries/feat/NcFormGroup
feat: add `NcFormBox` and adjust `NcRadioGroup` to `NcFormbox` and `NcFormGroup`
2 parents 4e3b308 + 188fb84 commit e14b022

File tree

6 files changed

+211
-65
lines changed

6 files changed

+211
-65
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
9+
import { provide, useCssModule } from 'vue'
10+
import { NC_FORM_BOX_CONTEXT_KEY } from './useNcFormBox.ts'
11+
12+
defineProps<{
13+
/**
14+
* Display the group as a row instead of a column
15+
*/
16+
row?: boolean
17+
}>()
18+
19+
defineSlots<{
20+
/**
21+
* Grouped content
22+
*/
23+
default?: Slot<{
24+
/**
25+
* Class to add on a custom item to apply the border radius effect
26+
*/
27+
itemClass: string
28+
}>
29+
}>()
30+
31+
const style = useCssModule()
32+
33+
provide(NC_FORM_BOX_CONTEXT_KEY, {
34+
isInFormBox: true,
35+
formBoxItemClass: style.ncFormBox__item,
36+
})
37+
</script>
38+
39+
<template>
40+
<div :class="[$style.ncFormBox, row ? $style.ncFormBox_row : $style.ncFormBox_col]">
41+
<slot :item-class="$style.ncFormBox__item" />
42+
</div>
43+
</template>
44+
45+
<style lang="scss" module>
46+
.ncFormBox {
47+
display: flex;
48+
flex-direction: column;
49+
gap: calc(1 * var(--default-grid-baseline));
50+
51+
&.ncFormBox_row {
52+
flex-direction: row;
53+
}
54+
}
55+
56+
.ncFormBox__item {
57+
border-radius: var(--border-radius-small) !important;
58+
}
59+
60+
.ncFormBox_col {
61+
flex-direction: column;
62+
63+
.ncFormBox__item {
64+
&:first-child {
65+
border-start-start-radius: var(--border-radius-element) !important;
66+
border-start-end-radius: var(--border-radius-element) !important;
67+
}
68+
69+
&:last-child {
70+
border-end-start-radius: var(--border-radius-element) !important;
71+
border-end-end-radius: var(--border-radius-element) !important;
72+
}
73+
}
74+
}
75+
76+
.ncFormBox_row {
77+
flex-direction: row;
78+
79+
.ncFormBox__item {
80+
flex: 1 1;
81+
82+
&:first-child {
83+
border-start-start-radius: var(--border-radius-element) !important;
84+
border-end-start-radius: var(--border-radius-element) !important;
85+
}
86+
87+
&:last-child {
88+
border-end-end-radius: var(--border-radius-element) !important;
89+
border-start-end-radius: var(--border-radius-element) !important;
90+
}
91+
}
92+
}
93+
</style>
94+
95+
<docs>
96+
### General
97+
98+
Visually group form elements with a small gap and rounded corners forming a solid group for supported components.
99+
100+
**Note**: if the group has a semantic meaning, consider using the `<NcFormGroup>` component.
101+
102+
```vue
103+
<script>
104+
export default {
105+
data() {
106+
return {
107+
text: 'Text',
108+
option: 'One'
109+
}
110+
}
111+
}
112+
</script>
113+
114+
<template>
115+
<NcFormBox>
116+
<NcTextField v-model="text" label="Text Field" />
117+
<NcTextField v-model="text" label="Text Field" />
118+
<NcTextField v-model="text" label="Text Field" />
119+
<NcSelect v-model="option" input-label="Select Field" :options="['One', 'Two', 'Three']" />
120+
</NcFormBox>
121+
</template>
122+
```
123+
124+
### Advanced usage
125+
126+
Use scoped slots params to apply the item class to custom items.
127+
128+
```vue
129+
<template>
130+
<div>
131+
<h4>NcButton without a group</h4>
132+
<div>
133+
<NcButton wide>
134+
First button
135+
</NcButton>
136+
<NcButton wide>
137+
Second button
138+
</NcButton>
139+
<NcButton wide>
140+
Third button
141+
</NcButton>
142+
</div>
143+
144+
<h4>NcButton inside NcFormBox with scoped-slot</h4>
145+
<NcFormBox v-slot="{ itemClass }">
146+
<NcButton :class="itemClass" wide>
147+
First button
148+
</NcButton>
149+
<NcButton :class="itemClass" wide>
150+
Second button
151+
</NcButton>
152+
<NcButton :class="itemClass" wide>
153+
Third button
154+
</NcButton>
155+
</NcFormBox>
156+
</div>
157+
</template>
158+
```
159+
</docs>

src/components/NcFormBox/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
export { default } from './NcFormBox.vue'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { InjectionKey } from 'vue'
7+
8+
import { inject } from 'vue'
9+
10+
export const NC_FORM_BOX_CONTEXT_KEY: InjectionKey<{
11+
isInFormBox: false
12+
formBoxItemClass: undefined
13+
} | {
14+
isInFormBox: true
15+
formBoxItemClass: string
16+
}> = Symbol.for('NcFormBox:context')
17+
18+
/**
19+
* Get NcFormBox context with a fallback
20+
* TODO: make it public?
21+
*/
22+
export function useNcFormBox() {
23+
return inject(NC_FORM_BOX_CONTEXT_KEY, {
24+
isInFormBox: false,
25+
formBoxItemClass: undefined,
26+
})
27+
}

src/components/NcRadioGroup/NcRadioGroup.vue

Lines changed: 14 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import type { Slot } from 'vue'
88
99
import { computed, provide, ref, warn } from 'vue'
10-
import { createElementId } from '../../utils/createElementId.ts'
10+
import NcFormBox from '../NcFormBox/NcFormBox.vue'
11+
import NcFormGroup from '../NcFormGroup/NcFormGroup.vue'
1112
import { INSIDE_RADIO_GROUP_KEY } from './useNcRadioGroup.ts'
1213
1314
const modelValue = defineModel<string>({ required: false, default: '' })
@@ -40,7 +41,6 @@ defineSlots<{
4041
default?: Slot
4142
}>()
4243
43-
const descriptionId = createElementId()
4444
const buttonVariant = ref<boolean>()
4545
4646
provide(INSIDE_RADIO_GROUP_KEY, computed(() => ({
@@ -72,62 +72,22 @@ function onUpdate(value: string) {
7272
</script>
7373

7474
<template>
75-
<fieldset
76-
:aria-describedby="description ? descriptionId : undefined"
77-
:class="[{
78-
[$style.radioGroup_buttonVariant]: buttonVariant,
79-
}, $style.radioGroup]">
80-
<legend :class="[$style.radioGroup__label, { 'hidden-visually': labelHidden }]">
81-
{{ label }}
82-
</legend>
83-
<p v-if="description" :id="descriptionId" :class="$style.radioGroup__description">
84-
{{ description }}
85-
</p>
86-
<div :class="$style.radioGroup__wrapper">
75+
<NcFormGroup
76+
:label
77+
:description
78+
:hide-label="labelHidden">
79+
<NcFormBox v-if="buttonVariant" row>
8780
<slot />
88-
</div>
89-
</fieldset>
81+
</NcFormBox>
82+
<span v-else :class="$style.radioGroup_checkboxRadioContainer">
83+
<slot />
84+
</span>
85+
</NcFormGroup>
9086
</template>
9187

9288
<style module lang="scss">
93-
.radioGroup {
94-
display: flex;
95-
flex-direction: column;
96-
97-
&:not(.radioGroup_buttonVariant) :global(.checkbox-content) {
98-
max-width: unset !important;
99-
}
100-
}
101-
102-
.radioGroup__label {
103-
font-size: 1.2em;
104-
font-weight: bold;
105-
margin-inline-start: var(--border-radius-element);
106-
}
107-
108-
.radioGroup__description {
109-
color: var(--color-text-maxcontrast);
110-
margin-block-end: var(--default-grid-baseline);
111-
margin-inline-start: var(--border-radius-element);
112-
}
113-
114-
.radioGroup__wrapper {
115-
display: flex;
116-
flex-direction: column;
117-
118-
> * {
119-
flex: 1 0 1px;
120-
}
121-
}
122-
123-
.radioGroup__label + .radioGroup__wrapper {
124-
// when there is no description we need to add some margin between wrapper and label
125-
margin-block-start: var(--default-grid-baseline);
126-
}
127-
128-
.radioGroup_buttonVariant .radioGroup__wrapper {
129-
flex-direction: row;
130-
gap: var(--default-grid-baseline);
89+
.radioGroup_checkboxRadioContainer :global(.checkbox-content) {
90+
max-width: unset !important;
13191
}
13292
</style>
13393

src/components/NcRadioGroupButton/NcRadioGroupButton.vue

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Slot } from 'vue'
88
99
import { computed, onMounted } from 'vue'
1010
import { createElementId } from '../../utils/createElementId.ts'
11+
import { useNcFormBox } from '../NcFormBox/useNcFormBox.ts'
1112
import { useInsideRadioGroup } from '../NcRadioGroup/useNcRadioGroup.ts'
1213
1314
const props = defineProps<{
@@ -36,6 +37,8 @@ defineSlots<{
3637
3738
const labelId = createElementId()
3839
const radioGroup = useInsideRadioGroup()
40+
const { formBoxItemClass } = useNcFormBox()
41+
3942
onMounted(() => radioGroup!.value.register(true))
4043
4144
const isChecked = computed(() => radioGroup?.value.modelValue === props.value)
@@ -52,7 +55,7 @@ function onUpdate() {
5255
<div
5356
:class="[{
5457
[$style.radioGroupButton_active]: isChecked,
55-
}, $style.radioGroupButton]"
58+
}, $style.radioGroupButton, formBoxItemClass]"
5659
@click="onUpdate">
5760
<div v-if="$slots.icon" :class="$style.radioGroupButton__icon">
5861
<slot name="icon" />
@@ -120,16 +123,6 @@ function onUpdate() {
120123
border: var(--radio-group-button--border-width) solid var(--color-main-text) !important;
121124
outline: calc(var(--default-grid-baseline) / 2) var(--color-main-background);
122125
}
123-
124-
&:first-of-type {
125-
border-start-start-radius: var(--border-radius-element);
126-
border-end-start-radius: var(--border-radius-element);
127-
}
128-
129-
&:last-of-type {
130-
border-start-end-radius: var(--border-radius-element);
131-
border-end-end-radius: var(--border-radius-element);
132-
}
133126
}
134127
135128
.radioGroupButton_active {

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export { default as NcDialogButton } from './NcDialogButton/index.ts'
5858
export { default as NcEllipsisedOption } from './NcEllipsisedOption/index.js'
5959
export { default as NcEmojiPicker } from './NcEmojiPicker/index.js'
6060
export { default as NcEmptyContent } from './NcEmptyContent/index.ts'
61+
export { default as NcFormBox } from './NcFormBox/index.ts'
6162
export { default as NcFormGroup } from './NcFormGroup/index.ts'
6263
export { default as NcGuestContent } from './NcGuestContent/index.ts'
6364
export { default as NcHeaderButton } from './NcHeaderButton/index.ts'

0 commit comments

Comments
 (0)