@rotki/ui-library 3.0.0 → 3.1.1
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.
- package/dist/components/accordions/accordion/RuiAccordion.js.map +1 -1
- package/dist/components/accordions/accordion/RuiAccordion.vue_vue_type_script_setup_true_lang.js +13 -2
- package/dist/components/accordions/accordion/RuiAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/alerts/RuiAlert.js.map +1 -1
- package/dist/components/alerts/RuiAlert.vue.d.ts +2 -2
- package/dist/components/alerts/RuiAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/buttons/button/RuiButton.js.map +1 -1
- package/dist/components/buttons/button/RuiButton.vue.d.ts +7 -1
- package/dist/components/buttons/button/RuiButton.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/components/buttons/button/RuiButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/buttons/button/button-props.d.ts +3 -0
- package/dist/components/buttons/button/button-props.js +2 -1
- package/dist/components/buttons/button/button-props.js.map +1 -1
- package/dist/components/buttons/button/button-styles.d.ts +75 -0
- package/dist/components/buttons/button/button-styles.js +135 -4
- package/dist/components/buttons/button/button-styles.js.map +1 -1
- package/dist/components/cards/RuiCard.js.map +1 -1
- package/dist/components/cards/RuiCard.vue.d.ts +6 -0
- package/dist/components/cards/RuiCard.vue_vue_type_script_setup_true_lang.js +14 -2
- package/dist/components/cards/RuiCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/chips/RuiChip.js.map +1 -1
- package/dist/components/chips/RuiChip.vue_vue_type_script_setup_true_lang.js +15 -1
- package/dist/components/chips/RuiChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/chips/chip-props.d.ts +2 -0
- package/dist/components/chips/chip-props.js +3 -0
- package/dist/components/chips/chip-props.js.map +1 -1
- package/dist/components/color-picker/RuiColorInput.js.map +1 -1
- package/dist/components/color-picker/RuiColorInput.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/color-picker/RuiColorInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/date-time-picker/RuiDateTimePicker.js.map +1 -1
- package/dist/components/date-time-picker/RuiDateTimePicker.vue.d.ts +4 -0
- package/dist/components/date-time-picker/RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js +6 -2
- package/dist/components/date-time-picker/RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/date-time-picker/RuiDateTimePickerMenu.js.map +1 -1
- package/dist/components/date-time-picker/RuiDateTimePickerMenu.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/date-time-picker/RuiDateTimePickerMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/date-time-picker/RuiTimezoneSelect.js.map +1 -1
- package/dist/components/date-time-picker/RuiTimezoneSelect.vue.d.ts +4 -0
- package/dist/components/date-time-picker/RuiTimezoneSelect.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/components/date-time-picker/RuiTimezoneSelect.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/auto-complete/RuiAutoComplete.js.map +1 -1
- package/dist/components/forms/auto-complete/RuiAutoComplete.vue_vue_type_script_setup_true_lang.js +6 -2
- package/dist/components/forms/auto-complete/RuiAutoComplete.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/auto-complete/auto-complete-props.d.ts +4 -0
- package/dist/components/forms/category-picker/RuiCategoryPicker.js.map +1 -1
- package/dist/components/forms/category-picker/RuiCategoryPicker.vue.d.ts +4 -1
- package/dist/components/forms/category-picker/RuiCategoryPicker.vue_vue_type_script_setup_true_lang.js +7 -4
- package/dist/components/forms/category-picker/RuiCategoryPicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/check-control-styles.d.ts +27 -0
- package/dist/components/forms/check-control-styles.js +26 -1
- package/dist/components/forms/check-control-styles.js.map +1 -1
- package/dist/components/forms/checkbox/RuiCheckbox.js.map +1 -1
- package/dist/components/forms/checkbox/RuiCheckbox.vue.d.ts +6 -0
- package/dist/components/forms/checkbox/RuiCheckbox.vue_vue_type_script_setup_true_lang.js +14 -4
- package/dist/components/forms/checkbox/RuiCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.js.map +1 -1
- package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.vue.d.ts +4 -0
- package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.vue_vue_type_script_setup_true_lang.js +7 -2
- package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/field-defaults/RuiFieldDefaults.js.map +1 -1
- package/dist/components/forms/field-defaults/RuiFieldDefaults.vue.d.ts +3 -0
- package/dist/components/forms/field-defaults/RuiFieldDefaults.vue_vue_type_script_setup_true_lang.js +8 -2
- package/dist/components/forms/field-defaults/RuiFieldDefaults.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/file-upload/RuiFileUpload.js.map +1 -1
- package/dist/components/forms/file-upload/RuiFileUpload.vue.d.ts +4 -1
- package/dist/components/forms/file-upload/RuiFileUpload.vue_vue_type_script_setup_true_lang.js +18 -6
- package/dist/components/forms/file-upload/RuiFileUpload.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/radio-button/radio/RuiRadio.js.map +1 -1
- package/dist/components/forms/radio-button/radio/RuiRadio.vue.d.ts +6 -0
- package/dist/components/forms/radio-button/radio/RuiRadio.vue_vue_type_script_setup_true_lang.js +14 -3
- package/dist/components/forms/radio-button/radio/RuiRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.js.map +1 -1
- package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.vue.d.ts +4 -0
- package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.vue_vue_type_script_setup_true_lang.js +7 -2
- package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/revealable-text-field/RuiRevealableTextField.js.map +1 -1
- package/dist/components/forms/revealable-text-field/RuiRevealableTextField.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/components/forms/revealable-text-field/RuiRevealableTextField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/select/RuiMenuSelect.js.map +1 -1
- package/dist/components/forms/select/RuiMenuSelect.vue.d.ts +4 -0
- package/dist/components/forms/select/RuiMenuSelect.vue_vue_type_script_setup_true_lang.js +6 -2
- package/dist/components/forms/select/RuiMenuSelect.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/slider/RuiSlider.js.map +1 -1
- package/dist/components/forms/slider/RuiSlider.vue.d.ts +4 -0
- package/dist/components/forms/slider/RuiSlider.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/components/forms/slider/RuiSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/switch/RuiSwitch.js.map +1 -1
- package/dist/components/forms/switch/RuiSwitch.vue.d.ts +6 -0
- package/dist/components/forms/switch/RuiSwitch.vue_vue_type_script_setup_true_lang.js +25 -5
- package/dist/components/forms/switch/RuiSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/text-area/RuiTextArea.js.map +1 -1
- package/dist/components/forms/text-area/RuiTextArea.vue.d.ts +4 -0
- package/dist/components/forms/text-area/RuiTextArea.vue_vue_type_script_setup_true_lang.js +6 -4
- package/dist/components/forms/text-area/RuiTextArea.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/text-area/text-area-styles.d.ts +0 -1
- package/dist/components/forms/text-area/text-area-styles.js +6 -7
- package/dist/components/forms/text-area/text-area-styles.js.map +1 -1
- package/dist/components/forms/text-field/RuiTextField.js.map +1 -1
- package/dist/components/forms/text-field/RuiTextField.vue.d.ts +6 -2
- package/dist/components/forms/text-field/RuiTextField.vue_vue_type_script_setup_true_lang.js +6 -4
- package/dist/components/forms/text-field/RuiTextField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/forms/text-field/text-field-styles.d.ts +0 -1
- package/dist/components/forms/text-field/text-field-styles.js +6 -7
- package/dist/components/forms/text-field/text-field-styles.js.map +1 -1
- package/dist/components/helpers/RuiFormTextDetail.js.map +1 -1
- package/dist/components/helpers/RuiFormTextDetail.vue.d.ts +14 -0
- package/dist/components/helpers/RuiFormTextDetail.vue_vue_type_script_setup_true_lang.js +41 -11
- package/dist/components/helpers/RuiFormTextDetail.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/loaders/RuiSkeletonBase.js.map +1 -1
- package/dist/components/loaders/RuiSkeletonBase.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/overlays/menu/RuiMenu.js.map +1 -1
- package/dist/components/overlays/menu/RuiMenu.vue.d.ts +3 -0
- package/dist/components/overlays/menu/RuiMenu.vue_vue_type_script_setup_true_lang.js +9 -1
- package/dist/components/overlays/menu/RuiMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/tables/RuiDataTable.js.map +1 -1
- package/dist/components/tables/RuiDataTable.vue.d.ts +4 -0
- package/dist/components/tables/RuiDataTable.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/components/tables/RuiDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/tables/RuiExpandButton.js.map +1 -1
- package/dist/components/tables/RuiExpandButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/tables/RuiTableHead.js.map +1 -1
- package/dist/components/tables/RuiTableHead.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/tables/RuiTableHead.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/tables/RuiTablePagination.js.map +1 -1
- package/dist/components/tables/RuiTablePagination.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/tables/RuiTablePagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/tables/data-table/RuiDataTableAppendRow.js.map +1 -1
- package/dist/components/tables/data-table/RuiDataTableAppendRow.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/tables/data-table/RuiDataTableRow.js.map +1 -1
- package/dist/components/tables/data-table/RuiDataTableRow.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/tables/data-table/RuiDataTableRow.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/tables/data-table-styles.d.ts +8 -1
- package/dist/components/tables/data-table-styles.js +14 -5
- package/dist/components/tables/data-table-styles.js.map +1 -1
- package/dist/components/tabs/tab-items/RuiTabItems.js.map +1 -1
- package/dist/components/tabs/tab-items/RuiTabItems.vue_vue_type_script_setup_true_lang.js +9 -2
- package/dist/components/tabs/tab-items/RuiTabItems.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/clip-while-resizing.d.ts +15 -0
- package/dist/composables/clip-while-resizing.js +46 -0
- package/dist/composables/clip-while-resizing.js.map +1 -0
- package/dist/composables/defaults/field.d.ts +19 -3
- package/dist/composables/defaults/field.js +32 -6
- package/dist/composables/defaults/field.js.map +1 -1
- package/dist/composables/forms/auto-complete/keyboard-navigation.js.map +1 -1
- package/dist/composables/sticky-blocker.d.ts +20 -0
- package/dist/composables/sticky-blocker.js +62 -0
- package/dist/composables/sticky-blocker.js.map +1 -0
- package/dist/composables/tables/data-table/columns.js.map +1 -1
- package/dist/composables/tables/data-table/selection.js +23 -21
- package/dist/composables/tables/data-table/selection.js.map +1 -1
- package/dist/icons/icons_1.js.map +1 -1
- package/dist/icons/icons_2.js.map +1 -1
- package/dist/icons/icons_3.js.map +1 -1
- package/dist/icons/icons_4.js.map +1 -1
- package/dist/icons/icons_5.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js.map +1 -1
- package/dist/style.css +325 -19
- package/dist/styles/colors.css +4 -0
- package/dist/theme/theme.css +1 -1
- package/dist/theme.css +1 -1
- package/dist/utils/development.d.ts +6 -0
- package/dist/utils/development.js +17 -0
- package/dist/utils/development.js.map +1 -0
- package/dist/utils/form-text-detail.d.ts +15 -0
- package/dist/utils/form-text-detail.js +11 -1
- package/dist/utils/form-text-detail.js.map +1 -1
- package/dist/vite-plugin/types.d.ts +1 -1
- package/dist/web-types.json +232 -3
- package/package.json +1 -1
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
import { type FormDetails } from '../../utils/form-text-detail.js';
|
|
2
|
+
/**
|
|
3
|
+
* The line under a control. It is 20px: 4px of padding on the message above a 16px caption line,
|
|
4
|
+
* so an `auto` line with nothing to show takes no space. The TransitionGroup has no `appear`: a
|
|
5
|
+
* hint present on first render is in place when the field shows, and only a change of message
|
|
6
|
+
* animates. The template keeps no comments, since a root-level one makes the component a fragment
|
|
7
|
+
* in development and drops the class a parent passes.
|
|
8
|
+
*
|
|
9
|
+
* An error is clamped to two lines, since a backend error can run to several sentences and push a
|
|
10
|
+
* dialog's footer down; the full text goes in `title` only while it is cut off. Screen readers get
|
|
11
|
+
* all of it either way. Hints and success messages are not clamped: they are written for the field.
|
|
12
|
+
*/
|
|
1
13
|
type __VLS_Props = {
|
|
2
14
|
errorMessages?: string | string[];
|
|
3
15
|
successMessages?: string | string[];
|
|
4
16
|
hint?: string;
|
|
17
|
+
/** `reserve` keeps the line while empty; `auto` takes no space until there is a message. */
|
|
18
|
+
details?: Exclude<FormDetails, 'hidden'>;
|
|
5
19
|
};
|
|
6
20
|
type __VLS_Slots = {
|
|
7
21
|
/** Rich hint content, shown in place of the `hint` text while there is no error or success message. */
|
|
@@ -1,31 +1,55 @@
|
|
|
1
|
+
import { get, set } from "../../vendor/@vueuse/shared/dist/index.js";
|
|
2
|
+
import { useResizeObserver } from "../../vendor/@vueuse/core/dist/index.js";
|
|
1
3
|
import { useFormTextDetail } from "../../utils/form-text-detail.js";
|
|
2
|
-
import { TransitionGroup, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent, openBlock, renderSlot, toDisplayString, unref, withCtx } from "vue";
|
|
4
|
+
import { TransitionGroup, computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent, nextTick, normalizeClass, openBlock, ref, renderSlot, toDisplayString, unref, useTemplateRef, watch, withCtx } from "vue";
|
|
3
5
|
//#region src/components/helpers/RuiFormTextDetail.vue?vue&type=script&setup=true&lang.ts
|
|
4
|
-
var _hoisted_1 =
|
|
5
|
-
key: "error",
|
|
6
|
-
class: "text-rui-error dark:text-rui-error-lighter text-caption"
|
|
7
|
-
};
|
|
6
|
+
var _hoisted_1 = ["title"];
|
|
8
7
|
var _hoisted_2 = {
|
|
9
8
|
key: "success",
|
|
10
|
-
class: "text-rui-success
|
|
9
|
+
class: "pt-1 text-xs/4 text-rui-success"
|
|
11
10
|
};
|
|
12
11
|
var _hoisted_3 = {
|
|
13
12
|
key: "hint",
|
|
14
|
-
class: "text-rui-text-secondary
|
|
13
|
+
class: "pt-1 text-xs/4 text-rui-text-secondary"
|
|
15
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The line under a control. It is 20px: 4px of padding on the message above a 16px caption line,
|
|
17
|
+
* so an `auto` line with nothing to show takes no space. The TransitionGroup has no `appear`: a
|
|
18
|
+
* hint present on first render is in place when the field shows, and only a change of message
|
|
19
|
+
* animates. The template keeps no comments, since a root-level one makes the component a fragment
|
|
20
|
+
* in development and drops the class a parent passes.
|
|
21
|
+
*
|
|
22
|
+
* An error is clamped to two lines, since a backend error can run to several sentences and push a
|
|
23
|
+
* dialog's footer down; the full text goes in `title` only while it is cut off. Screen readers get
|
|
24
|
+
* all of it either way. Hints and success messages are not clamped: they are written for the field.
|
|
25
|
+
*/
|
|
16
26
|
var RuiFormTextDetail_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
17
27
|
__name: "RuiFormTextDetail",
|
|
18
28
|
props: {
|
|
19
29
|
errorMessages: { default: () => [] },
|
|
20
30
|
successMessages: { default: () => [] },
|
|
21
|
-
hint: { default: "" }
|
|
31
|
+
hint: { default: "" },
|
|
32
|
+
details: { default: "reserve" }
|
|
22
33
|
},
|
|
23
34
|
setup(__props) {
|
|
24
35
|
const { formattedErrorMessages, formattedSuccessMessages, hasError, hasSuccess } = useFormTextDetail(() => __props.errorMessages, () => __props.successMessages);
|
|
36
|
+
const errorEl = useTemplateRef("error");
|
|
37
|
+
const errorClamped = ref(false);
|
|
38
|
+
const errorMessage = computed(() => get(formattedErrorMessages)[0]);
|
|
39
|
+
function checkClamped() {
|
|
40
|
+
const el = get(errorEl);
|
|
41
|
+
set(errorClamped, !!el && el.scrollHeight > el.clientHeight + 1);
|
|
42
|
+
}
|
|
43
|
+
useResizeObserver(errorEl, checkClamped);
|
|
44
|
+
watch(errorMessage, async () => {
|
|
45
|
+
await nextTick();
|
|
46
|
+
checkClamped();
|
|
47
|
+
}, { flush: "post" });
|
|
25
48
|
return (_ctx, _cache) => {
|
|
26
49
|
return openBlock(), createBlock(TransitionGroup, {
|
|
27
50
|
tag: "div",
|
|
28
|
-
class: "details min-h-
|
|
51
|
+
class: normalizeClass(["details", { "min-h-5": __props.details === "reserve" }]),
|
|
52
|
+
"data-details": __props.details,
|
|
29
53
|
"enter-from-class": "opacity-0 -translate-y-2 h-0",
|
|
30
54
|
"enter-active-class": "transform transition",
|
|
31
55
|
"enter-to-class": "opacity-100 translate-y-0",
|
|
@@ -33,9 +57,15 @@ var RuiFormTextDetail_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
33
57
|
"leave-active-class": "transform transition",
|
|
34
58
|
"leave-to-class": "opacity-0 -translate-y-2 h-0"
|
|
35
59
|
}, {
|
|
36
|
-
default: withCtx(() => [unref(hasError) ? (openBlock(), createElementBlock("div",
|
|
60
|
+
default: withCtx(() => [unref(hasError) ? (openBlock(), createElementBlock("div", {
|
|
61
|
+
key: "error",
|
|
62
|
+
ref: "error",
|
|
63
|
+
class: "pt-1 text-xs/4 text-rui-error dark:text-rui-error-lighter line-clamp-2",
|
|
64
|
+
title: unref(errorClamped) ? unref(errorMessage) : void 0,
|
|
65
|
+
"data-id": "details-error"
|
|
66
|
+
}, toDisplayString(unref(errorMessage)), 9, _hoisted_1)) : unref(hasSuccess) ? (openBlock(), createElementBlock("div", _hoisted_2, toDisplayString(unref(formattedSuccessMessages)[0]), 1)) : _ctx.$slots.hint || __props.hint ? (openBlock(), createElementBlock("div", _hoisted_3, [renderSlot(_ctx.$slots, "hint", {}, () => [createTextVNode(toDisplayString(__props.hint), 1)])])) : createCommentVNode("", true)]),
|
|
37
67
|
_: 3
|
|
38
|
-
});
|
|
68
|
+
}, 8, ["class", "data-details"]);
|
|
39
69
|
};
|
|
40
70
|
}
|
|
41
71
|
});
|
package/dist/components/helpers/RuiFormTextDetail.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuiFormTextDetail.vue_vue_type_script_setup_true_lang.js","names":["$slots","_openBlock","_createBlock","_TransitionGroup","_withCtx","_unref","_createElementBlock","_toDisplayString","_renderSlot","_createTextVNode","_createCommentVNode"],"sources":["../../../src/components/helpers/RuiFormTextDetail.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useFormTextDetail } from '@/utils/form-text-detail';\n\nconst {\n errorMessages = [],\n successMessages = [],\n hint = '',\n} = defineProps<{\n errorMessages?: string | string[];\n successMessages?: string | string[];\n hint?: string;\n}>();\n\ndefineSlots<{\n /** Rich hint content, shown in place of the `hint` text while there is no error or success message. */\n hint?: () => any;\n}>();\n\nconst { formattedErrorMessages, formattedSuccessMessages, hasError, hasSuccess } =\n useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n );\n
|
|
1
|
+
{"version":3,"file":"RuiFormTextDetail.vue_vue_type_script_setup_true_lang.js","names":["$slots","_openBlock","_createBlock","_TransitionGroup","_normalizeClass","_withCtx","_unref","_createElementBlock","_toDisplayString","_renderSlot","_createTextVNode","_createCommentVNode"],"sources":["../../../src/components/helpers/RuiFormTextDetail.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { type FormDetails, useFormTextDetail } from '@/utils/form-text-detail';\n\n/**\n * The line under a control. It is 20px: 4px of padding on the message above a 16px caption line,\n * so an `auto` line with nothing to show takes no space. The TransitionGroup has no `appear`: a\n * hint present on first render is in place when the field shows, and only a change of message\n * animates. The template keeps no comments, since a root-level one makes the component a fragment\n * in development and drops the class a parent passes.\n *\n * An error is clamped to two lines, since a backend error can run to several sentences and push a\n * dialog's footer down; the full text goes in `title` only while it is cut off. Screen readers get\n * all of it either way. Hints and success messages are not clamped: they are written for the field.\n */\nconst {\n errorMessages = [],\n successMessages = [],\n hint = '',\n details = 'reserve',\n} = defineProps<{\n errorMessages?: string | string[];\n successMessages?: string | string[];\n hint?: string;\n /** `reserve` keeps the line while empty; `auto` takes no space until there is a message. */\n details?: Exclude<FormDetails, 'hidden'>;\n}>();\n\ndefineSlots<{\n /** Rich hint content, shown in place of the `hint` text while there is no error or success message. */\n hint?: () => any;\n}>();\n\nconst { formattedErrorMessages, formattedSuccessMessages, hasError, hasSuccess } =\n useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n );\n\nconst errorEl = useTemplateRef<HTMLDivElement>('error');\n\nconst errorClamped = ref<boolean>(false);\n\nconst errorMessage = computed<string | undefined>(() => get(formattedErrorMessages)[0]);\n\nfunction checkClamped(): void {\n const el = get(errorEl);\n set(errorClamped, !!el && el.scrollHeight > el.clientHeight + 1);\n}\n\nuseResizeObserver(errorEl, checkClamped);\n\nwatch(errorMessage, async () => {\n await nextTick();\n checkClamped();\n}, { flush: 'post' });\n</script>\n\n<template>\n <TransitionGroup\n tag=\"div\"\n class=\"details\"\n :class=\"{ 'min-h-5': details === 'reserve' }\"\n :data-details=\"details\"\n enter-from-class=\"opacity-0 -translate-y-2 h-0\"\n enter-active-class=\"transform transition\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-from-class=\"opacity-100 -translate-y-2\"\n leave-active-class=\"transform transition\"\n leave-to-class=\"opacity-0 -translate-y-2 h-0\"\n >\n <div\n v-if=\"hasError\"\n key=\"error\"\n ref=\"error\"\n class=\"pt-1 text-xs/4 text-rui-error dark:text-rui-error-lighter line-clamp-2\"\n :title=\"errorClamped ? errorMessage : undefined\"\n data-id=\"details-error\"\n >\n {{ errorMessage }}\n </div>\n <div\n v-else-if=\"hasSuccess\"\n key=\"success\"\n class=\"pt-1 text-xs/4 text-rui-success\"\n >\n {{ formattedSuccessMessages[0] }}\n </div>\n <div\n v-else-if=\"$slots.hint || hint\"\n key=\"hint\"\n class=\"pt-1 text-xs/4 text-rui-text-secondary\"\n >\n <slot name=\"hint\">\n {{ hint }}\n </slot>\n </div>\n </TransitionGroup>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCA,MAAM,EAAE,wBAAwB,0BAA0B,UAAU,eAClE,wBACQ,QAAA,qBACA,QAAA,eACR;EAEF,MAAM,UAAU,eAA+B,OAAO;EAEtD,MAAM,eAAe,IAAa,KAAK;EAEvC,MAAM,eAAe,eAAmC,IAAI,sBAAsB,CAAC,CAAC,EAAE;EAEtF,SAAS,eAAqB;GAC5B,MAAM,KAAK,IAAI,OAAO;GACtB,IAAI,cAAc,CAAC,CAAC,MAAM,GAAG,eAAe,GAAG,eAAe,CAAC;EACjE;EAEA,kBAAkB,SAAS,YAAY;EAEvC,MAAM,cAAc,YAAY;GAC9B,MAAM,SAAS;GACf,aAAa;EACf,GAAG,EAAE,OAAO,OAAO,CAAC;;GAIlB,OAAAC,UAAA,GAAAC,YAsCkBC,iBAAA;IArChB,KAAI;IACJ,OAAKC,eAAA,CAAC,WAAS,EAAA,WACM,QAAA,YAAO,UAAA,CAAA,CAAA;IAC3B,gBAAc,QAAA;IACf,oBAAiB;IACjB,sBAAmB;IACnB,kBAAe;IACf,oBAAiB;IACjB,sBAAmB;IACnB,kBAAe;;IAEf,SAAAC,cASM,CAREC,MAAA,QAAA,KADRL,UAAA,GAAAM,mBASM,OAAA;KAPJ,KAAI;KACJ,KAAI;KACJ,OAAM;KACL,OAAOD,MAAA,YAAA,IAAeA,MAAA,YAAA,IAAe,KAAA;KACtC,WAAQ;IAEL,GAAAE,gBAAAF,MAAA,YAAA,CAAY,GAAA,GAAA,UAAA,KAGJA,MAAA,UAAA,KADbL,UAAA,GAAAM,mBAMM,OANN,YAMMC,gBADDF,MAAA,wBAAA,CAAwB,CAAA,EAAA,GAAA,CAAA,KAGhBN,KAAAA,OAAO,QAAQ,QAAA,QAD5BC,UAAA,GAAAM,mBAQM,OARN,YAQM,CAHJE,WAEO,KAAA,QAAA,QAAA,CAAA,SAAA,CADFC,gBAAAF,gBAAA,QAAA,IAAI,GAAA,CAAA,CAAA,CAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuiSkeletonBase.js","names":["_sfc_main"],"sources":["../../../src/components/loaders/RuiSkeletonBase.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport { tv } from '@/utils/tv';\n\nexport interface Props {\n rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full';\n}\n\ndefineOptions({\n name: 'RuiSkeletonLoaderBase',\n});\n\nconst { rounded } = defineProps<Props>();\n\nconst skeleton = tv({\n // zinc steps rather than translucent black and white, which in dark came out lighter than any surface\n base: 'animate-pulse bg-rui-neutral-200 dark:bg-rui-neutral-800',\n variants: {\n rounded: {\n none: 'rounded-none',\n sm: 'rounded-xs',\n md: 'rounded-rui-control',\n lg: 'rounded-rui-card',\n full: 'rounded-full',\n },\n },\n});\n\nconst ui = computed<string>(() => skeleton({ rounded }));\n</script>\n\n<template>\n
|
|
1
|
+
{"version":3,"file":"RuiSkeletonBase.js","names":["_sfc_main"],"sources":["../../../src/components/loaders/RuiSkeletonBase.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport { tv } from '@/utils/tv';\n\nexport interface Props {\n rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full';\n}\n\ndefineOptions({\n name: 'RuiSkeletonLoaderBase',\n});\n\nconst { rounded } = defineProps<Props>();\n\nconst skeleton = tv({\n // zinc steps rather than translucent black and white, which in dark came out lighter than any surface\n base: 'animate-pulse bg-rui-neutral-200 dark:bg-rui-neutral-800',\n variants: {\n rounded: {\n none: 'rounded-none',\n sm: 'rounded-xs',\n md: 'rounded-rui-control',\n lg: 'rounded-rui-card',\n full: 'rounded-full',\n },\n },\n});\n\n// the bar is decoration only, so it is aria-hidden: an alert role made a screen reader announce every bar\nconst ui = computed<string>(() => skeleton({ rounded }));\n</script>\n\n<template>\n <div\n :class=\"ui\"\n data-id=\"skeleton\"\n aria-hidden=\"true\"\n />\n</template>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuiSkeletonBase.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_normalizeClass","_unref"],"sources":["../../../src/components/loaders/RuiSkeletonBase.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport { tv } from '@/utils/tv';\n\nexport interface Props {\n rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full';\n}\n\ndefineOptions({\n name: 'RuiSkeletonLoaderBase',\n});\n\nconst { rounded } = defineProps<Props>();\n\nconst skeleton = tv({\n // zinc steps rather than translucent black and white, which in dark came out lighter than any surface\n base: 'animate-pulse bg-rui-neutral-200 dark:bg-rui-neutral-800',\n variants: {\n rounded: {\n none: 'rounded-none',\n sm: 'rounded-xs',\n md: 'rounded-rui-control',\n lg: 'rounded-rui-card',\n full: 'rounded-full',\n },\n },\n});\n\nconst ui = computed<string>(() => skeleton({ rounded }));\n</script>\n\n<template>\n
|
|
1
|
+
{"version":3,"file":"RuiSkeletonBase.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_normalizeClass","_unref"],"sources":["../../../src/components/loaders/RuiSkeletonBase.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport { tv } from '@/utils/tv';\n\nexport interface Props {\n rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full';\n}\n\ndefineOptions({\n name: 'RuiSkeletonLoaderBase',\n});\n\nconst { rounded } = defineProps<Props>();\n\nconst skeleton = tv({\n // zinc steps rather than translucent black and white, which in dark came out lighter than any surface\n base: 'animate-pulse bg-rui-neutral-200 dark:bg-rui-neutral-800',\n variants: {\n rounded: {\n none: 'rounded-none',\n sm: 'rounded-xs',\n md: 'rounded-rui-control',\n lg: 'rounded-rui-card',\n full: 'rounded-full',\n },\n },\n});\n\n// the bar is decoration only, so it is aria-hidden: an alert role made a screen reader announce every bar\nconst ui = computed<string>(() => skeleton({ rounded }));\n</script>\n\n<template>\n <div\n :class=\"ui\"\n data-id=\"skeleton\"\n aria-hidden=\"true\"\n />\n</template>\n"],"mappings":";;;;;;;;EAaA,MAAM,WAAW,GAAG;GAElB,MAAM;GACN,UAAU,EACR,SAAS;IACP,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,MAAM;GACR,EACF;EACF,CAAC;EAGD,MAAM,KAAK,eAAuB,SAAS,EAAE,SAAM,QAAA,QAAE,CAAC,CAAC;;GAIrD,OAAAA,UAAA,GAAAC,mBAIE,OAAA;IAHC,OAAKC,eAAEC,MAAA,EAAA,CAAE;IACV,WAAQ;IACR,eAAY"}
|
|
@@ -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 { 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 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 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: 'pt-1',\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 */\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 <!--\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.\n -->\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 :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 */\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,5 +1,6 @@
|
|
|
1
1
|
import type { VueClassValue } from '../../../types/class-value.js';
|
|
2
2
|
import { type FloatingOptions } from '../../../composables/floating.js';
|
|
3
|
+
import { type FormDetails } from '../../../utils/form-text-detail.js';
|
|
3
4
|
export interface RuiMenuClassNames {
|
|
4
5
|
root?: VueClassValue;
|
|
5
6
|
wrapper?: VueClassValue;
|
|
@@ -28,6 +29,8 @@ export interface MenuProps {
|
|
|
28
29
|
errorMessages?: string | string[];
|
|
29
30
|
successMessages?: string | string[];
|
|
30
31
|
showDetails?: boolean;
|
|
32
|
+
/** With `showDetails`: `reserve` keeps the line while empty (default), `auto` takes no space until there is a message. */
|
|
33
|
+
details?: Exclude<FormDetails, 'hidden'>;
|
|
31
34
|
dense?: boolean;
|
|
32
35
|
/**
|
|
33
36
|
* Keeps the menu open through the interactions that would otherwise dismiss
|
|
@@ -50,6 +50,7 @@ var RuiMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
|
|
|
50
50
|
type: Boolean,
|
|
51
51
|
default: false
|
|
52
52
|
},
|
|
53
|
+
details: { default: "reserve" },
|
|
53
54
|
dense: {
|
|
54
55
|
type: Boolean,
|
|
55
56
|
default: false
|
|
@@ -86,7 +87,7 @@ var RuiMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
|
|
|
86
87
|
wrapper: "relative inline-flex max-w-full",
|
|
87
88
|
popover: "w-max z-rui-menu",
|
|
88
89
|
content: "rounded-rui-panel overflow-hidden shadow-rui-menu bg-rui-menu border border-rui-divider text-rui-text focus:outline-hidden p-1",
|
|
89
|
-
details: "
|
|
90
|
+
details: ""
|
|
90
91
|
},
|
|
91
92
|
variants: {
|
|
92
93
|
fullWidth: { true: { wrapper: "w-full" } },
|
|
@@ -171,6 +172,11 @@ var RuiMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
|
|
|
171
172
|
* A persistent menu leaves the key alone instead of swallowing it, so an inner
|
|
172
173
|
* popover can close on Escape while the menu stays open, and an ancestor still
|
|
173
174
|
* sees a key nothing consumed.
|
|
175
|
+
*
|
|
176
|
+
* Escape is swallowed only while the menu is open and not persistent
|
|
177
|
+
* (`onLeave` stops the event itself); a closed or persistent menu must let it
|
|
178
|
+
* through, or a consumer that closes its own editor on Escape never sees the
|
|
179
|
+
* key. The root element listens for it.
|
|
174
180
|
*/
|
|
175
181
|
function onEscape(event) {
|
|
176
182
|
if (__props.persistent) return;
|
|
@@ -256,11 +262,13 @@ var RuiMenu_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
|
|
|
256
262
|
})], 42, _hoisted_2)) : createCommentVNode("", true)])) : createCommentVNode("", true),
|
|
257
263
|
__props.showDetails ? (openBlock(), createBlock(RuiFormTextDetail_default, {
|
|
258
264
|
key: 1,
|
|
265
|
+
details: __props.details,
|
|
259
266
|
class: normalizeClass(unref(ui).details({ class: unref(cn)(__props.classNames?.details) })),
|
|
260
267
|
"error-messages": __props.errorMessages,
|
|
261
268
|
"success-messages": __props.successMessages,
|
|
262
269
|
hint: __props.hint
|
|
263
270
|
}, null, 8, [
|
|
271
|
+
"details",
|
|
264
272
|
"class",
|
|
265
273
|
"error-messages",
|
|
266
274
|
"success-messages",
|
|
@@ -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 { 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 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 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: 'pt-1',\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 */\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 <!--\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.\n -->\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 :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 */\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuiDataTable.js","names":["_sfc_main"],"sources":["../../../src/components/tables/RuiDataTable.vue"],"sourcesContent":["<script lang=\"ts\" setup generic=\"T extends object, IdType extends keyof T = keyof T\">\nimport type { GroupHeader } from '@/composables/tables/data-table/types';\nimport { dataTableStyles } from '@/components/tables/data-table-styles';\nimport { type DataTableClasses, type DataTableGroupLabel, provideDataTableContext, provideDataTableNested, useDataTableNested } from '@/components/tables/data-table/context';\nimport RuiDataTableBody from '@/components/tables/data-table/RuiDataTableBody.vue';\nimport RuiDataTableMobileSort from '@/components/tables/data-table/RuiDataTableMobileSort.vue';\nimport RuiTableHead, {\n type GroupData,\n type TableColumn,\n type TableRowKeyData,\n type TableSortData,\n} from '@/components/tables/RuiTableHead.vue';\nimport RuiTablePagination, {\n type TablePaginationData,\n} from '@/components/tables/RuiTablePagination.vue';\nimport { GroupExpandButtonPosition } from '@/components/tables/table-props';\nimport { useTable } from '@/composables/defaults/table';\nimport { useKeyboardScroll } from '@/composables/keyboard-scroll';\nimport { useStickyTableHeader } from '@/composables/sticky-header';\nimport { useTableColumns } from '@/composables/tables/data-table/columns';\nimport { useTableExpansion } from '@/composables/tables/data-table/expansion';\nimport { useTableGrouping } from '@/composables/tables/data-table/grouping';\nimport { usePageScroll, useStickyBottomInset, useStickyPaginationBar } from '@/composables/tables/data-table/page-scroll';\nimport { useTablePagination } from '@/composables/tables/data-table/pagination';\nimport { useTableSelection } from '@/composables/tables/data-table/selection';\nimport { useTableSort } from '@/composables/tables/data-table/sort';\n\nexport type { GroupedTableRow, GroupHeader, TableOptions } from '@/composables/tables/data-table/types';\n\nexport interface Props<T, K extends keyof T> {\n /**\n * list of items for each row\n */\n rows: readonly T[];\n /**\n * the attribute used to identify each row uniquely for selection, usually `id`\n */\n rowAttr: K;\n /**\n * model for internal searching\n */\n search?: string;\n /**\n * model for items per page\n * will be used if the `pagination` model isn't specified\n */\n itemsPerPage?: number;\n /**\n * list of column definitions\n */\n cols?: TableColumn<T>[];\n /**\n * attribute to use from column definitions to display column titles\n */\n columnAttr?: keyof TableColumn<T>;\n /**\n * flag to show a more or less spacious table\n */\n dense?: boolean;\n /**\n * flag to outline the flag with a border\n */\n outlined?: boolean;\n /**\n * should add zebra-striping to the table row\n */\n striped?: boolean;\n /**\n * flag to show loading state of the table\n * triggers indefinite progress at the bottom of the table header\n */\n loading?: boolean;\n disablePerPage?: boolean;\n /**\n * Maximum number of pages before the jump-to-page dropdown is replaced with\n * a numeric input. Defaults to `500`. Forwarded to `RuiTablePagination`.\n */\n rangesThreshold?: number;\n /**\n * The text and icon shown when there are no rows, e.g.\n * `:empty=\"\\{ icon: 'transactions-line', label: 'No transactions found' \\}\"`\n */\n empty?: {\n label?: string;\n description?: string;\n };\n /**\n * Why the last read failed. Shown above any rows kept from an earlier read,\n * or in place of the empty state; `loading` still takes precedence.\n */\n error?: string;\n /** Heading for that failure. */\n errorTitle?: string;\n /** Label for the retry control; without one, no control is offered. */\n retryText?: string;\n /**\n * Hide the pagination bar. The bar sits under the table and sticks to the bottom of the view\n * while the table runs past it.\n */\n hidePagination?: boolean;\n\n rounded?: 'sm' | 'md' | 'lg';\n /**\n * make expansion work like accordion\n */\n singleExpand?: boolean;\n /**\n * make table head stick to top on scroll\n */\n stickyHeader?: boolean;\n stickyOffset?: number;\n /**\n * keep the pagination bar at the table's end instead of pinning it to the bottom of the view while\n * the rows run past it, for a table that is one section of a longer page\n */\n disableStickyPagination?: boolean;\n /**\n * Affects how the items per page work across the app.\n * When true, changing the items per page setting in one table will affect other tables.\n */\n globalItemsPerPage?: boolean;\n groupExpandButtonPosition?: GroupExpandButtonPosition;\n disabledRows?: readonly T[];\n multiPageSelect?: boolean;\n itemClass?: ((item: T) => string) | string;\n /**\n * Render each row as a stacked card instead of a table row once the measured\n * width is narrower than this width (in pixels). Columns flagged with\n * `mobileHidden` are dropped in this layout. Falls back to the global\n * `mobileBreakpoint` table default; leave both unset to keep the default table\n * layout at every width.\n */\n mobileBreakpoint?: number;\n /**\n * What the `mobileBreakpoint` is measured against: the browser `viewport`\n * (default) or this table's own `container` width. Use `container` when the\n * table lives in a narrow region of a wider window. Falls back to the global\n * `mobileBreakpointBasis` table default.\n */\n mobileBreakpointBasis?: 'viewport' | 'container';\n /**\n * Force the stacked mobile card layout on (`true`) or off (`false`),\n * regardless of `mobileBreakpoint`. Leave unset to derive it from the width.\n */\n mobile?: boolean;\n}\n\ndefineOptions({\n name: 'RuiDataTable',\n});\n\nconst selectedData = defineModel<T[IdType][]>();\n\nconst expanded = defineModel<T[]>('expanded');\n\nconst [pagination, paginationModifiers] = defineModel<TablePaginationData, 'external'>('pagination');\n\nconst [sort, sortModifiers] = defineModel<TableSortData<T>, 'external'>('sort');\n\nconst group = defineModel<TableRowKeyData<T>>('group');\n\nconst collapsed = defineModel<T[]>('collapsed');\n\nconst {\n rows,\n rowAttr,\n search = '',\n itemsPerPage = 10,\n cols,\n columnAttr = 'label',\n dense = false,\n outlined = false,\n striped = false,\n loading = false,\n disablePerPage = false,\n rangesThreshold = 500,\n empty = { label: 'No item found' },\n error = '',\n errorTitle = '',\n retryText = '',\n hidePagination = false,\n rounded = 'md',\n singleExpand = false,\n stickyHeader = false,\n stickyOffset,\n disableStickyPagination = false,\n globalItemsPerPage = undefined,\n groupExpandButtonPosition = GroupExpandButtonPosition.start,\n disabledRows,\n multiPageSelect = false,\n itemClass = '',\n mobileBreakpoint,\n mobileBreakpointBasis,\n mobile,\n} = defineProps<Props<T, IdType>>();\n\nconst emit = defineEmits<{\n 'update:options': [value: { pagination?: TablePaginationData; sort?: TableSortData<T> }];\n 'copy:group': [value: GroupData<T>];\n 'retry': [];\n}>();\n\nconst slots = defineSlots<Partial<\n Record<`header.${string}`, (props: { column: TableColumn<T> }) => any> &\n Record<`item.${string}`, (props: { column: TableColumn<T>; row: T; index: number }) => any> & {\n 'body.prepend': (props: { colspan: number }) => any;\n 'body.append': (props: { colspan: number }) => any;\n 'group.header': (props: {\n colspan: number;\n header: GroupHeader<T>;\n isOpen: boolean;\n toggle: () => void;\n }) => any;\n 'group.header.content': (props: { header: GroupHeader<T>; groupKey: string }) => any;\n 'expanded-item': (props: { row: T; index: number }) => any;\n /**\n * A full-width row after every row, read as the second half of the same item: no divider between\n * them, one hover, the row's selected or open shade and stripe. For a long field, such as notes,\n * that would otherwise widen the table. It comes before an open expanded panel.\n */\n 'row-append': (props: { row: T; index: number }) => any;\n 'no-data': () => any;\n 'empty-description': () => any;\n 'error': () => any;\n 'tfoot': () => any;\n }\n>>();\n\nconst tableDefaults = useTable();\n\nconst tableWrapper = useTemplateRef<HTMLElement>('tableWrapper');\nconst paginationSentinel = useTemplateRef<HTMLElement>('paginationSentinel');\nconst paginationBar = useTemplateRef<InstanceType<typeof RuiTablePagination>>('paginationBar');\nconst { width: windowWidth } = useWindowSize();\nconst { width: containerWidth } = useElementSize(tableWrapper);\n\n// Per-table props win, then the global table defaults.\nconst resolvedMobileBreakpoint = computed<number | undefined>(() => {\n if (mobileBreakpoint !== undefined)\n return mobileBreakpoint;\n const globalBreakpoint = tableDefaults.mobileBreakpoint;\n return globalBreakpoint === undefined ? undefined : get(globalBreakpoint);\n});\nconst resolvedMobileBasis = computed<'viewport' | 'container'>(() => {\n if (mobileBreakpointBasis !== undefined)\n return mobileBreakpointBasis;\n const globalBasis = tableDefaults.mobileBreakpointBasis;\n return globalBasis === undefined ? 'viewport' : get(globalBasis);\n});\n\n/**\n * Whether the stacked card layout is showing.\n *\n * Vue coerces an absent Boolean prop to `false`, so `mobile` cannot say\n * \"unset\": with no breakpoint configured it is the manual switch, defaulting\n * to desktop, and with one configured the breakpoint drives the layout while\n * `mobile === true` can still force the stacked layout on.\n */\nconst isMobile = computed<boolean>(() => {\n const breakpoint = get(resolvedMobileBreakpoint);\n if (breakpoint === undefined)\n return mobile;\n if (mobile)\n return true;\n // `containerWidth` is 0 until measured; the viewport keeps the first paint off mobile\n const width = get(resolvedMobileBasis) === 'container'\n ? (get(containerWidth) || get(windowWidth))\n : get(windowWidth);\n return width < breakpoint;\n});\n\nconst stickyHeaderOffset = computed<number | undefined>(() =>\n stickyOffset !== undefined ? stickyOffset : get(tableDefaults.stickyOffset),\n);\n\n/**\n * A table rendered inside an expanded row must not pin its own toolbar: only\n * the outermost table sticks, otherwise the bars stack and collide.\n */\nconst isNestedTable = useDataTableNested();\nprovideDataTableNested();\n\n/**\n * On mobile there is no column header to pin, so `stickyHeader` pins the\n * pagination and sort toolbar instead while the card list scrolls.\n */\nconst stickyMobileToolbar = computed<boolean>(() => stickyHeader && get(isMobile) && !isNestedTable);\n\nconst table = useTemplateRef<HTMLTableElement>('table');\nconst tableScroller = useTemplateRef<HTMLElement>('tableScroller');\nconst scrollerTabindex = useKeyboardScroll(tableScroller);\n\nconst { stick } = useStickyTableHeader(\n () => stickyHeader,\n stickyHeaderOffset,\n { table },\n);\n\nconst { width: scrollerWidth } = useElementSize(tableScroller);\n\n/**\n * The visible width of the table, which an expanded row caps its content to.\n * Without the cap a wide panel, such as a nested table, sets the minimum width\n * of the whole table and pushes every column past the scroller. `initial`\n * resets the variable, so a nested table never inherits its parent's width.\n */\nconst viewportStyle = computed<Record<string, string>>(() => {\n const width = get(scrollerWidth);\n return { '--rui-table-viewport': width > 0 && !get(isMobile) ? `${width}px` : 'initial' };\n});\n\nconst { expandable, isExpanded, onToggleExpand } = useTableExpansion<T, IdType>(\n { rowAttr, singleExpand },\n { expanded },\n);\n\nfunction emitUpdateOptions(opts: { sort?: TableSortData<T>; pagination?: TablePaginationData }): void {\n emit('update:options', opts);\n}\n\nconst {\n sortData,\n sortedMap,\n sorted,\n onSort: applySort,\n} = useTableSort<T>(\n { rows: () => rows, search: () => search, sortModifiersExternal: sortModifiers.external },\n {\n sort,\n pagination,\n emitUpdateOptions,\n },\n);\n\nconst {\n groupKeys,\n groupKey,\n grouped,\n mappedGroups,\n isExpandedGroup,\n isHiddenRow,\n onToggleExpandGroup,\n onUngroup,\n} = useTableGrouping<T, IdType>(\n { rowAttr },\n {\n group,\n collapsed,\n sorted,\n },\n);\n\nconst {\n paginationData,\n filtered,\n setInternalTotal,\n resetPagination,\n} = useTablePagination<T>(\n {\n itemsPerPage,\n paginationModifiersExternal: paginationModifiers.external,\n globalItemsPerPage,\n },\n {\n pagination,\n grouped,\n isHiddenRow,\n sort,\n emitUpdateOptions,\n tableDefaults,\n },\n);\n\nconst noData = computed<boolean>(() => get(filtered).length === 0);\n\n// one loader at a time: the header bar marks a refetch over rows, and an empty table shows only its spinner row\nconst headerLoading = computed<boolean>(() => loading && !get(noData));\n\n/**\n * A nested table (a breakdown in an expanded row) that pages its own rows drops the bar when they all fit\n * the smallest page size: no control on it could change what shows, and in a breakdown it is pure noise.\n * A top-level table keeps it as a summary; a server-paged one keeps it, since only the server knows the total.\n */\nconst fitsSmallestPage = computed<boolean>(() => {\n if (!isNestedTable || paginationModifiers.external)\n return false;\n const { limits } = get(paginationData);\n const sizes = limits ?? toValue(tableDefaults.limits);\n // the rows themselves, not the pagination total, which is only filled in after mount\n return sizes.length > 0 && get(sorted).length <= Math.min(...sizes);\n});\n\nconst showPagination = computed<boolean>(() => !!get(paginationData) && !hidePagination && !get(fitsSmallestPage));\n\n/**\n * The one pagination bar sticks to the bottom of the view, so a long page can be turned without\n * scrolling down to it. `position: sticky` keeps it inside this table, so stacked and side-by-side\n * tables each hold their own bar. A nested table's bar stays in place, since two stuck bars would\n * stack, as does the bar of a table with `disableStickyPagination`.\n */\nconst paginationMayStick = computed<boolean>(() => !disableStickyPagination && !isNestedTable);\nconst canStickPagination = useStickyPaginationBar(tableWrapper, () => get(paginationMayStick) && get(showPagination));\nconst paginationStuck = computed<boolean>(() => get(paginationMayStick) && get(canStickPagination));\n\nconst paginationBarElement = computed<HTMLElement | undefined>(() => {\n const element: unknown = get(paginationBar)?.$el;\n return element instanceof HTMLElement ? element : undefined;\n});\n\n// while the bar is stuck to the page, `--rui-sticky-bottom` tells the page's floating parts to rise above it\nuseStickyBottomInset(paginationSentinel, paginationBarElement, paginationStuck);\n\n/**\n * Grouped columns leave the column list, so their labels come from `cols`.\n */\nconst groupLabels = computed<DataTableGroupLabel[]>(() => get(groupKeys).map((key) => {\n const label = cols?.find(column => column.key === key)?.[columnAttr];\n return {\n key: String(key),\n label: label === undefined || label === null ? String(key) : String(label),\n };\n}));\n\n/** Each group's list starts with its header entry, which is not a row. */\nfunction getGroupSize(identifier: string): number {\n return Math.max((get(mappedGroups)[identifier]?.length ?? 0) - 1, 0);\n}\n\nconst ui = computed<ReturnType<typeof dataTableStyles>>(() => dataTableStyles({\n outlined,\n rounded,\n dense,\n striped,\n mobile: get(isMobile),\n}));\n\nconst { columns, colspan, headerSlots, cellValue } = useTableColumns<T, IdType>({\n cols: () => cols,\n columnAttr,\n rows: () => rows,\n expandable,\n groupKeys,\n selectedData,\n slots,\n isMobile,\n tdResolver: options => get(ui).td(options),\n});\n\nconst showMobileSort = computed<boolean>(() =>\n get(isMobile) && !!get(sortData) && get(columns).some(column => column.sortable),\n);\n\nconst ITEM_SLOT_PREFIX = 'item.';\n\n// Slot keys are static for the component lifetime — no reactivity needed\nconst itemSlotKeys: Set<string> = new Set(\n Object.keys(slots)\n .filter((key): key is `item.${string}` => key.startsWith(ITEM_SLOT_PREFIX))\n .map(key => key.slice(ITEM_SLOT_PREFIX.length)),\n);\n\nconst {\n isAllSelected,\n indeterminate,\n isSelected,\n isDisabledRow,\n onToggleAll,\n onSelect,\n onCheckboxClick,\n deselectRemovedRows,\n resetCheckboxShiftState,\n} = useTableSelection<T, IdType>(\n { rowAttr, multiPageSelect, disabledRows: () => disabledRows },\n { selectedData, filtered },\n);\n\n/**\n * Sorting clears the selection. That lives here rather than in either\n * composable, which would make sort and selection depend on each other.\n */\nfunction onSort(payload: Parameters<typeof applySort>[0]): void {\n applySort(payload);\n if (!multiPageSelect)\n onToggleAll(false);\n resetCheckboxShiftState();\n}\n\nconst { revealTableTop } = usePageScroll(tableWrapper, stickyHeaderOffset);\n\nasync function onPaginate(): Promise<void> {\n if ((get(expanded)?.length ?? 0) > 0)\n set(expanded, []);\n if (!multiPageSelect)\n onToggleAll(false);\n resetCheckboxShiftState();\n await revealTableTop();\n}\n\n// Reset pagination page to 1 on search query change\nwatch(() => search, () => {\n resetPagination();\n onToggleAll(false);\n resetCheckboxShiftState();\n});\n\nwatch(sorted, (items) => {\n if (!multiPageSelect)\n deselectRemovedRows();\n setInternalTotal(items);\n});\n\nonMounted(() => {\n setInternalTotal(get(sorted));\n});\n\nconst classes = computed<DataTableClasses>(() => {\n const s = get(ui);\n return {\n td: s.td(),\n tr: s.tr(),\n trSelected: s.tr({ rowVariant: 'selected' }),\n trExpandable: s.tr({ rowVariant: 'expandable' }),\n trExpandedParent: s.tr({ rowVariant: 'expandedParent' }),\n trGroup: s.tr({ rowVariant: 'group' }),\n trEmpty: s.tr({ rowVariant: 'empty' }),\n checkbox: s.checkbox(),\n tbody: s.tbody(),\n tbodyLoader: s.tbodyLoader(),\n };\n});\n\nprovideDataTableContext<T, IdType>({\n // Reactive (ComputedRef/Ref) — destructuring preserves reactivity via Vue template auto-unwrap\n classes,\n columns,\n colspan,\n expandable,\n groupKey,\n groupLabels,\n isMobile,\n selectedData,\n // Static values\n cellValue,\n columnAttr,\n dense,\n getGroupSize,\n getRowId: (row: T) => row[rowAttr],\n itemSlotKeys,\n groupExpandButtonPosition,\n isDisabledRow,\n isExpanded,\n isExpandedGroup,\n isSelected,\n itemClass,\n onCheckboxClick,\n onCopyGroup: (header: GroupHeader<T>) => emit('copy:group', { key: get(groupKey) ?? '', value: header.group }),\n onSelect,\n onToggleExpand,\n onToggleExpandGroup,\n onUngroup,\n});\n</script>\n\n<template>\n <div\n ref=\"tableWrapper\"\n :class=\"ui.wrapper()\"\n data-id=\"table-wrapper\"\n >\n <div\n v-if=\"showMobileSort\"\n :class=\"stickyMobileToolbar ? 'sticky z-rui-raised bg-rui-background pt-2' : 'contents'\"\n :style=\"stickyMobileToolbar ? { top: `${stickyHeaderOffset ?? 0}px` } : undefined\"\n data-id=\"table-mobile-toolbar-sticky\"\n >\n <div\n class=\"flex justify-end p-2\"\n data-id=\"table-mobile-toolbar\"\n >\n <RuiDataTableMobileSort\n :columns=\"columns\"\n :column-attr=\"columnAttr\"\n :sorted-map=\"sortedMap\"\n :dense=\"dense\"\n @sort=\"onSort($event)\"\n />\n </div>\n </div>\n <div\n ref=\"tableScroller\"\n :class=\"[ui.scroller(), isMobile && !showMobileSort ? 'pt-2' : '']\"\n :tabindex=\"scrollerTabindex\"\n data-id=\"table-scroller\"\n >\n <table\n ref=\"table\"\n :class=\"ui.table()\"\n :style=\"viewportStyle\"\n :aria-busy=\"loading\"\n >\n <RuiTableHead\n v-if=\"!isMobile\"\n :loading=\"headerLoading\"\n :indeterminate=\"indeterminate\"\n :capitalize-headers=\"!cols\"\n :colspan=\"colspan\"\n :column-attr=\"columnAttr\"\n :columns=\"columns\"\n :dense=\"dense\"\n :disable-check-all=\"!filtered?.length\"\n :is-all-selected=\"isAllSelected\"\n :selectable=\"!!selectedData\"\n :sort-data=\"sortData\"\n :sorted-map=\"sortedMap\"\n :stick=\"stick\"\n :sticky-header=\"stickyHeader\"\n @sort=\"onSort($event)\"\n @select:all=\"onToggleAll($event)\"\n >\n <template\n v-for=\"headerSlot in headerSlots\"\n #[headerSlot]=\"slotData\"\n >\n <slot\n :name=\"headerSlot\"\n v-bind=\"slotData\"\n />\n </template>\n </RuiTableHead>\n <RuiTableHead\n v-if=\"stickyHeader && !isMobile\"\n :loading=\"headerLoading\"\n :capitalize-headers=\"!cols\"\n :colspan=\"colspan\"\n :column-attr=\"columnAttr\"\n :columns=\"columns\"\n :dense=\"dense\"\n :selectable=\"!!selectedData\"\n :sort-data=\"sortData\"\n :sorted-map=\"sortedMap\"\n data-id=\"head-clone\"\n class=\"opacity-0 invisible\"\n >\n <template\n v-for=\"headerSlot in headerSlots\"\n #[headerSlot]=\"slotData\"\n >\n <slot\n :name=\"headerSlot\"\n v-bind=\"slotData\"\n />\n </template>\n </RuiTableHead>\n <RuiDataTableBody\n :filtered=\"filtered\"\n :loading=\"loading\"\n :no-data=\"noData\"\n :empty=\"empty\"\n :error=\"error\"\n :error-title=\"errorTitle\"\n :retry-text=\"retryText\"\n @retry=\"emit('retry')\"\n >\n <template\n v-if=\"slots.error\"\n #error\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"error\" />\n </template>\n <template\n v-if=\"slots['body.prepend']\"\n #body.prepend=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"body.prepend\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['group.header']\"\n #group.header=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"group.header\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['group.header.content']\"\n #group.header.content=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"group.header.content\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-for=\"key in itemSlotKeys\"\n #[`item.${key}`]=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n :name=\"`item.${key}`\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['expanded-item']\"\n #expanded-item=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"expanded-item\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['row-append']\"\n #row-append=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"row-append\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['no-data']\"\n #no-data\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"no-data\" />\n </template>\n <template\n v-if=\"slots['empty-description']\"\n #empty-description\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"empty-description\" />\n </template>\n <template\n v-if=\"slots['body.append']\"\n #body.append=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"body.append\"\n v-bind=\"slotData\"\n />\n </template>\n </RuiDataTableBody>\n <tfoot>\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"tfoot\" />\n </tfoot>\n </table>\n </div>\n <!-- the bar's own place after the rows: below the view, the bar is stuck; no divider of its own -->\n <div\n v-if=\"paginationData && showPagination && !isNestedTable\"\n ref=\"paginationSentinel\"\n class=\"border-0!\"\n aria-hidden=\"true\"\n data-id=\"table-pagination-sentinel\"\n />\n <RuiTablePagination\n v-if=\"paginationData && showPagination\"\n ref=\"paginationBar\"\n v-model=\"paginationData\"\n :class=\"ui.pagination({ sticky: paginationStuck })\"\n :dense=\"dense\"\n :loading=\"loading\"\n :mobile=\"isMobile\"\n :disable-per-page=\"disablePerPage\"\n :ranges-threshold=\"rangesThreshold\"\n :data-sticky=\"paginationStuck || undefined\"\n data-id=\"table-pagination\"\n @update:model-value=\"onPaginate()\"\n />\n </div>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"RuiDataTable.js","names":["_sfc_main"],"sources":["../../../src/components/tables/RuiDataTable.vue"],"sourcesContent":["<script lang=\"ts\" setup generic=\"T extends object, IdType extends keyof T = keyof T\">\nimport type { GroupHeader } from '@/composables/tables/data-table/types';\nimport { dataTableStyles } from '@/components/tables/data-table-styles';\nimport { type DataTableClasses, type DataTableGroupLabel, provideDataTableContext, provideDataTableNested, useDataTableNested } from '@/components/tables/data-table/context';\nimport RuiDataTableBody from '@/components/tables/data-table/RuiDataTableBody.vue';\nimport RuiDataTableMobileSort from '@/components/tables/data-table/RuiDataTableMobileSort.vue';\nimport RuiTableHead, {\n type GroupData,\n type TableColumn,\n type TableRowKeyData,\n type TableSortData,\n} from '@/components/tables/RuiTableHead.vue';\nimport RuiTablePagination, {\n type TablePaginationData,\n} from '@/components/tables/RuiTablePagination.vue';\nimport { GroupExpandButtonPosition } from '@/components/tables/table-props';\nimport { useTable } from '@/composables/defaults/table';\nimport { useKeyboardScroll } from '@/composables/keyboard-scroll';\nimport { useStickyBlockerWarning } from '@/composables/sticky-blocker';\nimport { useStickyTableHeader } from '@/composables/sticky-header';\nimport { useTableColumns } from '@/composables/tables/data-table/columns';\nimport { useTableExpansion } from '@/composables/tables/data-table/expansion';\nimport { useTableGrouping } from '@/composables/tables/data-table/grouping';\nimport { usePageScroll, useStickyBottomInset, useStickyPaginationBar } from '@/composables/tables/data-table/page-scroll';\nimport { useTablePagination } from '@/composables/tables/data-table/pagination';\nimport { useTableSelection } from '@/composables/tables/data-table/selection';\nimport { useTableSort } from '@/composables/tables/data-table/sort';\n\nexport type { GroupedTableRow, GroupHeader, TableOptions } from '@/composables/tables/data-table/types';\n\nexport interface Props<T, K extends keyof T> {\n /**\n * list of items for each row\n */\n rows: readonly T[];\n /**\n * the attribute used to identify each row uniquely for selection, usually `id`\n */\n rowAttr: K;\n /**\n * model for internal searching\n */\n search?: string;\n /**\n * model for items per page\n * will be used if the `pagination` model isn't specified\n */\n itemsPerPage?: number;\n /**\n * list of column definitions\n */\n cols?: TableColumn<T>[];\n /**\n * attribute to use from column definitions to display column titles\n */\n columnAttr?: keyof TableColumn<T>;\n /**\n * flag to show a more or less spacious table\n */\n dense?: boolean;\n /**\n * flag to outline the flag with a border\n */\n outlined?: boolean;\n /**\n * should add zebra-striping to the table row\n */\n striped?: boolean;\n /**\n * flag to show loading state of the table\n * triggers indefinite progress at the bottom of the table header\n */\n loading?: boolean;\n disablePerPage?: boolean;\n /**\n * Maximum number of pages before the jump-to-page dropdown is replaced with\n * a numeric input. Defaults to `500`. Forwarded to `RuiTablePagination`.\n */\n rangesThreshold?: number;\n /**\n * The text and icon shown when there are no rows, e.g.\n * `:empty=\"\\{ icon: 'transactions-line', label: 'No transactions found' \\}\"`\n */\n empty?: {\n label?: string;\n description?: string;\n };\n /**\n * Why the last read failed. Shown above any rows kept from an earlier read,\n * or in place of the empty state; `loading` still takes precedence.\n */\n error?: string;\n /** Heading for that failure. */\n errorTitle?: string;\n /** Label for the retry control; without one, no control is offered. */\n retryText?: string;\n /**\n * Hide the pagination bar. The bar sits under the table and sticks to the bottom of the view\n * while the table runs past it.\n */\n hidePagination?: boolean;\n\n rounded?: 'sm' | 'md' | 'lg';\n /**\n * make expansion work like accordion\n */\n singleExpand?: boolean;\n /**\n * make table head stick to top on scroll\n */\n stickyHeader?: boolean;\n stickyOffset?: number;\n /**\n * keep the pagination bar at the table's end instead of pinning it to the bottom of the view while\n * the rows run past it, for a table that is one section of a longer page\n */\n disableStickyPagination?: boolean;\n /**\n * Affects how the items per page work across the app.\n * When true, changing the items per page setting in one table will affect other tables.\n */\n globalItemsPerPage?: boolean;\n groupExpandButtonPosition?: GroupExpandButtonPosition;\n disabledRows?: readonly T[];\n multiPageSelect?: boolean;\n itemClass?: ((item: T) => string) | string;\n /**\n * Render each row as a stacked card instead of a table row once the measured\n * width is narrower than this width (in pixels). Columns flagged with\n * `mobileHidden` are dropped in this layout. Falls back to the global\n * `mobileBreakpoint` table default; leave both unset to keep the default table\n * layout at every width.\n */\n mobileBreakpoint?: number;\n /**\n * What the `mobileBreakpoint` is measured against: the browser `viewport`\n * (default) or this table's own `container` width. Use `container` when the\n * table lives in a narrow region of a wider window. Falls back to the global\n * `mobileBreakpointBasis` table default.\n */\n mobileBreakpointBasis?: 'viewport' | 'container';\n /**\n * Force the stacked mobile card layout on (`true`) or off (`false`),\n * regardless of `mobileBreakpoint`. Leave unset to derive it from the width.\n */\n mobile?: boolean;\n}\n\ndefineOptions({\n name: 'RuiDataTable',\n});\n\nconst selectedData = defineModel<T[IdType][]>();\n\nconst expanded = defineModel<T[]>('expanded');\n\nconst [pagination, paginationModifiers] = defineModel<TablePaginationData, 'external'>('pagination');\n\nconst [sort, sortModifiers] = defineModel<TableSortData<T>, 'external'>('sort');\n\nconst group = defineModel<TableRowKeyData<T>>('group');\n\nconst collapsed = defineModel<T[]>('collapsed');\n\nconst {\n rows,\n rowAttr,\n search = '',\n itemsPerPage = 10,\n cols,\n columnAttr = 'label',\n dense = false,\n outlined = false,\n striped = false,\n loading = false,\n disablePerPage = false,\n rangesThreshold = 500,\n empty = { label: 'No item found' },\n error = '',\n errorTitle = '',\n retryText = '',\n hidePagination = false,\n rounded = 'md',\n singleExpand = false,\n stickyHeader = false,\n stickyOffset,\n disableStickyPagination = false,\n globalItemsPerPage = undefined,\n groupExpandButtonPosition = GroupExpandButtonPosition.start,\n disabledRows,\n multiPageSelect = false,\n itemClass = '',\n mobileBreakpoint,\n mobileBreakpointBasis,\n mobile,\n} = defineProps<Props<T, IdType>>();\n\nconst emit = defineEmits<{\n 'update:options': [value: { pagination?: TablePaginationData; sort?: TableSortData<T> }];\n 'copy:group': [value: GroupData<T>];\n 'retry': [];\n}>();\n\nconst slots = defineSlots<Partial<\n Record<`header.${string}`, (props: { column: TableColumn<T> }) => any> &\n Record<`item.${string}`, (props: { column: TableColumn<T>; row: T; index: number }) => any> & {\n 'body.prepend': (props: { colspan: number }) => any;\n 'body.append': (props: { colspan: number }) => any;\n 'group.header': (props: {\n colspan: number;\n header: GroupHeader<T>;\n isOpen: boolean;\n toggle: () => void;\n }) => any;\n 'group.header.content': (props: { header: GroupHeader<T>; groupKey: string }) => any;\n 'expanded-item': (props: { row: T; index: number }) => any;\n /**\n * A full-width row after every row, read as the second half of the same item: no divider between\n * them, one hover, the row's selected or open shade and stripe. For a long field, such as notes,\n * that would otherwise widen the table. It comes before an open expanded panel.\n */\n 'row-append': (props: { row: T; index: number }) => any;\n 'no-data': () => any;\n 'empty-description': () => any;\n 'error': () => any;\n /**\n * Rows under the body, such as a total, rendered in `<tfoot>`. Its cells take the body cells'\n * padding, height (dense included) and text, under `:where()`, so a cell's own classes still win.\n */\n 'tfoot': () => any;\n }\n>>();\n\nconst tableDefaults = useTable();\n\nconst tableWrapper = useTemplateRef<HTMLElement>('tableWrapper');\nconst paginationSentinel = useTemplateRef<HTMLElement>('paginationSentinel');\nconst paginationBar = useTemplateRef<InstanceType<typeof RuiTablePagination>>('paginationBar');\nconst { width: windowWidth } = useWindowSize();\nconst { width: containerWidth } = useElementSize(tableWrapper);\n\n// Per-table props win, then the global table defaults.\nconst resolvedMobileBreakpoint = computed<number | undefined>(() => {\n if (mobileBreakpoint !== undefined)\n return mobileBreakpoint;\n const globalBreakpoint = tableDefaults.mobileBreakpoint;\n return globalBreakpoint === undefined ? undefined : get(globalBreakpoint);\n});\nconst resolvedMobileBasis = computed<'viewport' | 'container'>(() => {\n if (mobileBreakpointBasis !== undefined)\n return mobileBreakpointBasis;\n const globalBasis = tableDefaults.mobileBreakpointBasis;\n return globalBasis === undefined ? 'viewport' : get(globalBasis);\n});\n\n/**\n * Whether the stacked card layout is showing.\n *\n * Vue coerces an absent Boolean prop to `false`, so `mobile` cannot say\n * \"unset\": with no breakpoint configured it is the manual switch, defaulting\n * to desktop, and with one configured the breakpoint drives the layout while\n * `mobile === true` can still force the stacked layout on.\n */\nconst isMobile = computed<boolean>(() => {\n const breakpoint = get(resolvedMobileBreakpoint);\n if (breakpoint === undefined)\n return mobile;\n if (mobile)\n return true;\n // `containerWidth` is 0 until measured; the viewport keeps the first paint off mobile\n const width = get(resolvedMobileBasis) === 'container'\n ? (get(containerWidth) || get(windowWidth))\n : get(windowWidth);\n return width < breakpoint;\n});\n\nconst stickyHeaderOffset = computed<number | undefined>(() =>\n stickyOffset !== undefined ? stickyOffset : get(tableDefaults.stickyOffset),\n);\n\n/**\n * A table rendered inside an expanded row must not pin its own toolbar: only\n * the outermost table sticks, otherwise the bars stack and collide.\n */\nconst isNestedTable = useDataTableNested();\nprovideDataTableNested();\n\n/**\n * On mobile there is no column header to pin, so `stickyHeader` pins the\n * pagination and sort toolbar instead while the card list scrolls.\n */\nconst stickyMobileToolbar = computed<boolean>(() => stickyHeader && get(isMobile) && !isNestedTable);\n\nconst table = useTemplateRef<HTMLTableElement>('table');\nconst tableScroller = useTemplateRef<HTMLElement>('tableScroller');\nconst scrollerTabindex = useKeyboardScroll(tableScroller);\n\nconst { stick } = useStickyTableHeader(\n () => stickyHeader,\n stickyHeaderOffset,\n { table },\n);\n\nconst { width: scrollerWidth } = useElementSize(tableScroller);\n\n/**\n * The visible width of the table, which an expanded row caps its content to.\n * Without the cap a wide panel, such as a nested table, sets the minimum width\n * of the whole table and pushes every column past the scroller. `initial`\n * resets the variable, so a nested table never inherits its parent's width.\n */\nconst viewportStyle = computed<Record<string, string>>(() => {\n const width = get(scrollerWidth);\n return { '--rui-table-viewport': width > 0 && !get(isMobile) ? `${width}px` : 'initial' };\n});\n\nconst { expandable, isExpanded, onToggleExpand } = useTableExpansion<T, IdType>(\n { rowAttr, singleExpand },\n { expanded },\n);\n\nfunction emitUpdateOptions(opts: { sort?: TableSortData<T>; pagination?: TablePaginationData }): void {\n emit('update:options', opts);\n}\n\nconst {\n sortData,\n sortedMap,\n sorted,\n onSort: applySort,\n} = useTableSort<T>(\n { rows: () => rows, search: () => search, sortModifiersExternal: sortModifiers.external },\n {\n sort,\n pagination,\n emitUpdateOptions,\n },\n);\n\nconst {\n groupKeys,\n groupKey,\n grouped,\n mappedGroups,\n isExpandedGroup,\n isHiddenRow,\n onToggleExpandGroup,\n onUngroup,\n} = useTableGrouping<T, IdType>(\n { rowAttr },\n {\n group,\n collapsed,\n sorted,\n },\n);\n\nconst {\n paginationData,\n filtered,\n setInternalTotal,\n resetPagination,\n} = useTablePagination<T>(\n {\n itemsPerPage,\n paginationModifiersExternal: paginationModifiers.external,\n globalItemsPerPage,\n },\n {\n pagination,\n grouped,\n isHiddenRow,\n sort,\n emitUpdateOptions,\n tableDefaults,\n },\n);\n\nconst noData = computed<boolean>(() => get(filtered).length === 0);\n\n// one loader at a time: the header bar marks a refetch over rows, and an empty table shows only its spinner row\nconst headerLoading = computed<boolean>(() => loading && !get(noData));\n\n/**\n * A nested table (a breakdown in an expanded row) that pages its own rows drops the bar when they all fit\n * the smallest page size: no control on it could change what shows, and in a breakdown it is pure noise.\n * A top-level table keeps it as a summary; a server-paged one keeps it, since only the server knows the total.\n */\nconst fitsSmallestPage = computed<boolean>(() => {\n if (!isNestedTable || paginationModifiers.external)\n return false;\n const { limits } = get(paginationData);\n const sizes = limits ?? toValue(tableDefaults.limits);\n // the rows themselves, not the pagination total, which is only filled in after mount\n return sizes.length > 0 && get(sorted).length <= Math.min(...sizes);\n});\n\nconst showPagination = computed<boolean>(() => !!get(paginationData) && !hidePagination && !get(fitsSmallestPage));\n\n/**\n * The one pagination bar sticks to the bottom of the view, so a long page can be turned without\n * scrolling down to it. `position: sticky` keeps it inside this table, so stacked and side-by-side\n * tables each hold their own bar. A nested table's bar stays in place, since two stuck bars would\n * stack, as does the bar of a table with `disableStickyPagination`.\n */\nconst paginationMayStick = computed<boolean>(() => !disableStickyPagination && !isNestedTable);\nconst canStickPagination = useStickyPaginationBar(tableWrapper, () => get(paginationMayStick) && get(showPagination));\nconst paginationStuck = computed<boolean>(() => get(paginationMayStick) && get(canStickPagination));\n\nconst paginationBarElement = computed<HTMLElement | undefined>(() => {\n const element: unknown = get(paginationBar)?.$el;\n return element instanceof HTMLElement ? element : undefined;\n});\n\n// while the bar is stuck to the page, `--rui-sticky-bottom` tells the page's floating parts to rise above it\nuseStickyBottomInset(paginationSentinel, paginationBarElement, paginationStuck);\n\nuseStickyBlockerWarning(paginationBarElement, () => get(paginationMayStick) && get(showPagination), 'The RuiDataTable pagination bar');\n\n/**\n * Grouped columns leave the column list, so their labels come from `cols`.\n */\nconst groupLabels = computed<DataTableGroupLabel[]>(() => get(groupKeys).map((key) => {\n const label = cols?.find(column => column.key === key)?.[columnAttr];\n return {\n key: String(key),\n label: label === undefined || label === null ? String(key) : String(label),\n };\n}));\n\n/** Each group's list starts with its header entry, which is not a row. */\nfunction getGroupSize(identifier: string): number {\n return Math.max((get(mappedGroups)[identifier]?.length ?? 0) - 1, 0);\n}\n\nconst ui = computed<ReturnType<typeof dataTableStyles>>(() => dataTableStyles({\n outlined,\n rounded,\n dense,\n striped,\n mobile: get(isMobile),\n}));\n\nconst { columns, colspan, headerSlots, cellValue } = useTableColumns<T, IdType>({\n cols: () => cols,\n columnAttr,\n rows: () => rows,\n expandable,\n groupKeys,\n selectedData,\n slots,\n isMobile,\n tdResolver: options => get(ui).td(options),\n});\n\nconst showMobileSort = computed<boolean>(() =>\n get(isMobile) && !!get(sortData) && get(columns).some(column => column.sortable),\n);\n\nconst ITEM_SLOT_PREFIX = 'item.';\n\n// Slot keys are static for the component lifetime, so no reactivity is needed\nconst itemSlotKeys: Set<string> = new Set(\n Object.keys(slots)\n .filter((key): key is `item.${string}` => key.startsWith(ITEM_SLOT_PREFIX))\n .map(key => key.slice(ITEM_SLOT_PREFIX.length)),\n);\n\nconst {\n isAllSelected,\n indeterminate,\n isSelected,\n isDisabledRow,\n onToggleAll,\n onSelect,\n onCheckboxClick,\n deselectRemovedRows,\n resetCheckboxShiftState,\n} = useTableSelection<T, IdType>(\n { rowAttr, multiPageSelect, disabledRows: () => disabledRows },\n { selectedData, filtered },\n);\n\n/**\n * Sorting clears the selection. That lives here rather than in either\n * composable, which would make sort and selection depend on each other.\n */\nfunction onSort(payload: Parameters<typeof applySort>[0]): void {\n applySort(payload);\n if (!multiPageSelect)\n onToggleAll(false);\n resetCheckboxShiftState();\n}\n\nconst { revealTableTop } = usePageScroll(tableWrapper, stickyHeaderOffset);\n\nasync function onPaginate(): Promise<void> {\n if ((get(expanded)?.length ?? 0) > 0)\n set(expanded, []);\n if (!multiPageSelect)\n onToggleAll(false);\n resetCheckboxShiftState();\n await revealTableTop();\n}\n\n// Reset pagination page to 1 on search query change\nwatch(() => search, () => {\n resetPagination();\n onToggleAll(false);\n resetCheckboxShiftState();\n});\n\nwatch(sorted, (items) => {\n if (!multiPageSelect)\n deselectRemovedRows();\n setInternalTotal(items);\n});\n\nonMounted(() => {\n setInternalTotal(get(sorted));\n});\n\nconst classes = computed<DataTableClasses>(() => {\n const s = get(ui);\n return {\n td: s.td(),\n tr: s.tr(),\n trSelected: s.tr({ rowVariant: 'selected' }),\n trExpandable: s.tr({ rowVariant: 'expandable' }),\n trExpandedParent: s.tr({ rowVariant: 'expandedParent' }),\n trGroup: s.tr({ rowVariant: 'group' }),\n trEmpty: s.tr({ rowVariant: 'empty' }),\n checkbox: s.checkbox(),\n tbody: s.tbody(),\n tbodyLoader: s.tbodyLoader(),\n };\n});\n\nprovideDataTableContext<T, IdType>({\n // Reactive (ComputedRef/Ref): destructuring preserves reactivity via Vue template auto-unwrap\n classes,\n columns,\n colspan,\n expandable,\n groupKey,\n groupLabels,\n isMobile,\n selectedData,\n // Static values\n cellValue,\n columnAttr,\n dense,\n getGroupSize,\n getRowId: (row: T) => row[rowAttr],\n itemSlotKeys,\n groupExpandButtonPosition,\n isDisabledRow,\n isExpanded,\n isExpandedGroup,\n isSelected,\n itemClass,\n onCheckboxClick,\n onCopyGroup: (header: GroupHeader<T>) => emit('copy:group', { key: get(groupKey) ?? '', value: header.group }),\n onSelect,\n onToggleExpand,\n onToggleExpandGroup,\n onUngroup,\n});\n</script>\n\n<template>\n <div\n ref=\"tableWrapper\"\n :class=\"ui.wrapper()\"\n data-id=\"table-wrapper\"\n >\n <div\n v-if=\"showMobileSort\"\n :class=\"stickyMobileToolbar ? 'sticky z-rui-raised bg-rui-background pt-2' : 'contents'\"\n :style=\"stickyMobileToolbar ? { top: `${stickyHeaderOffset ?? 0}px` } : undefined\"\n data-id=\"table-mobile-toolbar-sticky\"\n >\n <div\n class=\"flex justify-end p-2\"\n data-id=\"table-mobile-toolbar\"\n >\n <RuiDataTableMobileSort\n :columns=\"columns\"\n :column-attr=\"columnAttr\"\n :sorted-map=\"sortedMap\"\n :dense=\"dense\"\n @sort=\"onSort($event)\"\n />\n </div>\n </div>\n <div\n ref=\"tableScroller\"\n :class=\"[ui.scroller(), isMobile && !showMobileSort ? 'pt-2' : '']\"\n :tabindex=\"scrollerTabindex\"\n data-id=\"table-scroller\"\n >\n <table\n ref=\"table\"\n :class=\"ui.table()\"\n :style=\"viewportStyle\"\n :aria-busy=\"loading\"\n >\n <RuiTableHead\n v-if=\"!isMobile\"\n :loading=\"headerLoading\"\n :indeterminate=\"indeterminate\"\n :capitalize-headers=\"!cols\"\n :colspan=\"colspan\"\n :column-attr=\"columnAttr\"\n :columns=\"columns\"\n :dense=\"dense\"\n :disable-check-all=\"!filtered?.length\"\n :is-all-selected=\"isAllSelected\"\n :selectable=\"!!selectedData\"\n :sort-data=\"sortData\"\n :sorted-map=\"sortedMap\"\n :stick=\"stick\"\n :sticky-header=\"stickyHeader\"\n @sort=\"onSort($event)\"\n @select:all=\"onToggleAll($event)\"\n >\n <template\n v-for=\"headerSlot in headerSlots\"\n #[headerSlot]=\"slotData\"\n >\n <slot\n :name=\"headerSlot\"\n v-bind=\"slotData\"\n />\n </template>\n </RuiTableHead>\n <RuiTableHead\n v-if=\"stickyHeader && !isMobile\"\n :loading=\"headerLoading\"\n :capitalize-headers=\"!cols\"\n :colspan=\"colspan\"\n :column-attr=\"columnAttr\"\n :columns=\"columns\"\n :dense=\"dense\"\n :selectable=\"!!selectedData\"\n :sort-data=\"sortData\"\n :sorted-map=\"sortedMap\"\n data-id=\"head-clone\"\n class=\"opacity-0 invisible\"\n >\n <template\n v-for=\"headerSlot in headerSlots\"\n #[headerSlot]=\"slotData\"\n >\n <slot\n :name=\"headerSlot\"\n v-bind=\"slotData\"\n />\n </template>\n </RuiTableHead>\n <RuiDataTableBody\n :filtered=\"filtered\"\n :loading=\"loading\"\n :no-data=\"noData\"\n :empty=\"empty\"\n :error=\"error\"\n :error-title=\"errorTitle\"\n :retry-text=\"retryText\"\n @retry=\"emit('retry')\"\n >\n <template\n v-if=\"slots.error\"\n #error\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"error\" />\n </template>\n <template\n v-if=\"slots['body.prepend']\"\n #body.prepend=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"body.prepend\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['group.header']\"\n #group.header=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"group.header\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['group.header.content']\"\n #group.header.content=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"group.header.content\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-for=\"key in itemSlotKeys\"\n #[`item.${key}`]=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n :name=\"`item.${key}`\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['expanded-item']\"\n #expanded-item=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"expanded-item\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['row-append']\"\n #row-append=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"row-append\"\n v-bind=\"slotData\"\n />\n </template>\n <template\n v-if=\"slots['no-data']\"\n #no-data\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"no-data\" />\n </template>\n <template\n v-if=\"slots['empty-description']\"\n #empty-description\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"empty-description\" />\n </template>\n <template\n v-if=\"slots['body.append']\"\n #body.append=\"slotData\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot\n name=\"body.append\"\n v-bind=\"slotData\"\n />\n </template>\n </RuiDataTableBody>\n <tfoot\n v-if=\"slots.tfoot\"\n :class=\"ui.tfoot()\"\n data-id=\"table-tfoot\"\n >\n <!-- eslint-disable-next-line vue/require-explicit-slots -- defined via Partial<Record<...>> in defineSlots -->\n <slot name=\"tfoot\" />\n </tfoot>\n </table>\n </div>\n <!-- the bar's own place after the rows: below the view, the bar is stuck; no divider of its own -->\n <div\n v-if=\"paginationData && showPagination && !isNestedTable\"\n ref=\"paginationSentinel\"\n class=\"border-0!\"\n aria-hidden=\"true\"\n data-id=\"table-pagination-sentinel\"\n />\n <RuiTablePagination\n v-if=\"paginationData && showPagination\"\n ref=\"paginationBar\"\n v-model=\"paginationData\"\n :class=\"ui.pagination({ sticky: paginationStuck })\"\n :dense=\"dense\"\n :loading=\"loading\"\n :mobile=\"isMobile\"\n :disable-per-page=\"disablePerPage\"\n :ranges-threshold=\"rangesThreshold\"\n :data-sticky=\"paginationStuck || undefined\"\n data-id=\"table-pagination\"\n @update:model-value=\"onPaginate()\"\n />\n </div>\n</template>\n"],"mappings":""}
|
|
@@ -186,6 +186,10 @@ declare const __VLS_export: <T extends object, IdType extends keyof T = keyof T>
|
|
|
186
186
|
'no-data': () => any;
|
|
187
187
|
'empty-description': () => any;
|
|
188
188
|
error: () => any;
|
|
189
|
+
/**
|
|
190
|
+
* Rows under the body, such as a total, rendered in `<tfoot>`. Its cells take the body cells'
|
|
191
|
+
* padding, height (dense included) and text, under `:where()`, so a cell's own classes still win.
|
|
192
|
+
*/
|
|
189
193
|
tfoot: () => any;
|
|
190
194
|
}>;
|
|
191
195
|
emit: (((evt: "retry") => void) & ((evt: "update:options", value: {
|
|
@@ -9,6 +9,7 @@ import RuiDataTableBody_default from "./data-table/RuiDataTableBody.js";
|
|
|
9
9
|
import RuiDataTableMobileSort_default from "./data-table/RuiDataTableMobileSort.js";
|
|
10
10
|
import RuiTableHead_default from "./RuiTableHead.js";
|
|
11
11
|
import RuiTablePagination_default from "./RuiTablePagination.js";
|
|
12
|
+
import { useStickyBlockerWarning } from "../../composables/sticky-blocker.js";
|
|
12
13
|
import { useStickyTableHeader } from "../../composables/sticky-header.js";
|
|
13
14
|
import { useTableColumns } from "../../composables/tables/data-table/columns.js";
|
|
14
15
|
import { useTableExpansion } from "../../composables/tables/data-table/expansion.js";
|
|
@@ -249,6 +250,7 @@ var RuiDataTable_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
|
|
|
249
250
|
return element instanceof HTMLElement ? element : void 0;
|
|
250
251
|
});
|
|
251
252
|
useStickyBottomInset(paginationSentinel, paginationBarElement, paginationStuck);
|
|
253
|
+
useStickyBlockerWarning(paginationBarElement, () => get(paginationMayStick) && get(showPagination), "The RuiDataTable pagination bar");
|
|
252
254
|
/**
|
|
253
255
|
* Grouped columns leave the column list, so their labels come from `cols`.
|
|
254
256
|
*/
|
|
@@ -540,7 +542,11 @@ var RuiDataTable_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
|
|
|
540
542
|
"error-title",
|
|
541
543
|
"retry-text"
|
|
542
544
|
]),
|
|
543
|
-
|
|
545
|
+
slots.tfoot ? (openBlock(), createElementBlock("tfoot", {
|
|
546
|
+
key: 2,
|
|
547
|
+
class: normalizeClass(unref(ui).tfoot()),
|
|
548
|
+
"data-id": "table-tfoot"
|
|
549
|
+
}, [renderSlot(_ctx.$slots, "tfoot")], 2)) : createCommentVNode("", true)
|
|
544
550
|
], 14, _hoisted_3)], 10, _hoisted_2),
|
|
545
551
|
unref(paginationData) && unref(showPagination) && !unref(isNestedTable) ? (openBlock(), createElementBlock("div", {
|
|
546
552
|
key: 1,
|