feat(components): [form-item]: add label-position prop (#17111)

* feat(components): [form-item]: add `label-position` prop

* docs(components): form-item label-position docs

* test(components): form-item

closed form-item label-position test

* Update docs/en-US/component/form.md

Co-authored-by: btea <2356281422@qq.com>

* Update docs/en-US/component/form.md

Co-authored-by: btea <2356281422@qq.com>

* Update docs/en-US/component/form.md

Co-authored-by: btea <2356281422@qq.com>

* fix(components): form-item line-height is overridden by the form style
fix(components): form-item label-position style is invalid when label-width is auto

* docs(components):  update form and form-item alignment examples

* Update docs/en-US/component/form.md

Co-authored-by: kooriookami <38392315+kooriookami@users.noreply.github.com>

* Update docs/en-US/component/form.md

Co-authored-by: kooriookami <38392315+kooriookami@users.noreply.github.com>

---------

Co-authored-by: btea <2356281422@qq.com>
Co-authored-by: kooriookami <38392315+kooriookami@users.noreply.github.com>
This commit is contained in:
星如雨 2024-06-24 16:43:02 +08:00 committed by GitHub
parent e63d906d44
commit 91ee8606fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 119 additions and 56 deletions

View File

@ -45,7 +45,7 @@ form/inline-form
## Alignment
Depending on your design, there are several different ways to align your label element.
Depending on your design, there are several different ways to align your label element. You can set the position of `el-form-item` separately using `label-position` ^(2.7.7). If the value is not set, the `label-position` of `el-form` is used.
:::demo The `label-position` attribute decides how labels align, it can be `top` or `left`. When set to `top`, labels will be placed at the top of the form field.
@ -171,19 +171,20 @@ form/accessibility
### FormItem Attributes
| Name | Description | Type | Default |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ------- |
| prop | A key of `model`. It could be a path of the property (e.g `a.b.0` or `['a', 'b', '0']`). In the use of `validate` and `resetFields` method, the attribute is required. | ^[string] / ^[string&#91;&#93;] | — |
| label | Label text. | ^[string] | — |
| label-width | Width of label, e.g. `'50px'`. `'auto'` is supported. | ^[string] / ^[number] | '' |
| required | Whether the field is required or not, will be determined by validation rules if omitted. | ^[boolean] | — |
| rules | Validation rules of form, see the [following table](#formitemrule), more advanced usage at [async-validator](https://github.com/yiminghe/async-validator). | ^[object]`Arrayable<FormItemRule>` | — |
| error | Field error message, set its value and the field will validate error and show this message immediately. | ^[string] | — |
| show-message | Whether to show the error message. | ^[boolean] | true |
| inline-message | Inline style validate message. | ^[string] / ^[boolean] | '' |
| size | Control the size of components in this form-item. | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | — |
| for | Same as for in native label. | ^[string] | — |
| validate-status | Validation state of formItem. | ^[enum]`'' \| 'error' \| 'validating' \| 'success'` | — |
| Name | Description | Type | Default |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ------- |
| prop | A key of `model`. It could be a path of the property (e.g `a.b.0` or `['a', 'b', '0']`). In the use of `validate` and `resetFields` method, the attribute is required. | ^[string] / ^[string&#91;&#93;] | — |
| label | Label text. | ^[string] | — |
| label-position ^(2.7.7) | Position of item label. If set to `'left'` or `'right'`, `label-width` prop is also required. Default extend `label-postion` of `form`. | ^[enum]`'left' \| 'right' \| 'top'` | '' |
| label-width | Width of label, e.g. `'50px'`. `'auto'` is supported. | ^[string] / ^[number] | '' |
| required | Whether the field is required or not, will be determined by validation rules if omitted. | ^[boolean] | — |
| rules | Validation rules of form, see the [following table](#formitemrule), more advanced usage at [async-validator](https://github.com/yiminghe/async-validator). | ^[object]`Arrayable<FormItemRule>` | — |
| error | Field error message, set its value and the field will validate error and show this message immediately. | ^[string] | — |
| show-message | Whether to show the error message. | ^[boolean] | true |
| inline-message | Inline style validate message. | ^[string] / ^[boolean] | '' |
| size | Control the size of components in this form-item. | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | — |
| for | Same as for in native label. | ^[string] | — |
| validate-status | Validation state of formItem. | ^[enum]`'' \| 'error' \| 'validating' \| 'success'` | — |
#### FormItemRule

View File

@ -1,9 +1,23 @@
<template>
<el-radio-group v-model="labelPosition" aria-label="label position">
<el-radio-button value="left">Left</el-radio-button>
<el-radio-button value="right">Right</el-radio-button>
<el-radio-button value="top">Top</el-radio-button>
</el-radio-group>
<div style="margin: 20px">
<span>Form Align</span>
<el-radio-group v-model="labelPosition" aria-label="label position">
<el-radio-button value="left">Left</el-radio-button>
<el-radio-button value="right">Right</el-radio-button>
<el-radio-button value="top">Top</el-radio-button>
</el-radio-group>
</div>
<div>
<span>First Form Item Align</span>
<el-radio-group
v-model="itemLabelPosition"
aria-label="form item label position"
>
<el-radio-button value="left">Left</el-radio-button>
<el-radio-button value="right">Right</el-radio-button>
<el-radio-button value="top">Top</el-radio-button>
</el-radio-group>
</div>
<div style="margin: 20px" />
<el-form
:label-position="labelPosition"
@ -11,7 +25,7 @@
:model="formLabelAlign"
style="max-width: 600px"
>
<el-form-item label="Name">
<el-form-item label="Name" :label-position="itemLabelPosition">
<el-input v-model="formLabelAlign.name" />
</el-form-item>
<el-form-item label="Activity zone">
@ -28,7 +42,7 @@ import { reactive, ref } from 'vue'
import type { FormProps } from 'element-plus'
const labelPosition = ref<FormProps['labelPosition']>('right')
const itemLabelPosition = ref<FormProps['labelPosition']>('right')
const formLabelAlign = reactive({
name: '',
region: '',

View File

@ -132,4 +132,40 @@ describe('ElFormItem', () => {
})
})
})
it('form-item label position', () => {
const wrapper = mount({
setup() {
const form = reactive({
name: '',
nickName: '',
address: '',
})
return () => (
<div>
<Form model={form}>
<FormItem labelPosition="right" ref="labelRight">
<Input v-model={form.name} />
</FormItem>
<FormItem labelPosition="left" ref="labelLeft">
<Input v-model={form.nickName} />
</FormItem>
<FormItem labelPosition="top" ref="labelTop">
<Input v-model={form.address} />
</FormItem>
</Form>
</div>
)
},
})
expect(wrapper.findComponent({ ref: 'labelTop' }).classes()).toContain(
'el-form-item--label-top'
)
expect(wrapper.findComponent({ ref: 'labelLeft' }).classes()).toContain(
'el-form-item--label-left'
)
expect(wrapper.findComponent({ ref: 'labelRight' }).classes()).toContain(
'el-form-item--label-right'
)
})
})

View File

@ -27,6 +27,14 @@ export const formItemProps = buildProps({
type: [String, Number],
default: '',
},
/**
* @description Position of label. If set to `'left'` or `'right'`, `label-width` prop is also required. The default is extend from `form label-position`.
*/
labelPosition: {
type: String,
values: ['left', 'right', 'top', ''],
default: '',
},
/**
* @description A key of `model`. It could be an array of property paths (e.g `['a', 'b', '0']`). In the use of `validate` and `resetFields` method, the attribute is required.
*/

View File

@ -100,8 +100,12 @@ const formItemRef = ref<HTMLDivElement>()
let initialValue: any = undefined
let isResettingField = false
const labelPosition = computed(
() => props.labelPosition || formContext?.labelPosition
)
const labelStyle = computed<CSSProperties>(() => {
if (formContext?.labelPosition === 'top') {
if (labelPosition.value === 'top') {
return {}
}
@ -111,7 +115,7 @@ const labelStyle = computed<CSSProperties>(() => {
})
const contentStyle = computed<CSSProperties>(() => {
if (formContext?.labelPosition === 'top' || formContext?.inline) {
if (labelPosition.value === 'top' || formContext?.inline) {
return {}
}
if (!props.label && !props.labelWidth && isNested) {
@ -135,7 +139,10 @@ const formItemClasses = computed(() => [
formContext?.requireAsteriskPosition === 'right'
? 'asterisk-right'
: 'asterisk-left',
{ [ns.m('feedback')]: formContext?.statusIcon },
{
[ns.m('feedback')]: formContext?.statusIcon,
[ns.m(`label-${labelPosition.value}`)]: labelPosition.value,
},
])
const _inlineMessage = computed(() =>

View File

@ -95,8 +95,12 @@ export default defineComponent({
0,
Number.parseInt(autoLabelWidth, 10) - computedWidth.value
)
const labelPosition =
formItemContext.labelPosition || formContext.labelPosition
const marginPosition =
formContext.labelPosition === 'left' ? 'marginRight' : 'marginLeft'
labelPosition === 'left' ? 'marginRight' : 'marginLeft'
if (marginWidth) {
style[marginPosition] = `${marginWidth}px`
}

View File

@ -58,24 +58,6 @@ $form-item-label-top-margin-bottom: map.merge(
@include b(form) {
@include set-component-css-var('form', $form);
@include m(label-left) {
.#{$namespace}-form-item__label {
justify-content: flex-start;
}
}
@include m(label-top) {
.#{$namespace}-form-item {
display: block;
.#{$namespace}-form-item__label {
display: block;
height: auto;
text-align: left;
margin-bottom: #{map.get($form-item-label-top-margin-bottom, 'default')};
line-height: #{map.get($form-item-label-top-line-height, 'default')};
}
}
}
@include m(inline) {
.#{$namespace}-form-item {
display: inline-flex;
@ -92,19 +74,6 @@ $form-item-label-top-margin-bottom: map.merge(
}
}
}
@each $size in (large, default, small) {
@include m($size) {
&.#{$namespace}-form--label-top {
.#{$namespace}-form-item {
.#{$namespace}-form-item__label {
margin-bottom: #{map.get($form-item-label-top-margin-bottom, $size)};
line-height: #{map.get($form-item-label-top-line-height, $size)};
}
}
}
}
}
}
@include b(form-item) {
@ -131,15 +100,35 @@ $form-item-label-top-margin-bottom: map.merge(
height: #{map.get($form-item-line-height, $size)};
line-height: #{map.get($form-item-line-height, $size)};
}
@include e(content) {
line-height: #{map.get($form-item-line-height, $size)};
}
@include e(error) {
padding-top: #{map.get($form-item-error-padding-top, $size)};
}
}
}
@include m(label-left) {
.#{$namespace}-form-item__label {
justify-content: flex-start;
}
}
@include m(label-top) {
display: block;
.#{$namespace}-form-item__label {
display: block;
height: auto;
text-align: left;
margin-bottom: #{map.get($form-item-label-top-margin-bottom, 'default')};
line-height: #{map.get($form-item-label-top-line-height, 'default')};
}
}
@include e(label-wrap) {
display: flex;
}
@ -159,6 +148,7 @@ $form-item-label-top-margin-bottom: map.merge(
padding: 0 12px 0 0;
box-sizing: border-box;
}
@include e(content) {
display: flex;
flex-wrap: wrap;
@ -173,6 +163,7 @@ $form-item-label-top-margin-bottom: map.merge(
vertical-align: top;
}
}
@include e(error) {
color: getCssVar('color-danger');
font-size: 12px;
@ -202,6 +193,7 @@ $form-item-label-top-margin-bottom: map.merge(
margin-right: 4px;
}
}
&.asterisk-right {
> .#{$namespace}-form-item__label:after,
> .#{$namespace}-form-item__label-wrap
@ -232,6 +224,7 @@ $form-item-label-top-margin-bottom: map.merge(
box-shadow: 0 0 0 1px transparent inset;
}
}
.#{$namespace}-input__validateIcon {
color: getCssVar('color-danger');
}