Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 159 additions & 36 deletions internal/vue-sandbox/src/views/DefaultView.vue
Original file line number Diff line number Diff line change
@@ -1,52 +1,175 @@
<script lang="ts">
import { defineComponent } from "vue";
import { FTextField } from "@fkui/vue";
<script setup lang="ts">
import { type Ref, computed, ref, useTemplateRef } from "vue";
import { type ContextMenuItem, FContextMenu, FDatepickerField } from "@fkui/vue";
import { FTable, defineTableColumns } from "@fkui/vue-labs";

export default defineComponent({
components: { FTextField },
data() {
return {
awesomeModel: "",
};
interface Row {
id: string;
text1: string;
text2: string;
text3: string;
}

const columns = defineTableColumns<Row>([
{
type: "text",
header: "ID",
key: "id",
},
{
type: "text",
header: "Text 1",
key: "text1",
},
{
header: "Åtgärd",
type: "menu",
text(row) {
return `Visa åtgärder för rad "${row.id}"`;
},
actions: [
{
label: "Visa",
icon: "search",
onClick(row) {
window.alert(`Visa detaljer för rad "${row.id}"`);
},
},
{
label: "Redigera",
icon: "pen",
onClick(row) {
window.alert(`Redigera rad "${row.id}"`);
},
},
{
label: "Ta bort",
icon: "trashcan",
onClick(row) {
window.alert(`Ta bort rad "${row.id}"`);
},
},
],
},
{
type: "text",
header: "Text 2",
key: "text2",
},
{
type: "text",
header: "Text 3",
key: "text3",
},
]);

const rows = [
{
id: "1",
text1: "Text 1",
text2: "Text 2",
text3: "Text 3",
},
{
id: "2",
text1: "Text 4",
text2: "Text 5",
text3: "Text 6",
},
{
id: "3",
text1: "Text 7",
text2: "Text 8",
text3: "Text 9",
},
];

const items: Ref<ContextMenuItem[]> = ref([
{ label: "Påminnelse", key: "MENU_2", icon: "bell" },
{ label: "Ändra", key: "MENU_3", icon: "pen" },
{ separator: true },
{ label: "Ta bort", key: "MENU_4", icon: "trashcan" },
{ label: "Utan ikon", key: "MENU_5" },
]);
const selected = ref("");
const isOpen = ref(false);

const anch = useTemplateRef("popupAnchor");

const anchor = computed(() => {
return anch.value as HTMLElement;
});

function onClose(): void {
isOpen.value = false;
}
function onClick(): void {
isOpen.value = !isOpen.value;
}
function onSelect(value: string): void {
selected.value = value;
}
</script>

<template>
<div class="sandbox-root">
<h1>FKUI Sandbox</h1>
<p>
Ett internt paket som innehåller en avskalad Vue-applikation. Applikationen är konsument av övriga
FKUI-paket och innehåller enbart ett tomt exempel.
</p>
<p>
<strong>Ändra och labba gärna här men glöm inte återställa innan merge!</strong>
</p>
<hr />
<f-text-field
id="awesome-field"
v-model="awesomeModel"
v-validation.required.maxLength="{ maxLength: { length: 10 } }"
>
<template #default> Inmatningsfält. </template>
<template #description="{ descriptionClass }">
<span :class="descriptionClass"> Lorem ipsum dolor sit amet. </span>
</template>
</f-text-field>
<div class="sandbox-container">
<div class="space-box"></div>
<div class="table-container">
<div class="table-container-inner">
<h2>FTable</h2>
<f-table ref="table" :rows :columns striped></f-table>
</div>
</div>
<div class="menu-container">
<h2>FContextMenu</h2>
<button
ref="popupAnchor"
data-test="open-example-contextmenu-button"
type="button"
class="button button--primary"
aria-haspopup="menu"
@click="onClick"
>
Öppna
</button>
<pre>Selected: {{ selected }}</pre>
<f-context-menu
data-test="contextmenu-open"
:is-open
:items
:anchor
@close="onClose"
@select="onSelect"
></f-context-menu>
<h2>FDatepickerField</h2>
<f-datepicker-field></f-datepicker-field>
</div>
<div class="space-box"></div>
</div>
</template>

<style>
.sandbox-root {
width: min(100% - 2rem, 80ch);
margin: auto;
.sandbox-container {
min-height: 200vh;
min-width: 200vw;
}

.menu-container {
margin-left: 600px;
width: 400px;
}

.table-container {
margin-left: 600px;
width: 800px;
overflow: scroll;
}

h1 {
margin-top: 2rem;
.table-container-inner {
width: 1200px;
}

hr {
margin-bottom: 2rem;
.space-box {
height: 400px;
}
</style>
34 changes: 32 additions & 2 deletions packages/vue/src/internal-components/IPopup/IPopup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,27 +159,35 @@ export default defineComponent({
document.addEventListener("click", this.onDocumentClickHandler);
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
window.addEventListener("resize", this.onWindowResizeDebounced);
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
window.addEventListener("scroll", this.onScrollDebounced, { capture: true });
}
}, 0);
} else {
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
document.removeEventListener("click", this.onDocumentClickHandler);
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
window.removeEventListener("resize", this.onWindowResizeDebounced);
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
window.removeEventListener("scroll", this.onScrollDebounced, { capture: true });
}
},
},
},
created() {
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
this.onWindowResizeDebounced = debounce(this.onWindowResize, 100).bind(this);
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
this.onScrollDebounced = debounce(this.onScroll, 100).bind(this);
},
unmounted() {
// Clean up if unmounted but still opened
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
document.removeEventListener("click", this.onDocumentClickHandler);
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
window.removeEventListener("resize", this.onWindowResizeDebounced);
/* eslint-disable-next-line @typescript-eslint/unbound-method -- technical debt */
window.removeEventListener("scroll", this.onScrollDebounced, { capture: true });
},
methods: {
async toggleIsOpen(isOpen: boolean): Promise<void> {
Expand All @@ -202,7 +210,7 @@ export default defineComponent({
this.applyFocus();
this.$emit("open");
},
async calculatePlacement(): Promise<void> {
async calculatePlacement(options?: { horizontalOnly: boolean }): Promise<void> {
const popup = getHTMLElementFromVueRef(this.$refs.popup);
const wrapper = getHTMLElementFromVueRef(this.$refs.wrapper);
const anchor = getElement(this.anchor);
Expand All @@ -229,6 +237,10 @@ export default defineComponent({
const useOverlay = this.forceOverlay || result.placement !== Placement.Fallback;
if (useOverlay) {
wrapper.style.left = `${String(result.x)}px`;
if (options?.horizontalOnly) {
return;
}

wrapper.style.top = `${String(result.y)}px`;
return;
}
Expand Down Expand Up @@ -279,7 +291,25 @@ export default defineComponent({
// Overwritten in created so that the debounced `onWindowResize`
// method can be removed by removeEventListener.
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Need to match actual `onScroll` method.
onScrollDebounced(event: Event): void {
// Overwritten in created so that the debounced `onScroll`
// method can be removed by removeEventListener.
},
async onWindowResize(): Promise<void> {
await this.recalculatePlacement();
},
async onScroll(event: Event): Promise<void> {
if (this.isInline) {
return;
}
const isPopupTarget = event.target instanceof HTMLElement && Boolean(event.target.closest(".popup"));
if (isPopupTarget) {
return;
}
await this.recalculatePlacement({ horizontalOnly: true });
},
async recalculatePlacement(options?: { horizontalOnly: boolean }): Promise<void> {
// Abort if popup was closed during debounce.
if (!this.isOpen) {
return;
Expand All @@ -300,7 +330,7 @@ export default defineComponent({
await this.$nextTick();
}

await this.calculatePlacement();
await this.calculatePlacement(options);
const { placement, forceInline, forceOverlay } = this;
this.teleportDisabled = isTeleportDisabled({ window, placement, forceInline, forceOverlay });
},
Expand Down
Loading