From 39e5824699528108912653cb7f4b6b30c2d84efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=9C=E5=86=BB=E6=A9=99?= Date: Mon, 20 Feb 2023 00:17:22 +0800 Subject: [PATCH] refactor:select (#6295) * refactor:select * update doc * delete useless --- components/select/index.en-US.md | 2 +- components/select/index.tsx | 48 ++-- components/select/index.zh-CN.md | 6 +- components/select/style/dropdown.tsx | 166 +++++++++++ components/select/style/index.tsx | 345 ++++++++++++++++++++++- components/select/style/multiple.tsx | 239 ++++++++++++++++ components/select/style/single.tsx | 192 +++++++++++++ components/style.ts | 2 +- components/theme/interface/components.ts | 4 +- 9 files changed, 973 insertions(+), 31 deletions(-) create mode 100644 components/select/style/dropdown.tsx create mode 100644 components/select/style/multiple.tsx create mode 100644 components/select/style/single.tsx diff --git a/components/select/index.en-US.md b/components/select/index.en-US.md index 4e3563a03..707db3e9f 100644 --- a/components/select/index.en-US.md +++ b/components/select/index.en-US.md @@ -2,7 +2,7 @@ category: Components type: Data Entry title: Select -cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*zo76T7KQx2UAAAAAAAAAAAAADrJ8AQ/original --- Select component to select value from options. diff --git a/components/select/index.tsx b/components/select/index.tsx index 677047698..6ca61da99 100644 --- a/components/select/index.tsx +++ b/components/select/index.tsx @@ -1,4 +1,4 @@ -import type { App, PropType, Plugin, ExtractPropTypes } from 'vue'; +import type { App, Plugin, ExtractPropTypes } from 'vue'; import { computed, defineComponent, ref } from 'vue'; import classNames from '../_util/classNames'; import type { BaseSelectRef } from '../vc-select'; @@ -16,6 +16,10 @@ import type { SizeType } from '../config-provider'; import { initDefaultProps } from '../_util/props-util'; import type { InputStatus } from '../_util/statusUtils'; import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils'; +import { stringType, someType, functionType, booleanType } from '../_util/type'; + +// CSSINJS +import useStyle from './style'; type RawValue = string | number; @@ -37,23 +41,19 @@ export const selectProps = () => ({ 'getRawInputElement', 'backfill', ]), - value: { - type: [Array, Object, String, Number] as PropType, - }, - defaultValue: { - type: [Array, Object, String, Number] as PropType, - }, + value: someType([Array, Object, String, Number]), + defaultValue: someType([Array, Object, String, Number]), notFoundContent: PropTypes.any, suffixIcon: PropTypes.any, itemIcon: PropTypes.any, - size: String as PropType, - mode: String as PropType<'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE'>, - bordered: { type: Boolean, default: true }, + size: stringType(), + mode: stringType<'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE'>(), + bordered: booleanType(true), transitionName: String, - choiceTransitionName: { type: String, default: '' }, - placement: String as PropType, - status: String as PropType, - 'onUpdate:value': Function as PropType<(val: SelectValue) => void>, + choiceTransitionName: stringType(''), + placement: stringType(), + status: stringType(), + 'onUpdate:value': functionType<(val: SelectValue) => void>(), }); export type SelectProps = Partial>>; @@ -123,6 +123,10 @@ const Select = defineComponent({ getPrefixCls, getPopupContainer, } = useConfigInject('select', props); + + // style + const [wrapSSR, hashId] = useStyle(prefixCls); + const rootPrefixCls = computed(() => getPrefixCls()); // ===================== Placement ===================== const placement = computed(() => { @@ -150,6 +154,7 @@ const Select = defineComponent({ [`${prefixCls.value}-in-form-item`]: formItemInputContext.isFormItemInput, }, getStatusClassNames(prefixCls.value, mergedStatus.value, formItemInputContext.hasFeedback), + hashId.value, ), ); const triggerChange: SelectProps['onChange'] = (...args) => { @@ -224,10 +229,15 @@ const Select = defineComponent({ 'status', ]); - const rcSelectRtlDropdownClassName = classNames(dropdownClassName, { - [`${prefixCls.value}-dropdown-${direction.value}`]: direction.value === 'rtl', - }); - return ( + const rcSelectRtlDropdownClassName = classNames( + dropdownClassName, + { + [`${prefixCls.value}-dropdown-${direction.value}`]: direction.value === 'rtl', + }, + hashId.value, + ); + + return wrapSSR( + >, ); }; }, diff --git a/components/select/index.zh-CN.md b/components/select/index.zh-CN.md index 25abb3df8..8b0e5dbb1 100644 --- a/components/select/index.zh-CN.md +++ b/components/select/index.zh-CN.md @@ -3,7 +3,7 @@ category: Components subtitle: 选择器 type: 数据录入 title: Select -cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*zo76T7KQx2UAAAAAAAAAAAAADrJ8AQ/original --- 下拉选择器。 @@ -60,8 +60,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg | placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 | | removeIcon | 自定义的多选框清除图标 | VNode \| slot | - | | | searchValue | 控制搜索文本 | string | - | | -| showArrow | 是否显示下拉小箭头 | boolean | 单选为true,多选为false | | -| showSearch | 配置是否可搜索 | boolean | 单选为false,多选为true | | +| showArrow | 是否显示下拉小箭头 | boolean | 单选为 true,多选为 false | | +| showSearch | 配置是否可搜索 | boolean | 单选为 false,多选为 true | | | size | 选择框大小,可选 `large` `small` | string | default | | | status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - | | diff --git a/components/select/style/dropdown.tsx b/components/select/style/dropdown.tsx new file mode 100644 index 000000000..c990ef9cf --- /dev/null +++ b/components/select/style/dropdown.tsx @@ -0,0 +1,166 @@ +import type { CSSObject } from '../../_util/cssinjs'; +import type { SelectToken } from '.'; +import { + initMoveMotion, + initSlideMotion, + slideDownIn, + slideDownOut, + slideUpIn, + slideUpOut, +} from '../../_style/motion'; +import type { GenerateStyle } from '../../theme/internal'; +import { resetComponent, textEllipsis } from '../../_style'; + +const genItemStyle: GenerateStyle = token => { + const { controlPaddingHorizontal } = token; + + return { + position: 'relative', + display: 'block', + minHeight: token.controlHeight, + padding: `${ + (token.controlHeight - token.fontSize * token.lineHeight) / 2 + }px ${controlPaddingHorizontal}px`, + color: token.colorText, + fontWeight: 'normal', + fontSize: token.fontSize, + lineHeight: token.lineHeight, + boxSizing: 'border-box', + }; +}; + +const genSingleStyle: GenerateStyle = token => { + const { antCls, componentCls } = token; + + const selectItemCls = `${componentCls}-item`; + + return [ + { + [`${componentCls}-dropdown`]: { + // ========================== Popup ========================== + ...resetComponent(token), + + position: 'absolute', + top: -9999, + zIndex: token.zIndexPopup, + boxSizing: 'border-box', + padding: token.paddingXXS, + overflow: 'hidden', + fontSize: token.fontSize, + // Fix select render lag of long text in chrome + // https://github.com/ant-design/ant-design/issues/11456 + // https://github.com/ant-design/ant-design/issues/11843 + fontVariant: 'initial', + backgroundColor: token.colorBgElevated, + borderRadius: token.borderRadiusLG, + outline: 'none', + boxShadow: token.boxShadowSecondary, + + [` + &${antCls}-slide-up-enter${antCls}-slide-up-enter-active${componentCls}-dropdown-placement-bottomLeft, + &${antCls}-slide-up-appear${antCls}-slide-up-appear-active${componentCls}-dropdown-placement-bottomLeft + `]: { + animationName: slideUpIn, + }, + + [` + &${antCls}-slide-up-enter${antCls}-slide-up-enter-active${componentCls}-dropdown-placement-topLeft, + &${antCls}-slide-up-appear${antCls}-slide-up-appear-active${componentCls}-dropdown-placement-topLeft + `]: { + animationName: slideDownIn, + }, + + [`&${antCls}-slide-up-leave${antCls}-slide-up-leave-active${componentCls}-dropdown-placement-bottomLeft`]: + { + animationName: slideUpOut, + }, + + [`&${antCls}-slide-up-leave${antCls}-slide-up-leave-active${componentCls}-dropdown-placement-topLeft`]: + { + animationName: slideDownOut, + }, + + '&-hidden': { + display: 'none', + }, + + '&-empty': { + color: token.colorTextDisabled, + }, + + // ========================= Options ========================= + [`${selectItemCls}-empty`]: { + ...genItemStyle(token), + color: token.colorTextDisabled, + }, + + [`${selectItemCls}`]: { + ...genItemStyle(token), + cursor: 'pointer', + transition: `background ${token.motionDurationSlow} ease`, + borderRadius: token.borderRadiusSM, + + // =========== Group ============ + '&-group': { + color: token.colorTextDescription, + fontSize: token.fontSizeSM, + cursor: 'default', + }, + + // =========== Option =========== + '&-option': { + display: 'flex', + + '&-content': { + flex: 'auto', + ...textEllipsis, + }, + + '&-state': { + flex: 'none', + }, + + [`&-active:not(${selectItemCls}-option-disabled)`]: { + backgroundColor: token.controlItemBgHover, + }, + + [`&-selected:not(${selectItemCls}-option-disabled)`]: { + color: token.colorText, + fontWeight: token.fontWeightStrong, + backgroundColor: token.controlItemBgActive, + + [`${selectItemCls}-option-state`]: { + color: token.colorPrimary, + }, + }, + '&-disabled': { + [`&${selectItemCls}-option-selected`]: { + backgroundColor: token.colorBgContainerDisabled, + }, + + color: token.colorTextDisabled, + cursor: 'not-allowed', + }, + + '&-grouped': { + paddingInlineStart: token.controlPaddingHorizontal * 2, + }, + }, + }, + + // =========================== RTL =========================== + '&-rtl': { + direction: 'rtl', + }, + }, + }, + + // Follow code may reuse in other components + initSlideMotion(token, 'slide-up'), + initSlideMotion(token, 'slide-down'), + initMoveMotion(token, 'move-up'), + initMoveMotion(token, 'move-down'), + ]; +}; + +export default genSingleStyle; diff --git a/components/select/style/index.tsx b/components/select/style/index.tsx index 98037eecc..c51166749 100644 --- a/components/select/style/index.tsx +++ b/components/select/style/index.tsx @@ -1,7 +1,342 @@ -import '../../style/index.less'; -import './index.less'; +import type { CSSObject } from '../../_util/cssinjs'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import genDropdownStyle from './dropdown'; +import genMultipleStyle from './multiple'; +import genSingleStyle from './single'; +import { resetComponent, resetIcon, textEllipsis } from '../../_style'; +import { genCompactItemStyle } from '../../_style/compact-item'; -// style dependencies -import '../../empty/style'; +export interface ComponentToken { + zIndexPopup: number; +} -// deps-lint-skip: form +export interface SelectToken extends FullToken<'Select'> { + rootPrefixCls: string; + inputPaddingHorizontalBase: number; +} + +// ============================= Selector ============================= +const genSelectorStyle: GenerateStyle = token => { + const { componentCls } = token; + + return { + position: 'relative', + backgroundColor: token.colorBgContainer, + border: `${token.lineWidth}px ${token.lineType} ${token.colorBorder}`, + transition: `all ${token.motionDurationMid} ${token.motionEaseInOut}`, + + input: { + cursor: 'pointer', + }, + + [`${componentCls}-show-search&`]: { + cursor: 'text', + + input: { + cursor: 'auto', + color: 'inherit', + }, + }, + + [`${componentCls}-disabled&`]: { + color: token.colorTextDisabled, + background: token.colorBgContainerDisabled, + cursor: 'not-allowed', + + [`${componentCls}-multiple&`]: { + background: token.colorBgContainerDisabled, + }, + + input: { + cursor: 'not-allowed', + }, + }, + }; +}; + +// ============================== Status ============================== +const genStatusStyle = ( + rootSelectCls: string, + token: { + componentCls: string; + antCls: string; + borderHoverColor: string; + outlineColor: string; + controlOutlineWidth: number; + controlLineWidth: number; + }, + overwriteDefaultBorder: boolean = false, +): CSSObject => { + const { componentCls, borderHoverColor, outlineColor, antCls } = token; + + const overwriteStyle: CSSObject = overwriteDefaultBorder + ? { + [`${componentCls}-selector`]: { + borderColor: borderHoverColor, + }, + } + : {}; + + return { + [rootSelectCls]: { + [`&:not(${componentCls}-disabled):not(${componentCls}-customize-input):not(${antCls}-pagination-size-changer)`]: + { + ...overwriteStyle, + + [`${componentCls}-focused& ${componentCls}-selector`]: { + borderColor: borderHoverColor, + boxShadow: `0 0 0 ${token.controlOutlineWidth}px ${outlineColor}`, + borderInlineEndWidth: `${token.controlLineWidth}px !important`, + outline: 0, + }, + + [`&:hover ${componentCls}-selector`]: { + borderColor: borderHoverColor, + borderInlineEndWidth: `${token.controlLineWidth}px !important`, + }, + }, + }, + }; +}; + +// ============================== Styles ============================== +// /* Reset search input style */ +const getSearchInputWithoutBorderStyle: GenerateStyle = token => { + const { componentCls } = token; + + return { + [`${componentCls}-selection-search-input`]: { + margin: 0, + padding: 0, + background: 'transparent', + border: 'none', + outline: 'none', + appearance: 'none', + + '&::-webkit-search-cancel-button': { + display: 'none', + '-webkit-appearance': 'none', + }, + }, + }; +}; + +// =============================== Base =============================== +const genBaseStyle: GenerateStyle = token => { + const { componentCls, inputPaddingHorizontalBase, iconCls } = token; + + return { + [componentCls]: { + ...resetComponent(token), + position: 'relative', + display: 'inline-block', + cursor: 'pointer', + + [`&:not(${componentCls}-customize-input) ${componentCls}-selector`]: { + ...genSelectorStyle(token), + ...getSearchInputWithoutBorderStyle(token), + }, + + // [`&:not(&-disabled):hover ${selectCls}-selector`]: { + // ...genHoverStyle(token), + // }, + + // ======================== Selection ======================== + [`${componentCls}-selection-item`]: { + flex: 1, + fontWeight: 'normal', + ...textEllipsis, + }, + + // ======================= Placeholder ======================= + [`${componentCls}-selection-placeholder`]: { + ...textEllipsis, + flex: 1, + color: token.colorTextPlaceholder, + pointerEvents: 'none', + }, + + // ========================== Arrow ========================== + [`${componentCls}-arrow`]: { + ...resetIcon(), + position: 'absolute', + top: '50%', + insetInlineStart: 'auto', + insetInlineEnd: inputPaddingHorizontalBase, + height: token.fontSizeIcon, + marginTop: -token.fontSizeIcon / 2, + color: token.colorTextQuaternary, + fontSize: token.fontSizeIcon, + lineHeight: 1, + textAlign: 'center', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + + [iconCls]: { + verticalAlign: 'top', + transition: `transform ${token.motionDurationSlow}`, + + '> svg': { + verticalAlign: 'top', + }, + + [`&:not(${componentCls}-suffix)`]: { + pointerEvents: 'auto', + }, + }, + + [`${componentCls}-disabled &`]: { + cursor: 'not-allowed', + }, + + '> *:not(:last-child)': { + marginInlineEnd: 8, // FIXME: magic + }, + }, + + // ========================== Clear ========================== + [`${componentCls}-clear`]: { + position: 'absolute', + top: '50%', + insetInlineStart: 'auto', + insetInlineEnd: inputPaddingHorizontalBase, + zIndex: 1, + display: 'inline-block', + width: token.fontSizeIcon, + height: token.fontSizeIcon, + marginTop: -token.fontSizeIcon / 2, + color: token.colorTextQuaternary, + fontSize: token.fontSizeIcon, + fontStyle: 'normal', + lineHeight: 1, + textAlign: 'center', + textTransform: 'none', + background: token.colorBgContainer, + cursor: 'pointer', + opacity: 0, + transition: `color ${token.motionDurationMid} ease, opacity ${token.motionDurationSlow} ease`, + textRendering: 'auto', + + '&:before': { + display: 'block', + }, + + '&:hover': { + color: token.colorTextTertiary, + }, + }, + + '&:hover': { + [`${componentCls}-clear`]: { + opacity: 1, + }, + }, + }, + + // ========================= Feedback ========================== + [`${componentCls}-has-feedback`]: { + [`${componentCls}-clear`]: { + insetInlineEnd: inputPaddingHorizontalBase + token.fontSize + token.paddingXXS, + }, + }, + }; +}; + +// ============================== Styles ============================== +const genSelectStyle: GenerateStyle = token => { + const { componentCls } = token; + + return [ + { + [componentCls]: { + // ==================== BorderLess ==================== + [`&-borderless ${componentCls}-selector`]: { + backgroundColor: `transparent !important`, + borderColor: `transparent !important`, + boxShadow: `none !important`, + }, + + // ==================== In Form ==================== + [`&${componentCls}-in-form-item`]: { + width: '100%', + }, + }, + }, + + // ===================================================== + // == LTR == + // ===================================================== + // Base + genBaseStyle(token), + + // Single + genSingleStyle(token), + + // Multiple + genMultipleStyle(token), + + // Dropdown + genDropdownStyle(token), + + // ===================================================== + // == RTL == + // ===================================================== + { + [`${componentCls}-rtl`]: { + direction: 'rtl', + }, + }, + + // ===================================================== + // == Status == + // ===================================================== + genStatusStyle( + componentCls, + mergeToken(token, { + borderHoverColor: token.colorPrimaryHover, + outlineColor: token.controlOutline, + }), + ), + genStatusStyle( + `${componentCls}-status-error`, + mergeToken(token, { + borderHoverColor: token.colorErrorHover, + outlineColor: token.colorErrorOutline, + }), + true, + ), + genStatusStyle( + `${componentCls}-status-warning`, + mergeToken(token, { + borderHoverColor: token.colorWarningHover, + outlineColor: token.colorWarningOutline, + }), + true, + ), + // ===================================================== + // == Space Compact == + // ===================================================== + genCompactItemStyle(token, { + borderElCls: `${componentCls}-selector`, + focusElCls: `${componentCls}-focused`, + }), + ]; +}; + +// ============================== Export ============================== +export default genComponentStyleHook( + 'Select', + (token, { rootPrefixCls }) => { + const selectToken: SelectToken = mergeToken(token, { + rootPrefixCls, + inputPaddingHorizontalBase: token.paddingSM - 1, + }); + + return [genSelectStyle(selectToken)]; + }, + token => ({ + zIndexPopup: token.zIndexPopupBase + 50, + }), +); diff --git a/components/select/style/multiple.tsx b/components/select/style/multiple.tsx new file mode 100644 index 000000000..143ee932e --- /dev/null +++ b/components/select/style/multiple.tsx @@ -0,0 +1,239 @@ +import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; +import type { SelectToken } from '.'; +import { mergeToken } from '../../theme/internal'; +import { resetIcon } from '../../_style'; + +const FIXED_ITEM_MARGIN = 2; + +function getSelectItemStyle({ + controlHeightSM, + controlHeight, + lineWidth: borderWidth, +}: SelectToken) { + const selectItemDist = (controlHeight - controlHeightSM) / 2 - borderWidth; + const selectItemMargin = Math.ceil(selectItemDist / 2); + return [selectItemDist, selectItemMargin]; +} + +function genSizeStyle(token: SelectToken, suffix?: string): CSSObject { + const { componentCls, iconCls } = token; + + const selectOverflowPrefixCls = `${componentCls}-selection-overflow`; + + const selectItemHeight = token.controlHeightSM; + const [selectItemDist] = getSelectItemStyle(token); + + const suffixCls = suffix ? `${componentCls}-${suffix}` : ''; + + return { + [`${componentCls}-multiple${suffixCls}`]: { + fontSize: token.fontSize, + + /** + * Do not merge `height` & `line-height` under style with `selection` & `search`, since chrome + * may update to redesign with its align logic. + */ + // =========================== Overflow =========================== + [selectOverflowPrefixCls]: { + position: 'relative', + display: 'flex', + flex: 'auto', + flexWrap: 'wrap', + maxWidth: '100%', + + '&-item': { + flex: 'none', + alignSelf: 'center', + maxWidth: '100%', + display: 'inline-flex', + }, + }, + + // ========================= Selector ========================= + [`${componentCls}-selector`]: { + display: 'flex', + flexWrap: 'wrap', + alignItems: 'center', + // Multiple is little different that horizontal is follow the vertical + padding: `${selectItemDist - FIXED_ITEM_MARGIN}px ${FIXED_ITEM_MARGIN * 2}px`, + borderRadius: token.borderRadius, + + [`${componentCls}-show-search&`]: { + cursor: 'text', + }, + + [`${componentCls}-disabled&`]: { + background: token.colorBgContainerDisabled, + cursor: 'not-allowed', + }, + + '&:after': { + display: 'inline-block', + width: 0, + margin: `${FIXED_ITEM_MARGIN}px 0`, + lineHeight: `${selectItemHeight}px`, + content: '"\\a0"', + }, + }, + + [` + &${componentCls}-show-arrow ${componentCls}-selector, + &${componentCls}-allow-clear ${componentCls}-selector + `]: { + paddingInlineEnd: token.fontSizeIcon + token.controlPaddingHorizontal, + }, + + // ======================== Selections ======================== + [`${componentCls}-selection-item`]: { + position: 'relative', + display: 'flex', + flex: 'none', + boxSizing: 'border-box', + maxWidth: '100%', + height: selectItemHeight, + marginTop: FIXED_ITEM_MARGIN, + marginBottom: FIXED_ITEM_MARGIN, + lineHeight: `${selectItemHeight - token.lineWidth * 2}px`, + background: token.colorFillSecondary, + border: `${token.lineWidth}px solid ${token.colorSplit}`, + borderRadius: token.borderRadiusSM, + cursor: 'default', + transition: `font-size ${token.motionDurationSlow}, line-height ${token.motionDurationSlow}, height ${token.motionDurationSlow}`, + userSelect: 'none', + marginInlineEnd: FIXED_ITEM_MARGIN * 2, + paddingInlineStart: token.paddingXS, + paddingInlineEnd: token.paddingXS / 2, + + [`${componentCls}-disabled&`]: { + color: token.colorTextDisabled, + borderColor: token.colorBorder, + cursor: 'not-allowed', + }, + + // It's ok not to do this, but 24px makes bottom narrow in view should adjust + '&-content': { + display: 'inline-block', + marginInlineEnd: token.paddingXS / 2, + overflow: 'hidden', + whiteSpace: 'pre', // fix whitespace wrapping. custom tags display all whitespace within. + textOverflow: 'ellipsis', + }, + + '&-remove': { + ...resetIcon(), + + display: 'inline-block', + color: token.colorIcon, + fontWeight: 'bold', + fontSize: 10, + lineHeight: 'inherit', + cursor: 'pointer', + + [`> ${iconCls}`]: { + verticalAlign: '-0.2em', + }, + + '&:hover': { + color: token.colorIconHover, + }, + }, + }, + + // ========================== Input ========================== + [`${selectOverflowPrefixCls}-item + ${selectOverflowPrefixCls}-item`]: { + [`${componentCls}-selection-search`]: { + marginInlineStart: 0, + }, + }, + + [`${componentCls}-selection-search`]: { + display: 'inline-flex', + position: 'relative', + maxWidth: '100%', + marginInlineStart: token.inputPaddingHorizontalBase - selectItemDist, + + [` + &-input, + &-mirror + `]: { + height: selectItemHeight, + fontFamily: token.fontFamily, + lineHeight: `${selectItemHeight}px`, + transition: `all ${token.motionDurationSlow}`, + }, + + '&-input': { + width: '100%', + minWidth: 4.1, // fix search cursor missing + }, + + '&-mirror': { + position: 'absolute', + top: 0, + insetInlineStart: 0, + insetInlineEnd: 'auto', + zIndex: 999, + whiteSpace: 'pre', // fix whitespace wrapping caused width calculation bug + visibility: 'hidden', + }, + }, + + // ======================= Placeholder ======================= + [`${componentCls}-selection-placeholder `]: { + position: 'absolute', + top: '50%', + insetInlineStart: token.inputPaddingHorizontalBase, + insetInlineEnd: token.inputPaddingHorizontalBase, + transform: 'translateY(-50%)', + transition: `all ${token.motionDurationSlow}`, + }, + }, + }; +} + +export default function genMultipleStyle(token: SelectToken): CSSInterpolation { + const { componentCls } = token; + + const smallToken = mergeToken(token, { + controlHeight: token.controlHeightSM, + controlHeightSM: token.controlHeightXS, + borderRadius: token.borderRadiusSM, + borderRadiusSM: token.borderRadiusXS, + }); + const [, smSelectItemMargin] = getSelectItemStyle(token); + + return [ + genSizeStyle(token), + // ======================== Small ======================== + // Shared + genSizeStyle(smallToken, 'sm'), + + // Padding + { + [`${componentCls}-multiple${componentCls}-sm`]: { + [`${componentCls}-selection-placeholder`]: { + insetInlineStart: token.controlPaddingHorizontalSM - token.lineWidth, + insetInlineEnd: 'auto', + }, + + // https://github.com/ant-design/ant-design/issues/29559 + [`${componentCls}-selection-search`]: { + marginInlineStart: smSelectItemMargin, + }, + }, + }, + + // ======================== Large ======================== + // Shared + genSizeStyle( + mergeToken(token, { + fontSize: token.fontSizeLG, + controlHeight: token.controlHeightLG, + controlHeightSM: token.controlHeight, + borderRadius: token.borderRadiusLG, + borderRadiusSM: token.borderRadius, + }), + 'lg', + ), + ]; +} diff --git a/components/select/style/single.tsx b/components/select/style/single.tsx new file mode 100644 index 000000000..70a7f2823 --- /dev/null +++ b/components/select/style/single.tsx @@ -0,0 +1,192 @@ +import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; +import { resetComponent } from '../../_style'; +import type { SelectToken } from '.'; +import { mergeToken } from '../../theme/internal'; + +function genSizeStyle(token: SelectToken, suffix?: string): CSSObject { + const { componentCls, inputPaddingHorizontalBase, borderRadius } = token; + + const selectHeightWithoutBorder = token.controlHeight - token.lineWidth * 2; + + const selectionItemPadding = Math.ceil(token.fontSize * 1.25); + + const suffixCls = suffix ? `${componentCls}-${suffix}` : ''; + + return { + [`${componentCls}-single${suffixCls}`]: { + fontSize: token.fontSize, + + // ========================= Selector ========================= + [`${componentCls}-selector`]: { + ...resetComponent(token), + + display: 'flex', + borderRadius, + + [`${componentCls}-selection-search`]: { + position: 'absolute', + top: 0, + insetInlineStart: inputPaddingHorizontalBase, + insetInlineEnd: inputPaddingHorizontalBase, + bottom: 0, + + '&-input': { + width: '100%', + }, + }, + + [` + ${componentCls}-selection-item, + ${componentCls}-selection-placeholder + `]: { + padding: 0, + lineHeight: `${selectHeightWithoutBorder}px`, + transition: `all ${token.motionDurationSlow}`, + + // Firefox inline-block position calculation is not same as Chrome & Safari. Patch this: + '@supports (-moz-appearance: meterbar)': { + lineHeight: `${selectHeightWithoutBorder}px`, + }, + }, + + [`${componentCls}-selection-item`]: { + position: 'relative', + userSelect: 'none', + }, + + [`${componentCls}-selection-placeholder`]: { + transition: 'none', + pointerEvents: 'none', + }, + + // For common baseline align + [[ + '&:after', + /* For '' value baseline align */ + `${componentCls}-selection-item:after`, + /* For undefined value baseline align */ + `${componentCls}-selection-placeholder:after`, + ].join(',')]: { + display: 'inline-block', + width: 0, + visibility: 'hidden', + content: '"\\a0"', + }, + }, + + [` + &${componentCls}-show-arrow ${componentCls}-selection-item, + &${componentCls}-show-arrow ${componentCls}-selection-placeholder + `]: { + paddingInlineEnd: selectionItemPadding, + }, + + // Opacity selection if open + [`&${componentCls}-open ${componentCls}-selection-item`]: { + color: token.colorTextPlaceholder, + }, + + // ========================== Input ========================== + // We only change the style of non-customize input which is only support by `combobox` mode. + // Not customize + [`&:not(${componentCls}-customize-input)`]: { + [`${componentCls}-selector`]: { + width: '100%', + height: token.controlHeight, + padding: `0 ${inputPaddingHorizontalBase}px`, + + [`${componentCls}-selection-search-input`]: { + height: selectHeightWithoutBorder, + }, + + '&:after': { + lineHeight: `${selectHeightWithoutBorder}px`, + }, + }, + }, + + [`&${componentCls}-customize-input`]: { + [`${componentCls}-selector`]: { + '&:after': { + display: 'none', + }, + + [`${componentCls}-selection-search`]: { + position: 'static', + width: '100%', + }, + + [`${componentCls}-selection-placeholder`]: { + position: 'absolute', + insetInlineStart: 0, + insetInlineEnd: 0, + padding: `0 ${inputPaddingHorizontalBase}px`, + + '&:after': { + display: 'none', + }, + }, + }, + }, + }, + }; +} + +export default function genSingleStyle(token: SelectToken): CSSInterpolation { + const { componentCls } = token; + + const inputPaddingHorizontalSM = token.controlPaddingHorizontalSM - token.lineWidth; + + return [ + genSizeStyle(token), + + // ======================== Small ======================== + // Shared + genSizeStyle( + mergeToken(token, { + controlHeight: token.controlHeightSM, + borderRadius: token.borderRadiusSM, + }), + 'sm', + ), + + // padding + { + [`${componentCls}-single${componentCls}-sm`]: { + [`&:not(${componentCls}-customize-input)`]: { + [`${componentCls}-selection-search`]: { + insetInlineStart: inputPaddingHorizontalSM, + insetInlineEnd: inputPaddingHorizontalSM, + }, + + [`${componentCls}-selector`]: { + padding: `0 ${inputPaddingHorizontalSM}px`, + }, + + // With arrow should provides `padding-right` to show the arrow + [`&${componentCls}-show-arrow ${componentCls}-selection-search`]: { + insetInlineEnd: inputPaddingHorizontalSM + token.fontSize * 1.5, + }, + + [` + &${componentCls}-show-arrow ${componentCls}-selection-item, + &${componentCls}-show-arrow ${componentCls}-selection-placeholder + `]: { + paddingInlineEnd: token.fontSize * 1.5, + }, + }, + }, + }, + + // ======================== Large ======================== + // Shared + genSizeStyle( + mergeToken(token, { + controlHeight: token.controlHeightLG, + fontSize: token.fontSizeLG, + borderRadius: token.borderRadiusLG, + }), + 'lg', + ), + ]; +} diff --git a/components/style.ts b/components/style.ts index 0840764b1..7a14b582a 100644 --- a/components/style.ts +++ b/components/style.ts @@ -23,7 +23,7 @@ import './radio/style'; // import './notification/style'; // import './message/style'; // import './spin/style'; -import './select/style'; +// import './select/style'; // import './switch/style'; import './auto-complete/style'; // import './affix/style'; diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts index ce9cd3fdb..31b2238f6 100644 --- a/components/theme/interface/components.ts +++ b/components/theme/interface/components.ts @@ -31,7 +31,7 @@ import type { ComponentToken as ProgressComponentToken } from '../../progress/st import type { ComponentToken as RateComponentToken } from '../../rate/style'; import type { ComponentToken as ResultComponentToken } from '../../result/style'; // import type { ComponentToken as SegmentedComponentToken } from '../../segmented/style'; -// import type { ComponentToken as SelectComponentToken } from '../../select/style'; +import type { ComponentToken as SelectComponentToken } from '../../select/style'; import type { ComponentToken as SkeletonComponentToken } from '../../skeleton/style'; import type { ComponentToken as SliderComponentToken } from '../../slider/style'; import type { ComponentToken as SpaceComponentToken } from '../../space/style'; @@ -90,7 +90,7 @@ export interface ComponentTokenMap { // Radio?: RadioComponentToken; Result?: ResultComponentToken; // Segmented?: SegmentedComponentToken; - // Select?: SelectComponentToken; + Select?: SelectComponentToken; Skeleton?: SkeletonComponentToken; Slider?: SliderComponentToken; Spin?: SpinComponentToken;