Skip to content

Commit

Permalink
Incorporate design feedback for loading skeletons
Browse files Browse the repository at this point in the history
  • Loading branch information
MisRob committed Oct 15, 2024
1 parent aaac7b6 commit 4bd1184
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 64 deletions.
2 changes: 1 addition & 1 deletion docs/pages/kcard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@
mounted() {
setTimeout(() => {
this.loading = false;
}, 500);
}, 3000);
},
};
Expand Down
43 changes: 35 additions & 8 deletions docs/pages/kcardgrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,18 @@
<DocsAnchorTarget anchor="#loading-state" />
</h3>

<p>While data is loading, <code>KCardGrid</code> shows loading skeleton cards. Use the <code>loading</code> prop to toggle the loading state. <code>KCardGrid</code> ensures a minimum display time for the loading state to prevent a jarring user experience when data loads quickly.</p>
<p>While data is loading, <code>KCardGrid</code> shows loading skeleton cards. Use the <code>loading</code> prop to toggle the loading state. <code>KCardGrid</code> optimizes the loading experience by:</p>

<p>Use the <code>skeletonsConfig</code> prop to configure skeleton cards to match the expected visual output of loaded cards on all screen sizes. For easier development, enable the <code>debug</code> prop to display the current breakpoint in the top left corner of the grid. Preview the layout and height of cards with loaded data and adjust <code>skeletonsConfig</code> accordingly.</p>
<ul>
<li>The loading skeletons are not displayed for short loading times (&lt; 1s)</li>
<li>When the loading skeletons are displayed, they are visible for at least 1s</li>
</ul>

<p>Use the buttons in the example below to preview.</p>

<h4>Loading skeletons configuration</h4>

<p>Use the <code>skeletonsConfig</code> prop to configure skeleton cards to match the expected visual output of loaded cards on all screen sizes. Preview the layout and height of cards with loaded data and adjust <code>skeletonsConfig</code> accordingly.</p>

<p><code>skeletonsConfig</code> takes an array of objects <code>{ breakpoints, count, height, orientation, thumbnailDisplay, thumbnailAlign }</code>, where:</p>

Expand All @@ -608,16 +617,22 @@
<li><code>thumbnailAlign</code> sets the thumbnail alignment of skeleton cards for the specified breakpoints. Corresponds to <DocsInternalLink text="KCard's thumbnailAlign" href="/kcard#prop:thumbnailAlign" code />.</li>
</ul>

<p>For example:</p>
<p>For easier development, enable the <code>debug</code> prop to display the current breakpoint in the top left corner of the grid. Use the button in the example below to preview the debug mode.</p>

<div :style="{ display: 'flex', justifyContent: 'flex-end' }">
<KButtonGroup>
<KButton primary @click="load500">
Load (0.5 s)
</KButton>
<KButton primary @click="load1200">
Load (1.2 s)
</KButton>
<KButton primary @click="load4000">
Load (4 s)
</KButton>
<KButton @click="debug = !debug">
Debug: {{ debug ? 'On' : 'Off' }}
</KButton>
<KButton primary @click="reload">
Reload
</KButton>
</KButtonGroup>
</div>

Expand Down Expand Up @@ -888,15 +903,27 @@
mounted() {
setTimeout(() => {
this.loading = false;
}, 500);
}, 3000);
},
methods: {
reload() {
load500() {
this.loading = true;
setTimeout(() => {
this.loading = false;
}, 500);
},
load1200() {
this.loading = true;
setTimeout(() => {
this.loading = false;
}, 1200);
},
load4000() {
this.loading = true;
setTimeout(() => {
this.loading = false;
}, 4000);
},
},
};
Expand Down
15 changes: 3 additions & 12 deletions lib/cards/KCardGrid.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
<template>

