diff --git a/src/dropdown-item/index.js b/src/dropdown-item/index.js index e66c530e9..d7a796d9b 100644 --- a/src/dropdown-item/index.js +++ b/src/dropdown-item/index.js @@ -1,11 +1,12 @@ -import { Teleport } from 'vue'; +import { reactive, Teleport } from 'vue'; // Utils import { createNamespace } from '../utils'; -import { on, off } from '../utils/dom/event'; +import { DROPDOWN_KEY } from '../dropdown-menu'; -// Mixins -import { ChildrenMixin } from '../mixins/relation'; +// Composition +import { useParent } from '../composition/use-relation'; +import { usePublicApi } from '../composition/use-public-api'; // Components import Cell from '../cell'; @@ -15,8 +16,6 @@ import Popup from '../popup'; const [createComponent, bem] = createNamespace('dropdown-item'); export default createComponent({ - mixins: [ChildrenMixin('vanDropdownMenu')], - props: { title: String, disabled: Boolean, @@ -35,93 +34,79 @@ export default createComponent({ emits: ['open', 'opened', 'close', 'closed', 'change', 'update:modelValue'], - data() { - return { - transition: true, + setup(props, { emit, slots }) { + const state = reactive({ showPopup: false, + transition: true, showWrapper: false, - }; - }, + }); - computed: { - displayTitle() { - if (this.title) { - return this.title; + const renderTitle = () => { + if (slots.title) { + return slots.title(); } - const match = this.options.filter( - (option) => option.value === this.modelValue + if (props.title) { + return props.title; + } + + const match = props.options.filter( + (option) => option.value === props.modelValue ); + return match.length ? match[0].text : ''; - }, - }, + }; - watch: { - showPopup(val) { - this.bindScroll(val); - }, - }, - - beforeCreate() { - const createEmitter = (eventName) => () => this.$emit(eventName); - - this.onOpen = createEmitter('open'); - this.onClose = createEmitter('close'); - this.onOpened = createEmitter('opened'); - }, - - methods: { - // @exposed-api - toggle(show = !this.showPopup, options = {}) { - if (show === this.showPopup) { + const toggle = (show = !state.showPopup, options = {}) => { + if (show === state.showPopup) { return; } - this.transition = !options.immediate; - this.showPopup = show; + state.showPopup = show; + state.transition = !options.immediate; if (show) { - this.parent.updateOffset(); - this.showWrapper = true; + state.showWrapper = true; } - }, + }; - bindScroll(bind) { - const { scroller } = this.parent; - const action = bind ? on : off; - action(scroller, 'scroll', this.onScroll, true); - }, + const { parent } = useParent(DROPDOWN_KEY, { + props, + state, + toggle, + renderTitle, + }); - onScroll() { - this.parent.updateOffset(); - }, + const createEmitter = (eventName) => () => emit(eventName); + const onOpen = createEmitter('open'); + const onClose = createEmitter('close'); + const onOpened = createEmitter('opened'); - onClosed() { - this.showWrapper = false; - this.$emit('closed'); - }, + const onClosed = () => { + state.showWrapper = false; + emit('closed'); + }; - onClickWrapper(event) { + const onClickWrapper = (event) => { // prevent being identified as clicking outside and closed when using teleport - if (this.teleport) { + if (props.teleport) { event.stopPropagation(); } - }, - }, + }; - render() { - const { - zIndex, - offset, - overlay, - duration, - direction, - activeColor, - closeOnClickOverlay, - } = this.parent; + const renderOption = (option) => { + const { activeColor } = parent.props; + const active = option.value === props.modelValue; + + const onClick = () => { + state.showPopup = false; + + if (option.value !== props.modelValue) { + emit('update:modelValue', option.value); + emit('change', option.value); + } + }; - const Options = this.options.map((option) => { - const active = option.value === this.modelValue; return ( { - this.showPopup = false; - - if (option.value !== this.modelValue) { - this.$emit('update:modelValue', option.value); - this.$emit('change', option.value); - } - }} + onClick={onClick} > {active && ( )} ); - }); + }; - const style = { zIndex }; - if (direction === 'down') { - style.top = `${offset}px`; - } else { - style.bottom = `${offset}px`; - } + const renderContent = () => { + const { offset } = parent; + const { + zIndex, + overlay, + duration, + direction, + closeOnClickOverlay, + } = parent.props; - const Content = ( -
- - {Options} - {this.$slots.default?.()} - -
- ); + + {props.options.map(renderOption)} + {slots.default?.()} + + + ); + }; - if (this.teleport) { - return {Content}; - } + usePublicApi({ toggle }); - return Content; + return () => { + if (props.teleport) { + return {renderContent()}; + } + return renderContent(); + }; }, }); diff --git a/src/dropdown-menu/index.js b/src/dropdown-menu/index.js index 91d6ca75e..a8e9f0ad3 100644 --- a/src/dropdown-menu/index.js +++ b/src/dropdown-menu/index.js @@ -1,22 +1,19 @@ +import { ref, provide, reactive, computed } from 'vue'; + // Utils import { createNamespace, isDef } from '../utils'; -import { getScroller } from '../utils/dom/scroll'; -// Mixins -import { ParentMixin } from '../mixins/relation'; -import { ClickOutsideMixin } from '../mixins/click-outside'; +// Composition +import { useRect } from '../composition/use-rect'; +import { useScroller } from '../composition/use-scroller'; +import { useGlobalEvent } from '../composition/use-global-event'; +import { useClickOutside } from '../composition/use-click-outside'; const [createComponent, bem] = createNamespace('dropdown-menu'); -export default createComponent({ - mixins: [ - ParentMixin('vanDropdownMenu'), - ClickOutsideMixin({ - event: 'click', - method: 'onClickOutside', - }), - ], +export const DROPDOWN_KEY = 'vanDropdownMenu'; +export default createComponent({ props: { zIndex: [Number, String], activeColor: String, @@ -38,101 +35,108 @@ export default createComponent({ }, }, - data() { - return { - offset: 0, - }; - }, + setup(props, { slots }) { + const offset = ref(0); + const barRef = ref(); + const rootRef = ref(); + const children = reactive([]); - computed: { - scroller() { - return getScroller(this.$el); - }, + const scroller = useScroller(rootRef); - opened() { - return this.children.some((item) => item.showWrapper); - }, + const opened = computed(() => + children.some((item) => item.state.showWrapper) + ); - barStyle() { - if (this.opened && isDef(this.zIndex)) { + const barStyle = computed(() => { + if (opened.value && isDef(props.zIndex)) { return { - zIndex: 1 + this.zIndex, + zIndex: 1 + props.zIndex, }; } - }, - }, + }); - methods: { - updateOffset() { - if (!this.$refs.bar) { - return; + const onClickOutside = () => { + children.forEach((item) => { + item.toggle(false); + }); + }; + + const updateOffset = () => { + if (barRef.value) { + const rect = useRect(barRef); + if (props.direction === 'down') { + offset.value = rect.bottom; + } else { + offset.value = window.innerHeight - rect.top; + } } + }; - const rect = this.$refs.bar.getBoundingClientRect(); - - if (this.direction === 'down') { - this.offset = rect.bottom; - } else { - this.offset = window.innerHeight - rect.top; - } - }, - - toggleItem(active) { - this.children.forEach((item, index) => { + const toggleItem = (active) => { + children.forEach((item, index) => { if (index === active) { + updateOffset(); item.toggle(); - } else if (item.showPopup) { + } else if (item.state.showPopup) { item.toggle(false, { immediate: true }); } }); - }, + }; - onClickOutside() { - this.children.forEach((item) => { - item.toggle(false); - }); - }, - }, + const renderTitle = (item, index) => { + const { showPopup } = item.state; + const { disabled, titleClass } = item.props; - render() { - const Titles = this.children.map((item, index) => ( -
{ - if (!item.disabled) { - this.toggleItem(index); - } - }} - > - -
- {item.$slots.title ? item.$slots.title() : item.displayTitle} -
-
-
- )); - - return ( -
+ return (
{ + if (!disabled) { + toggleItem(index); + } + }} > - {Titles} + +
{item.renderTitle()}
+
- {this.$slots.default?.()} + ); + }; + + provide(DROPDOWN_KEY, { props, offset, children }); + + useClickOutside({ + element: rootRef, + callback: onClickOutside, + }); + + useGlobalEvent(scroller, 'scroll', () => { + if (opened.value) { + updateOffset(); + } + }); + + return () => ( +
+
+ {children.map(renderTitle)} +
+ {slots.default?.()}
); },