Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/issue -744 - Page is scrolling while entering text input or sorting data #953

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
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
185 changes: 17 additions & 168 deletions frontend/src/components/AppTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
<template>
<!-- table data -->
<AppFlex direction="col" align-h="left" :class="['container']">
<div ref="scroll" style="width: 100%">
<div v-if="total === 0" class="emptyState">
<AppIcon icon="face-meh" size="lg" />
<div class="noResults">No matching results!</div>
</div>
<div v-else ref="scroll" style="width: 100%">
<table
class="table"
:aria-colcount="cols.length"
Expand Down Expand Up @@ -101,90 +105,6 @@
</tbody>
</table>
</div>

<div class="controls">
<!-- left side controls -->
<div>
<template v-if="showControls">
<span>Per page</span>
<AppSelectSingle
name="Rows per page"
:options="[
{ id: '5' },
{ id: '10' },
{ id: '20' },
{ id: '50' },
{ id: '100' },
{ id: '500' },
]"
:model-value="{ id: String(perPage || 5) }"
@update:model-value="(value) => emitPerPage(value.id)"
/>
</template>
</div>

<!-- center controls -->
<div>
<template v-if="showControls">
<AppButton
v-tooltip="'Go to first page'"
:disabled="start <= 0"
icon="angles-left"
design="small"
@click="clickFirst"
/>
<AppButton
v-tooltip="'Go to previous page'"
:disabled="start - perPage < 0"
icon="angle-left"
design="small"
@click="clickPrev"
/>
</template>
<template v-if="total > 0">
<span v-if="showControls"
>{{ start + 1 }} &mdash; {{ end }} of {{ total }}</span
>
<span v-else>{{ total }} row(s)</span>
</template>
<span v-else>no data</span>
<template v-if="showControls">
<AppButton
v-tooltip="'Go to next page'"
:disabled="start + perPage > total"
icon="angle-right"
design="small"
@click="clickNext"
/>
<AppButton
v-tooltip="'Go to last page'"
:disabled="start + perPage > total"
icon="angles-right"
design="small"
@click="clickLast"
/>
</template>
</div>

<!-- right side controls -->
<div>
<AppTextbox
v-if="showControls"
v-tooltip="'Search table data'"
class="search"
icon="magnifying-glass"
:model-value="search"
@debounce="emitSearch"
@change="emitSearch"
/>
<AppButton
v-tooltip="'Download table data'"
icon="download"
design="small"
@click="emitDownload"
/>
</div>
</div>
</AppFlex>
</template>

Expand Down Expand Up @@ -222,8 +142,6 @@ export type Sort<Key extends string = string> = {
import { computed, type VNode } from "vue";
import type { Options } from "./AppSelectMulti.vue";
import AppSelectMulti from "./AppSelectMulti.vue";
import AppSelectSingle from "./AppSelectSingle.vue";
import AppTextbox from "./AppTextbox.vue";

/** possible keys on datum (remove number and symbol from default object type) */
type Keys = Extract<keyof Datum, string>;
Expand All @@ -240,46 +158,23 @@ type Props = {
/** filters */
filterOptions?: { [key: string]: Options };
selectedFilters?: { [key: string]: Options };
/** items per page (two-way bound) */
perPage?: number;
/** starting item index (two-way bound) */
start?: number;

/** total number of items */
total?: number;
/** text being searched (two-way bound) */
search?: string;
/**
* whether to show certain controls (temp solution, needed b/c this is a
* controlled component and cannot paginate/search/etc on its own where needed
* yet)
*/
showControls?: boolean;
};

const props = withDefaults(defineProps<Props>(), {
sort: undefined,
filterOptions: undefined,
selectedFilters: undefined,
perPage: 5,
start: 0,
total: 0,
search: "",
showControls: true,
});

type Emits = {
/** when sort changes (two-way bound) */
"update:sort": [Props["sort"]];
/** when selected filters change (two-way bound) */
"update:selectedFilters": [Props["selectedFilters"]];
/** when per page changes (two-way bound) */
"update:perPage": [Props["perPage"]];
/** when start row changes (two-way bound) */
"update:start": [Props["start"]];
/** when search changes (two-way bound) */
"update:search": [Props["search"]];
/** when user requests download */
download: [];
};

const emit = defineEmits<Emits>();
Expand All @@ -296,26 +191,6 @@ defineSlots<{
[slot in SlotNames]: (props: SlotProps) => VNode;
}>();

/** when user clicks to first page */
function clickFirst() {
emit("update:start", 0);
}

/** when user clicks to previous page */
function clickPrev() {
emit("update:start", props.start - props.perPage);
}

/** when user clicks to next page */
function clickNext() {
emit("update:start", props.start + props.perPage);
}

/** when user clicks to last page */
function clickLast() {
emit("update:start", Math.floor(props.total / props.perPage) * props.perPage);
}

/** when user clicks a sort button */
function emitSort(col: Cols<Keys>[number]) {
let newSort: Sort<Keys>;
Expand Down Expand Up @@ -347,26 +222,6 @@ function emitFilter(colKey: Cols<Keys>[number]["key"], value: Options) {
});
}

/** when user changes rows per page */
function emitPerPage(value: string) {
emit("update:perPage", Number(value));
emit("update:start", 0);
}

/** when user types in search */
function emitSearch(value: string) {
emit("update:search", value);
emit("update:start", 0);
}

/** when user clicks download */
function emitDownload() {
emit("download");
}

/** ending item index */
const end = computed((): number => props.start + props.rows.length);

/** aria sort direction attribute */
const ariaSort = computed(() => {
if (props.sort?.direction === "up") return "ascending";
Expand Down Expand Up @@ -459,24 +314,18 @@ const ariaSort = computed(() => {
border-bottom: solid 2px $light-gray;
}

.controls {
.emptyState {
display: flex;
justify-content: space-between;
gap: 20px 40px;

& > * {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}

@media (max-width: 850px) {
flex-direction: column;
}
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-height: 18em;
gap: 1em;
}

.search {
max-width: 150px;
}
.noResults {
font-size: 1.1em;
}
</style>
16 changes: 15 additions & 1 deletion frontend/src/components/AppTextbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</template>

<script setup lang="ts">
import { ref } from "vue";
import { ref, watch } from "vue";
import AppInput from "./AppInput.vue";

defineOptions({ inheritAttrs: false });
Expand Down Expand Up @@ -86,6 +86,7 @@ defineEmits<Emits>();
const textbox = ref<HTMLDivElement>();
/** element reference */
const input = ref<InstanceType<typeof AppInput>>();
const isTyping = ref(false); // Track if user has started typing

/** clear box */
function clear() {
Expand All @@ -95,6 +96,19 @@ function clear() {
input.value.input.dispatchEvent(new Event("change"));
}

/** Detect typing and retain focus */
watch(
() => input.value?.input?.value,
(newValue) => {
if (newValue && !isTyping.value) {
isTyping.value = true; // Mark typing as started
}
if (isTyping.value && input.value?.input) {
input.value.input.focus(); // Retain focus
}
},
);

/** allow parent to access ref */
defineExpose({ textbox });
</script>
Expand Down
Loading