From 0778970798c25dae995df3a36c4e90caacb6632e Mon Sep 17 00:00:00 2001 From: saller Date: Tue, 30 Jul 2024 17:07:22 +0800 Subject: [PATCH] feat(comp:*): add `overlayTabindex` prop for all overlayed controls (#1977) --- packages/cdk/utils/src/dom.ts | 23 +++++ .../_private/trigger/src/Trigger.tsx | 4 +- packages/components/cascader/src/Cascader.tsx | 13 +-- packages/components/cascader/src/types.ts | 1 + packages/components/config/src/types.ts | 5 + .../components/control-trigger/docs/Api.zh.md | 1 + .../control-trigger/src/ControlTrigger.tsx | 30 +++--- .../src/ControlTriggerOverlay.tsx | 31 ++++++- .../src/composables/useTriggerFocusState.ts | 93 +++++++++++++++++++ .../components/control-trigger/src/token.ts | 2 + .../components/control-trigger/src/types.ts | 2 + .../components/date-picker/docs/Api.zh.md | 1 + .../src/composables/useTriggerProps.ts | 1 + .../date-picker/src/content/RangeContent.tsx | 10 +- .../date-picker/src/panel/Panel.tsx | 12 +-- .../date-picker/src/panel/RangePanel.tsx | 8 +- packages/components/date-picker/src/types.ts | 1 + packages/components/select/docs/Api.zh.md | 1 + packages/components/select/src/Select.tsx | 13 +-- packages/components/select/src/types.ts | 1 + .../components/time-picker/docs/Api.zh.md | 1 + .../src/composables/useTriggerProps.ts | 1 + .../time-picker/src/content/Content.tsx | 10 +- .../time-picker/src/content/RangeContent.tsx | 10 +- packages/components/time-picker/src/types.ts | 1 + .../components/tree-select/docs/Api.zh.md | 1 + .../components/tree-select/src/TreeSelect.tsx | 9 +- packages/components/tree-select/src/types.ts | 1 + .../utils/src/useOverlayFocusMonitor.ts | 4 +- packages/pro/config/src/types.ts | 7 +- .../quickSelect/QuickSelectPanel.tsx | 4 +- .../pro/search/src/composables/useControl.ts | 4 +- packages/pro/tag-select/docs/Api.zh.md | 1 + packages/pro/tag-select/src/ProTagSelect.tsx | 3 +- .../src/content/TagDataEditPanel.tsx | 8 +- .../tag-select/src/content/TagSelectPanel.tsx | 3 +- packages/pro/tag-select/src/types.ts | 1 + 37 files changed, 216 insertions(+), 106 deletions(-) create mode 100644 packages/components/control-trigger/src/composables/useTriggerFocusState.ts diff --git a/packages/cdk/utils/src/dom.ts b/packages/cdk/utils/src/dom.ts index 418694c6e..e1959d3c7 100644 --- a/packages/cdk/utils/src/dom.ts +++ b/packages/cdk/utils/src/dom.ts @@ -158,6 +158,29 @@ export function isTouchEvent(evt: MouseEvent | TouchEvent): evt is TouchEvent { return evt.type.startsWith('touch') } +export function isFocusable(element: unknown): boolean { + if (!element || (!(element instanceof HTMLElement) && !(element instanceof SVGElement))) { + return false + } + + if (element.getAttribute('tabIndex') !== null) { + return true + } + + switch (element.nodeName) { + case 'A': + return !!(element as HTMLAnchorElement).href && (element as HTMLAnchorElement).rel != 'ignore' + case 'INPUT': + return (element as HTMLInputElement).type != 'hidden' && (element as HTMLInputElement).type != 'file' + case 'BUTTON': + case 'SELECT': + case 'TEXTAREA': + return true + default: + return element instanceof HTMLElement ? element.isContentEditable : false + } +} + export function getMouseEvent(evt: MouseEvent | TouchEvent): MouseEvent | Touch { return isTouchEvent(evt) ? evt.touches[0] || evt.changedTouches[0] : evt } diff --git a/packages/components/_private/trigger/src/Trigger.tsx b/packages/components/_private/trigger/src/Trigger.tsx index 37b75b655..0b61a6621 100644 --- a/packages/components/_private/trigger/src/Trigger.tsx +++ b/packages/components/_private/trigger/src/Trigger.tsx @@ -10,7 +10,7 @@ import { computed, defineComponent, normalizeClass, onBeforeUnmount, onMounted, import { isArray, isNil, toString } from 'lodash-es' import { useSharedFocusMonitor } from '@idux/cdk/a11y' -import { callEmit, isEmptyNode, useState } from '@idux/cdk/utils' +import { callEmit, isEmptyNode, isFocusable, useState } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' import { IxIcon } from '@idux/components/icon' @@ -97,7 +97,7 @@ export default defineComponent({ }) const handleMouseDown = (evt: MouseEvent) => { - if (evt.target instanceof HTMLInputElement) { + if (isFocusable(evt.target)) { return } diff --git a/packages/components/cascader/src/Cascader.tsx b/packages/components/cascader/src/Cascader.tsx index 8af0a07a2..81db58d3c 100644 --- a/packages/components/cascader/src/Cascader.tsx +++ b/packages/components/cascader/src/Cascader.tsx @@ -93,12 +93,6 @@ export default defineComponent({ clearInput() }) - const handleOverlayMousedown = () => { - if (props.searchable !== 'overlay') { - setTimeout(focus) - } - } - const onFocus = (evt: FocusEvent) => { callEmit(props.onFocus, evt) } @@ -212,11 +206,7 @@ export default defineComponent({ ) } - return ( -
- {overlayRender ? overlayRender(children) : children} -
- ) + return
{overlayRender ? overlayRender(children) : children}
} return () => { @@ -225,6 +215,7 @@ export default defineComponent({ autofocus: props.autofocus, overlayClassName: overlayClasses.value, overlayContainer: props.overlayContainer ?? config.overlayContainer, + overlayTabindex: props.overlayTabindex ?? config.overlayTabindex, overlayContainerFallback: `.${mergedPrefixCls.value}-overlay-container`, overlayMatchWidth: props.overlayMatchWidth ?? config.overlayMatchWidth, class: mergedPrefixCls.value, diff --git a/packages/components/cascader/src/types.ts b/packages/components/cascader/src/types.ts index d5ba81920..552b4385f 100644 --- a/packages/components/cascader/src/types.ts +++ b/packages/components/cascader/src/types.ts @@ -100,6 +100,7 @@ export const cascaderProps = { type: [String, HTMLElement, Function] as PropType, default: undefined, }, + overlayTabindex: { type: Number, default: undefined }, overlayMatchWidth: { type: [Boolean, String] as PropType, default: undefined }, overlayRender: { type: Function as PropType<(children: VNode[]) => VNodeChild>, default: undefined }, placeholder: { type: String, default: undefined }, diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index 4f2cc80c7..656d25aab 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -189,6 +189,7 @@ export interface CascaderConfig { getKey: string | ((data: CascaderData) => any) labelKey: string overlayContainer?: OverlayContainerType + overlayTabindex?: number overlayMatchWidth: boolean size: FormSize suffix: string @@ -216,6 +217,7 @@ export interface DatePickerConfig { size: FormSize suffix: string overlayContainer?: OverlayContainerType + overlayTabindex?: number } export interface DescConfig { @@ -429,6 +431,7 @@ export interface SelectConfig { labelKey: string offset: [number, number] overlayContainer?: OverlayContainerType + overlayTabindex?: number overlayMatchWidth: boolean size: FormSize suffix: string @@ -548,6 +551,7 @@ export interface TimePickerConfig { size: FormSize suffix: string overlayContainer?: OverlayContainerType + overlayTabindex?: number allowInput: boolean | 'overlay' format: string } @@ -604,6 +608,7 @@ export interface TreeSelectConfig { labelKey: string offset: [number, number] overlayContainer?: OverlayContainerType + overlayTabindex?: number overlayMatchWidth: boolean size: FormSize suffix: string diff --git a/packages/components/control-trigger/docs/Api.zh.md b/packages/components/control-trigger/docs/Api.zh.md index 7d578265e..2d5757799 100644 --- a/packages/components/control-trigger/docs/Api.zh.md +++ b/packages/components/control-trigger/docs/Api.zh.md @@ -15,6 +15,7 @@ | `overlayClassName` | 下拉菜单的 `class` | `string` | - | - | - | | `overlayContainer` | 自定义浮层容器节点 | `string \| HTMLElement \| (trigger?: Element) => string \| HTMLElement` | - | - | - | | `overlayContainerFallback` | 默认的浮层容器节点class | `string` | - | - | - | +| `overlayTabindex` | 自定义浮层tabindex | `number` | - | - | - | | `overlayLazy` | 浮层是否懒渲染 | `boolean` | `true` | - | - | | `overlayMatchWidth` | 下拉菜单和选择器同宽 | `boolean` | `false` | - | - | | `paddingless` | 是否去掉内边距 | `boolean` | `false` | - | - | diff --git a/packages/components/control-trigger/src/ControlTrigger.tsx b/packages/components/control-trigger/src/ControlTrigger.tsx index 94b8d024b..1c7a638d9 100644 --- a/packages/components/control-trigger/src/ControlTrigger.tsx +++ b/packages/components/control-trigger/src/ControlTrigger.tsx @@ -15,6 +15,7 @@ import { useMergedCommonControlProps, useOverlayFocusMonitor } from '@idux/compo import ControlTriggerOverlay from './ControlTriggerOverlay' import { useOverlayState } from './composables/useOverlayState' +import { useTriggerFocusState } from './composables/useTriggerFocusState' import { controlTriggerToken } from './token' import { type ControlTriggerSlots, controlTriggerProps } from './types' @@ -37,27 +38,21 @@ export default defineComponent({ const mergedPrefixCls = computed(() => `${common.prefixCls}-control-trigger`) const triggerRef = ref<ɵTriggerInstance>() - const focus = () => triggerRef.value?.focus() - const blur = () => triggerRef.value?.blur() - - expose({ - focus, - blur, - }) - const mergedControlProps = useMergedCommonControlProps(props, defaultTriggerProps) const onFocus = (evt: FocusEvent) => { callEmit(props.onFocus, evt) + handleTriggerFocus(evt) } const onBlur = (evt: FocusEvent) => { setOverlayOpened(false) callEmit(props.onBlur, evt) } - const { focused, triggerFocused, bindOverlayMonitor, handleFocus, handleBlur } = useOverlayFocusMonitor( - onFocus, - onBlur, - ) + const { focused, triggerFocused, overlayFocused, focusVia, blurVia, bindOverlayMonitor, handleFocus, handleBlur } = + useOverlayFocusMonitor(onFocus, onBlur) + + const { resetTriggerFocus, handleTriggerFocus } = useTriggerFocusState(triggerRef, triggerFocused) + const { overlayOpened, overlayRef, overlayMatchWidth, overlayStyle, setOverlayOpened } = useOverlayState( props, defaultTriggerProps, @@ -66,9 +61,19 @@ export default defineComponent({ triggerFocused, ) + const focus = () => focusVia(triggerRef.value) + const blur = () => blurVia(triggerRef.value) + + expose({ + focus, + blur, + }) + provide(controlTriggerToken, { props, mergedPrefixCls, + overlayFocused, + resetTriggerFocus, bindOverlayMonitor, }) @@ -175,6 +180,7 @@ export default defineComponent({ style={overlayStyle.value} visible={overlayOpened.value} lazy={props.overlayLazy} + tabindex={props.overlayTabindex} trigger="manual" v-slots={{ default: renderTrigger, content: renderContent }} /> diff --git a/packages/components/control-trigger/src/ControlTriggerOverlay.tsx b/packages/components/control-trigger/src/ControlTriggerOverlay.tsx index 2051b52c8..00dd0801e 100644 --- a/packages/components/control-trigger/src/ControlTriggerOverlay.tsx +++ b/packages/components/control-trigger/src/ControlTriggerOverlay.tsx @@ -5,8 +5,9 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, inject, onMounted, ref } from 'vue' +import { computed, defineComponent, inject, onMounted, shallowRef, watch } from 'vue' +import { isFocusable, useEventListener } from '@idux/cdk/utils' import { ɵOverlay, type ɵOverlayInstance } from '@idux/components/_private/overlay' import { useGlobalConfig } from '@idux/components/config' @@ -21,9 +22,16 @@ export default defineComponent({ props: controlTrigglerOverlayProps, setup(props, { expose, slots, attrs }) { const common = useGlobalConfig('common') - const { props: controlTriggerProps, mergedPrefixCls, bindOverlayMonitor } = inject(controlTriggerToken)! + const { + props: controlTriggerProps, + mergedPrefixCls, + overlayFocused, + bindOverlayMonitor, + resetTriggerFocus, + } = inject(controlTriggerToken)! - const overlayRef = ref<ɵOverlayInstance>() + const overlayRef = shallowRef<ɵOverlayInstance>() + const popperElRef = computed(() => overlayRef.value?.getPopperElement()) const updatePopper = () => { overlayRef.value?.updatePopper() } @@ -52,10 +60,25 @@ export default defineComponent({ const overlayOpened = computed(() => props.visible ?? false) + watch(overlayOpened, opened => { + if (!opened && overlayFocused.value) { + resetTriggerFocus() + } + }) + useEventListener(popperElRef, 'mousedown', evt => { + if (isFocusable(evt.target) || isFocusable(evt.currentTarget)) { + return + } + + resetTriggerFocus() + }) + onMounted(() => { bindOverlayMonitor(overlayRef, overlayOpened) }) - return () => <ɵOverlay ref={overlayRef} tabindex={-1} {...overlayProps.value} {...attrs} v-slots={slots} /> + return () => ( + <ɵOverlay ref={overlayRef} tabindex={props.tabindex} {...overlayProps.value} {...attrs} v-slots={slots} /> + ) }, }) diff --git a/packages/components/control-trigger/src/composables/useTriggerFocusState.ts b/packages/components/control-trigger/src/composables/useTriggerFocusState.ts new file mode 100644 index 000000000..62b276335 --- /dev/null +++ b/packages/components/control-trigger/src/composables/useTriggerFocusState.ts @@ -0,0 +1,93 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { ɵTriggerInstance } from '@idux/components/_private/trigger' + +import { type ComputedRef, type Ref, nextTick, watch } from 'vue' + +import { useEventListener, useState } from '@idux/cdk/utils' + +export interface TriggerFocusStateContext { + handleTriggerFocus: (evt: FocusEvent) => void + resetTriggerFocus: () => void +} + +export function useTriggerFocusState( + triggerRef: Ref<ɵTriggerInstance | undefined>, + triggerFocused: ComputedRef, +): TriggerFocusStateContext { + const [focusTarget, setFocusTarget] = useState(null) + const [selectionStart, setSelectionStart] = useState(null) + + const updateSelectionStart = () => { + const element = focusTarget.value as HTMLInputElement + + setTimeout(() => { + if (element.selectionDirection === 'backward') { + setSelectionStart(element.selectionStart) + } else { + setSelectionStart(element.selectionEnd) + } + }) + } + + let stops: ((() => void) | undefined)[] | undefined + const destroyListeners = () => stops?.forEach(stop => stop?.()) + + watch(focusTarget, (target, oldTarget) => { + if (target === oldTarget) { + return + } + + if (!(target instanceof HTMLInputElement)) { + setSelectionStart(null) + destroyListeners() + return + } + + updateSelectionStart() + + if (stops?.length) { + return + } + + stops = [ + useEventListener(focusTarget, 'select', updateSelectionStart), + useEventListener(focusTarget, 'input', updateSelectionStart), + useEventListener(focusTarget, 'mousedown', updateSelectionStart), + useEventListener(focusTarget, 'keydown', updateSelectionStart), + ] + }) + + const handleTriggerFocus = (evt: FocusEvent) => { + nextTick(() => { + if (triggerFocused.value && evt.target instanceof HTMLElement) { + setFocusTarget(evt.target) + } + }) + } + + const resetTriggerFocus = () => { + const target = focusTarget.value + + if (!target) { + triggerRef.value?.focus() + return + } + + target.focus() + + if (target instanceof HTMLInputElement) { + target.setSelectionRange(selectionStart.value, selectionStart.value) + } + } + + return { + handleTriggerFocus, + resetTriggerFocus, + } +} diff --git a/packages/components/control-trigger/src/token.ts b/packages/components/control-trigger/src/token.ts index 2a0548b02..79353cf9f 100644 --- a/packages/components/control-trigger/src/token.ts +++ b/packages/components/control-trigger/src/token.ts @@ -11,8 +11,10 @@ import type { ComputedRef, InjectionKey, Ref } from 'vue' export interface ControlTriggerContext { props: ControlTriggerProps + overlayFocused: ComputedRef mergedPrefixCls: ComputedRef bindOverlayMonitor: (overlayRef: Ref<ɵOverlayInstance | undefined>, overlayOpened: Ref) => void + resetTriggerFocus: () => void } export const controlTriggerToken: InjectionKey = Symbol('controlTriggerToken') diff --git a/packages/components/control-trigger/src/types.ts b/packages/components/control-trigger/src/types.ts index a255b2600..8bf6c9ec2 100644 --- a/packages/components/control-trigger/src/types.ts +++ b/packages/components/control-trigger/src/types.ts @@ -23,6 +23,7 @@ export const controlTriggerProps = { type: [String, HTMLElement, Function] as PropType, default: undefined, }, + overlayTabindex: { type: Number, default: undefined }, overlayContainerFallback: String, overlayLazy: { type: Boolean, default: true }, overlayMatchWidth: { type: [Boolean, String] as PropType, default: undefined }, @@ -50,6 +51,7 @@ export const controlTrigglerOverlayProps = { type: Boolean, default: undefined, }, + tabindex: { type: Number, default: undefined }, onAfterLeave: [Function, Array] as PropType void>>, } diff --git a/packages/components/date-picker/docs/Api.zh.md b/packages/components/date-picker/docs/Api.zh.md index 7e27c69a4..adecb40a8 100644 --- a/packages/components/date-picker/docs/Api.zh.md +++ b/packages/components/date-picker/docs/Api.zh.md @@ -21,6 +21,7 @@ | `overlayClassName` | 日期面板的 `class` | `string` | - | - | - | | `overlayContainer` | 自定义浮层容器节点 | `string \| HTMLElement \| (trigger?: Element) => string \| HTMLElement` | - | ✅ | - | | `overlayRender` | 自定义日期面板内容的渲染 | `(children:VNode[]) => VNodeChild` | - | - | - | +| `overlayTabindex` | 自定义浮层tabindex | `number` | - | ✅ | - | | `readonly` | 只读模式 | `boolean` | - | - | - | | `size` | 设置选择器大小 | `'sm' \| 'md' \| 'lg'` | `md` | ✅ | - | | `status` | 手动指定校验状态 | `valid \| invalid \| validating` | - | - | - | diff --git a/packages/components/date-picker/src/composables/useTriggerProps.ts b/packages/components/date-picker/src/composables/useTriggerProps.ts index 19b08734c..effa89eb9 100644 --- a/packages/components/date-picker/src/composables/useTriggerProps.ts +++ b/packages/components/date-picker/src/composables/useTriggerProps.ts @@ -38,6 +38,7 @@ export function useTriggerProps(context: DatePickerContext | DateRangePickerCont open: overlayOpened.value, overlayContainer: props.overlayContainer ?? config.overlayContainer, overlayContainerFallback: `.${mergedPrefixCls.value}-overlay-container`, + overlayTabindex: props.overlayTabindex ?? config.overlayTabindex, readonly: props.readonly, size: mergedSize.value, status: mergedStatus.value, diff --git a/packages/components/date-picker/src/content/RangeContent.tsx b/packages/components/date-picker/src/content/RangeContent.tsx index 03fc016a7..8ba04848f 100644 --- a/packages/components/date-picker/src/content/RangeContent.tsx +++ b/packages/components/date-picker/src/content/RangeContent.tsx @@ -53,12 +53,6 @@ export default defineComponent({ setOverlayOpened(false) } - const handleMouseDown = (e: MouseEvent) => { - if (!(e.target instanceof HTMLInputElement)) { - e.preventDefault() - } - } - const inputProps = useInputProps(context) const timePanelProps = useRangeTimePanelProps(props, hourEnabled, minuteEnabled, secondEnabled, use12Hours) @@ -155,7 +149,7 @@ export default defineComponent({ } const children = [ -
+
{inputEnableStatus.value.enableOverlayDateInput && (
{renderInputsSide(inputsCls, true)} @@ -176,7 +170,7 @@ export default defineComponent({ />, ] - return props.overlayRender ? props.overlayRender(children) :
{children}
+ return props.overlayRender ? props.overlayRender(children) :
{children}
} }, }) diff --git a/packages/components/date-picker/src/panel/Panel.tsx b/packages/components/date-picker/src/panel/Panel.tsx index 827996621..cf71f7703 100644 --- a/packages/components/date-picker/src/panel/Panel.tsx +++ b/packages/components/date-picker/src/panel/Panel.tsx @@ -53,12 +53,6 @@ export default defineComponent({ handleDatePanelChange(value) } - const handleMouseDown = (e: MouseEvent) => { - if (!(e.target instanceof HTMLInputElement)) { - e.preventDefault() - } - } - return () => { const datePanelType = convertPickerTypeToConfigType(props.type) @@ -82,11 +76,7 @@ export default defineComponent({ } return ( -
+
<ɵDatePanel v-show={props.visible !== 'timePanel'} v-slots={slots} {...datePanelProps} /> {props.type === 'datetime' && <ɵTimePanel v-show={props.visible === 'timePanel'} {..._timePanelProps} />}
diff --git a/packages/components/date-picker/src/panel/RangePanel.tsx b/packages/components/date-picker/src/panel/RangePanel.tsx index e04c922b9..91d6cf15e 100644 --- a/packages/components/date-picker/src/panel/RangePanel.tsx +++ b/packages/components/date-picker/src/panel/RangePanel.tsx @@ -43,12 +43,6 @@ export default defineComponent({ isSelecting, ) - const handleMouseDown = (e: MouseEvent) => { - if (!(e.target instanceof HTMLInputElement)) { - e.preventDefault() - } - } - const renderSide = (isFrom: boolean) => { const timeValue = panelValue.value?.[isFrom ? 0 : 1] const datePanelType = convertPickerTypeToConfigType(props.type) @@ -90,7 +84,7 @@ export default defineComponent({ const prefixCls = mergedPrefixCls.value return ( -
+
{renderSide(true)} {slots.separator?.() ??
} {renderSide(false)} diff --git a/packages/components/date-picker/src/types.ts b/packages/components/date-picker/src/types.ts index 877a8beb5..01ffaf5c3 100644 --- a/packages/components/date-picker/src/types.ts +++ b/packages/components/date-picker/src/types.ts @@ -74,6 +74,7 @@ const datePickerCommonProps = { type: [String, HTMLElement, Function] as PropType, default: undefined, }, + overlayTabindex: { type: Number, default: undefined }, overlayRender: Function as PropType<(children: VNode[]) => VNodeChild>, readonly: { type: Boolean as PropType, diff --git a/packages/components/select/docs/Api.zh.md b/packages/components/select/docs/Api.zh.md index 75ab19548..1288ea9f2 100644 --- a/packages/components/select/docs/Api.zh.md +++ b/packages/components/select/docs/Api.zh.md @@ -30,6 +30,7 @@ | `overlayContainer` | 自定义下拉框容器节点 | `string \| HTMLElement \| (trigger?: Element) => string \| HTMLElement` | - | ✅ | - | | `overlayMatchWidth` | 下拉菜单和选择器同宽 | `boolean \| 'minWidth'` | `true` | ✅ | - | | `overlayRender` | 自定义下拉菜单内容的渲染 | `(children:VNode[]) => VNodeTypes` | - | - | - | +| `overlayTabindex` | 自定义浮层tabindex | `number` | - | ✅ | - | | `placeholder` | 选择框默认文本 | `string \| #placeholder` | - | - | - | | `readonly` | 只读模式 | `boolean` | - | - | - | | `searchable` | 是否可搜索 | `boolean \| 'overlay'` | `false` | - | 当为 `true` 时搜索功能集成在选择器上,当为 `overlay` 时,搜索功能集成在悬浮层上 | diff --git a/packages/components/select/src/Select.tsx b/packages/components/select/src/Select.tsx index 6eba5e418..167d064ac 100644 --- a/packages/components/select/src/Select.tsx +++ b/packages/components/select/src/Select.tsx @@ -199,16 +199,6 @@ export default defineComponent({ return spinProps ? {children} : children } - const handleContentMouseDown = (evt: MouseEvent) => { - if (evt.target instanceof HTMLInputElement) { - return - } - - setTimeout(() => { - focus() - }) - } - const renderContent: ControlTriggerSlots['overlay'] = () => { const children = [renderLoading()] const { searchable, overlayRender } = props @@ -241,7 +231,7 @@ export default defineComponent({ ) } - return [
{overlayRender ? overlayRender(children) : children}
] + return [
{overlayRender ? overlayRender(children) : children}
] } return () => { @@ -251,6 +241,7 @@ export default defineComponent({ overlayClassName: overlayClasses.value, overlayContainer: props.overlayContainer ?? config.overlayContainer, overlayContainerFallback: `.${mergedPrefixCls.value}-overlay-container`, + overlayTabindex: props.overlayTabindex ?? config.overlayTabindex, overlayMatchWidth: props.overlayMatchWidth ?? config.overlayMatchWidth, class: mergedPrefixCls.value, borderless, diff --git a/packages/components/select/src/types.ts b/packages/components/select/src/types.ts index 8586d098e..024d5a820 100644 --- a/packages/components/select/src/types.ts +++ b/packages/components/select/src/types.ts @@ -89,6 +89,7 @@ export const selectProps = { type: [String, HTMLElement, Function] as PropType, default: undefined, }, + overlayTabindex: { type: Number, default: undefined }, overlayMatchWidth: { type: [Boolean, String] as PropType, default: undefined }, overlayRender: { type: Function as PropType<(children: VNode[]) => VNodeChild>, default: undefined }, placeholder: { type: String, default: undefined }, diff --git a/packages/components/time-picker/docs/Api.zh.md b/packages/components/time-picker/docs/Api.zh.md index faf6edc53..47c048552 100644 --- a/packages/components/time-picker/docs/Api.zh.md +++ b/packages/components/time-picker/docs/Api.zh.md @@ -29,6 +29,7 @@ | `defaultOpenValue` | 打开面板时默认高亮的值 | `Date \| string \| number` | - | - | 如果value不为空,则高亮value的值 | | `overlayClassName` | 浮层的类名 |`string` | - | - | - | | `overlayContainer` | 自定义浮层容器节点 | `string \| HTMLElement \| (trigger?: Element) => string \| HTMLElement` | - | ✅ | - | + | `overlayTabindex` | 自定义浮层tabindex | `number` | - | ✅ | - | | `onChange` | 时间选择回调函数 |`(value: Date \| undefined) => void` | - | - | - | | `onClear` | 清除事件回调函数 |`(evt: MouseEvent) => void` | - | - | - | | `onFocus` | focus事件回调函数 |`(evt: FocusEvent) => void` | - | - | - | diff --git a/packages/components/time-picker/src/composables/useTriggerProps.ts b/packages/components/time-picker/src/composables/useTriggerProps.ts index d41410daf..e1bfe8271 100644 --- a/packages/components/time-picker/src/composables/useTriggerProps.ts +++ b/packages/components/time-picker/src/composables/useTriggerProps.ts @@ -39,6 +39,7 @@ export function useTriggerProps(context: TimePickerContext | TimeRangePickerCont open: overlayOpened.value, overlayContainer: props.overlayContainer ?? config.overlayContainer, overlayContainerFallback: `.${mergedPrefixCls.value}-overlay-container`, + overlayTabindex: props.overlayTabindex ?? config.overlayTabindex, readonly: props.readonly, size: mergedSize.value, status: mergedStatus.value, diff --git a/packages/components/time-picker/src/content/Content.tsx b/packages/components/time-picker/src/content/Content.tsx index ae5aef63f..0a7908a1a 100644 --- a/packages/components/time-picker/src/content/Content.tsx +++ b/packages/components/time-picker/src/content/Content.tsx @@ -56,12 +56,6 @@ export default defineComponent({ const { activeValue, setActiveValue } = useActiveValue(props, dateConfig, formatRef, panelValue) - const handleMouseDown = (e: MouseEvent) => { - if (!(e.target instanceof HTMLInputElement)) { - e.preventDefault() - } - } - return () => { const prefixCls = `${mergedPrefixCls.value}-overlay` const boardPrefixCls = `${mergedPrefixCls.value}-board` @@ -76,7 +70,7 @@ export default defineComponent({ } const children = [ -
+
{inputEnableStatus.value.enableInternalInput && ( <ɵInput ref={inputInstance} @@ -99,7 +93,7 @@ export default defineComponent({
, <ɵFooter v-slots={slots} class={`${prefixCls}-footer`} footer={props.footer} />, ] - return props.overlayRender ? props.overlayRender(children) :
{children}
+ return props.overlayRender ? props.overlayRender(children) :
{children}
} }, }) diff --git a/packages/components/time-picker/src/content/RangeContent.tsx b/packages/components/time-picker/src/content/RangeContent.tsx index 4fbc542fc..bf24318a1 100644 --- a/packages/components/time-picker/src/content/RangeContent.tsx +++ b/packages/components/time-picker/src/content/RangeContent.tsx @@ -60,12 +60,6 @@ export default defineComponent({ setOverlayOpened(false) } - const handleMouseDown = (e: MouseEvent) => { - if (!(e.target instanceof HTMLInputElement)) { - e.preventDefault() - } - } - function renderBoard(isFrom: boolean) { const { inputValue, @@ -124,7 +118,7 @@ export default defineComponent({ } const children = [ -
+
{renderBoard(true)}
{inputEnableStatus.value.enableInternalInput && renderSeparator()}
{renderBoard(false)} @@ -139,7 +133,7 @@ export default defineComponent({ ok={handleConfirm} />, ] - return props.overlayRender ? props.overlayRender(children) :
{children}
+ return props.overlayRender ? props.overlayRender(children) :
{children}
} }, }) diff --git a/packages/components/time-picker/src/types.ts b/packages/components/time-picker/src/types.ts index ee961b1e4..28c458ed3 100644 --- a/packages/components/time-picker/src/types.ts +++ b/packages/components/time-picker/src/types.ts @@ -53,6 +53,7 @@ const timePickerCommonProps = { type: [String, HTMLElement, Function] as PropType, default: undefined, }, + overlayTabindex: { type: Number, default: undefined }, overlayRender: Function as PropType<(children: VNode[]) => VNodeChild>, readonly: { type: Boolean as PropType, diff --git a/packages/components/tree-select/docs/Api.zh.md b/packages/components/tree-select/docs/Api.zh.md index 2a2198d8c..0b08871fe 100644 --- a/packages/components/tree-select/docs/Api.zh.md +++ b/packages/components/tree-select/docs/Api.zh.md @@ -36,6 +36,7 @@ | `overlayContainer` | 自定义浮层容器节点 | `string \| HTMLElement \| (trigger?: Element) => string \| HTMLElement` | - | ✅ | - | | `overlayMatchWidth` | 下拉菜单和选择器同宽 | `boolean \| 'minWidth'` | `true` | ✅ | - | | `overlayRender` | 自定义下拉菜单内容的渲染 | `(children:VNode[]) => VNodeTypes` | - | - | - | +| `overlayTabindex` | 自定义浮层tabindex | `number` | - | ✅ | - | | `placeholder` | 选择框默认文本 | `string` | - | - | - | | `readonly` | 只读模式 | `boolean` | - | - | - | | `searchFn` | 搜索函数 | `boolean | (node: TreeSelectNode, searchValue?: string) => boolean` | `true` | - | 为 `true` 时使用默认的搜索规则, 如果使用远程搜索,应该设置为 `false` | diff --git a/packages/components/tree-select/src/TreeSelect.tsx b/packages/components/tree-select/src/TreeSelect.tsx index 99c478218..70e97f53f 100644 --- a/packages/components/tree-select/src/TreeSelect.tsx +++ b/packages/components/tree-select/src/TreeSelect.tsx @@ -87,12 +87,6 @@ export default defineComponent({ clearInput() }) - const handleOverlayClick = () => { - if (props.searchable !== 'overlay') { - setTimeout(focus) - } - } - const handleNodeClick = () => { if (!props.multiple) { setOverlayOpened(false) @@ -182,7 +176,7 @@ export default defineComponent({ return spinProps ? {children} : children } - const renderContent = () => renderLoading() + const renderContent = () => renderLoading() return () => { const triggerProps = { @@ -198,6 +192,7 @@ export default defineComponent({ overlayClassName: overlayClasses.value, overlayContainer: props.overlayContainer ?? config.overlayContainer, overlayContainerFallback: `.${mergedPrefixCls.value}-overlay-container`, + overlayTabindex: props.overlayTabindex ?? config.overlayTabindex, overlayMatchWidth: props.overlayMatchWidth ?? config.overlayMatchWidth, 'onUpdate:open': setOverlayOpened, onFocus: handleFocus, diff --git a/packages/components/tree-select/src/types.ts b/packages/components/tree-select/src/types.ts index 554d727c7..903e695ea 100644 --- a/packages/components/tree-select/src/types.ts +++ b/packages/components/tree-select/src/types.ts @@ -68,6 +68,7 @@ export const treeSelectProps = { type: [String, HTMLElement, Function] as PropType, default: undefined, }, + overlayTabindex: { type: Number, default: undefined }, overlayMatchWidth: { type: [Boolean, String] as PropType, default: undefined }, overlayRender: { type: Function as PropType<(children: VNode[]) => VNodeChild>, default: undefined }, placeholder: { type: String, default: undefined }, diff --git a/packages/components/utils/src/useOverlayFocusMonitor.ts b/packages/components/utils/src/useOverlayFocusMonitor.ts index 6f8c538b8..3136899f9 100644 --- a/packages/components/utils/src/useOverlayFocusMonitor.ts +++ b/packages/components/utils/src/useOverlayFocusMonitor.ts @@ -90,7 +90,7 @@ export function useOverlayFocusMonitor( } const bindOverlayMonitor = (overlayRef: Ref<ɵOverlayInstance | undefined>, overlayOpened: Ref) => { - let stop: () => void | undefined + let stop: (() => void) | undefined const stopWatch = watch( [() => overlayRef.value?.getPopperElement(), overlayOpened], @@ -101,7 +101,7 @@ export function useOverlayFocusMonitor( return } - stop = _bindMonitor(el) + stop = _bindMonitor(el, true) }, { immediate: true, diff --git a/packages/pro/config/src/types.ts b/packages/pro/config/src/types.ts index d8f3b1f1d..a8a5e9b7e 100644 --- a/packages/pro/config/src/types.ts +++ b/packages/pro/config/src/types.ts @@ -7,8 +7,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { PortalTargetType } from '@idux/cdk/portal' import type { FormSize } from '@idux/components/form' +import type { OverlayContainerType } from '@idux/components/utils' import type { ProFormSchemaFormatter } from '@idux/pro/form' import type { ProLocale } from '@idux/pro/locales' import type { ProSearchSize } from '@idux/pro/search' @@ -85,6 +85,9 @@ export interface ProTagSelectConfig { borderless: boolean clearable: boolean clearIcon: string + overlayContainer?: OverlayContainerType + overlayTabindex?: number + size: FormSize suffix: string } @@ -99,7 +102,7 @@ export interface ProSearchConfig { clearIcon: string | VNode searchIcon: string | VNode size: ProSearchSize - overlayContainer?: PortalTargetType + overlayContainer?: OverlayContainerType } export interface ProTextareaConfig { diff --git a/packages/pro/search/src/components/quickSelect/QuickSelectPanel.tsx b/packages/pro/search/src/components/quickSelect/QuickSelectPanel.tsx index 92652f78e..19f5d22cf 100644 --- a/packages/pro/search/src/components/quickSelect/QuickSelectPanel.tsx +++ b/packages/pro/search/src/components/quickSelect/QuickSelectPanel.tsx @@ -11,7 +11,7 @@ import { type VNodeChild, computed, defineComponent, inject, watch } from 'vue' import { isNil } from 'lodash-es' -import { type VKey, isEmptyNode, useState } from '@idux/cdk/utils' +import { type VKey, isEmptyNode, isFocusable, useState } from '@idux/cdk/utils' import QuickSelectPanelItem from './QuickSelectItem' import QuickSelectPanelShortcut from './QuickSelectShortcut' @@ -61,7 +61,7 @@ export default defineComponent({ }) const handleMouseDown = (evt: MouseEvent) => { - if (!(evt.target instanceof HTMLInputElement)) { + if (!isFocusable(evt.target)) { evt.preventDefault() tempSegmentInputRef.value?.focus() } diff --git a/packages/pro/search/src/composables/useControl.ts b/packages/pro/search/src/composables/useControl.ts index 496ed77e5..2d7b3a5d4 100644 --- a/packages/pro/search/src/composables/useControl.ts +++ b/packages/pro/search/src/composables/useControl.ts @@ -11,7 +11,7 @@ import type { SearchStateContext } from './useSearchStates' import { type Ref, onBeforeUnmount, watch } from 'vue' -import { useEventListener } from '@idux/cdk/utils' +import { isFocusable, useEventListener } from '@idux/cdk/utils' export function useControl( elementRef: Ref, @@ -55,7 +55,7 @@ export function useControl( setOverlayOpened(true) } - if (!(evt.target instanceof HTMLInputElement)) { + if (!isFocusable(evt.target)) { evt.preventDefault() } }), diff --git a/packages/pro/tag-select/docs/Api.zh.md b/packages/pro/tag-select/docs/Api.zh.md index fa26e05c9..aa424d677 100644 --- a/packages/pro/tag-select/docs/Api.zh.md +++ b/packages/pro/tag-select/docs/Api.zh.md @@ -22,6 +22,7 @@ | `overlayClassName` | 下拉菜单的 `class` | `string` | - | - | - | | `overlayContainer` | 自定义下拉框容器节点 | `string \| HTMLElement \| (trigger?: Element) => string \| HTMLElement` | - | - | - | | `overlayMatchWidth` | 下拉菜单和选择器同宽 | `boolean \| 'minWidth'` | `true` | - | - | +| `overlayTabindex` | 自定义浮层tabindex | `number` | - | ✅ | - | | `placeholder` | 选择框默认文本 | `string \| #placeholder` | - | - | - | | `readonly` | 只读模式 | `boolean` | - | - | - | | `removeConfirmHeader` | 删除标签的确认弹窗头部配置 | `string \| HeaderProps` | - | - | - | diff --git a/packages/pro/tag-select/src/ProTagSelect.tsx b/packages/pro/tag-select/src/ProTagSelect.tsx index 1ff72e80e..0f080a83a 100644 --- a/packages/pro/tag-select/src/ProTagSelect.tsx +++ b/packages/pro/tag-select/src/ProTagSelect.tsx @@ -220,8 +220,9 @@ export default defineComponent({ const controlTriggerProps = { autofocus: false, overlayClassName: overlayClasses.value, - overlayContainer: props.overlayContainer, + overlayContainer: props.overlayContainer ?? config.overlayContainer, overlayContainerFallback: `${mergedPrefixCls.value}-overlay-container`, + overlayTabindex: props.overlayTabindex ?? config.overlayTabindex, overlayMatchWidth: props.overlayMatchWidth, class: [mergedPrefixCls.value, globalHashId.value, hashId.value], borderless, diff --git a/packages/pro/tag-select/src/content/TagDataEditPanel.tsx b/packages/pro/tag-select/src/content/TagDataEditPanel.tsx index 1ae9d41bf..1fbb9d40e 100644 --- a/packages/pro/tag-select/src/content/TagDataEditPanel.tsx +++ b/packages/pro/tag-select/src/content/TagDataEditPanel.tsx @@ -68,12 +68,6 @@ export default defineComponent({ handleTagDataRemove(props.data) } - const handlePanelMousedown = (evt: MouseEvent) => { - if (!(evt.target instanceof HTMLInputElement)) { - evt.preventDefault() - } - } - const renderColorItem = (prefixCls: string, color: TagSelectColor) => { const isSelected = color.key === props.data.color.key const colorItemPrefixCls = `${prefixCls}-item` @@ -119,7 +113,7 @@ export default defineComponent({ const prefixCls = `${mergedPrefixCls.value}-edit-panel` return ( -
+
{ setEditPanelOpened(false) - if (!(evt.target instanceof HTMLInputElement)) { + if (!isFocusable(evt.target)) { evt.preventDefault() } } diff --git a/packages/pro/tag-select/src/types.ts b/packages/pro/tag-select/src/types.ts index dfccf36e8..c7b0f41f0 100644 --- a/packages/pro/tag-select/src/types.ts +++ b/packages/pro/tag-select/src/types.ts @@ -55,6 +55,7 @@ export const proTagSelectProps = { type: [String, HTMLElement, Function] as PropType, default: undefined, }, + overlayTabindex: { type: Number, default: undefined }, overlayMatchWidth: { type: [Boolean, String] as PropType, default: 'minWidth' }, placeholder: { type: String, default: undefined }, readonly: { type: Boolean, default: false },