diff --git a/package.json b/package.json index 5aa057323..462cdc74a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@commitlint/cli": "^11.0.0", "@commitlint/config-angular": "^11.0.0", "@ls-lint/ls-lint": "^1.9.0", + "@popperjs/core": "^2.6.0", "@types/detect-port": "^1.3.0", "@types/fs-extra": "^9.0.0", "@types/gulp": "^4.0.0", diff --git a/packages/cdk/overlay/__tests__/overlay.spec.ts b/packages/cdk/overlay/__tests__/overlay.spec.ts new file mode 100644 index 000000000..ef9d0f2b7 --- /dev/null +++ b/packages/cdk/overlay/__tests__/overlay.spec.ts @@ -0,0 +1,278 @@ +import type { OverlayOptions, OverlayTrigger } from '@idux/cdk/overlay' + +import { defineComponent, onMounted, onUpdated, PropType, unref } from 'vue' +import { mount } from '@vue/test-utils' +import { IxButton } from '@idux/components' +import { useOverlay } from '../src/useOverlay' + +const defaultOverlayOptions: OverlayOptions = { + placement: 'bottom', + scrollStrategy: 'reposition', + trigger: 'click', + offset: [0, 0], + hideDelay: 1000, + showDelay: 1000, +} + +describe('useOverlay.ts', () => { + let options: OverlayOptions + let timer: (delay?: number) => Promise + + beforeEach(() => { + options = { ...defaultOverlayOptions } + timer = (delay = 0) => { + return new Promise(resolve => { + setTimeout(resolve, delay) + }) + } + }) + + test('init work', () => { + const instance = useOverlay(options) + expect(instance).toBeDefined() + }) + + test('visible work', async () => { + const TestComponent = defineComponent({ + setup() { + const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, visibility, show, hide } = useOverlay( + options, + ) + + onMounted(initialize) + + const handleClick = () => { + unref(visibility) ? hide(true) : show(true) + } + + return { overlayRef, triggerRef, triggerEvents, overlayEvents, handleClick } + }, + template: ` + + +
Overlay
+ `, + }) + const wrapper = mount(TestComponent) + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + + await wrapper.get('#trigger').trigger('click') + await timer(1000) + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + + await wrapper.get('#trigger').trigger('click') + await timer(1000) + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + + await wrapper.get('#immediate').trigger('click') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + + await wrapper.get('#immediate').trigger('click') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + }) + + test('component trigger work', async () => { + const TestComponent = defineComponent({ + components: { IxButton }, + setup() { + const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents } = useOverlay({ + ...options, + showDelay: 0, + hideDelay: 0, + }) + + onMounted(initialize) + + return { overlayRef, triggerRef, triggerEvents, overlayEvents } + }, + template: ` + Trigger +
Overlay
+ `, + }) + const wrapper = mount(TestComponent) + await wrapper.get('#trigger').trigger('click') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + + await wrapper.get('#trigger').trigger('click') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + }) + + test('destroy work', async () => { + const TestComponent = defineComponent({ + setup() { + const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, destroy } = useOverlay({ + ...options, + showDelay: 0, + hideDelay: 0, + }) + + onMounted(initialize) + + const handleClick = () => { + destroy() + console.log('destroy') + } + + return { overlayRef, triggerRef, triggerEvents, overlayEvents, handleClick } + }, + template: ` + + +
Overlay
+ `, + }) + const wrapper = mount(TestComponent) + + const log = jest.spyOn(console, 'log') + await wrapper.get('#destroy').trigger('click') + expect(log).toBeCalled() + }) + + test('update work', async () => { + const TestComponent = defineComponent({ + components: { IxButton }, + setup() { + const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, update } = useOverlay(options) + + onMounted(initialize) + + const handleClick = () => { + update({ showDelay: 0 }) + } + + return { overlayRef, triggerRef, triggerEvents, overlayEvents, handleClick } + }, + template: ` + + +
Overlay
+ `, + }) + const wrapper = mount(TestComponent) + + await wrapper.get('#trigger').trigger('click') + await timer(1000) + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + + await wrapper.get('#trigger').trigger('click') + await timer(1000) + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + + await wrapper.get('#update').trigger('click') + await wrapper.get('#trigger').trigger('click') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + }) + + test('trigger work', async () => { + const TestComponent = defineComponent({ + components: { IxButton }, + props: { + trigger: { + type: String as PropType, + default: 'click', + }, + }, + setup(props) { + const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, update } = useOverlay({ + ...options, + showDelay: 0, + hideDelay: 0, + trigger: props.trigger, + }) + + onMounted(initialize) + + onUpdated(() => { + update({ trigger: props.trigger }) + }) + + return { overlayRef, triggerRef, triggerEvents, overlayEvents } + }, + template: ` + +
Overlay
+ `, + }) + + const wrapper = mount(TestComponent) + await wrapper.get('#trigger').trigger('click') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + await wrapper.get('#trigger').trigger('click') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + + await wrapper.get('#overlay').trigger('mouseleave') + + await wrapper.setProps({ trigger: 'focus' }) + await wrapper.get('#trigger').trigger('focus') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + await wrapper.get('#trigger').trigger('focus') + + await wrapper.get('#trigger').trigger('blur') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + + await wrapper.setProps({ trigger: 'hover' }) + await wrapper.get('#trigger').trigger('mouseenter') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + await wrapper.get('#trigger').trigger('mouseleave') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + }) + + test('hover overlay work', async () => { + const TestComponent = defineComponent({ + components: { IxButton }, + setup() { + const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents } = useOverlay({ + ...options, + trigger: 'hover', + visible: true, + allowEnter: true, + showDelay: 0, + hideDelay: 0, + }) + + onMounted(initialize) + + return { overlayRef, triggerRef, triggerEvents, overlayEvents } + }, + template: ` + +
Overlay
+ `, + }) + const wrapper = mount(TestComponent) + await wrapper.get('#overlay').trigger('mouseenter') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;') + + await wrapper.get('#overlay').trigger('mouseleave') + expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;') + }) + + test('arrow work', async () => { + const TestComponent = defineComponent({ + components: { IxButton }, + setup() { + const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, arrowRef } = useOverlay({ + ...options, + showDelay: 0, + showArrow: true, + }) + + onMounted(initialize) + + return { overlayRef, triggerRef, triggerEvents, overlayEvents, arrowRef } + }, + template: ` + +
Overlay +
+
+ `, + }) + const wrapper = mount(TestComponent) + await wrapper.get('#trigger').trigger('click') + expect(wrapper.get('#arrow')).toBeDefined() + }) + + // todo global scroll +}) diff --git a/packages/cdk/overlay/demo/Basic.vue b/packages/cdk/overlay/demo/Basic.vue new file mode 100644 index 000000000..84c2d7386 --- /dev/null +++ b/packages/cdk/overlay/demo/Basic.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/cdk/overlay/demo/basic.md b/packages/cdk/overlay/demo/basic.md new file mode 100644 index 000000000..9a0348fcb --- /dev/null +++ b/packages/cdk/overlay/demo/basic.md @@ -0,0 +1,26 @@ +--- +order: 0 +title: + zh: 基本使用 + en: Basic usage +--- + +## zh + +- 如何创建一个浮层 +- 如何初始化浮层 +- 如何更新浮层 +- 如何在模板中绑定对应的事件 + +另外,在组件中使用当前cdk,按照规范请配套使用组件Portal. + +## en + +- How to create an overlay +- How to initialize the overlay +- How to update the configuration of the overlay +- How to bind the events in the template + +By the way, to use the cdk in components, please use the component Portal according to the specification. + +## demo diff --git a/packages/cdk/overlay/docs/index.zh.md b/packages/cdk/overlay/docs/index.zh.md new file mode 100644 index 000000000..24a948e7f --- /dev/null +++ b/packages/cdk/overlay/docs/index.zh.md @@ -0,0 +1,158 @@ +--- +category: cdk +type: +title: Overlay +subtitle: +cover: +--- + +参考自 [element-plus](https://github.com/element-plus/element-plus/tree/dev/packages/popper/src/use-popper) + +- 创建定位浮层:`useOverlay` + +## 何时使用 + +- `useOverlay`:创建定位浮层 + +## API + +### `useOverlay` + +> 创建一个浮层实例 + +```typescript +import type { Options, Placement } from '@popperjs/core' +import type { ComponentPublicInstance, ComputedRef, Ref } from 'vue' + +type OverlayScrollStrategy = 'close' | 'reposition' +type OverlayTrigger = 'click' | 'hover' | 'focus' +type RefElement = Nullable +type VueElement = Nullable + +declare function useOverlay(options: OverlayOptions): OverlayInstance + +interface OverlayTriggerEvents { + onClick?: (event: Event) => void + onMouseEnter?: (event: Event) => void + onMouseLeave?: (event: Event) => void + onFocus?: (event: Event) => void + onBlur?: (event: Event) => void +} + +interface OverlayPopperEvents { + onMouseEnter: () => void + onMouseLeave: () => void +} + +interface OverlayOptions { + /* The class name of the overlay container. */ + className?: string + /** + * Control the visibility of the overlay + */ + visible?: boolean + /* Scroll strategy for overlay */ + scrollStrategy?: OverlayScrollStrategy + /* Disable the overlay */ + disable?: boolean + /** + * The distance between the arrow and the starting point at both ends. + *Acting when the overlay uses border-radius. + */ + arrowOffset?: number + /* Whether to show arrow. */ + showArrow?: boolean + /* Alignment of floating layer. */ + placement: Placement + /** + * The options of popper. + * Used when ConnectOverlayOptions cannot meet the demand. + * Priority is higher than ConnectOverlayOptions. + */ + popperOptions?: Partial + /* Trigger method. */ + trigger: OverlayTrigger + /* Whether to allow the mouse to enter the overlay. */ + allowEnter?: boolean + /** + * Overlay offset. + * [Horizontal axis offset, vertical axis offset] + */ + offset: [number, number] + /** + * The delay of hiding overlay. + * Send false if you don't need it. + */ + hideDelay: number | false + /** + * The delay of showing overlay. + * Send false if you don't need it. + */ + showDelay: number | false +} + +interface OverlayInstance { + /** + * Initialize the overlay. + * The life cycle of the overlay will enter mounted. + */ + initialize: () => void + /** + * Show the overlay. + * The style of the overlay container will be set to block. + */ + show: () => void + /** + * Hide the overlay. + * The style of the overlay container will be set to none. + */ + hide: () => void + /** + * Destroy the overlay. + * The life cycle of the overlay will enter beforeDestroy. + * To show the overlay again, please recreate. + */ + destroy: () => void + /** + * TODO + * The unique id of the overlay. + * Provide subsequent components with markings for the specified overlay treatment. + */ + overlayId: string + /** + * The display status of the current overlay. + * Control by visible and disable. + */ + visibility: ComputedRef + /** + * The truth DOM node of the overlay. + * The caller needs to bind the variable to the view. + */ + overlayRef: Ref + /** + * Update overlay. + * If the overlay has not been initialized, the overlay will be initialized first, otherwise the overlay will be update directly. + * @param options + */ + update: (options: Partial) => void + /** + * The truth DOM node of the arrow. + * If showArrow is false, we won't return arrowRef. + * The caller needs to bind the variable to the view. + */ + arrowRef?: Ref + /** + * The truth DOM node of the trigger. + * The caller needs to bind the variable to the view. + */ + triggerRef: Ref + /** + * Manually bind to the event on the trigger. + */ + triggerEvents: OverlayTriggerEvents + /** + * Manually bind to events on the overlay. + */ + overlayEvents: OverlayPopperEvents +} +``` diff --git a/packages/cdk/overlay/index.ts b/packages/cdk/overlay/index.ts new file mode 100644 index 000000000..db46bf44c --- /dev/null +++ b/packages/cdk/overlay/index.ts @@ -0,0 +1,2 @@ +export * from './src/useOverlay' +export * from './src/types' diff --git a/packages/cdk/overlay/src/types.ts b/packages/cdk/overlay/src/types.ts new file mode 100644 index 000000000..2f1b1329f --- /dev/null +++ b/packages/cdk/overlay/src/types.ts @@ -0,0 +1,130 @@ +import type { Options, Placement } from '@popperjs/core' +import type { ComponentPublicInstance, ComputedRef, Ref } from 'vue' + +export type OverlayScrollStrategy = 'close' | 'reposition' +export type OverlayTrigger = 'click' | 'hover' | 'focus' +export type RefElement = Nullable +export type TriggerElement = Nullable + +export interface OverlayTriggerEvents { + onClick?: (event: Event) => void + onMouseEnter?: (event: Event) => void + onMouseLeave?: (event: Event) => void + onFocus?: (event: Event) => void + onBlur?: (event: Event) => void +} + +export interface OverlayPopperEvents { + onMouseEnter: () => void + onMouseLeave: () => void +} + +export interface OverlayOptions { + /** + * Control the visibility of the overlay + */ + visible?: boolean + /* Scroll strategy for overlay */ + scrollStrategy: OverlayScrollStrategy + /* Disable the overlay */ + disabled?: boolean + /** + * The distance between the arrow and the starting point at both ends. + *Acting when the overlay uses border-radius. + */ + arrowOffset?: number + /* Whether to show arrow. */ + showArrow?: boolean + /* Alignment of floating layer. */ + placement: Placement + /** + * The options of popper. + * Used when ConnectOverlayOptions cannot meet the demand. + * Priority is higher than ConnectOverlayOptions. + */ + popperOptions?: Partial + /* Trigger method. */ + trigger: OverlayTrigger + /* Whether to allow the mouse to enter the overlay. */ + allowEnter?: boolean + /** + * Overlay offset. + * [Horizontal axis offset, vertical axis offset] + */ + offset: [number, number] + /** + * The delay of hiding overlay. + * Send 0 if you don't need it. + */ + hideDelay: number + /** + * The delay of showing overlay. + * Send 0 if you don't need it. + */ + showDelay: number +} + +export interface OverlayInstance { + /** + * Initialize the overlay. + * The life cycle of the overlay will enter mounted. + */ + initialize: () => void + /** + * Show the overlay. + * The style of the overlay container will be set to block. + */ + show: (immediate?: boolean) => void + /** + * Hide the overlay. + * The style of the overlay container will be set to none. + */ + hide: (immediate?: boolean) => void + /** + * Update overlay. + * If the overlay has not been initialized, the overlay will be initialized first, otherwise the overlay will be update directly. + * @param options + */ + update: (options: Partial) => void + /** + * Destroy the overlay. + * The life cycle of the overlay will enter beforeDestroy. + * To show the overlay again, please recreate. + */ + destroy: () => void + /** + * TODO + * The unique id of the overlay. + * Provide subsequent components with markings for the specified overlay treatment. + */ + id: string + /** + * The display status of the current overlay. + * Control by visible and disable. + */ + visibility: ComputedRef + /** + * The truth DOM node of the overlay. + * The caller needs to bind the variable to the view. + */ + overlayRef: Ref + /** + * The truth DOM node of the arrow. + * If showArrow is false, we won't return arrowRef. + * The caller needs to bind the variable to the view. + */ + arrowRef?: Ref + /** + * The truth DOM node of the trigger. + * The caller needs to bind the variable to the view. + */ + triggerRef: Ref + /** + * Manually bind to the event on the trigger. + */ + triggerEvents: ComputedRef + /** + * Manually bind to events on the overlay. + */ + overlayEvents: OverlayPopperEvents +} diff --git a/packages/cdk/overlay/src/useModifiers.ts b/packages/cdk/overlay/src/useModifiers.ts new file mode 100644 index 000000000..e68190fb6 --- /dev/null +++ b/packages/cdk/overlay/src/useModifiers.ts @@ -0,0 +1,33 @@ +import type { StrictModifiers } from '@popperjs/core' +import type { RefElement } from './types' + +interface ModifierProps { + offset: [number, number] + arrow?: RefElement + arrowOffset?: number + showArrow?: boolean +} + +export function useModifiers( + { offset, arrow, arrowOffset, showArrow }: ModifierProps, + externalModifiers: StrictModifiers[] = [], +): StrictModifiers[] { + const modifiers: StrictModifiers[] = [ + { name: 'offset', options: { offset } }, + { name: 'preventOverflow', options: { padding: { top: 2, bottom: 2, left: 5, right: 5 } } }, + { name: 'flip', options: { padding: 5 } }, + ] + if (showArrow) { + modifiers.push({ + name: 'arrow', + options: { + element: arrow, + padding: arrowOffset ?? 5, + }, + }) + } + + modifiers.push(...externalModifiers) + + return modifiers +} diff --git a/packages/cdk/overlay/src/useOverlay.ts b/packages/cdk/overlay/src/useOverlay.ts new file mode 100644 index 000000000..a6341505e --- /dev/null +++ b/packages/cdk/overlay/src/useOverlay.ts @@ -0,0 +1,195 @@ +import type { Instance as PopperInstance } from '@popperjs/core' +import type { ComputedRef } from 'vue' + +import type { + OverlayInstance, + OverlayOptions, + OverlayPopperEvents, + OverlayTrigger, + OverlayTriggerEvents, + RefElement, + TriggerElement, +} from './types' + +import { ComponentPublicInstance, computed, reactive, ref, watch } from 'vue' +import { createPopper } from '@popperjs/core' +import { isHTMLElement, uniqueId } from '@idux/cdk/utils' + +import { usePopperOptions } from './usePopperOptions' + +export const useOverlay = (options: OverlayOptions): OverlayInstance => { + const arrowRef = ref(null) + const triggerRef = ref(null) + const overlayRef = ref(null) + let popperInstance: Nullable = null + + let showTimer: Nullable = null + let hideTimer: Nullable = null + + const state = reactive(options) + const popperOptions = usePopperOptions(options, { arrow: arrowRef }) + + const overlayVisibility = (visible: boolean): HTMLElement | null => { + const overlayElement = overlayRef.value + if (overlayElement) { + overlayElement.style.display = visible ? 'block' : 'none' + } + return overlayElement + } + + const initialize = (): void => { + const overlayElement = overlayVisibility(visibility.value) + if (!visibility.value) { + return + } + const unrefTrigger = triggerRef.value + if (!unrefTrigger) { + return + } + const triggerElement = isHTMLElement(unrefTrigger) ? unrefTrigger : (unrefTrigger as ComponentPublicInstance).$el + popperInstance = createPopper(triggerElement, overlayElement as HTMLElement, popperOptions.value) + popperInstance.update() + window.addEventListener('scroll', globalScroll) + } + + const _toggle = (visible: boolean) => { + state.visible = visible + } + + const _clearTimer = () => { + window.clearTimeout(showTimer as number) + window.clearTimeout(hideTimer as number) + } + + const show = (immediate?: boolean): void => { + _clearTimer() + if (immediate || state.showDelay === 0) { + _toggle(true) + } else { + showTimer = window.setTimeout(() => { + _toggle(true) + }, state.showDelay) + } + } + + const hide = (immediate?: boolean): void => { + _clearTimer() + if (immediate || state.hideDelay === 0) { + _toggle(false) + } else { + hideTimer = window.setTimeout(() => { + _toggle(false) + }, state.hideDelay) + } + } + + const destroy = (): void => { + if (!popperInstance) { + return + } + popperInstance.destroy() + popperInstance = null + window.removeEventListener('scroll', globalScroll) + } + + const update = (options: Partial): void => { + Object.assign(state, options) + } + + const visibility = computed(() => !state.disabled && !!state.visible) + + const onVisibilityChange = (visible: boolean) => { + overlayVisibility(visible) + if (!visible) { + // improve performance + destroy() + } + initialize() + } + + watch(visibility, onVisibilityChange) + + const overlayEventHandler = (e: Event): void => { + e.stopPropagation() + switch (e.type) { + case 'click': { + visibility.value ? hide() : show() + break + } + case 'mouseenter': { + show() + break + } + case 'mouseleave': { + hide() + break + } + case 'focus': { + show() + break + } + case 'blur': { + hide() + break + } + } + } + + const mapTriggerEvents = (state: OverlayOptions): ComputedRef => { + return computed(() => { + const triggerToEvents: Record> = { + click: ['onClick'], + focus: ['onFocus', 'onBlur'], + hover: ['onMouseEnter', 'onMouseLeave'], + } + return triggerToEvents[state.trigger].reduce((obj, key) => { + obj[key] = overlayEventHandler + return obj + }, {} as OverlayTriggerEvents) + }) + } + + const triggerEvents: ComputedRef = mapTriggerEvents(state) + + const onOverlayMouseEnter = () => { + if (state.allowEnter && state.trigger !== 'click') { + clearTimeout(hideTimer as number) + } + } + + const onOverlayMouseLeave = () => { + if (state.trigger !== 'hover') { + return + } + hide() + } + + const overlayEvents: OverlayPopperEvents = { + onMouseEnter: onOverlayMouseEnter, + onMouseLeave: onOverlayMouseLeave, + } + + const globalScroll = () => { + if (!visibility.value || !popperInstance) { + return + } + if (state.scrollStrategy === 'close') { + hide(true) + } + } + + return { + arrowRef, + triggerRef, + overlayRef, + initialize, + show, + hide, + destroy, + id: uniqueId('ix-overlay'), + update, + visibility, + triggerEvents, + overlayEvents, + } +} diff --git a/packages/cdk/overlay/src/usePopperOptions.ts b/packages/cdk/overlay/src/usePopperOptions.ts new file mode 100644 index 000000000..ee2039222 --- /dev/null +++ b/packages/cdk/overlay/src/usePopperOptions.ts @@ -0,0 +1,31 @@ +import type { ComputedRef, Ref } from 'vue' +import type { Options, Placement } from '@popperjs/core' +import type { RefElement } from './types' + +import { computed } from 'vue' +import { useModifiers } from './useModifiers' + +type PartialOptions = Partial + +interface PopperProps { + arrowOffset?: number + offset: [number, number] + popperOptions?: PartialOptions + placement: Placement + showArrow?: boolean +} + +interface PopperState { + arrow: Ref +} + +export function usePopperOptions(props: PopperProps, state: PopperState): ComputedRef { + return computed(() => ({ + placement: props.placement, + ...props.popperOptions, + modifiers: useModifiers( + { offset: props.offset, arrow: state.arrow.value, arrowOffset: props.arrowOffset, showArrow: props.showArrow }, + props.popperOptions?.modifiers, + ), + })) +} diff --git a/packages/cdk/package.json b/packages/cdk/package.json index bda99f614..5649f9e81 100644 --- a/packages/cdk/package.json +++ b/packages/cdk/package.json @@ -32,6 +32,7 @@ "url": "https://github.com/IduxFE/components/issues" }, "peerDependencies": { + "@popperjs/core": "^2.6.0", "vue": "^3.0.0" } -} \ No newline at end of file +} diff --git a/packages/cdk/utils/__tests__/typeof.spec.ts b/packages/cdk/utils/__tests__/typeof.spec.ts index adcb2bc80..1a316fefd 100644 --- a/packages/cdk/utils/__tests__/typeof.spec.ts +++ b/packages/cdk/utils/__tests__/typeof.spec.ts @@ -17,6 +17,7 @@ import { isNonNil, isNumeric, hasOwnProperty, + isHTMLElement, } from '../typeof' describe('typeof.ts', () => { @@ -172,4 +173,13 @@ describe('typeof.ts', () => { object2.test = undefined expect(hasOwnProperty(object2, 'test')).toEqual(true) }) + + test('isHTMLElement work', () => { + const div = document.createElement('div') + expect(isHTMLElement(div)).toBeTruthy() + expect(isHTMLElement(false)).toBeFalsy() + expect(isHTMLElement(10)).toBeFalsy() + expect(isHTMLElement('hello')).toBeFalsy() + expect(isHTMLElement({ key: 'Hello' })).toBeFalsy() + }) }) diff --git a/packages/cdk/utils/__tests__/uniqueId.spec.ts b/packages/cdk/utils/__tests__/uniqueId.spec.ts new file mode 100644 index 000000000..b2d7205b0 --- /dev/null +++ b/packages/cdk/utils/__tests__/uniqueId.spec.ts @@ -0,0 +1,25 @@ +import { uniqueId } from '../uniqueId' + +describe('uniqueId.ts', () => { + test('basic', () => { + const id = uniqueId() + expect(id).toBeDefined() + }) + + test('prefix', () => { + const prefix = 'prefix' + const id = uniqueId(prefix) + expect(id.startsWith(prefix)).toBeTruthy() + }) + + test('unique', () => { + const stack: string[] = [] + let num = 10 + while (num) { + const id = uniqueId() + expect(stack.includes(id)).toBeFalsy() + stack.push(id) + num-- + } + }) +}) diff --git a/packages/cdk/utils/index.ts b/packages/cdk/utils/index.ts index 585328bb9..54ea9cd57 100644 --- a/packages/cdk/utils/index.ts +++ b/packages/cdk/utils/index.ts @@ -3,3 +3,4 @@ export * from './propTypes' export * from './typeof' export * from './vNode' export * from './dom' +export * from './uniqueId' diff --git a/packages/cdk/utils/typeof.ts b/packages/cdk/utils/typeof.ts index 5a1d1f9ea..0ed3a468f 100644 --- a/packages/cdk/utils/typeof.ts +++ b/packages/cdk/utils/typeof.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { toRawType } from '@vue/shared' + const _toString = Object.prototype.toString /** The method checks whether the given value is a Numeric value or not and returns the corresponding boolean value. */ @@ -73,3 +75,5 @@ export function isPromise(val: unknown): val is Promise { export function hasOwnProperty(val: object, key: string | symbol): key is keyof typeof val { return Object.prototype.hasOwnProperty.call(val, key) } + +export const isHTMLElement = (val: unknown): boolean => toRawType(val).startsWith('HTML') diff --git a/packages/cdk/utils/uniqueId.ts b/packages/cdk/utils/uniqueId.ts new file mode 100644 index 000000000..d544e1995 --- /dev/null +++ b/packages/cdk/utils/uniqueId.ts @@ -0,0 +1,4 @@ +let nodeId = 0 +export function uniqueId(prefix = 'ix'): string { + return `${prefix}-${nodeId++}` +} diff --git a/typings/global.d.ts b/typings/global.d.ts index dcbd8607f..1531059bf 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -3,3 +3,5 @@ declare type IsNullable = undefined extends T ? K : never declare type OptionalKeys = { [K in keyof T]-?: IsNullable }[keyof T] declare type RequiredKeys = keyof Omit> + +declare type Nullable = T | null