diff --git a/codex-ui/dev/Playground.vue b/codex-ui/dev/Playground.vue index f17b7a6b..5c4d86d6 100644 --- a/codex-ui/dev/Playground.vue +++ b/codex-ui/dev/Playground.vue @@ -29,6 +29,7 @@ + @@ -36,7 +37,8 @@ import { computed } from 'vue'; import { VerticalMenu, - Tabbar + Tabbar, + Popover } from '../src/vue'; import { useTheme } from '../src/vue/composables/useTheme'; diff --git a/codex-ui/dev/pages/components/Popover.vue b/codex-ui/dev/pages/components/Popover.vue index d8410d8c..cd2c6b66 100644 --- a/codex-ui/dev/pages/components/Popover.vue +++ b/codex-ui/dev/pages/components/Popover.vue @@ -5,12 +5,117 @@ Component that will appear near a particular element. Can contain any content. + + Aligning + + You can choose vertical aligning from below and above and horizontal aligning from left and right. +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + + Width + + + By default, width of the popover depends on its content. You can also set it to fit-target to stretch to the width of the target. + +
+
+ +
+ +
+ +
+
diff --git a/codex-ui/src/styles/index.pcss b/codex-ui/src/styles/index.pcss index 7aa817eb..d610b922 100644 --- a/codex-ui/src/styles/index.pcss +++ b/codex-ui/src/styles/index.pcss @@ -2,3 +2,4 @@ @import './dimensions.pcss'; @import './typography.pcss'; @import './mixins.pcss'; +@import './z-axis.pcss'; diff --git a/codex-ui/src/styles/z-axis.pcss b/codex-ui/src/styles/z-axis.pcss new file mode 100644 index 00000000..0e7ac9c8 --- /dev/null +++ b/codex-ui/src/styles/z-axis.pcss @@ -0,0 +1,3 @@ +:root { + --z-popover: 3; +} diff --git a/codex-ui/src/vue/components/popover/Popover.types.ts b/codex-ui/src/vue/components/popover/Popover.types.ts new file mode 100644 index 00000000..a4806351 --- /dev/null +++ b/codex-ui/src/vue/components/popover/Popover.types.ts @@ -0,0 +1,45 @@ +import type { Component } from 'vue'; + +/** + * Popover content: component and props + */ +export interface PopoverContent { + /** + * Component to render in the popover + */ + component: Component; + + /** + * Props to pass to the component + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: Record; +} + +/** + * Popover showing configuration + */ +export interface PopoverShowParams { + /** + * Element to move popover to + */ + targetEl: HTMLElement; + + /** + * Popover content: component and props + */ + with: PopoverContent; + + /** + * Vertical and horizontal alignment + */ + align: { + vertically: 'above' | 'below'; + horizontally: 'left' | 'right'; + }; + + /** + * Allows to stretch popover to the target element width + */ + width?: 'fit-target' | 'auto'; +} diff --git a/codex-ui/src/vue/components/popover/Popover.vue b/codex-ui/src/vue/components/popover/Popover.vue new file mode 100644 index 00000000..706e3e48 --- /dev/null +++ b/codex-ui/src/vue/components/popover/Popover.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/codex-ui/src/vue/components/popover/index.ts b/codex-ui/src/vue/components/popover/index.ts new file mode 100644 index 00000000..3cd00853 --- /dev/null +++ b/codex-ui/src/vue/components/popover/index.ts @@ -0,0 +1,5 @@ +import Popover from './Popover.vue'; + +export * from './usePopover'; +export * from './Popover.types'; +export { Popover }; diff --git a/codex-ui/src/vue/components/popover/usePopover.ts b/codex-ui/src/vue/components/popover/usePopover.ts new file mode 100644 index 00000000..ea8e5766 --- /dev/null +++ b/codex-ui/src/vue/components/popover/usePopover.ts @@ -0,0 +1,163 @@ +import { reactive, ref, shallowRef } from 'vue'; +import { createSharedComposable } from '@vueuse/core'; +import type { PopoverContent, PopoverShowParams } from './Popover.types'; + +/** + * Shared composable for the Popover component + * + * Popover is the empty container that can be moved to the target element and contain any component + * @example + * showPopover({ + * targetEl: el, // element to move popover to + * with: { + * component: ContextMenu, + * props: { + * items: [ + * { title: 'Item 1' }, + * { title: 'Item 2' }, + * { title: 'Item 3' }, + * ], + * }, + * }, + * align: { + * vertically: 'below', + * horizontally: 'left', + * }, + * width: 'fit-target', + * }); + */ +export const usePopover = createSharedComposable(() => { + /** + * Popover opening state + */ + const isOpen = ref(false); + + /** + * Popover position info used in the Popover component + */ + const position = reactive({ + left: 0, + top: 0, + transform: 'translate(0, 0)', + }); + + /** + * Popover width. Allows to stretch popover to the target element width + */ + const width = ref('auto'); + + /** + * Popover content: component and props + */ + const content = shallowRef(null); + + /** + * Move popover to the target element + * Also, align and set width + * @param targetEl - element to move popover to + * @param align - vertical and horizontal alignment + * @param widthConfig - allows to stretch popover to the target element width + */ + function move(targetEl: HTMLElement, align: PopoverShowParams['align'], widthConfig: PopoverShowParams['width'] = 'auto'): void { + const MARGIN = 6; + + const rect = targetEl.getBoundingClientRect(); + + let top = 0; + let left = 0; + let transformX = '0'; + let transformY = '0'; + + switch (align.vertically) { + case 'above': + top = rect.top - MARGIN + window.scrollY; + transformY = '-100%'; + break; + case 'below': + top = rect.bottom + MARGIN + window.scrollY; + transformY = '0'; + break; + } + + switch (align.horizontally) { + case 'left': + left = rect.left; + transformX = '0'; + break; + case 'right': + left = rect.right; + transformX = '-100'; + break; + } + + switch (widthConfig) { + case 'fit-target': + width.value = `${rect.width}px`; + break; + case 'auto': + width.value = 'auto'; + break; + } + + position.left = left; + position.top = top; + position.transform = `translate(${transformX}%, ${transformY})`; + } + + /** + * Show popover + */ + function show(): void { + isOpen.value = true; + } + + /** + * Method for attaching a component to the popover + * @param component - component to attach + * @param props - props to pass to the component + */ + function mountComponent(component: PopoverContent['component'], props: PopoverContent['props']): void { + content.value = { + component, + props, + }; + } + + /** + * Move popover to the passed element and show it + * @param params - popover showing configuration + */ + function showPopover(params: PopoverShowParams): void { + move(params.targetEl, params.align, params.width); + mountComponent(params.with.component, params.with.props); + show(); + } + + /** + * Empty content, position and hide popover + */ + function resetPopover(): void { + content.value = null; + position.left = 0; + position.top = 0; + position.transform = 'translate(0, 0)'; + + isOpen.value = false; + } + + /** + * Hides and resets the popover + */ + function hide(): void { + resetPopover(); + } + + return { + isOpen, + showPopover, + position, + hide, + content, + width, + }; +}); diff --git a/codex-ui/src/vue/index.ts b/codex-ui/src/vue/index.ts index eb768950..64ae4d4c 100644 --- a/codex-ui/src/vue/index.ts +++ b/codex-ui/src/vue/index.ts @@ -15,4 +15,5 @@ export * from './components/context-menu'; export * from './components/vertical-menu'; export * from './components/colorShemeIcons'; export * from './components/theme-preview'; +export * from './components/popover'; export * from './composables/useTheme'; diff --git a/src/App.vue b/src/App.vue index a15070aa..ac2bd447 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,12 +1,13 @@