@rotki/ui-library 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"RuiDialog.js","names":["_sfc_main"],"sources":["../../../../src/components/overlays/dialog/RuiDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { DialogSize } from '@/consts/tokens';\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport { useDismissableOverlay } from '@/composables/overlay-stack';\nimport { useTimeoutManager } from '@/composables/timeout-manager';\nimport { getNonRootAttrs, getRootAttrs, transformPropsUnit } from '@/utils/helpers';\nimport { cn, tv } from '@/utils/tv';\n\nexport interface RuiDialogClassNames {\n root?: VueClassValue;\n content?: VueClassValue;\n}\n\nexport interface DialogProps {\n persistent?: boolean;\n width?: string | number;\n maxWidth?: string | number;\n /** A preset max width, `--rui-dialog-<size>`; an explicit `maxWidth` wins */\n size?: DialogSize;\n bottomSheet?: boolean;\n classNames?: RuiDialogClassNames;\n zIndex?: string | number;\n ariaLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiDialog',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n persistent = false,\n width = '98%',\n maxWidth,\n size,\n bottomSheet = false,\n classNames,\n zIndex = 'var(--rui-z-dialog)',\n ariaLabel,\n} = defineProps<DialogProps>();\n\nconst emit = defineEmits<{\n 'closed': [];\n 'click:outside': [];\n 'click:esc': [];\n /**\n * The overlay stack asked this dialog to go, which is what a back gesture\n * reaches it as. Closes unless persistent, like escape does, so a dialog that\n * has to ask before discarding should bind `persistent` and answer here.\n */\n 'dismiss': [];\n}>();\n\ndefineSlots<{\n activator?: (props: { attrs: { onClick: () => void }; isOpen: boolean }) => any;\n default?: (props: { isOpen: boolean; close: () => void }) => any;\n}>();\n\nconst transitioning = ref<boolean>(false);\nconst contentRef = useTemplateRef<HTMLDivElement>('contentRef');\n\nconst leaveTimeout = useTimeoutManager();\n\nconst alive = computed<boolean>(() => get(modelValue) || get(transitioning));\n\nconst style = computed<{ width: string | undefined; maxWidth: string | undefined }>(() => ({\n width: transformPropsUnit(width),\n maxWidth: transformPropsUnit(maxWidth) ?? (size ? `var(--rui-dialog-${size})` : undefined),\n}));\n\nconst dialog = tv({\n slots: {\n root: 'fixed inset-0',\n overlay: 'absolute inset-0 backdrop-blur-sm bg-rui-neutral-950/40 dark:bg-black/60',\n content: 'absolute left-1/2 bottom-0 -translate-x-1/2 outline-hidden overflow-y-auto max-h-(--rui-dialog-max-height)',\n },\n variants: {\n /*\n * The corners match a RuiCard's, the usual content, which the box clips while it scrolls.\n * A sheet sits on the screen's bottom edge, so its content's bottom corners are squared.\n */\n bottomSheet: {\n true: { content: 'rounded-t-rui-card shadow-rui-drawer *:rounded-b-none' },\n false: { content: 'top-1/2 -translate-y-1/2 bottom-auto rounded-rui-card shadow-rui-menu' },\n },\n },\n defaultVariants: { bottomSheet: false },\n});\n\nconst ui = computed<ReturnType<typeof dialog>>(() => dialog({ bottomSheet }));\n\nconst dialogAttrs: { onClick: () => void } = {\n onClick: toggle,\n};\n\nconst contentTransition = computed<Record<string, string>>(() => {\n if (!bottomSheet) {\n return {\n enterFromClass: 'opacity-0 scale-95',\n enterActiveClass: 'transition-all ease-out duration-200',\n enterToClass: 'opacity-100 scale-100',\n leaveFromClass: 'opacity-100 scale-100',\n leaveActiveClass: 'transition-all ease-in duration-150',\n leaveToClass: 'opacity-0 scale-95',\n };\n }\n\n return {\n enterFromClass: 'translate-y-full',\n enterActiveClass: 'transition-transform ease-out duration-300',\n enterToClass: 'translate-y-0',\n leaveFromClass: 'translate-y-0',\n leaveActiveClass: 'transition-transform ease-in duration-200',\n leaveToClass: 'translate-y-full',\n };\n});\n\nfunction toggle(): void {\n set(modelValue, !get(modelValue));\n}\n\nfunction close(): void {\n set(modelValue, false);\n}\n\nfunction onEscClick(): void {\n if (!persistent) {\n close();\n }\n emit('click:esc');\n}\n\nfunction onClickOutside(): void {\n if (!persistent) {\n close();\n }\n emit('click:outside');\n}\n\nfunction onDismiss(): void {\n if (!persistent) {\n close();\n }\n emit('dismiss');\n}\n\n/**\n * Registration follows `modelValue` alone, never `persistent`: a dialog that\n * turns persistent partway through its life has to stay in the stack and refuse\n * in `onDismiss`, or the gesture starts passing through exactly once there is\n * work to lose. A dialog playing its leave transition is already on its way out\n * and does not hold the stack.\n */\nuseDismissableOverlay(modelValue, onDismiss);\n\nfunction onLeaveComplete(): void {\n leaveTimeout.clear();\n if (get(transitioning)) {\n set(transitioning, false);\n emit('closed');\n }\n}\n\nwatch(modelValue, (value) => {\n if (!value) {\n set(transitioning, true);\n // Safety fallback if transitionend is not detected\n leaveTimeout.create(onLeaveComplete, 400);\n }\n});\n\nwatch(contentRef, (ref) => {\n if (ref) {\n nextTick(() => ref.focus());\n }\n else {\n onLeaveComplete();\n }\n});\n</script>\n\n<template>\n <div v-bind=\"getRootAttrs($attrs)\">\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: dialogAttrs, isOpen: modelValue }\"\n />\n <Teleport to=\"body\">\n <div\n v-if=\"alive\"\n :class=\"ui.root({ class: cn(classNames?.root) })\"\n :style=\"{ zIndex }\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"ariaLabel\"\n tabindex=\"0\"\n v-bind=\"getNonRootAttrs($attrs)\"\n @keydown.esc.stop=\"onEscClick()\"\n >\n <Transition\n enter-from-class=\"opacity-0\"\n enter-active-class=\"transition-opacity ease-out duration-200\"\n enter-to-class=\"opacity-100\"\n leave-from-class=\"opacity-100\"\n leave-active-class=\"transition-opacity ease-in duration-150\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"modelValue\"\n data-id=\"overlay\"\n :class=\"ui.overlay()\"\n @click.stop=\"onClickOutside()\"\n />\n </Transition>\n <Transition\n v-bind=\"contentTransition\"\n @after-leave=\"onLeaveComplete()\"\n >\n <div\n v-if=\"modelValue\"\n ref=\"contentRef\"\n data-id=\"content\"\n :style=\"style\"\n tabindex=\"0\"\n :data-bottom-sheet=\"bottomSheet || undefined\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ isOpen: modelValue, close }\" />\n </ButtonGroupBoundary>\n </div>\n </Transition>\n </div>\n </Teleport>\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"RuiDialog.js","names":["_sfc_main"],"sources":["../../../../src/components/overlays/dialog/RuiDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { DialogSize } from '@/consts/tokens';\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport { useDismissableOverlay } from '@/composables/overlay-stack';\nimport { useTimeoutManager } from '@/composables/timeout-manager';\nimport { getNonRootAttrs, getRootAttrs, transformPropsUnit } from '@/utils/helpers';\nimport { cn, tv } from '@/utils/tv';\n\nexport interface RuiDialogClassNames {\n root?: VueClassValue;\n content?: VueClassValue;\n}\n\nexport interface DialogProps {\n persistent?: boolean;\n width?: string | number;\n maxWidth?: string | number;\n /** A preset max width, `--rui-dialog-<size>`; an explicit `maxWidth` wins */\n size?: DialogSize;\n bottomSheet?: boolean;\n classNames?: RuiDialogClassNames;\n zIndex?: string | number;\n ariaLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiDialog',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n persistent = false,\n width = '98%',\n maxWidth,\n size,\n bottomSheet = false,\n classNames,\n zIndex = 'var(--rui-z-dialog)',\n ariaLabel,\n} = defineProps<DialogProps>();\n\nconst emit = defineEmits<{\n 'closed': [];\n 'click:outside': [];\n 'click:esc': [];\n /**\n * The overlay stack asked this dialog to go, which is what a back gesture\n * reaches it as. Closes unless persistent, like escape does, so a dialog that\n * has to ask before discarding should bind `persistent` and answer here.\n */\n 'dismiss': [];\n}>();\n\ndefineSlots<{\n activator?: (props: { attrs: { onClick: () => void }; isOpen: boolean }) => any;\n default?: (props: { isOpen: boolean; close: () => void }) => any;\n}>();\n\nconst transitioning = ref<boolean>(false);\nconst contentRef = useTemplateRef<HTMLDivElement>('contentRef');\n\nconst leaveTimeout = useTimeoutManager();\n\nconst alive = computed<boolean>(() => get(modelValue) || get(transitioning));\n\nconst style = computed<{ width: string | undefined; maxWidth: string | undefined }>(() => ({\n width: transformPropsUnit(width),\n maxWidth: transformPropsUnit(maxWidth) ?? (size ? `var(--rui-dialog-${size})` : undefined),\n}));\n\nconst dialog = tv({\n slots: {\n root: 'fixed inset-0',\n overlay: 'absolute inset-0 backdrop-blur-sm bg-rui-neutral-950/40 dark:bg-black/60',\n content: 'absolute left-1/2 bottom-0 -translate-x-1/2 outline-hidden overflow-y-auto max-h-(--rui-dialog-max-height)',\n },\n variants: {\n /*\n * The corners match a RuiCard's, the usual content, which the box clips while it scrolls.\n * A sheet sits on the screen's bottom edge, so its content's bottom corners are squared.\n */\n bottomSheet: {\n true: { content: 'rounded-t-rui-card shadow-rui-drawer *:rounded-b-none' },\n false: { content: 'top-1/2 -translate-y-1/2 bottom-auto rounded-rui-card shadow-rui-menu' },\n },\n },\n defaultVariants: { bottomSheet: false },\n});\n\nconst ui = computed<ReturnType<typeof dialog>>(() => dialog({ bottomSheet }));\n\nconst dialogAttrs: { onClick: () => void } = {\n onClick: toggle,\n};\n\nconst contentTransition = computed<Record<string, string>>(() => {\n if (!bottomSheet) {\n return {\n enterFromClass: 'opacity-0 scale-95',\n enterActiveClass: 'transition-all ease-out duration-200',\n enterToClass: 'opacity-100 scale-100',\n leaveFromClass: 'opacity-100 scale-100',\n leaveActiveClass: 'transition-all ease-in duration-150',\n leaveToClass: 'opacity-0 scale-95',\n };\n }\n\n return {\n enterFromClass: 'translate-y-full',\n enterActiveClass: 'transition-transform ease-out duration-300',\n enterToClass: 'translate-y-0',\n leaveFromClass: 'translate-y-0',\n leaveActiveClass: 'transition-transform ease-in duration-200',\n leaveToClass: 'translate-y-full',\n };\n});\n\nfunction toggle(): void {\n set(modelValue, !get(modelValue));\n}\n\nfunction close(): void {\n set(modelValue, false);\n}\n\n/**\n * Closes on Escape unless persistent, and reports the press as `click:esc`.\n *\n * @remarks\n * An Escape that arrives while an IME is composing belongs to the IME, which\n * cancels the candidate with it. The dialog leaves that key alone entirely: no\n * close, no `click:esc`, and no `stopPropagation`, so a search field inside the\n * dialog does not lose the dialog mid-word.\n */\nfunction onEscKeydown(event: KeyboardEvent): void {\n if (event.isComposing)\n return;\n\n event.stopPropagation();\n if (!persistent) {\n close();\n }\n emit('click:esc');\n}\n\nfunction onClickOutside(): void {\n if (!persistent) {\n close();\n }\n emit('click:outside');\n}\n\nfunction onDismiss(): void {\n if (!persistent) {\n close();\n }\n emit('dismiss');\n}\n\n/**\n * Registration follows `modelValue` alone, never `persistent`: a dialog that\n * turns persistent partway through its life has to stay in the stack and refuse\n * in `onDismiss`, or the gesture starts passing through exactly once there is\n * work to lose. A dialog playing its leave transition is already on its way out\n * and does not hold the stack.\n */\nuseDismissableOverlay(modelValue, onDismiss);\n\nfunction onLeaveComplete(): void {\n leaveTimeout.clear();\n if (get(transitioning)) {\n set(transitioning, false);\n emit('closed');\n }\n}\n\nwatch(modelValue, (value) => {\n if (!value) {\n set(transitioning, true);\n // Safety fallback if transitionend is not detected\n leaveTimeout.create(onLeaveComplete, 400);\n }\n});\n\nwatch(contentRef, (ref) => {\n if (ref) {\n nextTick(() => ref.focus());\n }\n else {\n onLeaveComplete();\n }\n});\n</script>\n\n<template>\n <div v-bind=\"getRootAttrs($attrs)\">\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: dialogAttrs, isOpen: modelValue }\"\n />\n <Teleport to=\"body\">\n <div\n v-if=\"alive\"\n :class=\"ui.root({ class: cn(classNames?.root) })\"\n :style=\"{ zIndex }\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"ariaLabel\"\n tabindex=\"0\"\n v-bind=\"getNonRootAttrs($attrs)\"\n @keydown.esc=\"onEscKeydown($event)\"\n >\n <Transition\n enter-from-class=\"opacity-0\"\n enter-active-class=\"transition-opacity ease-out duration-200\"\n enter-to-class=\"opacity-100\"\n leave-from-class=\"opacity-100\"\n leave-active-class=\"transition-opacity ease-in duration-150\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"modelValue\"\n data-id=\"overlay\"\n :class=\"ui.overlay()\"\n @click.stop=\"onClickOutside()\"\n />\n </Transition>\n <Transition\n v-bind=\"contentTransition\"\n @after-leave=\"onLeaveComplete()\"\n >\n <div\n v-if=\"modelValue\"\n ref=\"contentRef\"\n data-id=\"content\"\n :style=\"style\"\n tabindex=\"0\"\n :data-bottom-sheet=\"bottomSheet || undefined\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ isOpen: modelValue, close }\" />\n </ButtonGroupBoundary>\n </div>\n </Transition>\n </div>\n </Teleport>\n </div>\n</template>\n"],"mappings":""}
@@ -89,7 +89,18 @@ var RuiDialog_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
89
89
  function close() {
90
90
  set(modelValue, false);
91
91
  }
92
- function onEscClick() {
92
+ /**
93
+ * Closes on Escape unless persistent, and reports the press as `click:esc`.
94
+ *
95
+ * @remarks
96
+ * An Escape that arrives while an IME is composing belongs to the IME, which
97
+ * cancels the candidate with it. The dialog leaves that key alone entirely: no
98
+ * close, no `click:esc`, and no `stopPropagation`, so a search field inside the
99
+ * dialog does not lose the dialog mid-word.
100
+ */
101
+ function onEscKeydown(event) {
102
+ if (event.isComposing) return;
103
+ event.stopPropagation();
93
104
  if (!__props.persistent) close();
94
105
  emit("click:esc");
95
106
  }
@@ -138,7 +149,7 @@ var RuiDialog_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
138
149
  "aria-modal": "true",
139
150
  "aria-label": __props.ariaLabel,
140
151
  tabindex: "0"
141
- }, unref(getNonRootAttrs)(_ctx.$attrs), { onKeydown: _cache[2] || (_cache[2] = withKeys(withModifiers(($event) => onEscClick(), ["stop"]), ["esc"])) }), [createVNode(Transition, {
152
+ }, unref(getNonRootAttrs)(_ctx.$attrs), { onKeydown: _cache[2] || (_cache[2] = withKeys(($event) => onEscKeydown($event), ["esc"])) }), [createVNode(Transition, {
142
153
  "enter-from-class": "opacity-0",
143
154
  "enter-active-class": "transition-opacity ease-out duration-200",
144
155
  "enter-to-class": "opacity-100",
@@ -1 +1 @@
1
- {"version":3,"file":"RuiDialog.vue_vue_type_script_setup_true_lang.js","names":["$attrs","_useModel","_openBlock","_createElementBlock","_normalizeProps","_guardReactiveProps","_unref","_renderSlot","_createBlock","_Teleport","_mergeProps","_withKeys","_withModifiers","_createVNode","_Transition","_withCtx","_normalizeClass","_createCommentVNode","_normalizeStyle"],"sources":["../../../../src/components/overlays/dialog/RuiDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { DialogSize } from '@/consts/tokens';\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport { useDismissableOverlay } from '@/composables/overlay-stack';\nimport { useTimeoutManager } from '@/composables/timeout-manager';\nimport { getNonRootAttrs, getRootAttrs, transformPropsUnit } from '@/utils/helpers';\nimport { cn, tv } from '@/utils/tv';\n\nexport interface RuiDialogClassNames {\n root?: VueClassValue;\n content?: VueClassValue;\n}\n\nexport interface DialogProps {\n persistent?: boolean;\n width?: string | number;\n maxWidth?: string | number;\n /** A preset max width, `--rui-dialog-<size>`; an explicit `maxWidth` wins */\n size?: DialogSize;\n bottomSheet?: boolean;\n classNames?: RuiDialogClassNames;\n zIndex?: string | number;\n ariaLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiDialog',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n persistent = false,\n width = '98%',\n maxWidth,\n size,\n bottomSheet = false,\n classNames,\n zIndex = 'var(--rui-z-dialog)',\n ariaLabel,\n} = defineProps<DialogProps>();\n\nconst emit = defineEmits<{\n 'closed': [];\n 'click:outside': [];\n 'click:esc': [];\n /**\n * The overlay stack asked this dialog to go, which is what a back gesture\n * reaches it as. Closes unless persistent, like escape does, so a dialog that\n * has to ask before discarding should bind `persistent` and answer here.\n */\n 'dismiss': [];\n}>();\n\ndefineSlots<{\n activator?: (props: { attrs: { onClick: () => void }; isOpen: boolean }) => any;\n default?: (props: { isOpen: boolean; close: () => void }) => any;\n}>();\n\nconst transitioning = ref<boolean>(false);\nconst contentRef = useTemplateRef<HTMLDivElement>('contentRef');\n\nconst leaveTimeout = useTimeoutManager();\n\nconst alive = computed<boolean>(() => get(modelValue) || get(transitioning));\n\nconst style = computed<{ width: string | undefined; maxWidth: string | undefined }>(() => ({\n width: transformPropsUnit(width),\n maxWidth: transformPropsUnit(maxWidth) ?? (size ? `var(--rui-dialog-${size})` : undefined),\n}));\n\nconst dialog = tv({\n slots: {\n root: 'fixed inset-0',\n overlay: 'absolute inset-0 backdrop-blur-sm bg-rui-neutral-950/40 dark:bg-black/60',\n content: 'absolute left-1/2 bottom-0 -translate-x-1/2 outline-hidden overflow-y-auto max-h-(--rui-dialog-max-height)',\n },\n variants: {\n /*\n * The corners match a RuiCard's, the usual content, which the box clips while it scrolls.\n * A sheet sits on the screen's bottom edge, so its content's bottom corners are squared.\n */\n bottomSheet: {\n true: { content: 'rounded-t-rui-card shadow-rui-drawer *:rounded-b-none' },\n false: { content: 'top-1/2 -translate-y-1/2 bottom-auto rounded-rui-card shadow-rui-menu' },\n },\n },\n defaultVariants: { bottomSheet: false },\n});\n\nconst ui = computed<ReturnType<typeof dialog>>(() => dialog({ bottomSheet }));\n\nconst dialogAttrs: { onClick: () => void } = {\n onClick: toggle,\n};\n\nconst contentTransition = computed<Record<string, string>>(() => {\n if (!bottomSheet) {\n return {\n enterFromClass: 'opacity-0 scale-95',\n enterActiveClass: 'transition-all ease-out duration-200',\n enterToClass: 'opacity-100 scale-100',\n leaveFromClass: 'opacity-100 scale-100',\n leaveActiveClass: 'transition-all ease-in duration-150',\n leaveToClass: 'opacity-0 scale-95',\n };\n }\n\n return {\n enterFromClass: 'translate-y-full',\n enterActiveClass: 'transition-transform ease-out duration-300',\n enterToClass: 'translate-y-0',\n leaveFromClass: 'translate-y-0',\n leaveActiveClass: 'transition-transform ease-in duration-200',\n leaveToClass: 'translate-y-full',\n };\n});\n\nfunction toggle(): void {\n set(modelValue, !get(modelValue));\n}\n\nfunction close(): void {\n set(modelValue, false);\n}\n\nfunction onEscClick(): void {\n if (!persistent) {\n close();\n }\n emit('click:esc');\n}\n\nfunction onClickOutside(): void {\n if (!persistent) {\n close();\n }\n emit('click:outside');\n}\n\nfunction onDismiss(): void {\n if (!persistent) {\n close();\n }\n emit('dismiss');\n}\n\n/**\n * Registration follows `modelValue` alone, never `persistent`: a dialog that\n * turns persistent partway through its life has to stay in the stack and refuse\n * in `onDismiss`, or the gesture starts passing through exactly once there is\n * work to lose. A dialog playing its leave transition is already on its way out\n * and does not hold the stack.\n */\nuseDismissableOverlay(modelValue, onDismiss);\n\nfunction onLeaveComplete(): void {\n leaveTimeout.clear();\n if (get(transitioning)) {\n set(transitioning, false);\n emit('closed');\n }\n}\n\nwatch(modelValue, (value) => {\n if (!value) {\n set(transitioning, true);\n // Safety fallback if transitionend is not detected\n leaveTimeout.create(onLeaveComplete, 400);\n }\n});\n\nwatch(contentRef, (ref) => {\n if (ref) {\n nextTick(() => ref.focus());\n }\n else {\n onLeaveComplete();\n }\n});\n</script>\n\n<template>\n <div v-bind=\"getRootAttrs($attrs)\">\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: dialogAttrs, isOpen: modelValue }\"\n />\n <Teleport to=\"body\">\n <div\n v-if=\"alive\"\n :class=\"ui.root({ class: cn(classNames?.root) })\"\n :style=\"{ zIndex }\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"ariaLabel\"\n tabindex=\"0\"\n v-bind=\"getNonRootAttrs($attrs)\"\n @keydown.esc.stop=\"onEscClick()\"\n >\n <Transition\n enter-from-class=\"opacity-0\"\n enter-active-class=\"transition-opacity ease-out duration-200\"\n enter-to-class=\"opacity-100\"\n leave-from-class=\"opacity-100\"\n leave-active-class=\"transition-opacity ease-in duration-150\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"modelValue\"\n data-id=\"overlay\"\n :class=\"ui.overlay()\"\n @click.stop=\"onClickOutside()\"\n />\n </Transition>\n <Transition\n v-bind=\"contentTransition\"\n @after-leave=\"onLeaveComplete()\"\n >\n <div\n v-if=\"modelValue\"\n ref=\"contentRef\"\n data-id=\"content\"\n :style=\"style\"\n tabindex=\"0\"\n :data-bottom-sheet=\"bottomSheet || undefined\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ isOpen: modelValue, close }\" />\n </ButtonGroupBoundary>\n </div>\n </Transition>\n </div>\n </Teleport>\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"RuiDialog.vue_vue_type_script_setup_true_lang.js","names":["$attrs","_useModel","_openBlock","_createElementBlock","_normalizeProps","_guardReactiveProps","_unref","_renderSlot","_createBlock","_Teleport","_mergeProps","_withKeys","_createVNode","_Transition","_withCtx","_normalizeClass","_withModifiers","_createCommentVNode","_normalizeStyle"],"sources":["../../../../src/components/overlays/dialog/RuiDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { DialogSize } from '@/consts/tokens';\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport { useDismissableOverlay } from '@/composables/overlay-stack';\nimport { useTimeoutManager } from '@/composables/timeout-manager';\nimport { getNonRootAttrs, getRootAttrs, transformPropsUnit } from '@/utils/helpers';\nimport { cn, tv } from '@/utils/tv';\n\nexport interface RuiDialogClassNames {\n root?: VueClassValue;\n content?: VueClassValue;\n}\n\nexport interface DialogProps {\n persistent?: boolean;\n width?: string | number;\n maxWidth?: string | number;\n /** A preset max width, `--rui-dialog-<size>`; an explicit `maxWidth` wins */\n size?: DialogSize;\n bottomSheet?: boolean;\n classNames?: RuiDialogClassNames;\n zIndex?: string | number;\n ariaLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiDialog',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n persistent = false,\n width = '98%',\n maxWidth,\n size,\n bottomSheet = false,\n classNames,\n zIndex = 'var(--rui-z-dialog)',\n ariaLabel,\n} = defineProps<DialogProps>();\n\nconst emit = defineEmits<{\n 'closed': [];\n 'click:outside': [];\n 'click:esc': [];\n /**\n * The overlay stack asked this dialog to go, which is what a back gesture\n * reaches it as. Closes unless persistent, like escape does, so a dialog that\n * has to ask before discarding should bind `persistent` and answer here.\n */\n 'dismiss': [];\n}>();\n\ndefineSlots<{\n activator?: (props: { attrs: { onClick: () => void }; isOpen: boolean }) => any;\n default?: (props: { isOpen: boolean; close: () => void }) => any;\n}>();\n\nconst transitioning = ref<boolean>(false);\nconst contentRef = useTemplateRef<HTMLDivElement>('contentRef');\n\nconst leaveTimeout = useTimeoutManager();\n\nconst alive = computed<boolean>(() => get(modelValue) || get(transitioning));\n\nconst style = computed<{ width: string | undefined; maxWidth: string | undefined }>(() => ({\n width: transformPropsUnit(width),\n maxWidth: transformPropsUnit(maxWidth) ?? (size ? `var(--rui-dialog-${size})` : undefined),\n}));\n\nconst dialog = tv({\n slots: {\n root: 'fixed inset-0',\n overlay: 'absolute inset-0 backdrop-blur-sm bg-rui-neutral-950/40 dark:bg-black/60',\n content: 'absolute left-1/2 bottom-0 -translate-x-1/2 outline-hidden overflow-y-auto max-h-(--rui-dialog-max-height)',\n },\n variants: {\n /*\n * The corners match a RuiCard's, the usual content, which the box clips while it scrolls.\n * A sheet sits on the screen's bottom edge, so its content's bottom corners are squared.\n */\n bottomSheet: {\n true: { content: 'rounded-t-rui-card shadow-rui-drawer *:rounded-b-none' },\n false: { content: 'top-1/2 -translate-y-1/2 bottom-auto rounded-rui-card shadow-rui-menu' },\n },\n },\n defaultVariants: { bottomSheet: false },\n});\n\nconst ui = computed<ReturnType<typeof dialog>>(() => dialog({ bottomSheet }));\n\nconst dialogAttrs: { onClick: () => void } = {\n onClick: toggle,\n};\n\nconst contentTransition = computed<Record<string, string>>(() => {\n if (!bottomSheet) {\n return {\n enterFromClass: 'opacity-0 scale-95',\n enterActiveClass: 'transition-all ease-out duration-200',\n enterToClass: 'opacity-100 scale-100',\n leaveFromClass: 'opacity-100 scale-100',\n leaveActiveClass: 'transition-all ease-in duration-150',\n leaveToClass: 'opacity-0 scale-95',\n };\n }\n\n return {\n enterFromClass: 'translate-y-full',\n enterActiveClass: 'transition-transform ease-out duration-300',\n enterToClass: 'translate-y-0',\n leaveFromClass: 'translate-y-0',\n leaveActiveClass: 'transition-transform ease-in duration-200',\n leaveToClass: 'translate-y-full',\n };\n});\n\nfunction toggle(): void {\n set(modelValue, !get(modelValue));\n}\n\nfunction close(): void {\n set(modelValue, false);\n}\n\n/**\n * Closes on Escape unless persistent, and reports the press as `click:esc`.\n *\n * @remarks\n * An Escape that arrives while an IME is composing belongs to the IME, which\n * cancels the candidate with it. The dialog leaves that key alone entirely: no\n * close, no `click:esc`, and no `stopPropagation`, so a search field inside the\n * dialog does not lose the dialog mid-word.\n */\nfunction onEscKeydown(event: KeyboardEvent): void {\n if (event.isComposing)\n return;\n\n event.stopPropagation();\n if (!persistent) {\n close();\n }\n emit('click:esc');\n}\n\nfunction onClickOutside(): void {\n if (!persistent) {\n close();\n }\n emit('click:outside');\n}\n\nfunction onDismiss(): void {\n if (!persistent) {\n close();\n }\n emit('dismiss');\n}\n\n/**\n * Registration follows `modelValue` alone, never `persistent`: a dialog that\n * turns persistent partway through its life has to stay in the stack and refuse\n * in `onDismiss`, or the gesture starts passing through exactly once there is\n * work to lose. A dialog playing its leave transition is already on its way out\n * and does not hold the stack.\n */\nuseDismissableOverlay(modelValue, onDismiss);\n\nfunction onLeaveComplete(): void {\n leaveTimeout.clear();\n if (get(transitioning)) {\n set(transitioning, false);\n emit('closed');\n }\n}\n\nwatch(modelValue, (value) => {\n if (!value) {\n set(transitioning, true);\n // Safety fallback if transitionend is not detected\n leaveTimeout.create(onLeaveComplete, 400);\n }\n});\n\nwatch(contentRef, (ref) => {\n if (ref) {\n nextTick(() => ref.focus());\n }\n else {\n onLeaveComplete();\n }\n});\n</script>\n\n<template>\n <div v-bind=\"getRootAttrs($attrs)\">\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: dialogAttrs, isOpen: modelValue }\"\n />\n <Teleport to=\"body\">\n <div\n v-if=\"alive\"\n :class=\"ui.root({ class: cn(classNames?.root) })\"\n :style=\"{ zIndex }\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"ariaLabel\"\n tabindex=\"0\"\n v-bind=\"getNonRootAttrs($attrs)\"\n @keydown.esc=\"onEscKeydown($event)\"\n >\n <Transition\n enter-from-class=\"opacity-0\"\n enter-active-class=\"transition-opacity ease-out duration-200\"\n enter-to-class=\"opacity-100\"\n leave-from-class=\"opacity-100\"\n leave-active-class=\"transition-opacity ease-in duration-150\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"modelValue\"\n data-id=\"overlay\"\n :class=\"ui.overlay()\"\n @click.stop=\"onClickOutside()\"\n />\n </Transition>\n <Transition\n v-bind=\"contentTransition\"\n @after-leave=\"onLeaveComplete()\"\n >\n <div\n v-if=\"modelValue\"\n ref=\"contentRef\"\n data-id=\"content\"\n :style=\"style\"\n tabindex=\"0\"\n :data-bottom-sheet=\"bottomSheet || undefined\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ isOpen: modelValue, close }\" />\n </ButtonGroupBoundary>\n </div>\n </Transition>\n </div>\n </Teleport>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BA,MAAM,aAAaC,SAAoB,SAAA,YAAmB;EAa1D,MAAM,OAAO;EAiBb,MAAM,gBAAgB,IAAa,KAAK;EACxC,MAAM,aAAa,eAA+B,YAAY;EAE9D,MAAM,eAAe,kBAAkB;EAEvC,MAAM,QAAQ,eAAwB,IAAI,UAAU,KAAK,IAAI,aAAa,CAAC;EAE3E,MAAM,QAAQ,gBAA6E;GACzF,OAAO,mBAAmB,QAAA,KAAK;GAC/B,UAAU,mBAAmB,QAAA,QAAQ,MAAM,QAAA,OAAO,oBAAoB,QAAA,KAAK,KAAK,KAAA;EAClF,EAAE;EAEF,MAAM,SAAS,GAAG;GAChB,OAAO;IACL,MAAM;IACN,SAAS;IACT,SAAS;GACX;GACA,UAAU,EAKR,aAAa;IACX,MAAM,EAAE,SAAS,wDAAwD;IACzE,OAAO,EAAE,SAAS,wEAAwE;GAC5F,EACF;GACA,iBAAiB,EAAE,aAAa,MAAM;EACxC,CAAC;EAED,MAAM,KAAK,eAA0C,OAAO,EAAE,aAAU,QAAA,YAAE,CAAC,CAAC;EAE5E,MAAM,cAAuC,EAC3C,SAAS,OACX;EAEA,MAAM,oBAAoB,eAAuC;GAC/D,IAAI,CAAC,QAAA,aACH,OAAO;IACL,gBAAgB;IAChB,kBAAkB;IAClB,cAAc;IACd,gBAAgB;IAChB,kBAAkB;IAClB,cAAc;GAChB;GAGF,OAAO;IACL,gBAAgB;IAChB,kBAAkB;IAClB,cAAc;IACd,gBAAgB;IAChB,kBAAkB;IAClB,cAAc;GAChB;EACF,CAAC;EAED,SAAS,SAAe;GACtB,IAAI,YAAY,CAAC,IAAI,UAAU,CAAC;EAClC;EAEA,SAAS,QAAc;GACrB,IAAI,YAAY,KAAK;EACvB;;;;;;;;;;EAWA,SAAS,aAAa,OAA4B;GAChD,IAAI,MAAM,aACR;GAEF,MAAM,gBAAgB;GACtB,IAAI,CAAC,QAAA,YACH,MAAM;GAER,KAAK,WAAW;EAClB;EAEA,SAAS,iBAAuB;GAC9B,IAAI,CAAC,QAAA,YACH,MAAM;GAER,KAAK,eAAe;EACtB;EAEA,SAAS,YAAkB;GACzB,IAAI,CAAC,QAAA,YACH,MAAM;GAER,KAAK,SAAS;EAChB;;;;;;;;EASA,sBAAsB,YAAY,SAAS;EAE3C,SAAS,kBAAwB;GAC/B,aAAa,MAAM;GACnB,IAAI,IAAI,aAAa,GAAG;IACtB,IAAI,eAAe,KAAK;IACxB,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,aAAa,UAAU;GAC3B,IAAI,CAAC,OAAO;IACV,IAAI,eAAe,IAAI;IAEvB,aAAa,OAAO,iBAAiB,GAAG;GAC1C;EACF,CAAC;EAED,MAAM,aAAa,QAAQ;GACzB,IAAI,KACF,eAAe,IAAI,MAAM,CAAC;QAG1B,gBAAgB;EAEpB,CAAC;;GAIC,OAAAC,UAAA,GAAAC,mBAoDM,OAAAC,eAAAC,mBApDOC,MAAA,YAAA,CAAY,CAACN,KAAAA,MAAM,CAAA,CAAA,GAAA,CAC9BO,WAGE,KAAA,QAAA,aAAAH,eAAAC,mBAAA;IAAA,OADiB;IAAW,QAAU,WAAA;GAAU,CAAA,CAAA,CAAA,IAElDH,UAAA,GAAAM,YA8CWC,UAAA,EA9CD,IAAG,OAAM,GAAA,CAETH,MAAA,KAAA,KADRJ,UAAA,GAAAC,mBA4CM,OA5CNO,WA4CM;;IA1CH,OAAOJ,MAAA,EAAA,CAAE,CAAC,KAAI,EAAA,OAAUA,MAAA,EAAA,CAAE,CAAC,QAAA,YAAY,IAAI,EAAA,CAAA;IAC3C,OAAK,EAAA,QAAI,QAAA,OAAM;IAChB,MAAK;IACL,cAAW;IACV,cAAY,QAAA;IACb,UAAS;GACD,GAAAA,MAAA,eAAA,CAAe,CAACN,KAAAA,MAAM,GAAA,EAC7B,WAAO,OAAA,OAAA,OAAA,KAAAW,UAAA,WAAM,aAAa,MAAM,GAAA,CAAA,KAAA,CAAA,GAAA,CAAA,GAAA,CAEjCC,YAcaC,YAAA;IAbX,oBAAiB;IACjB,sBAAmB;IACnB,kBAAe;IACf,oBAAiB;IACjB,sBAAmB;IACnB,kBAAe;;IAEf,SAAAC,cAKE,CAJM,WAAA,SADRZ,UAAA,GAAAC,mBAKE,OAAA;;KAHA,WAAQ;KACP,OAAKY,eAAET,MAAA,EAAA,CAAE,CAAC,QAAO,CAAA;KACjB,SAAK,OAAA,OAAA,OAAA,KAAAU,eAAA,WAAO,eAAc,GAAA,CAAA,MAAA,CAAA;;;GAG/BJ,CAAAA,GAAAA,YAiBaC,YAjBbH,WACUJ,MAgBG,iBAAA,GAhBc,EACxB,cAAW,OAAA,OAAA,OAAA,MAAA,WAAE,gBAAe,GAAA,CAAA,GAAA;IAE7B,SAAAQ,cAYM,CAXE,WAAA,SADRZ,UAAA,GAAAC,mBAYM,OAAA;;KAVA,SAAA;KAAJ,KAAI;KACJ,WAAQ;KACP,OAAKe,eAAEZ,MAAA,KAAA,CAAK;KACb,UAAS;KACR,qBAAmB,QAAA,eAAe,KAAA;KAClC,OAAKS,eAAET,MAAA,EAAA,CAAE,CAAC,QAAO,EAAA,OAAUA,MAAA,EAAA,CAAE,CAAC,QAAA,YAAY,OAAO,EAAA,CAAA,CAAA;IAElDM,GAAAA,CAAAA,YAEsBN,MAAA,mBAAA,GAAA,MAAA;KADpB,SAAAQ,cAA+C,CAA/CP,WAA+C,KAAA,QAAA,WAAAH,eAAAC,mBAAA;MAAA,QAAvB,WAAA;MAAY;KAAK,CAAA,CAAA,CAAA,CAAA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"RuiMenu.js","names":["_sfc_main"],"sources":["../../../../src/components/overlays/menu/RuiMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport RuiFormTextDetail from '@/components/helpers/RuiFormTextDetail.vue';\nimport { type FloatingOptions, useFloating } from '@/composables/floating';\nimport { type FormDetails, useFormTextDetail } from '@/utils/form-text-detail';\nimport { cn, tv } from '@/utils/tv';\n\ninterface BaseMenuAttrs { onMouseover?: () => void; onMouseleave?: () => void }\n\n/**\n * What the activator slot binds onto its trigger. The ARIA keys belong on an\n * element that can carry them (a button, link or combobox), never on a plain\n * `div`; an activator that sets its own ARIA takes `activatorHandlers(attrs)`.\n */\ninterface MenuAttrs extends BaseMenuAttrs {\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n}\n\nexport interface RuiMenuClassNames {\n root?: VueClassValue;\n wrapper?: VueClassValue;\n menu?: VueClassValue;\n /** Overrides applied to the popover content box (e.g. to drop its default padding). */\n content?: VueClassValue;\n /** Overrides applied to the hint and messages under the activator. */\n details?: VueClassValue;\n}\n\n/**\n * Roles a popover container may take. These are the values `aria-haspopup`\n * accepts, which keeps the activator and the popover in agreement.\n */\nexport type RuiMenuRole = 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\n\nexport interface MenuProps {\n openOnHover?: boolean;\n fullWidth?: boolean;\n disabled?: boolean;\n openDelay?: number;\n closeDelay?: number;\n options?: FloatingOptions;\n classNames?: RuiMenuClassNames;\n closeOnContentClick?: boolean;\n persistOnActivatorClick?: boolean;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n showDetails?: boolean;\n /** With `showDetails`: `reserve` keeps the line while empty (default), `auto` takes no space until there is a message. */\n details?: Exclude<FormDetails, 'hidden'>;\n dense?: boolean;\n /**\n * Keeps the menu open through the interactions that would otherwise dismiss\n * it: a click outside and an Escape press. A programmatic close (`v-model`\n * set to `false`) still applies.\n */\n persistent?: boolean;\n disableAutoFocus?: boolean;\n /**\n * Optional external element to anchor the menu to, instead of the\n * activator slot's wrapper. When set, the menu's position is computed\n * against this element.\n */\n anchorEl?: HTMLElement;\n /**\n * Role of the teleported popover. Defaults to `menu`, which is correct when\n * the content is made of `menuitem`s. Set it to `listbox` (or `tree`, `grid`,\n * `dialog`) when the content follows a different ARIA model, e.g. a selection\n * list opened by a `combobox`. The activator's `aria-haspopup` follows it.\n */\n role?: RuiMenuRole;\n /**\n * Accessible name of the teleported popover. A `dialog` popover needs one;\n * the other roles take it from their activator or options.\n */\n menuLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiMenu',\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n openOnHover = false,\n disabled = false,\n fullWidth = false,\n openDelay = 0,\n closeDelay = 0,\n options,\n classNames,\n closeOnContentClick = false,\n persistOnActivatorClick = false,\n hint,\n errorMessages = [],\n successMessages = [],\n showDetails = false,\n details = 'reserve',\n dense = false,\n persistent = false,\n disableAutoFocus = false,\n anchorEl,\n role = 'menu',\n menuLabel,\n} = defineProps<MenuProps>();\n\ndefineSlots<{\n activator?: (props: {\n // An inline type literal, so callers can pass it on as `Record<string, unknown>`\n attrs: {\n 'onMouseover'?: () => void;\n 'onMouseleave'?: () => void;\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n };\n open: boolean;\n disabled: boolean;\n hasError: boolean;\n hasSuccess: boolean;\n }) => any;\n default?: (props: { width: number }) => any;\n /** Rendered above the activator, outside the element the menu positions against. */\n label?: () => any;\n}>();\n\nconst click = ref<boolean>(false);\nconst menuContent = useTemplateRef<HTMLElement>('menuContent');\nconst menuId = useId();\n\nconst FOCUSABLE_ELEMENTS_SELECTOR =\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\nconst {\n reference: activator,\n popover: menu,\n open,\n visible,\n currentPlacement,\n leavePending,\n onLeavePending,\n onOpen,\n onClose,\n onLeaveComplete,\n updatePosition,\n} = useFloating(\n () => options ?? {},\n () => disabled,\n () => openDelay,\n () => closeDelay,\n () => anchorEl,\n);\n\nconst { width } = useElementSize(activator);\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst menuStyles = tv({\n slots: {\n wrapper: 'relative inline-flex max-w-full',\n popover: 'w-max z-rui-menu',\n // a hairline edge keeps the popup apart from a surface of nearly the same shade in dark mode\n content: 'rounded-rui-panel overflow-hidden shadow-rui-menu bg-rui-menu border border-rui-divider text-rui-text focus:outline-hidden p-1',\n details: '',\n },\n variants: {\n fullWidth: {\n true: { wrapper: 'w-full' },\n },\n dense: {\n true: { details: 'px-2' },\n false: { details: 'px-4' },\n },\n },\n defaultVariants: { fullWidth: false, dense: false },\n});\n\nconst ui = computed<ReturnType<typeof menuStyles>>(() => menuStyles({\n fullWidth,\n dense,\n}));\n\n// `aria-haspopup=\"true\"` means `\"menu\"`, and the default keeps emitting `true` for existing callers\nconst ariaHasPopup = computed<RuiMenuRole | 'true'>(() => role === 'menu' ? 'true' : role);\n\n/**\n * The activator attributes.\n *\n * This and the computed below it must return the same object shape from every\n * branch. Otherwise vue-tsc infers the slot's `attrs` type as a discriminated\n * union where each key is either all defined or all undefined, which breaks\n * consumer code typing its own `attrs` handler with individually optional\n * keys.\n */\nconst baseMenuAttrs = computed<BaseMenuAttrs>(() => {\n const clickVal = get(click);\n return {\n onMouseover: disabled\n ? undefined\n : () => {\n if (openOnHover)\n onOpen();\n },\n onMouseleave: disabled\n ? undefined\n : () => {\n if (openOnHover && !clickVal)\n onClose();\n },\n };\n});\n\nconst menuAttrs = computed<MenuAttrs>(() => ({\n ...get(baseMenuAttrs),\n 'onClick': disabled ? undefined : checkClick,\n 'aria-haspopup': get(ariaHasPopup),\n 'aria-expanded': get(open),\n // Only while the popover is in the DOM, so the reference never points at a missing element\n 'aria-controls': get(visible) ? menuId : undefined,\n}));\n\n/**\n * Moves focus into the open menu without scrolling the page. A menu taller than the space below its\n * activator would otherwise make the browser scroll the whole document to reveal the focused\n * container, sliding the page out from under the activator.\n */\nfunction focusOnContent() {\n const content = get(menuContent);\n if (!content)\n return;\n\n content.focus({ preventScroll: true });\n}\n\nfunction focusMenu(): void {\n if (disableAutoFocus)\n return;\n\n nextTick(() => focusOnContent());\n}\n\n/** Hands focus back to the activator on close, likewise without scrolling to it. */\nfunction focusOnActivator() {\n const activatorEl = get(activator);\n if (!activatorEl) {\n return;\n }\n const focusableEl = activatorEl.querySelector<HTMLElement>(FOCUSABLE_ELEMENTS_SELECTOR);\n if (focusableEl) {\n focusableEl.focus({ preventScroll: true });\n }\n}\n\nfunction onLeave(event?: KeyboardEvent): void {\n if (!get(open))\n return;\n onClose();\n set(click, false);\n event?.stopPropagation();\n\n // Return focus to activator when menu closes\n if (disableAutoFocus) {\n return;\n }\n\n nextTick(() => focusOnActivator());\n}\n\n/**\n * `persistent` blocks the outside interactions that would close the menu, and\n * Escape is one of them, which is how `RuiDialog` and `RuiBottomSheet` already\n * read it. The guard sits here rather than in `onLeave`, because `onLeave`\n * also runs for a programmatic close through `v-model`, which must keep\n * working.\n *\n * A persistent menu leaves the key alone instead of swallowing it, so an inner\n * popover can close on Escape while the menu stays open, and an ancestor still\n * sees a key nothing consumed.\n *\n * Escape is swallowed only while the menu is open and not persistent\n * (`onLeave` stops the event itself); a closed or persistent menu must let it\n * through, or a consumer that closes its own editor on Escape never sees the\n * key. The root element listens for it.\n */\nfunction onEscape(event: KeyboardEvent): void {\n if (persistent)\n return;\n\n onLeave(event);\n}\n\nfunction checkClick(): void {\n if (get(open) && get(click)) {\n if (!persistOnActivatorClick)\n onLeave();\n }\n else {\n onOpen();\n set(click, true);\n }\n}\n\nwatch(modelValue, (value) => {\n if (get(open) === value)\n return;\n\n if (value) {\n onOpen();\n set(click, true);\n }\n else {\n onLeave();\n }\n});\n\nwatch(open, (open) => {\n set(modelValue, open);\n if (open) {\n focusMenu();\n }\n});\n\nonClickOutside(menu, () => {\n if (get(open) && !persistent)\n onLeave();\n}, { ignore: [activator] });\n</script>\n\n<template>\n <div\n :class=\"classNames?.root\"\n @keydown.esc=\"onEscape($event)\"\n >\n <slot name=\"label\" />\n <div\n ref=\"activator\"\n :class=\"ui.wrapper({ class: cn(classNames?.wrapper) })\"\n :data-menu-disabled=\"disabled\"\n >\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: menuAttrs, open, disabled, hasError, hasSuccess }\"\n />\n </div>\n <Teleport\n v-if=\"!disabled\"\n to=\"body\"\n >\n <div\n v-if=\"visible\"\n :id=\"menuId\"\n ref=\"menu\"\n :class=\"ui.popover({ class: cn(classNames?.menu) })\"\n :role=\"role\"\n :aria-label=\"menuLabel\"\n :data-placement=\"currentPlacement\"\n @click=\"closeOnContentClick ? onLeave() : undefined\"\n @keydown.esc=\"onEscape($event)\"\n >\n <TransitionGroup\n enter-active-class=\"transition ease-out duration-200\"\n enter-from-class=\"opacity-0 translate-y-1\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition ease-in duration-150\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-1\"\n @before-enter=\"updatePosition()\"\n @after-leave=\"leavePending ? onLeaveComplete() : undefined\"\n @before-leave=\"onLeavePending()\"\n >\n <div\n v-if=\"open\"\n ref=\"menuContent\"\n key=\"menu\"\n data-id=\"content\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n tabindex=\"-1\"\n v-bind=\"baseMenuAttrs\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ width }\" />\n </ButtonGroupBoundary>\n </div>\n </TransitionGroup>\n </div>\n </Teleport>\n <RuiFormTextDetail\n v-if=\"showDetails\"\n :details=\"details\"\n :class=\"ui.details({ class: cn(classNames?.details) })\"\n :error-messages=\"errorMessages\"\n :success-messages=\"successMessages\"\n :hint=\"hint\"\n />\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"RuiMenu.js","names":["_sfc_main"],"sources":["../../../../src/components/overlays/menu/RuiMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport RuiFormTextDetail from '@/components/helpers/RuiFormTextDetail.vue';\nimport { type FloatingOptions, useFloating } from '@/composables/floating';\nimport { type FormDetails, useFormTextDetail } from '@/utils/form-text-detail';\nimport { cn, tv } from '@/utils/tv';\n\ninterface BaseMenuAttrs { onMouseover?: () => void; onMouseleave?: () => void }\n\n/**\n * What the activator slot binds onto its trigger. The ARIA keys belong on an\n * element that can carry them (a button, link or combobox), never on a plain\n * `div`; an activator that sets its own ARIA takes `activatorHandlers(attrs)`.\n */\ninterface MenuAttrs extends BaseMenuAttrs {\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n}\n\nexport interface RuiMenuClassNames {\n root?: VueClassValue;\n wrapper?: VueClassValue;\n menu?: VueClassValue;\n /** Overrides applied to the popover content box (e.g. to drop its default padding). */\n content?: VueClassValue;\n /** Overrides applied to the hint and messages under the activator. */\n details?: VueClassValue;\n}\n\n/**\n * Roles a popover container may take. These are the values `aria-haspopup`\n * accepts, which keeps the activator and the popover in agreement.\n */\nexport type RuiMenuRole = 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\n\nexport interface MenuProps {\n openOnHover?: boolean;\n fullWidth?: boolean;\n disabled?: boolean;\n openDelay?: number;\n closeDelay?: number;\n options?: FloatingOptions;\n classNames?: RuiMenuClassNames;\n closeOnContentClick?: boolean;\n persistOnActivatorClick?: boolean;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n showDetails?: boolean;\n /** With `showDetails`: `reserve` keeps the line while empty (default), `auto` takes no space until there is a message. */\n details?: Exclude<FormDetails, 'hidden'>;\n dense?: boolean;\n /**\n * Keeps the menu open through the interactions that would otherwise dismiss\n * it: a click outside and an Escape press. A programmatic close (`v-model`\n * set to `false`) still applies.\n */\n persistent?: boolean;\n disableAutoFocus?: boolean;\n /**\n * Optional external element to anchor the menu to, instead of the\n * activator slot's wrapper. When set, the menu's position is computed\n * against this element.\n */\n anchorEl?: HTMLElement;\n /**\n * Role of the teleported popover. Defaults to `menu`, which is correct when\n * the content is made of `menuitem`s. Set it to `listbox` (or `tree`, `grid`,\n * `dialog`) when the content follows a different ARIA model, e.g. a selection\n * list opened by a `combobox`. The activator's `aria-haspopup` follows it.\n */\n role?: RuiMenuRole;\n /**\n * Accessible name of the teleported popover. A `dialog` popover needs one;\n * the other roles take it from their activator or options.\n */\n menuLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiMenu',\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n openOnHover = false,\n disabled = false,\n fullWidth = false,\n openDelay = 0,\n closeDelay = 0,\n options,\n classNames,\n closeOnContentClick = false,\n persistOnActivatorClick = false,\n hint,\n errorMessages = [],\n successMessages = [],\n showDetails = false,\n details = 'reserve',\n dense = false,\n persistent = false,\n disableAutoFocus = false,\n anchorEl,\n role = 'menu',\n menuLabel,\n} = defineProps<MenuProps>();\n\ndefineSlots<{\n activator?: (props: {\n // An inline type literal, so callers can pass it on as `Record<string, unknown>`\n attrs: {\n 'onMouseover'?: () => void;\n 'onMouseleave'?: () => void;\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n };\n open: boolean;\n disabled: boolean;\n hasError: boolean;\n hasSuccess: boolean;\n }) => any;\n default?: (props: { width: number }) => any;\n /** Rendered above the activator, outside the element the menu positions against. */\n label?: () => any;\n}>();\n\nconst click = ref<boolean>(false);\nconst menuContent = useTemplateRef<HTMLElement>('menuContent');\nconst menuId = useId();\n\nconst FOCUSABLE_ELEMENTS_SELECTOR =\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\nconst {\n reference: activator,\n popover: menu,\n open,\n visible,\n currentPlacement,\n leavePending,\n onLeavePending,\n onOpen,\n onClose,\n onLeaveComplete,\n updatePosition,\n} = useFloating(\n () => options ?? {},\n () => disabled,\n () => openDelay,\n () => closeDelay,\n () => anchorEl,\n);\n\nconst { width } = useElementSize(activator);\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst menuStyles = tv({\n slots: {\n wrapper: 'relative inline-flex max-w-full',\n popover: 'w-max z-rui-menu',\n // a hairline edge keeps the popup apart from a surface of nearly the same shade in dark mode\n content: 'rounded-rui-panel overflow-hidden shadow-rui-menu bg-rui-menu border border-rui-divider text-rui-text focus:outline-hidden p-1',\n details: '',\n },\n variants: {\n fullWidth: {\n true: { wrapper: 'w-full' },\n },\n dense: {\n true: { details: 'px-2' },\n false: { details: 'px-4' },\n },\n },\n defaultVariants: { fullWidth: false, dense: false },\n});\n\nconst ui = computed<ReturnType<typeof menuStyles>>(() => menuStyles({\n fullWidth,\n dense,\n}));\n\n// `aria-haspopup=\"true\"` means `\"menu\"`, and the default keeps emitting `true` for existing callers\nconst ariaHasPopup = computed<RuiMenuRole | 'true'>(() => role === 'menu' ? 'true' : role);\n\n/**\n * The activator attributes.\n *\n * This and the computed below it must return the same object shape from every\n * branch. Otherwise vue-tsc infers the slot's `attrs` type as a discriminated\n * union where each key is either all defined or all undefined, which breaks\n * consumer code typing its own `attrs` handler with individually optional\n * keys.\n */\nconst baseMenuAttrs = computed<BaseMenuAttrs>(() => {\n const clickVal = get(click);\n return {\n onMouseover: disabled\n ? undefined\n : () => {\n if (openOnHover)\n onOpen();\n },\n onMouseleave: disabled\n ? undefined\n : () => {\n if (openOnHover && !clickVal)\n onClose();\n },\n };\n});\n\nconst menuAttrs = computed<MenuAttrs>(() => ({\n ...get(baseMenuAttrs),\n 'onClick': disabled ? undefined : checkClick,\n 'aria-haspopup': get(ariaHasPopup),\n 'aria-expanded': get(open),\n // Only while the popover is in the DOM, so the reference never points at a missing element\n 'aria-controls': get(visible) ? menuId : undefined,\n}));\n\n/**\n * Moves focus into the open menu without scrolling the page. A menu taller than the space below its\n * activator would otherwise make the browser scroll the whole document to reveal the focused\n * container, sliding the page out from under the activator.\n */\nfunction focusOnContent() {\n const content = get(menuContent);\n if (!content)\n return;\n\n content.focus({ preventScroll: true });\n}\n\nfunction focusMenu(): void {\n if (disableAutoFocus)\n return;\n\n nextTick(() => focusOnContent());\n}\n\n/** Hands focus back to the activator on close, likewise without scrolling to it. */\nfunction focusOnActivator() {\n const activatorEl = get(activator);\n if (!activatorEl) {\n return;\n }\n const focusableEl = activatorEl.querySelector<HTMLElement>(FOCUSABLE_ELEMENTS_SELECTOR);\n if (focusableEl) {\n focusableEl.focus({ preventScroll: true });\n }\n}\n\nfunction onLeave(event?: KeyboardEvent): void {\n if (!get(open))\n return;\n onClose();\n set(click, false);\n event?.stopPropagation();\n\n // Return focus to activator when menu closes\n if (disableAutoFocus) {\n return;\n }\n\n nextTick(() => focusOnActivator());\n}\n\n/**\n * `persistent` blocks the outside interactions that would close the menu, and\n * Escape is one of them, which is how `RuiDialog` and `RuiBottomSheet` already\n * read it. The guard sits here rather than in `onLeave`, because `onLeave`\n * also runs for a programmatic close through `v-model`, which must keep\n * working.\n *\n * A persistent menu leaves the key alone instead of swallowing it, so an inner\n * popover can close on Escape while the menu stays open, and an ancestor still\n * sees a key nothing consumed.\n *\n * Escape is swallowed only while the menu is open and not persistent\n * (`onLeave` stops the event itself); a closed or persistent menu must let it\n * through, or a consumer that closes its own editor on Escape never sees the\n * key. The root element listens for it.\n *\n * An Escape that arrives while an IME is composing belongs to the IME, which\n * cancels the candidate with it, so the menu leaves it alone the same way: a\n * search field inside the menu would otherwise lose the whole menu mid-word.\n */\nfunction onEscape(event: KeyboardEvent): void {\n if (persistent || event.isComposing)\n return;\n\n onLeave(event);\n}\n\nfunction checkClick(): void {\n if (get(open) && get(click)) {\n if (!persistOnActivatorClick)\n onLeave();\n }\n else {\n onOpen();\n set(click, true);\n }\n}\n\nwatch(modelValue, (value) => {\n if (get(open) === value)\n return;\n\n if (value) {\n onOpen();\n set(click, true);\n }\n else {\n onLeave();\n }\n});\n\nwatch(open, (open) => {\n set(modelValue, open);\n if (open) {\n focusMenu();\n }\n});\n\nonClickOutside(menu, () => {\n if (get(open) && !persistent)\n onLeave();\n}, { ignore: [activator] });\n</script>\n\n<template>\n <div\n :class=\"classNames?.root\"\n @keydown.esc=\"onEscape($event)\"\n >\n <slot name=\"label\" />\n <div\n ref=\"activator\"\n :class=\"ui.wrapper({ class: cn(classNames?.wrapper) })\"\n :data-menu-disabled=\"disabled\"\n >\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: menuAttrs, open, disabled, hasError, hasSuccess }\"\n />\n </div>\n <Teleport\n v-if=\"!disabled\"\n to=\"body\"\n >\n <div\n v-if=\"visible\"\n :id=\"menuId\"\n ref=\"menu\"\n :class=\"ui.popover({ class: cn(classNames?.menu) })\"\n :role=\"role\"\n :aria-label=\"menuLabel\"\n :data-placement=\"currentPlacement\"\n @click=\"closeOnContentClick ? onLeave() : undefined\"\n @keydown.esc=\"onEscape($event)\"\n >\n <TransitionGroup\n enter-active-class=\"transition ease-out duration-200\"\n enter-from-class=\"opacity-0 translate-y-1\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition ease-in duration-150\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-1\"\n @before-enter=\"updatePosition()\"\n @after-leave=\"leavePending ? onLeaveComplete() : undefined\"\n @before-leave=\"onLeavePending()\"\n >\n <div\n v-if=\"open\"\n ref=\"menuContent\"\n key=\"menu\"\n data-id=\"content\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n tabindex=\"-1\"\n v-bind=\"baseMenuAttrs\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ width }\" />\n </ButtonGroupBoundary>\n </div>\n </TransitionGroup>\n </div>\n </Teleport>\n <RuiFormTextDetail\n v-if=\"showDetails\"\n :details=\"details\"\n :class=\"ui.details({ class: cn(classNames?.details) })\"\n :error-messages=\"errorMessages\"\n :success-messages=\"successMessages\"\n :hint=\"hint\"\n />\n </div>\n</template>\n"],"mappings":""}
@@ -177,9 +177,13 @@ var RuiMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
177
177
  * (`onLeave` stops the event itself); a closed or persistent menu must let it
178
178
  * through, or a consumer that closes its own editor on Escape never sees the
179
179
  * key. The root element listens for it.
180
+ *
181
+ * An Escape that arrives while an IME is composing belongs to the IME, which
182
+ * cancels the candidate with it, so the menu leaves it alone the same way: a
183
+ * search field inside the menu would otherwise lose the whole menu mid-word.
180
184
  */
181
185
  function onEscape(event) {
182
- if (__props.persistent) return;
186
+ if (__props.persistent || event.isComposing) return;
183
187
  onLeave(event);
184
188
  }
185
189
  function checkClick() {
@@ -1 +1 @@
1
- {"version":3,"file":"RuiMenu.vue_vue_type_script_setup_true_lang.js","names":["_useModel","_openBlock","_createElementBlock","_normalizeClass","_withKeys","_renderSlot","_createElementVNode","_unref","_normalizeProps","_guardReactiveProps","_createBlock","_Teleport","_createVNode","_TransitionGroup","_withCtx","_mergeProps","_createCommentVNode","RuiFormTextDetail"],"sources":["../../../../src/components/overlays/menu/RuiMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport RuiFormTextDetail from '@/components/helpers/RuiFormTextDetail.vue';\nimport { type FloatingOptions, useFloating } from '@/composables/floating';\nimport { type FormDetails, useFormTextDetail } from '@/utils/form-text-detail';\nimport { cn, tv } from '@/utils/tv';\n\ninterface BaseMenuAttrs { onMouseover?: () => void; onMouseleave?: () => void }\n\n/**\n * What the activator slot binds onto its trigger. The ARIA keys belong on an\n * element that can carry them (a button, link or combobox), never on a plain\n * `div`; an activator that sets its own ARIA takes `activatorHandlers(attrs)`.\n */\ninterface MenuAttrs extends BaseMenuAttrs {\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n}\n\nexport interface RuiMenuClassNames {\n root?: VueClassValue;\n wrapper?: VueClassValue;\n menu?: VueClassValue;\n /** Overrides applied to the popover content box (e.g. to drop its default padding). */\n content?: VueClassValue;\n /** Overrides applied to the hint and messages under the activator. */\n details?: VueClassValue;\n}\n\n/**\n * Roles a popover container may take. These are the values `aria-haspopup`\n * accepts, which keeps the activator and the popover in agreement.\n */\nexport type RuiMenuRole = 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\n\nexport interface MenuProps {\n openOnHover?: boolean;\n fullWidth?: boolean;\n disabled?: boolean;\n openDelay?: number;\n closeDelay?: number;\n options?: FloatingOptions;\n classNames?: RuiMenuClassNames;\n closeOnContentClick?: boolean;\n persistOnActivatorClick?: boolean;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n showDetails?: boolean;\n /** With `showDetails`: `reserve` keeps the line while empty (default), `auto` takes no space until there is a message. */\n details?: Exclude<FormDetails, 'hidden'>;\n dense?: boolean;\n /**\n * Keeps the menu open through the interactions that would otherwise dismiss\n * it: a click outside and an Escape press. A programmatic close (`v-model`\n * set to `false`) still applies.\n */\n persistent?: boolean;\n disableAutoFocus?: boolean;\n /**\n * Optional external element to anchor the menu to, instead of the\n * activator slot's wrapper. When set, the menu's position is computed\n * against this element.\n */\n anchorEl?: HTMLElement;\n /**\n * Role of the teleported popover. Defaults to `menu`, which is correct when\n * the content is made of `menuitem`s. Set it to `listbox` (or `tree`, `grid`,\n * `dialog`) when the content follows a different ARIA model, e.g. a selection\n * list opened by a `combobox`. The activator's `aria-haspopup` follows it.\n */\n role?: RuiMenuRole;\n /**\n * Accessible name of the teleported popover. A `dialog` popover needs one;\n * the other roles take it from their activator or options.\n */\n menuLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiMenu',\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n openOnHover = false,\n disabled = false,\n fullWidth = false,\n openDelay = 0,\n closeDelay = 0,\n options,\n classNames,\n closeOnContentClick = false,\n persistOnActivatorClick = false,\n hint,\n errorMessages = [],\n successMessages = [],\n showDetails = false,\n details = 'reserve',\n dense = false,\n persistent = false,\n disableAutoFocus = false,\n anchorEl,\n role = 'menu',\n menuLabel,\n} = defineProps<MenuProps>();\n\ndefineSlots<{\n activator?: (props: {\n // An inline type literal, so callers can pass it on as `Record<string, unknown>`\n attrs: {\n 'onMouseover'?: () => void;\n 'onMouseleave'?: () => void;\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n };\n open: boolean;\n disabled: boolean;\n hasError: boolean;\n hasSuccess: boolean;\n }) => any;\n default?: (props: { width: number }) => any;\n /** Rendered above the activator, outside the element the menu positions against. */\n label?: () => any;\n}>();\n\nconst click = ref<boolean>(false);\nconst menuContent = useTemplateRef<HTMLElement>('menuContent');\nconst menuId = useId();\n\nconst FOCUSABLE_ELEMENTS_SELECTOR =\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\nconst {\n reference: activator,\n popover: menu,\n open,\n visible,\n currentPlacement,\n leavePending,\n onLeavePending,\n onOpen,\n onClose,\n onLeaveComplete,\n updatePosition,\n} = useFloating(\n () => options ?? {},\n () => disabled,\n () => openDelay,\n () => closeDelay,\n () => anchorEl,\n);\n\nconst { width } = useElementSize(activator);\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst menuStyles = tv({\n slots: {\n wrapper: 'relative inline-flex max-w-full',\n popover: 'w-max z-rui-menu',\n // a hairline edge keeps the popup apart from a surface of nearly the same shade in dark mode\n content: 'rounded-rui-panel overflow-hidden shadow-rui-menu bg-rui-menu border border-rui-divider text-rui-text focus:outline-hidden p-1',\n details: '',\n },\n variants: {\n fullWidth: {\n true: { wrapper: 'w-full' },\n },\n dense: {\n true: { details: 'px-2' },\n false: { details: 'px-4' },\n },\n },\n defaultVariants: { fullWidth: false, dense: false },\n});\n\nconst ui = computed<ReturnType<typeof menuStyles>>(() => menuStyles({\n fullWidth,\n dense,\n}));\n\n// `aria-haspopup=\"true\"` means `\"menu\"`, and the default keeps emitting `true` for existing callers\nconst ariaHasPopup = computed<RuiMenuRole | 'true'>(() => role === 'menu' ? 'true' : role);\n\n/**\n * The activator attributes.\n *\n * This and the computed below it must return the same object shape from every\n * branch. Otherwise vue-tsc infers the slot's `attrs` type as a discriminated\n * union where each key is either all defined or all undefined, which breaks\n * consumer code typing its own `attrs` handler with individually optional\n * keys.\n */\nconst baseMenuAttrs = computed<BaseMenuAttrs>(() => {\n const clickVal = get(click);\n return {\n onMouseover: disabled\n ? undefined\n : () => {\n if (openOnHover)\n onOpen();\n },\n onMouseleave: disabled\n ? undefined\n : () => {\n if (openOnHover && !clickVal)\n onClose();\n },\n };\n});\n\nconst menuAttrs = computed<MenuAttrs>(() => ({\n ...get(baseMenuAttrs),\n 'onClick': disabled ? undefined : checkClick,\n 'aria-haspopup': get(ariaHasPopup),\n 'aria-expanded': get(open),\n // Only while the popover is in the DOM, so the reference never points at a missing element\n 'aria-controls': get(visible) ? menuId : undefined,\n}));\n\n/**\n * Moves focus into the open menu without scrolling the page. A menu taller than the space below its\n * activator would otherwise make the browser scroll the whole document to reveal the focused\n * container, sliding the page out from under the activator.\n */\nfunction focusOnContent() {\n const content = get(menuContent);\n if (!content)\n return;\n\n content.focus({ preventScroll: true });\n}\n\nfunction focusMenu(): void {\n if (disableAutoFocus)\n return;\n\n nextTick(() => focusOnContent());\n}\n\n/** Hands focus back to the activator on close, likewise without scrolling to it. */\nfunction focusOnActivator() {\n const activatorEl = get(activator);\n if (!activatorEl) {\n return;\n }\n const focusableEl = activatorEl.querySelector<HTMLElement>(FOCUSABLE_ELEMENTS_SELECTOR);\n if (focusableEl) {\n focusableEl.focus({ preventScroll: true });\n }\n}\n\nfunction onLeave(event?: KeyboardEvent): void {\n if (!get(open))\n return;\n onClose();\n set(click, false);\n event?.stopPropagation();\n\n // Return focus to activator when menu closes\n if (disableAutoFocus) {\n return;\n }\n\n nextTick(() => focusOnActivator());\n}\n\n/**\n * `persistent` blocks the outside interactions that would close the menu, and\n * Escape is one of them, which is how `RuiDialog` and `RuiBottomSheet` already\n * read it. The guard sits here rather than in `onLeave`, because `onLeave`\n * also runs for a programmatic close through `v-model`, which must keep\n * working.\n *\n * A persistent menu leaves the key alone instead of swallowing it, so an inner\n * popover can close on Escape while the menu stays open, and an ancestor still\n * sees a key nothing consumed.\n *\n * Escape is swallowed only while the menu is open and not persistent\n * (`onLeave` stops the event itself); a closed or persistent menu must let it\n * through, or a consumer that closes its own editor on Escape never sees the\n * key. The root element listens for it.\n */\nfunction onEscape(event: KeyboardEvent): void {\n if (persistent)\n return;\n\n onLeave(event);\n}\n\nfunction checkClick(): void {\n if (get(open) && get(click)) {\n if (!persistOnActivatorClick)\n onLeave();\n }\n else {\n onOpen();\n set(click, true);\n }\n}\n\nwatch(modelValue, (value) => {\n if (get(open) === value)\n return;\n\n if (value) {\n onOpen();\n set(click, true);\n }\n else {\n onLeave();\n }\n});\n\nwatch(open, (open) => {\n set(modelValue, open);\n if (open) {\n focusMenu();\n }\n});\n\nonClickOutside(menu, () => {\n if (get(open) && !persistent)\n onLeave();\n}, { ignore: [activator] });\n</script>\n\n<template>\n <div\n :class=\"classNames?.root\"\n @keydown.esc=\"onEscape($event)\"\n >\n <slot name=\"label\" />\n <div\n ref=\"activator\"\n :class=\"ui.wrapper({ class: cn(classNames?.wrapper) })\"\n :data-menu-disabled=\"disabled\"\n >\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: menuAttrs, open, disabled, hasError, hasSuccess }\"\n />\n </div>\n <Teleport\n v-if=\"!disabled\"\n to=\"body\"\n >\n <div\n v-if=\"visible\"\n :id=\"menuId\"\n ref=\"menu\"\n :class=\"ui.popover({ class: cn(classNames?.menu) })\"\n :role=\"role\"\n :aria-label=\"menuLabel\"\n :data-placement=\"currentPlacement\"\n @click=\"closeOnContentClick ? onLeave() : undefined\"\n @keydown.esc=\"onEscape($event)\"\n >\n <TransitionGroup\n enter-active-class=\"transition ease-out duration-200\"\n enter-from-class=\"opacity-0 translate-y-1\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition ease-in duration-150\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-1\"\n @before-enter=\"updatePosition()\"\n @after-leave=\"leavePending ? onLeaveComplete() : undefined\"\n @before-leave=\"onLeavePending()\"\n >\n <div\n v-if=\"open\"\n ref=\"menuContent\"\n key=\"menu\"\n data-id=\"content\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n tabindex=\"-1\"\n v-bind=\"baseMenuAttrs\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ width }\" />\n </ButtonGroupBoundary>\n </div>\n </TransitionGroup>\n </div>\n </Teleport>\n <RuiFormTextDetail\n v-if=\"showDetails\"\n :details=\"details\"\n :class=\"ui.details({ class: cn(classNames?.details) })\"\n :error-messages=\"errorMessages\"\n :success-messages=\"successMessages\"\n :hint=\"hint\"\n />\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"RuiMenu.vue_vue_type_script_setup_true_lang.js","names":["_useModel","_openBlock","_createElementBlock","_normalizeClass","_withKeys","_renderSlot","_createElementVNode","_unref","_normalizeProps","_guardReactiveProps","_createBlock","_Teleport","_createVNode","_TransitionGroup","_withCtx","_mergeProps","_createCommentVNode","RuiFormTextDetail"],"sources":["../../../../src/components/overlays/menu/RuiMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { VueClassValue } from '@/types/class-value';\nimport { ButtonGroupBoundary } from '@/components/buttons/button-group/button-group-context';\nimport RuiFormTextDetail from '@/components/helpers/RuiFormTextDetail.vue';\nimport { type FloatingOptions, useFloating } from '@/composables/floating';\nimport { type FormDetails, useFormTextDetail } from '@/utils/form-text-detail';\nimport { cn, tv } from '@/utils/tv';\n\ninterface BaseMenuAttrs { onMouseover?: () => void; onMouseleave?: () => void }\n\n/**\n * What the activator slot binds onto its trigger. The ARIA keys belong on an\n * element that can carry them (a button, link or combobox), never on a plain\n * `div`; an activator that sets its own ARIA takes `activatorHandlers(attrs)`.\n */\ninterface MenuAttrs extends BaseMenuAttrs {\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n}\n\nexport interface RuiMenuClassNames {\n root?: VueClassValue;\n wrapper?: VueClassValue;\n menu?: VueClassValue;\n /** Overrides applied to the popover content box (e.g. to drop its default padding). */\n content?: VueClassValue;\n /** Overrides applied to the hint and messages under the activator. */\n details?: VueClassValue;\n}\n\n/**\n * Roles a popover container may take. These are the values `aria-haspopup`\n * accepts, which keeps the activator and the popover in agreement.\n */\nexport type RuiMenuRole = 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\n\nexport interface MenuProps {\n openOnHover?: boolean;\n fullWidth?: boolean;\n disabled?: boolean;\n openDelay?: number;\n closeDelay?: number;\n options?: FloatingOptions;\n classNames?: RuiMenuClassNames;\n closeOnContentClick?: boolean;\n persistOnActivatorClick?: boolean;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n showDetails?: boolean;\n /** With `showDetails`: `reserve` keeps the line while empty (default), `auto` takes no space until there is a message. */\n details?: Exclude<FormDetails, 'hidden'>;\n dense?: boolean;\n /**\n * Keeps the menu open through the interactions that would otherwise dismiss\n * it: a click outside and an Escape press. A programmatic close (`v-model`\n * set to `false`) still applies.\n */\n persistent?: boolean;\n disableAutoFocus?: boolean;\n /**\n * Optional external element to anchor the menu to, instead of the\n * activator slot's wrapper. When set, the menu's position is computed\n * against this element.\n */\n anchorEl?: HTMLElement;\n /**\n * Role of the teleported popover. Defaults to `menu`, which is correct when\n * the content is made of `menuitem`s. Set it to `listbox` (or `tree`, `grid`,\n * `dialog`) when the content follows a different ARIA model, e.g. a selection\n * list opened by a `combobox`. The activator's `aria-haspopup` follows it.\n */\n role?: RuiMenuRole;\n /**\n * Accessible name of the teleported popover. A `dialog` popover needs one;\n * the other roles take it from their activator or options.\n */\n menuLabel?: string;\n}\n\ndefineOptions({\n name: 'RuiMenu',\n});\n\nconst modelValue = defineModel<boolean>({ default: false });\n\nconst {\n openOnHover = false,\n disabled = false,\n fullWidth = false,\n openDelay = 0,\n closeDelay = 0,\n options,\n classNames,\n closeOnContentClick = false,\n persistOnActivatorClick = false,\n hint,\n errorMessages = [],\n successMessages = [],\n showDetails = false,\n details = 'reserve',\n dense = false,\n persistent = false,\n disableAutoFocus = false,\n anchorEl,\n role = 'menu',\n menuLabel,\n} = defineProps<MenuProps>();\n\ndefineSlots<{\n activator?: (props: {\n // An inline type literal, so callers can pass it on as `Record<string, unknown>`\n attrs: {\n 'onMouseover'?: () => void;\n 'onMouseleave'?: () => void;\n 'onClick'?: () => void;\n 'aria-haspopup': RuiMenuRole | 'true';\n 'aria-expanded': boolean;\n 'aria-controls'?: string;\n };\n open: boolean;\n disabled: boolean;\n hasError: boolean;\n hasSuccess: boolean;\n }) => any;\n default?: (props: { width: number }) => any;\n /** Rendered above the activator, outside the element the menu positions against. */\n label?: () => any;\n}>();\n\nconst click = ref<boolean>(false);\nconst menuContent = useTemplateRef<HTMLElement>('menuContent');\nconst menuId = useId();\n\nconst FOCUSABLE_ELEMENTS_SELECTOR =\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\nconst {\n reference: activator,\n popover: menu,\n open,\n visible,\n currentPlacement,\n leavePending,\n onLeavePending,\n onOpen,\n onClose,\n onLeaveComplete,\n updatePosition,\n} = useFloating(\n () => options ?? {},\n () => disabled,\n () => openDelay,\n () => closeDelay,\n () => anchorEl,\n);\n\nconst { width } = useElementSize(activator);\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst menuStyles = tv({\n slots: {\n wrapper: 'relative inline-flex max-w-full',\n popover: 'w-max z-rui-menu',\n // a hairline edge keeps the popup apart from a surface of nearly the same shade in dark mode\n content: 'rounded-rui-panel overflow-hidden shadow-rui-menu bg-rui-menu border border-rui-divider text-rui-text focus:outline-hidden p-1',\n details: '',\n },\n variants: {\n fullWidth: {\n true: { wrapper: 'w-full' },\n },\n dense: {\n true: { details: 'px-2' },\n false: { details: 'px-4' },\n },\n },\n defaultVariants: { fullWidth: false, dense: false },\n});\n\nconst ui = computed<ReturnType<typeof menuStyles>>(() => menuStyles({\n fullWidth,\n dense,\n}));\n\n// `aria-haspopup=\"true\"` means `\"menu\"`, and the default keeps emitting `true` for existing callers\nconst ariaHasPopup = computed<RuiMenuRole | 'true'>(() => role === 'menu' ? 'true' : role);\n\n/**\n * The activator attributes.\n *\n * This and the computed below it must return the same object shape from every\n * branch. Otherwise vue-tsc infers the slot's `attrs` type as a discriminated\n * union where each key is either all defined or all undefined, which breaks\n * consumer code typing its own `attrs` handler with individually optional\n * keys.\n */\nconst baseMenuAttrs = computed<BaseMenuAttrs>(() => {\n const clickVal = get(click);\n return {\n onMouseover: disabled\n ? undefined\n : () => {\n if (openOnHover)\n onOpen();\n },\n onMouseleave: disabled\n ? undefined\n : () => {\n if (openOnHover && !clickVal)\n onClose();\n },\n };\n});\n\nconst menuAttrs = computed<MenuAttrs>(() => ({\n ...get(baseMenuAttrs),\n 'onClick': disabled ? undefined : checkClick,\n 'aria-haspopup': get(ariaHasPopup),\n 'aria-expanded': get(open),\n // Only while the popover is in the DOM, so the reference never points at a missing element\n 'aria-controls': get(visible) ? menuId : undefined,\n}));\n\n/**\n * Moves focus into the open menu without scrolling the page. A menu taller than the space below its\n * activator would otherwise make the browser scroll the whole document to reveal the focused\n * container, sliding the page out from under the activator.\n */\nfunction focusOnContent() {\n const content = get(menuContent);\n if (!content)\n return;\n\n content.focus({ preventScroll: true });\n}\n\nfunction focusMenu(): void {\n if (disableAutoFocus)\n return;\n\n nextTick(() => focusOnContent());\n}\n\n/** Hands focus back to the activator on close, likewise without scrolling to it. */\nfunction focusOnActivator() {\n const activatorEl = get(activator);\n if (!activatorEl) {\n return;\n }\n const focusableEl = activatorEl.querySelector<HTMLElement>(FOCUSABLE_ELEMENTS_SELECTOR);\n if (focusableEl) {\n focusableEl.focus({ preventScroll: true });\n }\n}\n\nfunction onLeave(event?: KeyboardEvent): void {\n if (!get(open))\n return;\n onClose();\n set(click, false);\n event?.stopPropagation();\n\n // Return focus to activator when menu closes\n if (disableAutoFocus) {\n return;\n }\n\n nextTick(() => focusOnActivator());\n}\n\n/**\n * `persistent` blocks the outside interactions that would close the menu, and\n * Escape is one of them, which is how `RuiDialog` and `RuiBottomSheet` already\n * read it. The guard sits here rather than in `onLeave`, because `onLeave`\n * also runs for a programmatic close through `v-model`, which must keep\n * working.\n *\n * A persistent menu leaves the key alone instead of swallowing it, so an inner\n * popover can close on Escape while the menu stays open, and an ancestor still\n * sees a key nothing consumed.\n *\n * Escape is swallowed only while the menu is open and not persistent\n * (`onLeave` stops the event itself); a closed or persistent menu must let it\n * through, or a consumer that closes its own editor on Escape never sees the\n * key. The root element listens for it.\n *\n * An Escape that arrives while an IME is composing belongs to the IME, which\n * cancels the candidate with it, so the menu leaves it alone the same way: a\n * search field inside the menu would otherwise lose the whole menu mid-word.\n */\nfunction onEscape(event: KeyboardEvent): void {\n if (persistent || event.isComposing)\n return;\n\n onLeave(event);\n}\n\nfunction checkClick(): void {\n if (get(open) && get(click)) {\n if (!persistOnActivatorClick)\n onLeave();\n }\n else {\n onOpen();\n set(click, true);\n }\n}\n\nwatch(modelValue, (value) => {\n if (get(open) === value)\n return;\n\n if (value) {\n onOpen();\n set(click, true);\n }\n else {\n onLeave();\n }\n});\n\nwatch(open, (open) => {\n set(modelValue, open);\n if (open) {\n focusMenu();\n }\n});\n\nonClickOutside(menu, () => {\n if (get(open) && !persistent)\n onLeave();\n}, { ignore: [activator] });\n</script>\n\n<template>\n <div\n :class=\"classNames?.root\"\n @keydown.esc=\"onEscape($event)\"\n >\n <slot name=\"label\" />\n <div\n ref=\"activator\"\n :class=\"ui.wrapper({ class: cn(classNames?.wrapper) })\"\n :data-menu-disabled=\"disabled\"\n >\n <slot\n name=\"activator\"\n v-bind=\"{ attrs: menuAttrs, open, disabled, hasError, hasSuccess }\"\n />\n </div>\n <Teleport\n v-if=\"!disabled\"\n to=\"body\"\n >\n <div\n v-if=\"visible\"\n :id=\"menuId\"\n ref=\"menu\"\n :class=\"ui.popover({ class: cn(classNames?.menu) })\"\n :role=\"role\"\n :aria-label=\"menuLabel\"\n :data-placement=\"currentPlacement\"\n @click=\"closeOnContentClick ? onLeave() : undefined\"\n @keydown.esc=\"onEscape($event)\"\n >\n <TransitionGroup\n enter-active-class=\"transition ease-out duration-200\"\n enter-from-class=\"opacity-0 translate-y-1\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition ease-in duration-150\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-1\"\n @before-enter=\"updatePosition()\"\n @after-leave=\"leavePending ? onLeaveComplete() : undefined\"\n @before-leave=\"onLeavePending()\"\n >\n <div\n v-if=\"open\"\n ref=\"menuContent\"\n key=\"menu\"\n data-id=\"content\"\n :class=\"ui.content({ class: cn(classNames?.content) })\"\n tabindex=\"-1\"\n v-bind=\"baseMenuAttrs\"\n >\n <ButtonGroupBoundary>\n <slot v-bind=\"{ width }\" />\n </ButtonGroupBoundary>\n </div>\n </TransitionGroup>\n </div>\n </Teleport>\n <RuiFormTextDetail\n v-if=\"showDetails\"\n :details=\"details\"\n :class=\"ui.details({ class: cn(classNames?.details) })\"\n :error-messages=\"errorMessages\"\n :success-messages=\"successMessages\"\n :hint=\"hint\"\n />\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;AAwIA,IAAM,8BACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAnDF,MAAM,aAAaA,SAAoB,SAAA,YAAmB;EA8C1D,MAAM,QAAQ,IAAa,KAAK;EAChC,MAAM,cAAc,eAA4B,aAAa;EAC7D,MAAM,SAAS,MAAM;EAKrB,MAAM,EACJ,WAAW,WACX,SAAS,MACT,MACA,SACA,kBACA,cACA,gBACA,QACA,SACA,iBACA,mBACE,kBACI,QAAA,WAAW,CAAC,SACZ,QAAA,gBACA,QAAA,iBACA,QAAA,kBACA,QAAA,QACR;EAEA,MAAM,EAAE,UAAU,eAAe,SAAS;EAE1C,MAAM,EAAE,UAAU,eAAe,wBACzB,QAAA,qBACA,QAAA,eACR;EAEA,MAAM,aAAa,GAAG;GACpB,OAAO;IACL,SAAS;IACT,SAAS;IAET,SAAS;IACT,SAAS;GACX;GACA,UAAU;IACR,WAAW,EACT,MAAM,EAAE,SAAS,SAAS,EAC5B;IACA,OAAO;KACL,MAAM,EAAE,SAAS,OAAO;KACxB,OAAO,EAAE,SAAS,OAAO;IAC3B;GACF;GACA,iBAAiB;IAAE,WAAW;IAAO,OAAO;GAAM;EACpD,CAAC;EAED,MAAM,KAAK,eAA8C,WAAW;GAClE,WAAQ,QAAA;GACR,OAAI,QAAA;EACN,CAAC,CAAC;EAGF,MAAM,eAAe,eAAqC,QAAA,SAAS,SAAS,SAAS,QAAA,IAAI;;;;;;;;;;EAWzF,MAAM,gBAAgB,eAA8B;GAClD,MAAM,WAAW,IAAI,KAAK;GAC1B,OAAO;IACL,aAAa,QAAA,WACT,KAAA,UACM;KACJ,IAAI,QAAA,aACF,OAAO;IACX;IACJ,cAAc,QAAA,WACV,KAAA,UACM;KACJ,IAAI,QAAA,eAAe,CAAC,UAClB,QAAQ;IACZ;GACN;EACF,CAAC;EAED,MAAM,YAAY,gBAA2B;GAC3C,GAAG,IAAI,aAAa;GACpB,WAAW,QAAA,WAAW,KAAA,IAAY;GAClC,iBAAiB,IAAI,YAAY;GACjC,iBAAiB,IAAI,IAAI;GAEzB,iBAAiB,IAAI,OAAO,IAAI,SAAS,KAAA;EAC3C,EAAE;;;;;;EAOF,SAAS,iBAAiB;GACxB,MAAM,UAAU,IAAI,WAAW;GAC/B,IAAI,CAAC,SACH;GAEF,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;EACvC;EAEA,SAAS,YAAkB;GACzB,IAAI,QAAA,kBACF;GAEF,eAAe,eAAe,CAAC;EACjC;;EAGA,SAAS,mBAAmB;GAC1B,MAAM,cAAc,IAAI,SAAS;GACjC,IAAI,CAAC,aACH;GAEF,MAAM,cAAc,YAAY,cAA2B,2BAA2B;GACtF,IAAI,aACF,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC;EAE7C;EAEA,SAAS,QAAQ,OAA6B;GAC5C,IAAI,CAAC,IAAI,IAAI,GACX;GACF,QAAQ;GACR,IAAI,OAAO,KAAK;GAChB,OAAO,gBAAgB;GAGvB,IAAI,QAAA,kBACF;GAGF,eAAe,iBAAiB,CAAC;EACnC;;;;;;;;;;;;;;;;;;;;;EAsBA,SAAS,SAAS,OAA4B;GAC5C,IAAI,QAAA,cAAc,MAAM,aACtB;GAEF,QAAQ,KAAK;EACf;EAEA,SAAS,aAAmB;GAC1B,IAAI,IAAI,IAAI,KAAK,IAAI,KAAK,GACpB;QAAA,CAAC,QAAA,yBACH,QAAQ;GAAA,OAEP;IACH,OAAO;IACP,IAAI,OAAO,IAAI;GACjB;EACF;EAEA,MAAM,aAAa,UAAU;GAC3B,IAAI,IAAI,IAAI,MAAM,OAChB;GAEF,IAAI,OAAO;IACT,OAAO;IACP,IAAI,OAAO,IAAI;GACjB,OAEE,QAAQ;EAEZ,CAAC;EAED,MAAM,OAAO,SAAS;GACpB,IAAI,YAAY,IAAI;GACpB,IAAI,MACF,UAAU;EAEd,CAAC;EAED,eAAe,YAAY;GACzB,IAAI,IAAI,IAAI,KAAK,CAAC,QAAA,YAChB,QAAQ;EACZ,GAAG,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;;GAIxB,OAAAC,UAAA,GAAAC,mBAiEM,OAAA;IAhEH,OAAKC,eAAE,QAAA,YAAY,IAAI;IACvB,WAAO,OAAA,OAAA,OAAA,KAAAC,UAAA,WAAM,SAAS,MAAM,GAAA,CAAA,KAAA,CAAA;;IAE7BC,WAAqB,KAAA,QAAA,OAAA;IACrBC,mBASM,OAAA;KARA,SAAA;KAAJ,KAAI;KACH,OAAKH,eAAEI,MAAA,EAAA,CAAE,CAAC,QAAO,EAAA,OAAUA,MAAA,EAAA,CAAE,CAAC,QAAA,YAAY,OAAO,EAAA,CAAA,CAAA;KACjD,sBAAoB,QAAA;IAErBF,GAAAA,CAAAA,WAGE,KAAA,QAAA,aAAAG,eAAAC,mBAAA;KAAA,OADiBF,MAAA,SAAA;KAAS,MAAEA,MAAA,IAAA;KAAI,UAAE,QAAA;KAAQ,UAAEA,MAAA,QAAA;KAAQ,YAAEA,MAAA,UAAA;IAAU,CAAA,CAAA,CAAA,CAAA,GAAA,IAAA,UAAA;IAI3D,CAAA,QAAA,YADTN,UAAA,GAAAS,YAyCWC,UAAA;;KAvCT,IAAG;IAGKJ,GAAAA,CAAAA,MAAA,OAAA,KADRN,UAAA,GAAAC,mBAoCM,OAAA;;KAlCH,IAAIK,MAAA,MAAA;KACD,SAAA;KAAJ,KAAI;KACH,OAAKJ,eAAEI,MAAA,EAAA,CAAE,CAAC,QAAO,EAAA,OAAUA,MAAA,EAAA,CAAE,CAAC,QAAA,YAAY,IAAI,EAAA,CAAA,CAAA;KAC9C,MAAM,QAAA;KACN,cAAY,QAAA;KACZ,kBAAgBA,MAAA,gBAAA;KAChB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,QAAA,sBAAsB,QAAO,IAAK,KAAA;KACzC,WAAO,OAAA,OAAA,OAAA,KAAAH,UAAA,WAAM,SAAS,MAAM,GAAA,CAAA,KAAA,CAAA;IAE7BQ,GAAAA,CAAAA,YAwBkBC,iBAAA;KAvBhB,sBAAmB;KACnB,oBAAiB;KACjB,kBAAe;KACf,sBAAmB;KACnB,oBAAiB;KACjB,kBAAe;KACd,eAAY,OAAA,OAAA,OAAA,MAAA,WAAEN,MAAA,cAAA,CAAc,CAAA;KAC5B,cAAW,OAAA,OAAA,OAAA,MAAA,WAAEA,MAAA,YAAA,IAAeA,MAAA,eAAA,CAAe,CAAA,IAAK,KAAA;KAChD,eAAY,OAAA,OAAA,OAAA,MAAA,WAAEA,MAAA,cAAA,CAAc,CAAA;;KAE7B,SAAAO,cAYM,CAXEP,MAAA,IAAA,KADRN,UAAA,GAAAC,mBAYM,OAZNa,WAYM;MAVA,SAAA;MAAJ,KAAI;MACJ,KAAI;MACJ,WAAQ;MACP,OAAOR,MAAA,EAAA,CAAE,CAAC,QAAO,EAAA,OAAUA,MAAA,EAAA,CAAE,CAAC,QAAA,YAAY,OAAO,EAAA,CAAA;MAClD,UAAS;KACD,GAAAA,MAAA,aAAA,CAAa,GAAA,CAErBK,YAEsBL,MAAA,mBAAA,GAAA,MAAA;MADpB,SAAAO,cAA2B,CAA3BT,WAA2B,KAAA,QAAA,WAAAG,eAAAC,mBAAA,EAAA,OAAXF,MAAA,KAAA,EAAK,CAAA,CAAA,CAAA,CAAA,CAAA;;;;;IAOvB,QAAA,eADRN,UAAA,GAAAS,YAOEO,2BAAA;;KALC,SAAS,QAAA;KACT,OAAKd,eAAEI,MAAA,EAAA,CAAE,CAAC,QAAO,EAAA,OAAUA,MAAA,EAAA,CAAE,CAAC,QAAA,YAAY,OAAO,EAAA,CAAA,CAAA;KACjD,kBAAgB,QAAA;KAChB,oBAAkB,QAAA;KAClB,MAAM,QAAA"}
@@ -2,8 +2,25 @@ import { computed, hasInjectionContext, inject, isRef, provide, toValue } from "
2
2
  //#region src/composables/defaults/field.ts
3
3
  /** The app-wide default, set through `createRui({ defaults: { field } })`. */
4
4
  var FieldSymbol = Symbol.for("rui:field");
5
- /** A default for every field below a component, set with `provideFieldDefaults` or `RuiFieldDefaults`. */
5
+ /**
6
+ * A default for every field below a component, set with `provideFieldDefaults` or `RuiFieldDefaults`.
7
+ * Every copy of the library on a page shares this key, so a change to what it holds must keep
8
+ * reading every shape an older copy provides.
9
+ */
6
10
  var FieldScopeSymbol = Symbol.for("rui:field-scope");
11
+ function isFieldScope(scope) {
12
+ return typeof scope === "object" && !isRef(scope) && typeof scope.labelPlacement === "function";
13
+ }
14
+ /** The nearest scope, read in either shape. */
15
+ function injectFieldScope() {
16
+ const scope = inject(FieldScopeSymbol, void 0);
17
+ if (scope === void 0 || scope === null) return void 0;
18
+ if (isFieldScope(scope)) return scope;
19
+ return {
20
+ labelPlacement: () => toValue(scope),
21
+ details: () => void 0
22
+ };
23
+ }
7
24
  function createFieldDefaults(options) {
8
25
  return {
9
26
  labelPlacement: "top",
@@ -23,7 +40,7 @@ function provideFieldDefaults(options) {
23
40
  labelPlacement: options,
24
41
  details: void 0
25
42
  };
26
- const parent = inject(FieldScopeSymbol, void 0);
43
+ const parent = injectFieldScope();
27
44
  provide(FieldScopeSymbol, {
28
45
  labelPlacement: () => toValue(labelPlacement) ?? parent?.labelPlacement(),
29
46
  details: () => toValue(details) ?? parent?.details()
@@ -34,7 +51,7 @@ function provideFieldDefaults(options) {
34
51
  * app default, then `top`.
35
52
  */
36
53
  function useLabelPlacement(placement) {
37
- const scope = inject(FieldScopeSymbol, void 0);
54
+ const scope = injectFieldScope();
38
55
  const app = inject(FieldSymbol, void 0);
39
56
  return computed(() => toValue(placement) ?? scope?.labelPlacement() ?? (app ? toValue(app.labelPlacement) : void 0) ?? "top");
40
57
  }
@@ -45,7 +62,7 @@ function useLabelPlacement(placement) {
45
62
  */
46
63
  function useInheritedDetails() {
47
64
  if (!hasInjectionContext()) return () => void 0;
48
- const scope = inject(FieldScopeSymbol, void 0);
65
+ const scope = injectFieldScope();
49
66
  const app = inject(FieldSymbol, void 0);
50
67
  return () => scope?.details() ?? (app ? toValue(app.details) : void 0);
51
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"field.js","names":[],"sources":["../../../src/composables/defaults/field.ts"],"sourcesContent":["import type { FormDetails } from '@/utils/form-text-detail';\nimport { type ComputedRef, hasInjectionContext, type InjectionKey, type MaybeRef, type MaybeRefOrGetter } from 'vue';\n\n/**\n * Where a field shows its label.\n *\n * - `top`: a label above a bordered field (the default).\n * - `hidden`: no visible label; the label is still announced to screen readers.\n * For search and filter fields whose placeholder and icon say what they are.\n */\nexport type LabelPlacement = 'top' | 'hidden';\n\nexport interface FieldOptions {\n labelPlacement: MaybeRef<LabelPlacement>;\n details: MaybeRef<FormDetails>;\n}\n\n/** What a `provideFieldDefaults` or `RuiFieldDefaults` scope sets; an unset key falls through to the scope above. */\nexport interface FieldScopeOptions {\n labelPlacement?: MaybeRefOrGetter<LabelPlacement | undefined>;\n details?: MaybeRefOrGetter<FormDetails | undefined>;\n}\n\ninterface FieldScope {\n labelPlacement: () => LabelPlacement | undefined;\n details: () => FormDetails | undefined;\n}\n\n/** The app-wide default, set through `createRui({ defaults: { field } })`. */\nexport const FieldSymbol: InjectionKey<FieldOptions> = Symbol.for('rui:field');\n\n/** A default for every field below a component, set with `provideFieldDefaults` or `RuiFieldDefaults`. */\nconst FieldScopeSymbol: InjectionKey<FieldScope> = Symbol.for('rui:field-scope');\n\nexport function createFieldDefaults(options?: Partial<FieldOptions>): FieldOptions {\n return {\n labelPlacement: 'top',\n details: 'reserve',\n ...options,\n };\n}\n\nfunction isScopeOptions(\n options: FieldScopeOptions | MaybeRefOrGetter<LabelPlacement | undefined>,\n): options is FieldScopeOptions {\n return typeof options === 'object' && !isRef(options);\n}\n\n/**\n * Sets defaults for every field rendered below the calling component. A key left unset\n * keeps what an enclosing scope set. Passing a placement alone is the 3.0 form.\n */\nexport function provideFieldDefaults(options: FieldScopeOptions | MaybeRefOrGetter<LabelPlacement | undefined>): void {\n const { labelPlacement, details } = isScopeOptions(options) ? options : { labelPlacement: options, details: undefined };\n const parent = inject(FieldScopeSymbol, undefined);\n\n provide(FieldScopeSymbol, {\n labelPlacement: () => toValue(labelPlacement) ?? parent?.labelPlacement(),\n details: () => toValue(details) ?? parent?.details(),\n });\n}\n\n/**\n * The placement a field uses: its own prop, then the nearest scope, then the\n * app default, then `top`.\n */\nexport function useLabelPlacement(placement: MaybeRefOrGetter<LabelPlacement | undefined>): ComputedRef<LabelPlacement> {\n const scope = inject(FieldScopeSymbol, undefined);\n const app = inject(FieldSymbol, undefined);\n\n return computed<LabelPlacement>(() =>\n toValue(placement)\n ?? scope?.labelPlacement()\n ?? (app ? toValue(app.labelPlacement) : undefined)\n ?? 'top',\n );\n}\n\n/**\n * The details mode the scope and app give a control that sets none itself: the nearest\n * scope, then the app default. `undefined` when neither sets one. Outside a component\n * setup there is nothing to inject, so it is always `undefined` there.\n */\nexport function useInheritedDetails(): () => FormDetails | undefined {\n if (!hasInjectionContext())\n return () => undefined;\n\n const scope = inject(FieldScopeSymbol, undefined);\n const app = inject(FieldSymbol, undefined);\n\n return () => scope?.details() ?? (app ? toValue(app.details) : undefined);\n}\n"],"mappings":";;;AA6BA,IAAa,cAA0C,OAAO,IAAI,WAAW;;AAG7E,IAAM,mBAA6C,OAAO,IAAI,iBAAiB;AAE/E,SAAgB,oBAAoB,SAA+C;CACjF,OAAO;EACL,gBAAgB;EAChB,SAAS;EACT,GAAG;CACL;AACF;AAEA,SAAS,eACP,SAC8B;CAC9B,OAAO,OAAO,YAAY,YAAY,CAAC,MAAM,OAAO;AACtD;;;;;AAMA,SAAgB,qBAAqB,SAAiF;CACpH,MAAM,EAAE,gBAAgB,YAAY,eAAe,OAAO,IAAI,UAAU;EAAE,gBAAgB;EAAS,SAAS,KAAA;CAAU;CACtH,MAAM,SAAS,OAAO,kBAAkB,KAAA,CAAS;CAEjD,QAAQ,kBAAkB;EACxB,sBAAsB,QAAQ,cAAc,KAAK,QAAQ,eAAe;EACxE,eAAe,QAAQ,OAAO,KAAK,QAAQ,QAAQ;CACrD,CAAC;AACH;;;;;AAMA,SAAgB,kBAAkB,WAAsF;CACtH,MAAM,QAAQ,OAAO,kBAAkB,KAAA,CAAS;CAChD,MAAM,MAAM,OAAO,aAAa,KAAA,CAAS;CAEzC,OAAO,eACL,QAAQ,SAAS,KACd,OAAO,eAAe,MACrB,MAAM,QAAQ,IAAI,cAAc,IAAI,KAAA,MACrC,KACL;AACF;;;;;;AAOA,SAAgB,sBAAqD;CACnE,IAAI,CAAC,oBAAoB,GACvB,aAAa,KAAA;CAEf,MAAM,QAAQ,OAAO,kBAAkB,KAAA,CAAS;CAChD,MAAM,MAAM,OAAO,aAAa,KAAA,CAAS;CAEzC,aAAa,OAAO,QAAQ,MAAM,MAAM,QAAQ,IAAI,OAAO,IAAI,KAAA;AACjE"}
1
+ {"version":3,"file":"field.js","names":[],"sources":["../../../src/composables/defaults/field.ts"],"sourcesContent":["import type { FormDetails } from '@/utils/form-text-detail';\nimport { type ComputedRef, hasInjectionContext, type InjectionKey, type MaybeRef, type MaybeRefOrGetter } from 'vue';\n\n/**\n * Where a field shows its label.\n *\n * - `top`: a label above a bordered field (the default).\n * - `hidden`: no visible label; the label is still announced to screen readers.\n * For search and filter fields whose placeholder and icon say what they are.\n */\nexport type LabelPlacement = 'top' | 'hidden';\n\nexport interface FieldOptions {\n labelPlacement: MaybeRef<LabelPlacement>;\n details: MaybeRef<FormDetails>;\n}\n\n/** What a `provideFieldDefaults` or `RuiFieldDefaults` scope sets; an unset key falls through to the scope above. */\nexport interface FieldScopeOptions {\n labelPlacement?: MaybeRefOrGetter<LabelPlacement | undefined>;\n details?: MaybeRefOrGetter<FormDetails | undefined>;\n}\n\ninterface FieldScope {\n labelPlacement: () => LabelPlacement | undefined;\n details: () => FormDetails | undefined;\n}\n\n/** The app-wide default, set through `createRui({ defaults: { field } })`. */\nexport const FieldSymbol: InjectionKey<FieldOptions> = Symbol.for('rui:field');\n\n/**\n * What 3.0 provided under the scope key: the placement itself. A 3.0 copy bundled into\n * another package still provides it, and `Symbol.for` hands it to this copy.\n */\ntype LegacyFieldScope = MaybeRefOrGetter<LabelPlacement | undefined>;\n\n/**\n * A default for every field below a component, set with `provideFieldDefaults` or `RuiFieldDefaults`.\n * Every copy of the library on a page shares this key, so a change to what it holds must keep\n * reading every shape an older copy provides.\n */\nconst FieldScopeSymbol: InjectionKey<FieldScope | LegacyFieldScope | null> = Symbol.for('rui:field-scope');\n\nfunction isFieldScope(scope: FieldScope | LegacyFieldScope): scope is FieldScope {\n return typeof scope === 'object' && !isRef(scope) && typeof scope.labelPlacement === 'function';\n}\n\n/** The nearest scope, read in either shape. */\nfunction injectFieldScope(): FieldScope | undefined {\n const scope = inject(FieldScopeSymbol, undefined);\n if (scope === undefined || scope === null)\n return undefined;\n if (isFieldScope(scope))\n return scope;\n\n return {\n labelPlacement: () => toValue(scope),\n details: () => undefined,\n };\n}\n\nexport function createFieldDefaults(options?: Partial<FieldOptions>): FieldOptions {\n return {\n labelPlacement: 'top',\n details: 'reserve',\n ...options,\n };\n}\n\nfunction isScopeOptions(\n options: FieldScopeOptions | MaybeRefOrGetter<LabelPlacement | undefined>,\n): options is FieldScopeOptions {\n return typeof options === 'object' && !isRef(options);\n}\n\n/**\n * Sets defaults for every field rendered below the calling component. A key left unset\n * keeps what an enclosing scope set. Passing a placement alone is the 3.0 form.\n */\nexport function provideFieldDefaults(options: FieldScopeOptions | MaybeRefOrGetter<LabelPlacement | undefined>): void {\n const { labelPlacement, details } = isScopeOptions(options) ? options : { labelPlacement: options, details: undefined };\n const parent = injectFieldScope();\n\n provide(FieldScopeSymbol, {\n labelPlacement: () => toValue(labelPlacement) ?? parent?.labelPlacement(),\n details: () => toValue(details) ?? parent?.details(),\n });\n}\n\n/**\n * The placement a field uses: its own prop, then the nearest scope, then the\n * app default, then `top`.\n */\nexport function useLabelPlacement(placement: MaybeRefOrGetter<LabelPlacement | undefined>): ComputedRef<LabelPlacement> {\n const scope = injectFieldScope();\n const app = inject(FieldSymbol, undefined);\n\n return computed<LabelPlacement>(() =>\n toValue(placement)\n ?? scope?.labelPlacement()\n ?? (app ? toValue(app.labelPlacement) : undefined)\n ?? 'top',\n );\n}\n\n/**\n * The details mode the scope and app give a control that sets none itself: the nearest\n * scope, then the app default. `undefined` when neither sets one. Outside a component\n * setup there is nothing to inject, so it is always `undefined` there.\n */\nexport function useInheritedDetails(): () => FormDetails | undefined {\n if (!hasInjectionContext())\n return () => undefined;\n\n const scope = injectFieldScope();\n const app = inject(FieldSymbol, undefined);\n\n return () => scope?.details() ?? (app ? toValue(app.details) : undefined);\n}\n"],"mappings":";;;AA6BA,IAAa,cAA0C,OAAO,IAAI,WAAW;;;;;;AAa7E,IAAM,mBAAuE,OAAO,IAAI,iBAAiB;AAEzG,SAAS,aAAa,OAA2D;CAC/E,OAAO,OAAO,UAAU,YAAY,CAAC,MAAM,KAAK,KAAK,OAAO,MAAM,mBAAmB;AACvF;;AAGA,SAAS,mBAA2C;CAClD,MAAM,QAAQ,OAAO,kBAAkB,KAAA,CAAS;CAChD,IAAI,UAAU,KAAA,KAAa,UAAU,MACnC,OAAO,KAAA;CACT,IAAI,aAAa,KAAK,GACpB,OAAO;CAET,OAAO;EACL,sBAAsB,QAAQ,KAAK;EACnC,eAAe,KAAA;CACjB;AACF;AAEA,SAAgB,oBAAoB,SAA+C;CACjF,OAAO;EACL,gBAAgB;EAChB,SAAS;EACT,GAAG;CACL;AACF;AAEA,SAAS,eACP,SAC8B;CAC9B,OAAO,OAAO,YAAY,YAAY,CAAC,MAAM,OAAO;AACtD;;;;;AAMA,SAAgB,qBAAqB,SAAiF;CACpH,MAAM,EAAE,gBAAgB,YAAY,eAAe,OAAO,IAAI,UAAU;EAAE,gBAAgB;EAAS,SAAS,KAAA;CAAU;CACtH,MAAM,SAAS,iBAAiB;CAEhC,QAAQ,kBAAkB;EACxB,sBAAsB,QAAQ,cAAc,KAAK,QAAQ,eAAe;EACxE,eAAe,QAAQ,OAAO,KAAK,QAAQ,QAAQ;CACrD,CAAC;AACH;;;;;AAMA,SAAgB,kBAAkB,WAAsF;CACtH,MAAM,QAAQ,iBAAiB;CAC/B,MAAM,MAAM,OAAO,aAAa,KAAA,CAAS;CAEzC,OAAO,eACL,QAAQ,SAAS,KACd,OAAO,eAAe,MACrB,MAAM,QAAQ,IAAI,cAAc,IAAI,KAAA,MACrC,KACL;AACF;;;;;;AAOA,SAAgB,sBAAqD;CACnE,IAAI,CAAC,oBAAoB,GACvB,aAAa,KAAA;CAEf,MAAM,QAAQ,iBAAiB;CAC/B,MAAM,MAAM,OAAO,aAAa,KAAA,CAAS;CAEzC,aAAa,OAAO,QAAQ,MAAM,MAAM,QAAQ,IAAI,OAAO,IAAI,KAAA;AACjE"}
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json.schemastore.org/web-types",
3
3
  "framework": "vue",
4
4
  "name": "@rotki/ui-library",
5
- "version": "3.1.1",
5
+ "version": "3.1.3",
6
6
  "js-types-syntax": "typescript",
7
7
  "description-markup": "markdown",
8
8
  "contributions": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rotki/ui-library",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "A vue design system and component library for rotki",
5
5
  "type": "module",
6
6
  "keywords": [