-
Notifications
You must be signed in to change notification settings - Fork 95
feat: add NcFormGroup #7689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add NcFormGroup #7689
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| <!-- | ||
| - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
| - SPDX-License-Identifier: AGPL-3.0-or-later | ||
| --> | ||
|
|
||
| <script setup lang="ts"> | ||
| import type { Slot } from 'vue' | ||
|
|
||
| import { createElementId } from '../../utils/createElementId.ts' | ||
|
|
||
| const { | ||
| label = undefined, | ||
| description = undefined, | ||
| hideLabel = false, | ||
| hideDescription = false, | ||
| noGap = false, | ||
| } = defineProps<{ | ||
| /** | ||
| * Group label #label slot can be used for custom label content | ||
| */ | ||
| label?: string | ||
| /** | ||
| * Optional fieldset description. #description slot can be used for custom description content | ||
| */ | ||
| description?: string | ||
| /** | ||
| * Hide the label visually but keep it accessible for screen readers | ||
| */ | ||
| hideLabel?: boolean | ||
| /** | ||
| * Hide the description visually but keep it accessible for screen readers | ||
| */ | ||
| hideDescription?: boolean | ||
| /** | ||
| * Disable default fieldset content gap between content elements | ||
| */ | ||
| noGap?: boolean | ||
| }>() | ||
|
|
||
| const slots = defineSlots<{ | ||
| /** | ||
| * Content | ||
| */ | ||
| default?: Slot | ||
| /** | ||
| * Custom label content | ||
| */ | ||
| label?: Slot | ||
| /** | ||
| * Custom description content | ||
| */ | ||
| description?: Slot | ||
| }>() | ||
|
|
||
| const id = `nc-form-group-${createElementId()}` | ||
| const labelId = `${id}-label` | ||
| const descriptionId = `${id}-description` | ||
|
|
||
| const hasDescription = () => !!description || !!slots.description | ||
| const getDescriptionId = () => hasDescription() ? descriptionId : undefined | ||
| </script> | ||
|
|
||
| <template> | ||
| <fieldset | ||
| :class="[$style.formGroup, { [$style.formGroup_noGap]: noGap }]" | ||
| :aria-describedby="getDescriptionId()"> | ||
| <legend :id="labelId" :class="[$style.formGroup__label, { 'hidden-visually': hideLabel }]"> | ||
| <slot name="label"> | ||
| {{ label || '⚠️ Missing label' }} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to always have a label? I can imagine a use-case like having a Fieldset inside a Modal which already has a title. I feel something might eventually come up where you would want to just not have a label, and having it show "Missing label" would be uncomfortable. If we want to always have a standard label as a design decision though, I could understand it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to ask again:
But from what I understand right now, from a design POV, there is definitely a case where there is no label, including in the mockups.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If its a fieldset we must. Its obligatory for wcag. If its only visually then its the form box not a real group. |
||
| </slot> | ||
| </legend> | ||
| <div v-if="hasDescription()" :id="descriptionId" :class="[$style.formGroup__description, { 'hidden-visually': hideDescription }]"> | ||
| <slot name="description"> | ||
| {{ description }} | ||
| </slot> | ||
| </div> | ||
| <div :class="$style.formGroup__content"> | ||
| <slot /> | ||
| </div> | ||
| </fieldset> | ||
| </template> | ||
|
|
||
| <style lang="scss" module> | ||
| .formGroup { | ||
| --form-element-label-offset: calc(var(--border-radius-element) + var(--default-grid-baseline)); | ||
| --form-group-content-gap: calc(2 * var(--default-grid-baseline)); | ||
|
|
||
| &.formGroup_noGap { | ||
| --form-group-content-gap: 0; | ||
| } | ||
| } | ||
|
|
||
| .formGroup__label { | ||
| padding-inline: var(--form-element-label-offset); | ||
| font-size: var(--font-size); | ||
| font-weight: bold; | ||
| } | ||
|
|
||
| .formGroup__description { | ||
| padding-inline: var(--form-element-label-offset); | ||
| color: var(--color-text-maxcontrast); | ||
| } | ||
|
|
||
| .formGroup__content { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: var(--form-group-content-gap); | ||
| margin-block-start: calc(4 * var(--default-grid-baseline)); | ||
|
|
||
| &:first-child { | ||
| margin-block-start: 0; | ||
| } | ||
| } | ||
| </style> | ||
|
|
||
| <docs> | ||
| ### General | ||
|
|
||
| Labelled group of form elements. | ||
|
|
||
| ```vue | ||
| <template> | ||
| <NcFormGroup label="Personal information"> | ||
| <NcTextField label="First name" /> | ||
| <NcTextField label="Last name" /> | ||
| </NcFormGroup> | ||
| </template> | ||
| ``` | ||
|
|
||
| ### With description | ||
|
|
||
| ```vue | ||
| <template> | ||
| <NcFormGroup label="Personal information" description="Your contact details"> | ||
| <NcTextField label="First name" /> | ||
| <NcTextField label="Last name" /> | ||
| </NcFormGroup> | ||
| </template> | ||
| ``` | ||
| </docs> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| /*! | ||
| * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| export { default } from './NcFormGroup.vue' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats the use case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General good practice for UI components — if a component has not only its own styles but also its position/content (its margin, slots styles) - it should be optional.
It could be a list of checkboxes without a gap, or some custom set of elements.
Alternative - ask to wrap content in a div.