diff --git a/components/form/Form.tsx b/components/form/Form.tsx index 97b3402f0..75ed99579 100755 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -4,7 +4,7 @@ import classNames from '../_util/classNames'; import warning from '../_util/warning'; import FormItem from './FormItem'; import { getSlot } from '../_util/props-util'; -import { defaultConfigProvider } from '../config-provider'; +import { defaultConfigProvider, SizeType } from '../config-provider'; import { getNamePath, containsNamePath } from './utils/valueUtil'; import { defaultValidateMessages } from './utils/messages'; import { allPromiseFinish } from './utils/asyncUtil'; @@ -62,6 +62,7 @@ export const formProps = { onFinishFailed: PropTypes.func, name: PropTypes.string, validateTrigger: { type: [String, Array] as PropType }, + size: { type: String as PropType }, }; export type FormProps = Partial>; @@ -280,15 +281,15 @@ const Form = defineComponent({ }, render() { - const { prefixCls: customizePrefixCls, hideRequiredMark, layout, handleSubmit } = this; + const { prefixCls: customizePrefixCls, hideRequiredMark, layout, handleSubmit, size } = this; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('form', customizePrefixCls); const { class: className, ...restProps } = this.$attrs; const formClassName = classNames(prefixCls, className, { - [`${prefixCls}-horizontal`]: layout === 'horizontal', - [`${prefixCls}-vertical`]: layout === 'vertical', - [`${prefixCls}-inline`]: layout === 'inline', + [`${prefixCls}-${layout}`]: true, + // [`${prefixCls}-rtl`]: direction === 'rtl', + [`${prefixCls}-${size}`]: size, [`${prefixCls}-hide-required-mark`]: hideRequiredMark, }); return ( diff --git a/components/form/FormItem.tsx b/components/form/FormItem.tsx index 3f9c05598..09d47a220 100644 --- a/components/form/FormItem.tsx +++ b/components/form/FormItem.tsx @@ -95,6 +95,7 @@ export const formItemProps = { validateStatus: PropTypes.oneOf(tuple('', 'success', 'warning', 'error', 'validating')), validateTrigger: { type: [String, Array] as PropType }, messageVariables: { type: Object as PropType> }, + hidden: Boolean, }; export type FormItemProps = Partial>; @@ -383,7 +384,7 @@ export default defineComponent({ const { wrapperCol } = this; const mergedWrapperCol = wrapperCol || contextWrapperCol || {}; const { style, id, ...restProps } = mergedWrapperCol; - const className = classNames(`${prefixCls}-item-control-wrapper`, mergedWrapperCol.class); + const className = classNames(`${prefixCls}-item-control`, mergedWrapperCol.class); const colProps = { ...restProps, class: className, @@ -468,7 +469,8 @@ export default defineComponent({ ]; }, renderFormItem(child: any[]) { - const { prefixCls: customizePrefixCls } = this.$props; + const validateStatus = this.validateState; + const { prefixCls: customizePrefixCls, hidden, hasFeedback } = this.$props; const { class: className, ...restProps } = this.$attrs as any; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('form', customizePrefixCls); @@ -477,6 +479,14 @@ export default defineComponent({ [className]: className, [`${prefixCls}-item`]: true, [`${prefixCls}-item-with-help`]: this.helpShow, + + // Status + [`${prefixCls}-item-has-feedback`]: validateStatus && hasFeedback, + [`${prefixCls}-item-has-success`]: validateStatus === 'success', + [`${prefixCls}-item-has-warning`]: validateStatus === 'warning', + [`${prefixCls}-item-has-error`]: validateStatus === 'error', + [`${prefixCls}-item-is-validating`]: validateStatus === 'validating', + [`${prefixCls}-item-hidden`]: hidden, }; return ( diff --git a/components/form/style/components.less b/components/form/style/components.less new file mode 100644 index 000000000..0379fbdc7 --- /dev/null +++ b/components/form/style/components.less @@ -0,0 +1,71 @@ +@import './index'; + +// ================================================================ +// = Children Component = +// ================================================================ +.@{form-item-prefix-cls} { + .@{ant-prefix}-mentions, + textarea.@{ant-prefix}-input { + height: auto; + } + + // input[type=file] + .@{ant-prefix}-upload { + background: transparent; + } + .@{ant-prefix}-upload.@{ant-prefix}-upload-drag { + background: @background-color-light; + } + + input[type='radio'], + input[type='checkbox'] { + width: 14px; + height: 14px; + } + + // Radios and checkboxes on same line + .@{ant-prefix}-radio-inline, + .@{ant-prefix}-checkbox-inline { + display: inline-block; + margin-left: 8px; + font-weight: normal; + vertical-align: middle; + cursor: pointer; + + &:first-child { + margin-left: 0; + } + } + + .@{ant-prefix}-checkbox-vertical, + .@{ant-prefix}-radio-vertical { + display: block; + } + + .@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical, + .@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical { + margin-left: 0; + } + + .@{ant-prefix}-input-number { + + .@{form-prefix-cls}-text { + margin-left: 8px; + } + &-handler-wrap { + z-index: 2; // https://github.com/ant-design/ant-design/issues/6289 + } + } + + .@{ant-prefix}-select, + .@{ant-prefix}-cascader-picker { + width: 100%; + } + + // Don't impact select inside input group and calendar header select + .@{ant-prefix}-picker-calendar-year-select, + .@{ant-prefix}-picker-calendar-month-select, + .@{ant-prefix}-input-group .@{ant-prefix}-select, + .@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker { + width: auto; + } +} diff --git a/components/form/style/horizontal.less b/components/form/style/horizontal.less new file mode 100644 index 000000000..83b664d44 --- /dev/null +++ b/components/form/style/horizontal.less @@ -0,0 +1,10 @@ +@import './index'; + +.@{form-prefix-cls}-horizontal { + .@{form-item-prefix-cls}-label { + flex-grow: 0; + } + .@{form-item-prefix-cls}-control { + flex: 1 1 0; + } +} diff --git a/components/form/style/index.less b/components/form/style/index.less index e2d1cfe1f..5ea9f43c2 100644 --- a/components/form/style/index.less +++ b/components/form/style/index.less @@ -3,94 +3,82 @@ @import '../../input/style/mixin'; @import '../../button/style/mixin'; @import '../../grid/style/mixin'; +@import './components'; +@import './inline'; +@import './horizontal'; +@import './vertical'; +@import './status'; @import './mixin'; @form-prefix-cls: ~'@{ant-prefix}-form'; -@form-component-height: @input-height-base; -@form-component-max-height: @input-height-lg; -@form-feedback-icon-size: @font-size-base; -@form-help-margin-top: ((@form-component-height - @form-component-max-height) / 2) + 2px; -@form-explain-font-size: @font-size-base; -// Extends additional 1px to fix precision issue. -// https://github.com/ant-design/ant-design/issues/12803 -// https://github.com/ant-design/ant-design/issues/8220 -@form-explain-precision: 1px; -@form-explain-height: floor(@form-explain-font-size * @line-height-base); +@form-item-prefix-cls: ~'@{form-prefix-cls}-item'; +@form-font-height: ceil(@font-size-base * @line-height-base); .@{form-prefix-cls} { .reset-component(); .reset-form(); -} -.@{form-prefix-cls}-item-required::before { - display: inline-block; - margin-right: 4px; - color: @label-required-color; - font-size: @font-size-base; - font-family: SimSun, sans-serif; - line-height: 1; - content: '*'; - .@{form-prefix-cls}-hide-required-mark & { - display: none; + .@{form-prefix-cls}-text { + display: inline-block; + padding-right: 8px; } -} -.@{form-prefix-cls}-item-label > label { - color: @label-color; - - &::after { - & when (@form-item-trailing-colon=true) { - content: ':'; - } - & when not (@form-item-trailing-colon=true) { - content: ' '; + // ================================================================ + // = Size = + // ================================================================ + .formSize(@input-height) { + .@{form-item-prefix-cls}-label > label { + height: @input-height; } - position: relative; - top: -0.5px; - margin: 0 @form-item-label-colon-margin-right 0 @form-item-label-colon-margin-left; - } - - &.@{form-prefix-cls}-item-no-colon::after { - content: ' '; - } -} - -// Form items -// You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing -.@{form-prefix-cls}-item { - label { - position: relative; - - > .@{iconfont-css-prefix} { - font-size: @font-size-base; - vertical-align: top; + .@{form-item-prefix-cls}-control-input { + min-height: @input-height; } } + &-small { + .formSize(@input-height-sm); + } + &-large { + .formSize(@input-height-lg); + } +} + +.explainAndExtraDistance(@num) when (@num >= 0) { + padding-top: floor(@num); +} + +.explainAndExtraDistance(@num) when (@num < 0) { + margin-top: ceil(@num); + margin-bottom: ceil(@num); +} + +// ================================================================ +// = Item = +// ================================================================ +.@{form-item-prefix-cls} { .reset-component(); margin-bottom: @form-item-margin-bottom; vertical-align: top; - &-control { - position: relative; - line-height: @form-component-max-height; - .clearfix(); - } - - &-children { - position: relative; - } - &-with-help { - margin-bottom: max(0, @form-item-margin-bottom - @form-explain-height - @form-help-margin-top); + margin-bottom: 0; } + &-hidden, + &-hidden.@{ant-prefix}-row { + // https://github.com/ant-design/ant-design/issues/26141 + display: none; + } + + // ============================================================== + // = Label = + // ============================================================== &-label { display: inline-block; + flex-grow: 0; overflow: hidden; - line-height: @form-component-max-height - 0.0001px; white-space: nowrap; text-align: right; vertical-align: middle; @@ -98,505 +86,126 @@ &-left { text-align: left; } - } - .@{ant-prefix}-switch { - margin: 2px 0 4px; - } -} - -.@{form-prefix-cls}-explain, -.@{form-prefix-cls}-extra { - clear: both; - min-height: @form-explain-height + @form-explain-precision; - margin-top: @form-help-margin-top; - color: @text-color-secondary; - font-size: @form-explain-font-size; - line-height: @line-height-base; - transition: color 0.3s @ease-out; // sync input color transition -} - -.@{form-prefix-cls}-explain { - margin-bottom: -@form-explain-precision; -} - -.@{form-prefix-cls}-extra { - padding-top: 4px; -} - -.@{form-prefix-cls}-text { - display: inline-block; - padding-right: 8px; -} - -.@{form-prefix-cls}-split { - display: block; - text-align: center; -} - -form { - .has-feedback { - // https://github.com/ant-design/ant-design/issues/19884 - .@{ant-prefix}-input-affix-wrapper { - .@{ant-prefix}-input-suffix { - padding-right: 18px; - } - } - - // Fix overlapping between feedback icon and 's arrow. + // https://github.com/ant-design/ant-design/issues/4431 + > .@{ant-prefix}-select .@{ant-prefix}-select-arrow, + > .@{ant-prefix}-select .@{ant-prefix}-select-clear, + :not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow, + :not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-clear { + right: 32px; + } + > .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value, + :not(.@{ant-prefix}-input-group-addon) + > .@{ant-prefix}-select + .@{ant-prefix}-select-selection-selected-value { + padding-right: 42px; + } + + // ======================= Cascader ======================== + .@{ant-prefix}-cascader-picker { + &-arrow { + margin-right: 19px; + } + &-clear { + right: 32px; + } + } + + // ======================== Picker ========================= + // Fix issue: https://github.com/ant-design/ant-design/issues/4783 + .@{ant-prefix}-picker { + padding-right: @input-padding-horizontal-base + @font-size-base * 1.3; + + &-large { + padding-right: @input-padding-horizontal-lg + @font-size-base * 1.3; + } + + &-small { + padding-right: @input-padding-horizontal-sm + @font-size-base * 1.3; + } + } + + // ===================== Status Group ====================== + &.@{form-item-prefix-cls} { + &-has-success, + &-has-warning, + &-has-error, + &-is-validating { + // ====================== Icon ====================== + .@{form-item-prefix-cls}-children-icon { + position: absolute; + top: 50%; + right: 0; + z-index: 1; + width: @input-height-base; + height: 20px; + margin-top: -10px; + font-size: @font-size-base; + line-height: 20px; + text-align: center; + visibility: visible; + animation: zoomIn 0.3s @ease-out-back; + pointer-events: none; + } + } + } + } + + // ======================== Success ======================== + &-has-success { + &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon { + color: @success-color; + animation-name: diffZoomIn1 !important; + } + } + + // ======================== Warning ======================== + &-has-warning { + .form-control-validation(@warning-color; @warning-color; @form-warning-input-bg); + + &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon { + color: @warning-color; + animation-name: diffZoomIn3 !important; + } + + // Select + .@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) { + .@{ant-prefix}-select-selector { + background-color: @form-warning-input-bg; + border-color: @warning-color !important; + } + &.@{ant-prefix}-select-open .@{ant-prefix}-select-selector, + &.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector { + .active(@warning-color); + } + } + + // InputNumber, TimePicker + .@{ant-prefix}-input-number, + .@{ant-prefix}-picker { + background-color: @form-warning-input-bg; + border-color: @warning-color; + &-focused, + &:focus { + .active(@warning-color); + } + &:not([disabled]):hover { + background-color: @form-warning-input-bg; + border-color: @warning-color; + } + } + + .@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input { + .active(@warning-color); + } + } + + // ========================= Error ========================= + &-has-error { + .form-control-validation(@error-color; @error-color; @form-error-input-bg); + + &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon { + color: @error-color; + animation-name: diffZoomIn2 !important; + } + + // Select + .@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) { + .@{ant-prefix}-select-selector { + background-color: @form-error-input-bg; + border-color: @error-color !important; + } + &.@{ant-prefix}-select-open .@{ant-prefix}-select-selector, + &.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector { + .active(@error-color); + } + } + + // fixes https://github.com/ant-design/ant-design/issues/20482 + .@{ant-prefix}-input-group-addon .@{ant-prefix}-select { + &.@{ant-prefix}-select-single:not(.@{ant-prefix}-select-customize-input) + .@{ant-prefix}-select-selector { + background-color: inherit; + border: 0; + box-shadow: none; + } + } + + .@{ant-prefix}-select.@{ant-prefix}-select-auto-complete { + .@{ant-prefix}-input:focus { + border-color: @error-color; + } + } + + // InputNumber, TimePicker + .@{ant-prefix}-input-number, + .@{ant-prefix}-picker { + background-color: @form-error-input-bg; + border-color: @error-color; + &-focused, + &:focus { + .active(@error-color); + } + &:not([disabled]):hover { + background-color: @form-error-input-bg; + border-color: @error-color; + } + } + + .@{ant-prefix}-mention-wrapper { + .@{ant-prefix}-mention-editor { + &, + &:not([disabled]):hover { + background-color: @form-error-input-bg; + border-color: @error-color; + } + } + &.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor, + .@{ant-prefix}-mention-editor:not([disabled]):focus { + .active(@error-color); + } + } + + // cascader + .@{ant-prefix}-cascader-picker { + &:hover + .@{ant-prefix}-cascader-picker-label:hover + + .@{ant-prefix}-cascader-input.@{ant-prefix}-input { + border-color: @error-color; + } + + &:focus .@{ant-prefix}-cascader-input { + background-color: @form-error-input-bg; + .active(@error-color); + } + } + + // transfer + .@{ant-prefix}-transfer { + &-list { + border-color: @error-color; + + &-search:not([disabled]) { + border-color: @input-border-color; + + &:hover { + .hover(); + } + + &:focus { + .active(); + } + } + } + } + + // RadioGroup + .@{ant-prefix}-radio-button-wrapper { + border-color: @error-color !important; + + &:not(:first-child) { + &::before { + background-color: @error-color; + } + } + } + } + + // ====================== Validating ======================= + &-is-validating { + &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon { + display: inline-block; + color: @primary-color; + } + } +} diff --git a/components/form/style/vertical.less b/components/form/style/vertical.less new file mode 100644 index 000000000..8e2249554 --- /dev/null +++ b/components/form/style/vertical.less @@ -0,0 +1,84 @@ +@import './index'; + +// ================== Label ================== +.make-vertical-layout-label() { + & when (@form-vertical-label-margin > 0) { + margin: @form-vertical-label-margin; + } + padding: @form-vertical-label-padding; + line-height: @line-height-base; + white-space: initial; + text-align: left; + + > label { + margin: 0; + + &::after { + display: none; + } + } +} + +.make-vertical-layout() { + .@{form-prefix-cls}-item .@{form-prefix-cls}-item-label { + .make-vertical-layout-label(); + } + .@{form-prefix-cls} { + .@{form-prefix-cls}-item { + flex-wrap: wrap; + .@{form-prefix-cls}-item-label, + .@{form-prefix-cls}-item-control { + flex: 0 0 100%; + max-width: 100%; + } + } + } +} + +.@{form-prefix-cls}-vertical { + .@{form-item-prefix-cls} { + flex-direction: column; + + &-label > label { + height: auto; + } + } +} + +.@{form-prefix-cls}-vertical .@{form-item-prefix-cls}-label, + // when labelCol is 24, it is a vertical form +.@{ant-prefix}-col-24.@{form-item-prefix-cls}-label, +.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label { + .make-vertical-layout-label(); +} + +@media (max-width: @screen-xs-max) { + .make-vertical-layout(); + .@{ant-prefix}-col-xs-24.@{form-item-prefix-cls}-label { + .make-vertical-layout-label(); + } +} + +@media (max-width: @screen-sm-max) { + .@{ant-prefix}-col-sm-24.@{form-item-prefix-cls}-label { + .make-vertical-layout-label(); + } +} + +@media (max-width: @screen-md-max) { + .@{ant-prefix}-col-md-24.@{form-item-prefix-cls}-label { + .make-vertical-layout-label(); + } +} + +@media (max-width: @screen-lg-max) { + .@{ant-prefix}-col-lg-24.@{form-item-prefix-cls}-label { + .make-vertical-layout-label(); + } +} + +@media (max-width: @screen-xl-max) { + .@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label { + .make-vertical-layout-label(); + } +} diff --git a/components/style/themes/default.less b/components/style/themes/default.less index ab17864ab..96953fb66 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -357,6 +357,8 @@ @form-item-trailing-colon: true; @form-vertical-label-padding: 0 0 8px; @form-vertical-label-margin: 0; +@form-item-label-font-size: @font-size-base; +@form-item-label-height: @input-height-base; @form-item-label-colon-margin-right: 8px; @form-item-label-colon-margin-left: 2px; @form-error-input-bg: @input-bg;