Skip to content

Commit

Permalink
Merge pull request #224 from LUI-UI/popup-teleport
Browse files Browse the repository at this point in the history
feat(select,multiselect,popover,dropdown): create teleport prop to re…
  • Loading branch information
rhmkstk authored Oct 16, 2023
2 parents 531ceb8 + ca0f7a4 commit f9bc602
Show file tree
Hide file tree
Showing 16 changed files with 744 additions and 572 deletions.
46 changes: 45 additions & 1 deletion src/components/MenuDropdown/LuiMenuDropdown.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ const menuPositionTemplate = `
<lui-menu-dropdown v-for="position in positions" :key="position" :menu-position="position" :text="position">
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
</lui-menu-dropdown>
`
export const MenuPosition: Story = {
Expand All @@ -515,7 +517,7 @@ export const MenuPosition: Story = {
return { positions }
},
components: { LuiMenuDropdown, LuiMenuItem },
template: `<div class="px-28 pt-12 grid grid-cols-3 gap-x-2 gap-y-4 justify-center items-center">${menuPositionTemplate}</div>`,
template: `<div class="px-28 h-[4000px] py-[380px] grid grid-cols-3 gap-x-2 gap-y-4 justify-center items-center">${menuPositionTemplate}</div>`,
}),
parameters: {
docs: {
Expand All @@ -529,3 +531,45 @@ export const MenuPosition: Story = {
},
},
}
const teleportTemplate = `
<div class=h-[2000px]>
<div class="pt-[700px] flex space-x-6">
<lui-menu-dropdown text="leftTop" menu-position="leftTop" :teleport="true">
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
</lui-menu-dropdown>
<lui-menu-dropdown text="leftBottom" menu-position="leftBottom" :teleport="true">
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
<lui-menu-item>Some long items for center</lui-menu-item>
</lui-menu-dropdown>
</div>
</div>
`
export const Teleport: Story = {
render: () => ({
setup() {
return { }
},
components: { LuiMenuDropdown, LuiMenuItem },
template: `<div class="px-28 pt-12 grid grid-cols-3 gap-x-2 gap-y-4 justify-center items-center">${teleportTemplate}</div>`,
}),
parameters: {
docs: {
source: {
code: teleportTemplate,
},
description: {
story:
'The <b>menu-position</b> prop is used to customize the position and alignment of where the dropdown menu opens.',
},
},
},
}
215 changes: 66 additions & 149 deletions src/components/MenuDropdown/LuiMenuDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ export default {
</script>

<script setup lang="ts">
import { computed, nextTick, reactive, ref, useSlots } from 'vue'
import { Teleport as TeleportComp, computed, h, nextTick, reactive, ref, toRefs, useSlots } from 'vue'
import type { PropType } from 'vue'
import LuiButton from '../Button/LuiButton.vue'
import { useOutsideClick } from '../../composables/useOutsideClick'
import { useProperPosition } from '../../composables/useProperPosition'
import { useTeleportWrapper } from '../../composables/useTeleportWrapper'
// import { useMenuPositionStyles } from '../../composables/useMenuPositionStyles'
import { useMenuStyles } from '../../composables/useMenuStyles'
import { useId } from '../../utils/useId'
import type { Block, Color, Filter, Position, Rounded, Size, Variant } from '@/globals/types'
import type { TwClassInterface } from '@/globals/interfaces'
Expand Down Expand Up @@ -65,134 +68,34 @@ const props = defineProps({
default: false,
},
menuClasses: {
type: [String, Array] as PropType<String | String[]>,
type: [String, Array] as PropType<string | string[]>,
default: '',
},
teleport: {
type: Boolean as PropType<boolean>,
default: false,
},
})
const emit = defineEmits(['onTrigger'])
const slots = useSlots()
// VARIABLES
const luiDropdownWrapper = ref<HTMLElement>()
// const luiDropdownButton = ref<InstanceType<typeof LuiButton>>();
const luiDropdownTrigger = ref<HTMLDivElement>()
const luiDropdownMenu = ref<HTMLUListElement>()
const luiDropdownMenu = ref<HTMLElement | null>(null)
const menuActive = ref(false)
const buttonId = `lui-dropdown-button-${useId()}`
const menuId = `lui-dropdown-menu-${useId()}`
const menuState: MenuStateType = reactive({
items: [],
currentIndex: 0,
currentId: '',
})
const positionClasses = {
bottomLeft: {
classes: 'top-full mt-1',
oppositeClasses: 'bottom-full mb-1 left-0',
direction: 'bottom',
},
topLeft: {
classes: 'bottom-full mb-1 left-0',
oppositeClasses: 'top-full mt-1',
direction: 'top',
},
bottomRight: {
classes: 'top-full mt-1 right-0',
oppositeClasses: 'bottom-full mb-1 right-0',
direction: 'bottom',
},
topRight: {
classes: 'bottom-full mb-1 right-0',
oppositeClasses: 'top-full mt-1 right-0',
direction: 'top',
},
leftTop: {
classes: 'top-0 mr-1 right-full',
oppositeClasses: 'bottom-0 mr-1 right-full',
direction: 'bottom',
},
rightTop: {
classes: 'top-0 ml-1 left-full',
oppositeClasses: 'bottom-0 ml-1 left-full',
direction: 'bottom',
},
leftBottom: {
classes: 'bottom-0 mr-1 right-full',
oppositeClasses: 'top-0 mr-1 right-full',
direction: 'top',
},
rightBottom: {
classes: 'bottom-0 ml-1 left-full',
oppositeClasses: 'top-0 ml-1 left-full',
direction: 'top',
},
bottom: {
classes: 'top-full mt-1 left-1/2 -translate-x-1/2',
oppositeClasses: 'bottom-full mb-1 left-1/2 -translate-x-1/2',
direction: 'bottom',
},
top: {
classes: 'bottom-full mb-1 left-1/2 -translate-x-1/2',
oppositeClasses: 'top-full mt-1 left-1/2 -translate-x-1/2',
direction: 'bottom',
},
left: {
classes: 'mr-1 right-full top-1/2 -translate-y-1/2',
oppositeClasses: 'mr-1 right-full top-1/2 -translate-y-1/2',
direction: 'bottom',
},
right: {
classes: 'ml-1 left-full top-1/2 -translate-y-1/2',
oppositeClasses: 'ml-1 left-full top-1/2 -translate-y-1/2',
direction: 'bottom',
},
}
type TargetPositionType = 'bottom' | 'top'
function setTargetPosition(): TargetPositionType {
const positionLowerCase = props.menuPosition.toLowerCase()
return positionLowerCase.includes('bottom') ? 'bottom' : 'top'
}
const { properPosition } = useProperPosition({
triggerEl: luiDropdownWrapper,
MenuEl: luiDropdownMenu,
targetPosition: setTargetPosition(),
})
const teleportId = useTeleportWrapper('dropdown')
// COMPUTEDS
const computedMenuPosition = computed(() => {
return positionClasses[props.menuPosition].direction === properPosition.value
? positionClasses[props.menuPosition].classes
: positionClasses[props.menuPosition].oppositeClasses
})
const { classes: menuClasses, styles: menuStyles } = useMenuStyles({ ...toRefs(props), triggerEl: luiDropdownWrapper, menuEl: luiDropdownMenu })
const dropdownMenuClasses = computed(() => {
const optionsWrapper: TwClassInterface = {
position: 'absolute',
width: 'w-max',
zIndex: 'z-50',
maxHeight: 'max-h-96',
minWidth: 'min-w-full',
overflow: 'overflow-y-auto',
backgroundColor: 'bg-secondary-50 dark:bg-secondary-900',
borderWidth: 'border',
borderColor: 'border-secondary-200 dark:border-secondary-700',
borderRadius: {
'rounded-md': props.rounded === true,
'rounded-2xl': props.rounded === 'full',
},
padding: {
'p-1.5': props.size === 'xs' || props.size === 'sm',
'p-2': props.size === 'md',
'p-2.5': props.size === 'lg' || props.size === 'xl',
},
boxShadow: 'shadow-lg',
// bottom: properPosition.value === 'top' ? 'bottom-full' : '',
// top: properPosition.value === 'bottom' ? 'top-full' : '',
// margin: properPosition.value === 'bottom' ? 'mt-2' : 'mb-2',
space: props.size === 'xs' || props.size === 'sm' ? 'space-y-1.5' : 'space-y-2',
}
return Object.values({ ...optionsWrapper })
})
const dropdownWrapperClasses = computed(() => {
const classes: TwClassInterface = {
position: 'relative',
Expand Down Expand Up @@ -339,9 +242,27 @@ function focusAvailableElement(
}
}
function triggerIconSize(size: string) {
function triggerIconSize(size: Size) {
return size === 'xs' ? '12' : size === 'sm' ? '16' : size === 'xl' ? '24' : '20'
}
function ArrowDownIcon() {
return h(
'svg',
{
viewBox: '0 0 12 12',
fill: 'currentColor',
width: triggerIconSize(props.size),
height: triggerIconSize(props.size),
},
[
h('path',
{
fill: 'white',
d: 'M5.99999 6.58599L8.47499 4.11099L9.18199 4.81799L5.99999 7.99999L2.81799 4.81799L3.52499 4.11099L5.99999 6.58599Z',
}),
],
)
}
</script>

<template>
Expand Down Expand Up @@ -374,7 +295,8 @@ function triggerIconSize(size: string) {
{{ text }}
<template #append>
<slot name="append">
<svg
<ArrowDownIcon />
<!-- <svg
viewBox="0 0 12 12"
:width="triggerIconSize(size)"
:height="triggerIconSize(size)"
Expand All @@ -385,48 +307,43 @@ function triggerIconSize(size: string) {
d="M5.99999 6.58599L8.47499 4.11099L9.18199 4.81799L5.99999 7.99999L2.81799 4.81799L3.52499 4.11099L5.99999 6.58599Z"
fill="white"
/>
</svg>
</svg> -->
</slot>
</template>
</LuiButton>
</slot>
</div>

<!-- <lui-button
:id="buttonId"
ref="luiDropdownButton"
type="button"
aria-haspopup="true"
:aria-expanded="menuActive"
:aria-controls="menuId"
:block="block"
@click="toogleMenu"
@keydown="handleButtonKeyEvents"
<component
:is="teleport ? TeleportComp : 'div'"
v-bind="teleport ? { to: `#${teleportId}` } : undefined"
>
<span v-if="text !== null">{{ text }}</span>
<slot v-else name="button" />
</lui-button> -->
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<ul
v-show="menuActive"
:id="menuId"
ref="luiDropdownMenu"
role="menu"
:aria-labelledby="buttonId"
:aria-activedescendant="String(menuState.currentIndex)"
tabindex="0"
:class="[computedMenuPosition, menuClasses.length > 0 ? menuClasses : dropdownMenuClasses]"
@keydown="handleMenuKeyEvents"
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0 "
>
<slot />
</ul>
</transition>
<div
v-show="menuActive"
:id="menuId"
ref="luiDropdownMenu"
:class="menuClasses"
:style="menuStyles"
>
<ul
role="menu"
:aria-labelledby="buttonId"
:aria-activedescendant="String(menuState.currentIndex)"
tabindex="0"
:class="size === 'xs' || size === 'sm' ? 'p-1.5' : size === 'md' ? 'p-2' : 'p-2.5'"
@keydown="handleMenuKeyEvents"
>
<slot />
</ul>
</div>
</transition>
</component>
</div>
</template>
40 changes: 21 additions & 19 deletions src/components/MenuItem/LuiMenuItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,25 @@ export default {
</script>

<template>
<component
:is="tag"
:id="menuItemId"
ref="menuItemRef"
role="menuitem"
class="lui-menu-item"
:class="customStyle ? '' : computedMenuItemClasses"
v-bind="$attrs"
>
<span v-if="$slots.prepend" :class="computedAppendAndPrependClasses">
<slot name="prepend" />
</span>
<div :class="computedDefaultSlotClasses">
<slot />
</div>
<span v-if="$slots.append" :class="computedAppendAndPrependClasses">
<slot name="append" />
</span>
</component>
<li>
<component
:is="tag"
:id="menuItemId"
ref="menuItemRef"
role="menuitem"
class="lui-menu-item"
:class="customStyle ? '' : computedMenuItemClasses"
v-bind="$attrs"
>
<span v-if="$slots.prepend" :class="computedAppendAndPrependClasses">
<slot name="prepend" />
</span>
<div :class="computedDefaultSlotClasses">
<slot />
</div>
<span v-if="$slots.append" :class="computedAppendAndPrependClasses">
<slot name="append" />
</span>
</component>
</li>
</template>
Loading

0 comments on commit f9bc602

Please sign in to comment.