@featherds/menu
Advanced tools
Comparing version 0.9.6 to 0.10.0
import { getSafeId } from "@featherds/utils/id"; | ||
import { ref, computed, watch, openBlock, createElementBlock, renderSlot, withDirectives, createElementVNode, normalizeClass, vShow, nextTick } from "vue"; | ||
import { ref, computed, watch, openBlock, createElementBlock, renderSlot, createBlock, Teleport, withDirectives, createElementVNode, normalizeClass, normalizeStyle, vShow, nextTick } from "vue"; | ||
import { useResize } from "@featherds/composables/events/Resize"; | ||
import { useOutsideClick } from "@featherds/composables/events/OutsideClick"; | ||
import { useResize } from "@featherds/composables/events/Resize"; | ||
import { useScroll } from "@featherds/composables/events/Scroll"; | ||
var FeatherMenu_vue_vue_type_style_index_0_lang = ""; | ||
var FeatherMenu_vue_vue_type_style_index_1_scoped_true_lang = ""; | ||
import { getElements, addLayer, removeLayer } from "@featherds/composables/modal/Layers"; | ||
var FeatherMenu_vue_vue_type_style_index_0_scoped_true_lang = ""; | ||
var _export_sfc = (sfc, props) => { | ||
@@ -16,3 +16,3 @@ const target = sfc.__vccOpts || sfc; | ||
const _sfc_main = { | ||
emits: ["outside-click", "trigger-click", "close"], | ||
emits: ["trigger-click", "close", "outside-click"], | ||
props: { | ||
@@ -34,2 +34,6 @@ open: { | ||
default: false | ||
}, | ||
hasFocus: { | ||
type: Boolean, | ||
default: false | ||
} | ||
@@ -41,47 +45,22 @@ }, | ||
const menu = ref(null); | ||
const scrollX = ref(null); | ||
const scrollY = ref(null); | ||
const menuWidth = ref("auto"); | ||
const windowRef = ref(window); | ||
const triggerId = ref(getSafeId("feather-menu-trigger")); | ||
const menuId = ref(getSafeId("feather-menu-dropdown")); | ||
const position = ref(`bottom-${props.right ? "right" : "left"}`); | ||
const covered = computed(() => props.cover ? "covered" : ""); | ||
const updateScrollElements = () => { | ||
let node = root.value.parentNode; | ||
scrollX.value = null; | ||
scrollY.value = null; | ||
while (node) { | ||
if (node === document.body) { | ||
if (!scrollX.value) { | ||
scrollX.value = document; | ||
} | ||
if (!scrollY.value) { | ||
scrollY.value = document; | ||
} | ||
return; | ||
} | ||
if (!scrollY.value && node.scrollHeight > node.clientHeight) { | ||
const overflowYStyle = window.getComputedStyle(node).overflowY; | ||
if (overflowYStyle.indexOf("hidden") === -1 && overflowYStyle.indexOf("visible") === -1) { | ||
scrollY.value = node; | ||
} | ||
} | ||
if (!scrollX.value && node.scrollWidth > node.clientWidth) { | ||
const overflowXStyle = window.getComputedStyle(node).overflowX; | ||
if (overflowXStyle.indexOf("hidden") === -1 && overflowXStyle.indexOf("visible") === -1) { | ||
scrollX.value = node; | ||
} | ||
} | ||
if (scrollY.value && scrollX.value) { | ||
return; | ||
} | ||
node = node.parentNode; | ||
} | ||
const position = ref(""); | ||
const scrollTop = () => { | ||
if (!document) | ||
return 0; | ||
return (document.documentElement || document.body).scrollTop; | ||
}; | ||
const getScrollRect = (element) => { | ||
if (element && element.getBoundingClientRect) { | ||
return element.getBoundingClientRect(); | ||
} | ||
const scrollLeft = () => { | ||
if (!document) | ||
return 0; | ||
return (document.documentElement || document.body).scrollLeft; | ||
}; | ||
const calculating = ref(false); | ||
const getScrollRect = () => { | ||
return { | ||
height: window.innerHeight, | ||
width: window.innerWidth, | ||
height: windowRef.value.innerHeight, | ||
width: windowRef.value.innerWidth, | ||
left: 0, | ||
@@ -92,47 +71,72 @@ top: 0 | ||
const calculatePosition = () => { | ||
updateScrollElements(); | ||
const rect = trigger.value.getBoundingClientRect(); | ||
if (!menu.value) { | ||
return; | ||
} | ||
const containerRect = root.value.getBoundingClientRect(); | ||
calculating.value = true; | ||
nextTick(() => { | ||
const { height, width } = menu.value.getBoundingClientRect(); | ||
const scrollYRect = getScrollRect(scrollY.value); | ||
let scrollXRect = scrollYRect; | ||
if (scrollY.value !== scrollX.value) { | ||
scrollXRect = getScrollRect(scrollX.value); | ||
let { height, width } = menu.value.getBoundingClientRect(); | ||
const windowRect = getScrollRect(); | ||
const scrollHeight = windowRect.height; | ||
const scrollWidth = windowRect.width; | ||
if (width < containerRect.width) { | ||
menuWidth.value = containerRect.width + "px"; | ||
width = containerRect.width; | ||
} else { | ||
menuWidth.value = width + "px"; | ||
} | ||
const scrollHeight = scrollYRect.height; | ||
const scrollWidth = scrollXRect.width; | ||
const adjustedRect = { | ||
bottom: rect.bottom - scrollYRect.top, | ||
top: rect.top - scrollYRect.top, | ||
left: rect.left - scrollXRect.left, | ||
right: rect.right - scrollXRect.left | ||
}; | ||
let vertical = "bottom"; | ||
if (scrollHeight - adjustedRect.bottom < height && adjustedRect.top >= height) { | ||
vertical = "top"; | ||
let top = 0; | ||
if (scrollHeight - containerRect.bottom < height && containerRect.top >= height) { | ||
top = containerRect.top - height; | ||
if (props.cover) { | ||
top += containerRect.height; | ||
} | ||
} else { | ||
top = containerRect.bottom; | ||
if (props.cover) { | ||
top -= containerRect.height; | ||
} | ||
} | ||
let horizontal = props.right ? "right" : "left"; | ||
if (!props.right && adjustedRect.right >= width && scrollWidth - adjustedRect.left < width) { | ||
horizontal = "right"; | ||
top += scrollTop(); | ||
let left = props.right ? containerRect.right - width : containerRect.left; | ||
if (!props.right && containerRect.right >= width && scrollWidth - containerRect.left < width) { | ||
left = containerRect.right - width; | ||
} | ||
if (props.right && adjustedRect.right <= width && scrollWidth - adjustedRect.left > width) { | ||
horizontal = "left"; | ||
if (props.right && containerRect.right <= width && scrollWidth - containerRect.left > width) { | ||
left = containerRect.left; | ||
} | ||
position.value = `${vertical}-${horizontal}`; | ||
left += scrollLeft(); | ||
position.value = `translate(${left}px, ${top}px)`; | ||
calculating.value = false; | ||
}); | ||
}; | ||
const close = (e) => { | ||
if (layers.value.some((el) => el.contains(e.target))) { | ||
return; | ||
} | ||
context.emit("close", false); | ||
}; | ||
const outsideElementEvent = (e) => { | ||
context.emit("outside-click", e); | ||
}; | ||
const activateOutsideClick = useOutsideClick(root, outsideElementEvent); | ||
const activateResize = useResize(calculatePosition); | ||
const activateScrollX = useScroll(scrollX, calculatePosition); | ||
const activateScrollY = useScroll(scrollY, calculatePosition); | ||
watch(() => props.open, (v) => { | ||
if (v) { | ||
const layer = ref(null); | ||
const layers = computed(() => { | ||
if (layer.value) { | ||
return [root.value, ...getElements(layer.value).value]; | ||
} | ||
return [root.value]; | ||
}); | ||
const activateOutsideClick = useOutsideClick(layers, outsideElementEvent); | ||
const activateResize = useResize(close); | ||
const activateScrollY = useScroll(windowRef, close); | ||
watch([() => props.open, menu], ([v, m]) => { | ||
if (v && m && !layer.value) { | ||
calculatePosition(); | ||
layer.value = addLayer(menu, "dropdown"); | ||
} else if (!v && layer.value) { | ||
removeLayer(layer.value); | ||
layer.value = null; | ||
} | ||
activateOutsideClick.value = v; | ||
activateResize.value = v; | ||
activateScrollX.value = v; | ||
activateScrollY.value = v; | ||
@@ -160,11 +164,16 @@ }); | ||
}); | ||
const handleFocusOut = () => { | ||
context.emit("close"); | ||
}; | ||
return { | ||
position, | ||
covered, | ||
triggerId, | ||
menuId, | ||
menu, | ||
menuWidth, | ||
root, | ||
trigger, | ||
calculatePosition | ||
calculatePosition, | ||
handleFocusOut, | ||
calculating | ||
}; | ||
@@ -181,14 +190,25 @@ } | ||
renderSlot(_ctx.$slots, "trigger", {}, void 0, true), | ||
withDirectives(createElementVNode("div", { | ||
class: normalizeClass(["feather-menu-dropdown", [$setup.position, $setup.covered]]), | ||
ref: "menu", | ||
id: $setup.menuId | ||
}, [ | ||
renderSlot(_ctx.$slots, "default", { labelId: $setup.triggerId }, void 0, true) | ||
], 10, _hoisted_2), [ | ||
[vShow, $props.open] | ||
]) | ||
(openBlock(), createBlock(Teleport, { to: "body" }, [ | ||
withDirectives(createElementVNode("div", { | ||
class: normalizeClass(["feather-menu-dropdown", { hidden: $setup.calculating }]), | ||
ref: "menu", | ||
id: $setup.menuId, | ||
style: normalizeStyle({ transform: $setup.position, width: $setup.menuWidth }) | ||
}, [ | ||
createElementVNode("div", { | ||
tabindex: "0", | ||
onFocus: _cache[0] || (_cache[0] = (...args) => $setup.handleFocusOut && $setup.handleFocusOut(...args)) | ||
}, null, 32), | ||
renderSlot(_ctx.$slots, "default", { labelId: $setup.triggerId }, void 0, true), | ||
createElementVNode("div", { | ||
tabindex: "0", | ||
onFocus: _cache[1] || (_cache[1] = (...args) => $setup.handleFocusOut && $setup.handleFocusOut(...args)) | ||
}, null, 32) | ||
], 14, _hoisted_2), [ | ||
[vShow, $props.open] | ||
]) | ||
])) | ||
], 512); | ||
} | ||
var FeatherMenu = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-0e29b938"]]); | ||
var FeatherMenu = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-02a45add"]]); | ||
var MenuFocusLoop = { | ||
@@ -195,0 +215,0 @@ mounted(el, binding) { |
{ | ||
"name": "@featherds/menu", | ||
"version": "0.9.6", | ||
"version": "0.10.0", | ||
"publishConfig": { | ||
@@ -12,6 +12,6 @@ "access": "public" | ||
"dependencies": { | ||
"@featherds/composables": "^0.9.6", | ||
"@featherds/ripple": "^0.9.6", | ||
"@featherds/styles": "^0.9.6", | ||
"@featherds/utils": "^0.9.6", | ||
"@featherds/composables": "^0.10.0", | ||
"@featherds/ripple": "^0.10.0", | ||
"@featherds/styles": "^0.10.0", | ||
"@featherds/utils": "^0.10.0", | ||
"vue": "^3.1.0-0" | ||
@@ -24,3 +24,3 @@ }, | ||
"types": "./src/index.d.ts", | ||
"gitHead": "04a74207e8bfa8d39acc470365e30dd6c90b2e8e" | ||
"gitHead": "9871b17eaedcfc90174b78b21acb0cc06afd594c" | ||
} |
@@ -98,12 +98,3 @@ import { nextTick } from "vue"; | ||
}); | ||
it("should add covered class for cover property", async () => { | ||
const slots = { | ||
trigger: getTrigger(), | ||
default: [getContent()], | ||
}; | ||
const wrapper = await getWrapper({ slots, props: { cover: true } }); | ||
expect( | ||
wrapper.find(".feather-menu-dropdown").classes().includes("covered") | ||
).toBe(true); | ||
}); | ||
describe("calculatePosition", () => { | ||
@@ -126,6 +117,3 @@ describe("right", () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -141,3 +129,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("bottom-right"); | ||
expect(wrapper.vm.position).toBe("translate(120px, 60px)"); | ||
}); | ||
@@ -162,6 +150,3 @@ it("should flip to top right if not bottom space and enough top space", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -177,3 +162,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("top-right"); | ||
expect(wrapper.vm.position).toBe("translate(120px, 678px)"); | ||
}); | ||
@@ -197,6 +182,3 @@ it("should flip to bottom left if not right space and enough left space", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -212,3 +194,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("bottom-left"); | ||
expect(wrapper.vm.position).toBe("translate(30px, 60px)"); | ||
}); | ||
@@ -234,6 +216,3 @@ it("should stay bottom right if not enough room anywhere", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -249,3 +228,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("bottom-right"); | ||
expect(wrapper.vm.position).toBe("translate(974px, 718px)"); | ||
}); | ||
@@ -271,6 +250,3 @@ it("should flip to top left if there is no space right and bottom but enough top and left", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -286,3 +262,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("top-left"); | ||
expect(wrapper.vm.position).toBe("translate(10px, 678px)"); | ||
}); | ||
@@ -306,6 +282,3 @@ }); | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -321,3 +294,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("bottom-left"); | ||
expect(wrapper.vm.position).toBe("translate(50px, 60px)"); | ||
}); | ||
@@ -342,6 +315,3 @@ it("should flip to top left if not bottom space and enough top space", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -357,3 +327,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("top-left"); | ||
expect(wrapper.vm.position).toBe("translate(50px, 678px)"); | ||
}); | ||
@@ -378,6 +348,3 @@ it("should flip to bottom right if not right space and enough right space", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -393,3 +360,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("bottom-right"); | ||
expect(wrapper.vm.position).toBe("translate(994px, 60px)"); | ||
}); | ||
@@ -415,6 +382,3 @@ it("should stay bottom left if not enough room anywhere", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -430,3 +394,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("bottom-left"); | ||
expect(wrapper.vm.position).toBe("translate(50px, 718px)"); | ||
}); | ||
@@ -452,6 +416,3 @@ it("should flip to top right if there is no space right and bottom but enough top and left", async () => { | ||
jest | ||
.spyOn( | ||
wrapper.find("[menu-trigger]").element, | ||
"getBoundingClientRect" | ||
) | ||
.spyOn(wrapper.vm.$refs.root, "getBoundingClientRect") | ||
.mockImplementation(() => { | ||
@@ -467,3 +428,3 @@ return { | ||
await nextTick(); | ||
expect(wrapper.vm.position).toBe("top-right"); | ||
expect(wrapper.vm.position).toBe("translate(994px, 678px)"); | ||
}); | ||
@@ -479,3 +440,3 @@ }); | ||
const wrapper = await getWrapper({ slots }); | ||
await wrapper.setProps({ open: true }); | ||
wrapper.setProps({ open: true }); | ||
expect(await axe(wrapper.element)).toHaveNoViolations(); | ||
@@ -482,0 +443,0 @@ }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
43388
786
+ Added@featherds/composables@0.10.17(transitive)
+ Added@featherds/ripple@0.10.17(transitive)
+ Added@featherds/styles@0.10.17(transitive)
+ Added@featherds/utils@0.10.17(transitive)
- Removed@featherds/composables@0.9.6(transitive)
- Removed@featherds/ripple@0.9.6(transitive)
- Removed@featherds/styles@0.9.6(transitive)
- Removed@featherds/utils@0.9.6(transitive)
Updated@featherds/ripple@^0.10.0
Updated@featherds/styles@^0.10.0
Updated@featherds/utils@^0.10.0