diff --git a/packages/mirinae/src/controls/dropdown/select-dropdown/type.ts b/packages/mirinae/src/controls/dropdown/select-dropdown/type.ts index a7c6de4cc0..aef2a8c0bb 100644 --- a/packages/mirinae/src/controls/dropdown/select-dropdown/type.ts +++ b/packages/mirinae/src/controls/dropdown/select-dropdown/type.ts @@ -1,5 +1,5 @@ import type { MenuItem } from '@/controls/context-menu/type'; -import type { MenuAttachHandler, MenuAttachHandlerRes } from '@/hooks/use-context-menu-controller/use-context-menu-attach'; +import type { MenuAttachHandler, MenuAttachHandlerRes } from '@/hooks/use-context-menu-attach/use-context-menu-attach'; export interface SelectDropdownMenuItem extends MenuItem { name: string; diff --git a/packages/mirinae/src/hooks/use-context-menu-attach/story-helper.ts b/packages/mirinae/src/hooks/use-context-menu-attach/story-helper.ts index 1d99ce1842..65c7ca9529 100644 --- a/packages/mirinae/src/hooks/use-context-menu-attach/story-helper.ts +++ b/packages/mirinae/src/hooks/use-context-menu-attach/story-helper.ts @@ -8,7 +8,7 @@ export const getUseContextMenuAttachArgs = (): Args => ({ attachHandler: undefined, menu: getContextMenuItems(), searchText: '', - pageSize: undefined, + pageSize: 3, filterItems: [], }); @@ -70,7 +70,7 @@ export const getUseContextMenuAttachArgTypes = (): ArgTypes - Attach - Reset +
+ Attach + Reset Menu & Pagination +
@@ -67,7 +69,7 @@ export const Basic: Story = {
Attach - Reset + Reset Menu & Pagination
diff --git a/packages/mirinae/src/hooks/use-context-menu-controller/UseContextMenuController.mdx b/packages/mirinae/src/hooks/use-context-menu-controller/UseContextMenuController.mdx new file mode 100644 index 0000000000..4a26474fa5 --- /dev/null +++ b/packages/mirinae/src/hooks/use-context-menu-controller/UseContextMenuController.mdx @@ -0,0 +1,54 @@ +{/* useContextMenuController.mdx */} + +import {Canvas, Meta, Controls} from '@storybook/blocks'; + +import * as useContextMenuControllerStories from './use-context-menu-controller.stories'; + + + + +# useContextMenuController + +The main hook controlling the context menu, integrating item management and style. + +## Type Declarations + +```typescript +interface UseContextMenuControllerOptions extends + Omit, + UseContextMenuItemsOptions { + visibleMenu?: Ref|boolean; // used for visibility control. related to fixed style feature and focusing feature. give this option or use returned value. + contextMenuRef: UseContextMenuStyleOptions['menuRef']; + useFixedStyle?: UseContextMenuStyleOptions['useFixedMenuStyle']; +} + +interface UseContextMenuControllerReturns { + visibleMenu: Ref; + refinedMenu: ReturnType['refinedMenu']; + contextMenuStyle: ReturnType['contextMenuStyle']; + loading: ReturnType['loading']; + showContextMenu: () => void; + hideContextMenu: () => void; + toggleContextMenu: () => void; + focusOnContextMenu: FocusOnContextMenu; + initiateMenu: ReturnType['initiateMenu']; + reloadMenu: ReturnType['reloadMenu']; + showMoreMenu: ReturnType['showMoreMenu']; +} +``` + +## Usage +
+ +### Basic + + +### Reorder by Selection + + +### Playground + + + +
+
diff --git a/packages/mirinae/src/hooks/use-context-menu-controller/story-helper.ts b/packages/mirinae/src/hooks/use-context-menu-controller/story-helper.ts new file mode 100644 index 0000000000..23d3e58f7c --- /dev/null +++ b/packages/mirinae/src/hooks/use-context-menu-controller/story-helper.ts @@ -0,0 +1,48 @@ +import type { ArgTypes, Parameters, Args } from '@storybook/vue'; + +import { getUseContextMenuItemsArgs, getUseContextMenuItemsArgTypes } from '@/hooks/use-context-menu-items/story-helper'; +import { getUseContextMenuStyleArgs, getUseContextMenuStyleArgTypes } from '@/hooks/use-context-menu-style/story-helper'; + +import type { UseContextMenuControllerOptions } from './use-context-menu-controller'; + +export const getUseContextMenuControllerArgs = (): Args => { + const useContextMenuStyleArgs = getUseContextMenuStyleArgs(); + const useContextMenuItemsArgs = getUseContextMenuItemsArgs(); + + useContextMenuStyleArgs.contextMenuRef = useContextMenuStyleArgs.menuRef; + delete useContextMenuStyleArgs.menuRef; + + useContextMenuItemsArgs.useFixedStyle = useContextMenuStyleArgs.useFixedMenuStyle; + delete useContextMenuStyleArgs.useFixedMenuStyle; + + const args: Args = { + ...useContextMenuStyleArgs, + ...useContextMenuItemsArgs, + }; + + return args; +}; + +export const getUseContextMenuControllerParameters = (): Parameters => ({}); + +export const getUseContextMenuControllerArgTypes = (): ArgTypes => { + const useContextMenuStyleArgTypes: any = getUseContextMenuStyleArgTypes(); + const useContextMenuItemsArgTypes = getUseContextMenuItemsArgTypes(); + + const contextMenuRefArgType = { ...useContextMenuStyleArgTypes.menuRef }; + contextMenuRefArgType.name = 'contextMenuRef'; + delete useContextMenuStyleArgTypes.menuRef; + + const useFixedStyleArgType = { ...useContextMenuStyleArgTypes.useFixedMenuStyle }; + useFixedStyleArgType.name = 'useFixedStyle'; + delete useContextMenuStyleArgTypes.useFixedMenuStyle; + + const argTypes: ArgTypes = { + ...useContextMenuStyleArgTypes, + ...useContextMenuItemsArgTypes, + contextMenuRef: contextMenuRefArgType, + useFixedStyle: useFixedStyleArgType, + }; + + return argTypes; +}; diff --git a/packages/mirinae/src/hooks/use-context-menu-controller/use-context-menu-controller.stories.ts b/packages/mirinae/src/hooks/use-context-menu-controller/use-context-menu-controller.stories.ts new file mode 100644 index 0000000000..3568cb1b54 --- /dev/null +++ b/packages/mirinae/src/hooks/use-context-menu-controller/use-context-menu-controller.stories.ts @@ -0,0 +1,232 @@ +import { computed, ref } from 'vue'; + +import type { Meta, StoryObj } from '@storybook/vue'; +import type { ComponentProps } from 'vue-component-type-helpers'; + +import PButton from '@/controls/buttons/button/PButton.vue'; +import { getContextMenuItems } from '@/controls/context-menu/mock'; +import PContextMenu from '@/controls/context-menu/PContextMenu.vue'; +import type { MenuItem } from '@/controls/context-menu/type'; + +import { + getUseContextMenuControllerArgs, getUseContextMenuControllerArgTypes, getUseContextMenuControllerParameters, +} from './story-helper'; +import type { UseContextMenuControllerOptions } from './use-context-menu-controller'; +import { useContextMenuController } from './use-context-menu-controller'; + + +type UseContextMenuControllerPropsAndCustomArgs = ComponentProps; + +const meta: Meta = { + title: 'Hooks/useContextMenuController', + argTypes: { + ...getUseContextMenuControllerArgTypes(), + }, + parameters: { + ...getUseContextMenuControllerParameters(), + }, + args: { + ...getUseContextMenuControllerArgs(), + }, +}; + +export default meta; +type Story = StoryObj; + + +const Template: Story = { + render: (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { PContextMenu, PButton }, + template: ` +
+
+ Show Menu + Hide Menu + Toggle Menu + Initiate + Reload +
+
+
Target
+ +
+
+ `, + setup(props) { + const targetRef = ref(null); + const menuRef = ref(null); + const visibleMenu = ref(false); + const selected = computed(() => props.selected ?? []); + const { + refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, showContextMenu, hideContextMenu, toggleContextMenu, + } = useContextMenuController({ + targetRef, + contextMenuRef: menuRef, + visibleMenu, + selected, + useFixedStyle: computed(() => props.useFixedStyle), + position: computed(() => props.position), + menuWidth: computed(() => props.menuWidth), + useReorderBySelection: props.useReorderBySelection, + useMenuFiltering: props.useMenuFiltering, + hideHeaderWithoutItems: props.hideHeaderWithoutItems, + menu: computed(() => props.menu), + searchText: computed(() => props.searchText), + pageSize: computed(() => props.pageSize), + }); + return { + selected, targetRef, menuRef, visibleMenu, refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, showContextMenu, hideContextMenu, toggleContextMenu, + }; + }, + }), + decorators: [() => ({ + template: '', + })], +}; + +export const Basic: Story = { + render: () => ({ + components: { PContextMenu, PButton }, + template: ` +
+
+ Show Menu + Hide Menu + Toggle Menu + Initiate + Reload + Focus on Menu +
+
+
Target
+ +
+
+ `, + setup() { + const targetRef = ref(null); + const menuRef = ref(null); + const visibleMenu = ref(false); + const menu = ref(getContextMenuItems()); + const selected = ref([]); + const { + refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, showContextMenu, hideContextMenu, toggleContextMenu, focusOnContextMenu, + } = useContextMenuController({ + targetRef, + contextMenuRef: menuRef, + visibleMenu, + menu, + pageSize: 3, + selected, + }); + const handleSelect = (item: MenuItem) => { + selected.value = [...selected.value, item]; + }; + return { + selected, + targetRef, + menuRef, + visibleMenu, + refinedMenu, + loading, + initiateMenu, + reloadMenu, + showMoreMenu, + showContextMenu, + hideContextMenu, + toggleContextMenu, + focusOnContextMenu, + handleSelect, + }; + }, + }), + decorators: [() => ({ + template: '', + })], +}; + +export const ReorderBySelection: Story = { + render: () => ({ + components: { PContextMenu, PButton }, + template: ` +
+
+ Show Menu & Initiate & Reorder + Hide Menu + Reset Selection +
+
+
Target
+ +
+
+ `, + setup() { + const targetRef = ref(null); + const menuRef = ref(null); + const visibleMenu = ref(false); + const menu = ref(getContextMenuItems()); + const selected = ref([]); + const { + refinedMenu, loading, initiateMenu, showMoreMenu, showContextMenu, hideContextMenu, + } = useContextMenuController({ + targetRef, + contextMenuRef: menuRef, + visibleMenu, + menu, + pageSize: 3, + selected, + useReorderBySelection: true, + }); + const handleSelect = (item: MenuItem) => { + selected.value = [...selected.value, item]; + }; + const showAndInitiate = () => { + showContextMenu(); + initiateMenu(); + }; + return { + selected, + targetRef, + menuRef, + visibleMenu, + refinedMenu, + loading, + initiateMenu, + showMoreMenu, + showContextMenu, + hideContextMenu, + handleSelect, + showAndInitiate, + }; + }, + }), + decorators: [() => ({ + template: '', + })], +}; + +export const Playground: Story = { + ...Template, +}; diff --git a/packages/mirinae/src/hooks/use-context-menu-controller/use-context-menu-controller.ts b/packages/mirinae/src/hooks/use-context-menu-controller/use-context-menu-controller.ts index 7c45c9668f..8ba9cc98a2 100644 --- a/packages/mirinae/src/hooks/use-context-menu-controller/use-context-menu-controller.ts +++ b/packages/mirinae/src/hooks/use-context-menu-controller/use-context-menu-controller.ts @@ -1,202 +1,75 @@ -import type { ComputedRef, Ref, UnwrapRef } from 'vue'; -import type Vue from 'vue'; -import { - computed, isRef, reactive, ref, toRef, -} from 'vue'; - -import { isEmpty } from 'lodash'; +import type { Ref } from 'vue'; +import { reactive, toRef } from 'vue'; import type { MenuItem } from '@/controls/context-menu/type'; -import type { MenuAttachHandler } from '@/hooks/use-context-menu-controller/use-context-menu-attach'; -import { useContextMenuAttach } from '@/hooks/use-context-menu-controller/use-context-menu-attach'; +import type { + UseContextMenuItemsOptions, +} from '@/hooks/use-context-menu-items/use-context-menu-items'; +import { + useContextMenuItems, +} from '@/hooks/use-context-menu-items/use-context-menu-items'; +import type { UseContextMenuStyleOptions } from '@/hooks/use-context-menu-style/use-context-menu-style'; import { useContextMenuStyle } from '@/hooks/use-context-menu-style/use-context-menu-style'; -import { getTextHighlightRegex } from '@/utils/helpers'; -export interface UseContextMenuControllerOptions { - targetRef: Ref; // required for style - - contextMenuRef: Ref; // required when using focusing feature by focusOnContextMenu() - /* - Useful when used inside an element whose css position attribute value is fixed. - It automatically check targetRef's position and adjust the context menu's position. - */ - useFixedStyle?: Ref|boolean; +export interface UseContextMenuControllerOptions extends + Omit, + UseContextMenuItemsOptions { visibleMenu?: Ref|boolean; // used for visibility control. related to fixed style feature and focusing feature. give this option or use returned value. - - /* Whether to automatically reorder on initiateMenu(). */ - useReorderBySelection?: boolean; - /* Required values when using the reorder by selection feature: menu or handler, selected */ - menu?: Ref|Item[]; // The original menu that serves as the basis for order when reordering menus - handler?: Ref|undefined>; - selected?: Ref|ComputedRef|Item[]; // Items to be displayed at the top of the menu - - /* Whether to automatically filtering menu by searchText */ - useMenuFiltering?: boolean; - /* Required values when using the reorder by selection feature: menu or handler, searchText */ - searchText?: Ref; - - /* Required when to use show more button to attach items */ - pageSize?: Ref|number; - - /* Required for context menu style */ - position?: Ref<'left'|'right'|undefined>|'left'|'right'; - menuWidth?: Ref<'target-width'|string|undefined>|'target-width'|string|undefined; - boundary?: Ref|string; - - /* Whether to hide the header when there are no items in the header */ - hideHeaderWithoutItems?: Ref|boolean; + contextMenuRef?: UseContextMenuStyleOptions['menuRef']; + useFixedStyle?: UseContextMenuStyleOptions['useFixedMenuStyle']; } +interface UseContextMenuControllerReturns { + visibleMenu: Ref; + refinedMenu: ReturnType['refinedMenu']; + contextMenuStyle: ReturnType['contextMenuStyle']; + loading: ReturnType['loading']; + showContextMenu: () => void; + hideContextMenu: () => void; + toggleContextMenu: () => void; + focusOnContextMenu: FocusOnContextMenu; + initiateMenu: ReturnType['initiateMenu']; + reloadMenu: ReturnType['reloadMenu']; + showMoreMenu: ReturnType['showMoreMenu']; +} interface FocusOnContextMenu { (position?: number): void } export const useContextMenuController = ({ useFixedStyle, targetRef, contextMenuRef, visibleMenu, useReorderBySelection, menu, selected, useMenuFiltering, searchText, handler, pageSize, position, hideHeaderWithoutItems, menuWidth, boundary, -}: UseContextMenuControllerOptions) => { - if (!targetRef) throw new Error('\'targetRef\' option must be given.'); - if (useReorderBySelection) { - if (!menu && (!handler || (isRef(handler) && !handler.value))) { - throw new Error('If \'useReorderBySelection\' is \'true\', \'menu\' or \'handler\' option must be given.'); - } - if (!selected) { - throw new Error('If \'useReorderBySelection\' is \'true\', \'selected\' option must be given.'); - } - } - if (useMenuFiltering) { - if (!menu && (!handler || (isRef(handler) && !handler.value))) { - throw new Error('If \'useMenuFiltering\' is \'true\', \'menu\' or \'handler\' option must be given.'); - } - if (!searchText) { - throw new Error('If \'useMenuFiltering\' is \'true\', \'searchText\' option must be given.'); - } - } - +}: UseContextMenuControllerOptions): UseContextMenuControllerReturns => { const state = reactive({ - contextMenuRef, - useFixedStyle: useFixedStyle ?? false, + contextMenuRef: contextMenuRef ?? null, visibleMenu: visibleMenu ?? false, - menu: menu ?? [] as Item[], - selected: selected ?? [] as Item[], - pageSize, - searchText: searchText ?? '', - position: position ?? 'left', - hideHeaderWithoutItems, }); - /* fixed style */ + /* menu style */ const { contextMenuStyle, } = useContextMenuStyle({ - useFixedMenuStyle: toRef(state, 'useFixedStyle'), + useFixedMenuStyle: useFixedStyle, visibleMenu: toRef(state, 'visibleMenu'), targetRef, - position: toRef(state, 'position'), - menuRef: contextMenuRef, + position, + menuRef: toRef(state, 'contextMenuRef'), menuWidth, boundary, }); - // menu filtering - const filterItemsBySearchText = (text: string, items: Item[]) => { - let results: Item[]; - const trimmed = text.trim(); - if (trimmed) { - const regex = getTextHighlightRegex(trimmed); - results = items.filter((d) => { - if (d.type === undefined || d.type === 'item') return regex.test(d.label as string); - return true; - }); - } else { - results = [...items]; - } - - return results; - }; - - /* menu capturing */ - const selectedSnapshot = ref([]); - const capture = () => { - selectedSnapshot.value = [...state.selected] as UnwrapRef; - }; - - /* menu attaching */ - const defaultMenu = useMenuFiltering ? computed(() => filterItemsBySearchText(state.searchText, state.menu as Item[])) : toRef(state, 'menu'); + /* menu items */ const { - attachedMenu, - attachLoading, - resetMenuAndPagination: resetAttachedMenuAndPagination, - attachMenuItems, - } = useContextMenuAttach({ - attachHandler: handler, - menu: defaultMenu as Ref, - searchText, - pageSize: toRef(state, 'pageSize'), - filterItems: useReorderBySelection ? selectedSnapshot as Ref : undefined, - }); - - /* menu refining */ - const SELECTION_DIVIDER_KEY = 'selection-divider'; - const topItems = computed(() => { - const filtered = filterItemsBySearchText(state.searchText, selectedSnapshot.value as Item[]); - - // group by headerName - const headerNameItemsMap = getHeaderNameItemsMap(filtered, attachedMenu.value); - if (isEmpty(headerNameItemsMap)) return filtered; - - // reorder items by headerName and add divider - let reordered: Item[] = []; - const entries = Object.entries>(headerNameItemsMap); - const allIndices: number[] = []; - entries.forEach(([, [header, indices]], i) => { - reordered.push(header); - indices.forEach((idx) => { - reordered.push(filtered[idx]); - allIndices.push(idx); - }); - if (i < entries.length - 1) { - reordered.push({ type: 'divider', name: `selection-${header.name}-divider` } as Item); - } - }); - const restItems = filtered.filter((_, idx) => !allIndices.includes(idx)); - if (restItems.length) { - reordered.push({ type: 'divider', name: 'selection-rest-divider' } as Item); - reordered = reordered.concat(restItems); - } - reordered = reordered.concat(); - return reordered; - }); - const refinedMenu = computed(() => { - if (!useReorderBySelection) return attachedMenu.value; - - let newItems: Item[] = []; - if (topItems.value.length) { - newItems = newItems.concat(topItems.value); - newItems.push({ type: 'divider', name: SELECTION_DIVIDER_KEY } as Item); - - let restItems: Item[]; - if (state.hideHeaderWithoutItems) { - restItems = []; - const headerNameItemsMap = getHeaderNameItemsMap(attachedMenu.value, attachedMenu.value); - restItems = attachedMenu.value.filter((d) => { - if (d.type === 'header') { - return !!headerNameItemsMap[d.name as string]?.[1]?.length; - } - return true; - }); - } else { - restItems = attachedMenu.value; - } - - newItems = newItems.concat(restItems); - } else { - newItems = attachedMenu.value; - } - return newItems; + refinedMenu, + loading, + initiateMenu, + reloadMenu, + showMoreMenu, + } = useContextMenuItems({ + useReorderBySelection, menu, selected, useMenuFiltering, searchText, handler, pageSize, hideHeaderWithoutItems, }); - /* menu visibility, focusing, refining */ + /* menu visibility, focusing */ const showContextMenu = () => { if (!state.visibleMenu) { state.visibleMenu = true; @@ -215,43 +88,18 @@ export const useContextMenuController = ({ state.contextMenuRef.focus(focusPosition); // contextMenu component has focus method } }; - const initiateMenu = async () => { - resetAttachedMenuAndPagination(); - capture(); - await attachMenuItems(); - }; - const reloadMenu = async () => { - resetAttachedMenuAndPagination(); - await attachMenuItems(); - }; return { visibleMenu: toRef(state, 'visibleMenu'), refinedMenu, contextMenuStyle, - loading: attachLoading, + loading, showContextMenu, hideContextMenu, toggleContextMenu, focusOnContextMenu, initiateMenu, reloadMenu, - showMoreMenu: attachMenuItems, + showMoreMenu, }; }; - -type HeaderIndicesTuple = [header: Item, itemIndices: number[]]; -const getHeaderNameItemsMap = (targetItems: Item[], allItems: Item[]): Record> => { - const headerNameItemsMap: Record> = {}; - targetItems.forEach((item, index) => { - if (!item.headerName) return; - if (headerNameItemsMap[item.headerName]) { - headerNameItemsMap[item.headerName][1].push(index); - } else { - const header = allItems.find((d) => d.type === 'header' && d.name === item.headerName); - if (!header) return; - headerNameItemsMap[item.headerName] = [header, [index]]; - } - }); - return headerNameItemsMap; -}; diff --git a/packages/mirinae/src/hooks/use-context-menu-items/UseContextMenuItems.mdx b/packages/mirinae/src/hooks/use-context-menu-items/UseContextMenuItems.mdx index e01b9b2279..1fceac80ef 100644 --- a/packages/mirinae/src/hooks/use-context-menu-items/UseContextMenuItems.mdx +++ b/packages/mirinae/src/hooks/use-context-menu-items/UseContextMenuItems.mdx @@ -26,9 +26,9 @@ interface UseContextMenuItemsOptions extends interface UseContextMenuItemsReturns { refinedMenu: ComputedRef; loading: Ref; - initiateMenu: () => Promise; - reloadMenu: () => Promise; - showMoreMenu: () => Promise; + initiateMenu: () => Promise; // reset pagination, capture current items, and reattach menu items. + reloadMenu: () => Promise; // reset pagination and reattach menu items. + showMoreMenu: () => Promise; // attach more menu items. } ``` ## Usage @@ -37,6 +37,9 @@ interface UseContextMenuItemsReturns { ## Basic +## Reorder by Selection + + ## Playground diff --git a/packages/mirinae/src/hooks/use-context-menu-items/story-helper.ts b/packages/mirinae/src/hooks/use-context-menu-items/story-helper.ts index 06a7325441..9a13a28feb 100644 --- a/packages/mirinae/src/hooks/use-context-menu-items/story-helper.ts +++ b/packages/mirinae/src/hooks/use-context-menu-items/story-helper.ts @@ -7,11 +7,17 @@ import { import type { UseContextMenuItemsOptions } from './use-context-menu-items'; -export const getUseContextMenuItemsArgs = (): Args => () => { +export const getUseContextMenuItemsArgs = (): Args => { const useContextMenuAttachArgs = getUseContextMenuAttachArgs(); + + // rename attachHandler to handler const handler = useContextMenuAttachArgs.attachHandler; delete useContextMenuAttachArgs.attachHandler; useContextMenuAttachArgs.handler = handler; + + // remove filterItems + delete useContextMenuAttachArgs.filterItems; + return { ...useContextMenuAttachArgs, useReorderBySelection: false, @@ -26,9 +32,15 @@ export const getUseContextMenuItemsParameters = (): Parameters => ({}); export const getUseContextMenuItemsArgTypes = (): ArgTypes => { const useContextMenuAttachArgTypes: any = getUseContextMenuAttachArgTypes(); const handlerArgType = { ...useContextMenuAttachArgTypes.attachHandler }; + + // rename attachHandler to handler handlerArgType.name = 'handler'; delete useContextMenuAttachArgTypes.attachHandler; useContextMenuAttachArgTypes.handler = handlerArgType; + + // remove filterItems + delete useContextMenuAttachArgTypes.filterItems; + return { ...useContextMenuAttachArgTypes, useReorderBySelection: { diff --git a/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.stories.ts b/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.stories.ts index 66f43c19b3..b0690c413d 100644 --- a/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.stories.ts +++ b/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.stories.ts @@ -6,6 +6,7 @@ import type { ComponentProps } from 'vue-component-type-helpers'; import PButton from '@/controls/buttons/button/PButton.vue'; import { getContextMenuItems } from '@/controls/context-menu/mock'; import PContextMenu from '@/controls/context-menu/PContextMenu.vue'; +import type { MenuItem } from '@/controls/context-menu/type'; import { getUseContextMenuItemsArgs, getUseContextMenuItemsArgTypes, getUseContextMenuItemsParameters, @@ -39,29 +40,32 @@ const Template: Story = { components: { PContextMenu, PButton }, template: `
- Initiate - Reload +
+ Initiate + Reload +
`, setup(props) { + const selected = computed(() => props.selected); const { refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, } = useContextMenuItems({ + selected, useReorderBySelection: props.useReorderBySelection, - selected: computed(() => props.selected), useMenuFiltering: props.useMenuFiltering, hideHeaderWithoutItems: props.hideHeaderWithoutItems, menu: computed(() => props.menu), searchText: computed(() => props.searchText), pageSize: computed(() => props.pageSize), - filterItems: computed(() => props.filterItems), }); return { - refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, + selected, refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, }; }, }), @@ -72,24 +76,71 @@ export const Basic: Story = { components: { PContextMenu, PButton }, template: `
- Initiate - Reload +
+ Initiate + Reload +
`, setup() { const menu = ref(getContextMenuItems()); + const selected = ref([]); const { refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, } = useContextMenuItems({ menu, pageSize: 3, + selected, }); + const handleSelect = (item: MenuItem) => { + selected.value = [...selected.value, item]; + }; return { + selected, refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, handleSelect, + }; + }, + }), +}; + +export const ReorderBySelection: Story = { + render: () => ({ + components: { PContextMenu, PButton }, + template: ` +
+
+ Initiate & Reorder + Reset Selection +
+ +
+ `, + setup() { + const menu = ref(getContextMenuItems()); + const selected = ref([]); + const { refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, + } = useContextMenuItems({ + menu, + pageSize: 3, + selected, + useReorderBySelection: true, + }); + const handleSelect = (item: MenuItem) => { + selected.value = [...selected.value, item]; + }; + return { + selected, refinedMenu, loading, initiateMenu, reloadMenu, showMoreMenu, handleSelect, }; }, }), diff --git a/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.ts b/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.ts index d86e5f6702..173ad0a9d5 100644 --- a/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.ts +++ b/packages/mirinae/src/hooks/use-context-menu-items/use-context-menu-items.ts @@ -12,7 +12,7 @@ import type { UseContextMenuAttachOptions } from '../use-context-menu-attach/use import { useContextMenuAttach } from '../use-context-menu-attach/use-context-menu-attach'; export interface UseContextMenuItemsOptions extends - Omit, 'attachHandler'> { + Omit, 'attachHandler'|'filterItems'> { useReorderBySelection?: boolean; // Whether to automatically reorder on initiateMenu(). selected?: Ref|ComputedRef|Item[]; // Items to be displayed at the top of the menu for emphasize or quick access of selected items. useMenuFiltering?: boolean; // Whether to automatically filtering menu by searchText. works only with menu, not with handler. @@ -23,9 +23,9 @@ export interface UseContextMenuItemsOptions ex interface UseContextMenuItemsReturns { refinedMenu: ComputedRef; loading: Ref; - initiateMenu: () => Promise; - reloadMenu: () => Promise; - showMoreMenu: () => Promise; + initiateMenu: () => Promise; // reset pagination, capture current items, and reattach menu items. + reloadMenu: () => Promise; // reset pagination and reattach menu items. + showMoreMenu: () => Promise; // attach more menu items. } diff --git a/packages/mirinae/src/hooks/use-context-menu-style/story-helper.ts b/packages/mirinae/src/hooks/use-context-menu-style/story-helper.ts index df153b95ed..1281088282 100644 --- a/packages/mirinae/src/hooks/use-context-menu-style/story-helper.ts +++ b/packages/mirinae/src/hooks/use-context-menu-style/story-helper.ts @@ -2,7 +2,7 @@ import type { ArgTypes, Parameters, Args } from '@storybook/vue'; import type { UseContextMenuStyleOptions } from './use-context-menu-style'; -export const getArgs = (): Args => ({ +export const getUseContextMenuStyleArgs = (): Args => ({ useFixedMenuStyle: false, visibleMenu: false, targetRef: undefined, @@ -11,9 +11,9 @@ export const getArgs = (): Args => ({ menuWidth: 'auto', }); -export const getParameters = (): Parameters => ({}); +export const getUseContextMenuStyleParameters = (): Parameters => ({}); -export const getArgTypes = (): ArgTypes => ({ +export const getUseContextMenuStyleArgTypes = (): ArgTypes => ({ useFixedMenuStyle: { name: 'useFixedMenuStyle', type: { name: 'boolean' }, diff --git a/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.stories.ts b/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.stories.ts index e3079c0c01..c87ab720a3 100644 --- a/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.stories.ts +++ b/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.stories.ts @@ -7,7 +7,7 @@ import { getContextMenuItems } from '@/controls/context-menu/mock'; import PContextMenu from '@/controls/context-menu/PContextMenu.vue'; import { - getArgs, getArgTypes, getParameters, + getUseContextMenuStyleArgs, getUseContextMenuStyleArgTypes, getUseContextMenuStyleParameters, } from './story-helper'; import type { UseContextMenuStyleOptions } from './use-context-menu-style'; import { useContextMenuStyle } from './use-context-menu-style'; @@ -18,13 +18,13 @@ type UseContextMenuStylePropsAndCustomArgs = ComponentProps = { title: 'Hooks/useContextMenuStyle', argTypes: { - ...getArgTypes(), + ...getUseContextMenuStyleArgTypes(), }, parameters: { - ...getParameters(), + ...getUseContextMenuStyleParameters(), }, args: { - ...getArgs(), + ...getUseContextMenuStyleArgs(), }, }; diff --git a/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.ts b/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.ts index c07167739f..99693e16b9 100644 --- a/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.ts +++ b/packages/mirinae/src/hooks/use-context-menu-style/use-context-menu-style.ts @@ -9,6 +9,10 @@ import { } from '@floating-ui/dom'; import { throttle } from 'lodash'; +// HACK: This is the type definition of the context menu. Only defined exposed methods. This is to prevent type error due to vue2-vue3 incompatibility. +interface ContextMenu { + focus: (position?: number) => void; +} export interface UseContextMenuStyleOptions { /* Useful when used inside an element whose css position attribute value is fixed. @@ -18,7 +22,7 @@ export interface UseContextMenuStyleOptions { visibleMenu?: Ref; targetRef: Ref; - menuRef: Ref; + menuRef: Ref; /* Required for context menu style */ position?: Ref<'left'|'right'|undefined>|'left'|'right';