<!--
Avoid displaying anything until mounting is complete
to prevent flashes of unstyled content. Otherwise, cards
or skeletons would show to users with wrong dimensions
and then would be resized to expected appearance,
creating jarring UX. It is rather some kind of global
page-level loader that should handle the loading state
before the grid is ready to render.
-->
<div
v-if="finishedMounting"
v-if="showGrid"
class="card-grid"
>
<transition name="fade" mode="out-in" appear>
Expand Down Expand Up @@ -70,7 +61,7 @@
setup(props) {
const { currentBreakpointConfig, windowBreakpoint } = useGridLayout(props);
const {
finishedMounting,
showGrid,
isLoading,
skeletonCount,
skeletonHeight,
Expand Down Expand Up @@ -114,7 +105,7 @@
windowBreakpoint,
gridStyle,
isLoading,
finishedMounting,
showGrid,
skeletonCount,
skeletonHeight,
skeletonOrientation,
Expand Down
2 changes: 1 addition & 1 deletion lib/cards/SkeletonCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
height: 100%;
content: '';
background: linear-gradient(270deg, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 100%);
animation: loading 1.8s infinite ease-in-out;
animation: loading 1.5s infinite ease-in-out;
}
</style>
104 changes: 62 additions & 42 deletions lib/cards/useGridLoading.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import Vue from 'vue';
import { ref, watch, onMounted, toRefs } from '@vue/composition-api';
import { ref, watch, onMounted, toRefs, computed } from '@vue/composition-api';

import useKResponsiveWindow from '../composables/useKResponsiveWindow';
import { getBreakpointConfig } from './utils';

// Loading state lasts for at least this duration.
const MIN_LOADING_TIME = 2000;
// The skeleton loaders will be displayed after `LOADING_DELAY`
// for a duration of `MIN_LOADING_TIME`
// (https://www.nngroup.com/articles/skeleton-screens/)
const LOADING_DELAY = 1000;
const MIN_LOADING_TIME = 1000;

const DEFAULT_SKELETON = {
count: 3,
Expand All @@ -28,58 +31,75 @@ export default function useGridLoading(props) {
const skeletonThumbnailDisplay = ref(DEFAULT_SKELETON.thumbnailDisplay);
const skeletonThumbnailAlign = ref(DEFAULT_SKELETON.thumbnailAlign);

// Used by `KCardGrid` to prevent flashes of unstyled content
const isLoading = ref(false); // Used by `KCardGrid` to determine whether to display loading skeletons
const finishedMounting = ref(false);
onMounted(() => {
Vue.nextTick(() => {
finishedMounting.value = true;
});
});

// Handles `KCardGrid`'s `loading` prop changes and returns
// final `isLoading` state to be used by `KCardGrid`.
//
// After loading started, `isLoading` ensures that
// loading state is truthy for at least `MIN_LOADING_TIME`
// to avoid unexpected flashes during the transition.
const isLoadingDelayActive = ref(false);
let loadingDelayTimeout = null;
let loadingStartTime = null;
let loadingElapsedTime = null;
let remainingLoadingTime = 0;
const isLoading = ref(false);

// Handles `KCardGrid`'s `loading` prop changes and returns
// final `isLoading` state to be used by `KCardGrid`
watch(
[loading, finishedMounting],
([newLoading, newFinishedMounting]) => {
if (newLoading === true) {
loadingStartTime = Date.now();
isLoading.value = true;
loading,
(newLoading, oldLoading) => {
if (newLoading === oldLoading) {
return;
}

// if loading started, delay it
if (newLoading) {
isLoadingDelayActive.value = true;
loadingDelayTimeout = setTimeout(() => {
loadingStartTime = Date.now();
isLoading.value = true;
isLoadingDelayActive.value = false;
}, LOADING_DELAY);
}

// if loading finished before the loading delay completed,
// cancel display of the loading state
if (!newLoading && !loadingStartTime) {
isLoadingDelayActive.value = false;
clearTimeout(loadingDelayTimeout);
}

if (newFinishedMounting && loadingStartTime) {
// if loading was started before mounting,
// move loading start time forward to
// ensure that minimum loading time is respected
loadingStartTime = Date.now();

if (newLoading === false) {
loadingElapsedTime = Date.now() - loadingStartTime;

if (loadingElapsedTime < MIN_LOADING_TIME) {
remainingLoadingTime = MIN_LOADING_TIME - loadingElapsedTime;
} else {
remainingLoadingTime = 0;
}

setTimeout(() => {
loadingStartTime = null;
isLoading.value = false;
}, remainingLoadingTime);
// if loading finished some time after the loading delay completed,
// ensure that the loading state is visible for at least `MIN_LOADING_TIME`
if (!newLoading && loadingStartTime) {
loadingElapsedTime = Date.now() - loadingStartTime;
if (loadingElapsedTime < MIN_LOADING_TIME) {
remainingLoadingTime = MIN_LOADING_TIME - loadingElapsedTime;
} else {
remainingLoadingTime = 0;
}

setTimeout(() => {
isLoading.value = false;

loadingStartTime = null;
loadingElapsedTime = null;
remainingLoadingTime = 0;
}, remainingLoadingTime);
}
},
{ immediate: true }
);

// Used by `KCardGrid` to prevent jarring UX:
// - (1) prevents flashes of unstyled content during the mouning stage
// - (2) prevents uncomplete cards from being displayed during the loading delay period
const showGrid = computed(() => {
return finishedMounting.value && !isLoadingDelayActive.value;
});

onMounted(() => {
Vue.nextTick(() => {
finishedMounting.value = true;
});
});

// Updates the loading skeleton configuration
//for the current breakpoint
watch(
Expand Down Expand Up @@ -108,7 +128,7 @@ export default function useGridLoading(props) {
);

return {
finishedMounting,
showGrid,
isLoading,
skeletonCount,
skeletonHeight,
Expand Down

0 comments on commit 4bd1184

Please sign in to comment.