Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
VLogoLoader (#448)
Browse files Browse the repository at this point in the history
Co-authored-by: Olga Bulat <obulat@gmail.com>
Co-authored-by: Krystle Salazar <krystle.salazar@automattic.com>
Co-authored-by: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
  • Loading branch information
4 people authored Dec 3, 2021
1 parent bd626d2 commit b8f5a39
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/components/VLogoLoader/VLogoLoader.types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const propTypes = {
status: {
type: /** @type {'loading'|'idle'} */ (String),
default: 'idle',
required: false,
},
}

/** @typedef {import('@nuxtjs/composition-api').ExtractPropTypes<typeof propTypes>} Props */
182 changes: 182 additions & 0 deletions src/components/VLogoLoader/VLogoLoader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<template>
<svg
viewBox="0 0 34 32"
xmlns="http://www.w3.org/2000/svg"
:class="{
[$style.loading]: status === 'loading' && !prefersReducedMotion,
}"
aria-hidden="true"
:data-prefers-reduced-motion="prefersReducedMotion"
data-testid="logo-loader"
class="hover:bg-yellow w-16 h-16 p-4 rounded inline-flex justify-center items-center"
>
<path
data-logo-part-1
d="M0 6.962c0 3.831 3.09 6.962 6.916 6.962V0C3.09 0 0 3.111 0 6.962Z"
/>
<path
data-logo-part-2
d="M10.084 6.962c0 3.831 3.091 6.962 6.916 6.962V0c-3.806 0-6.916 3.111-6.916 6.962Z"
/>
<path
data-logo-part-3
d="M27.084 13.924c3.82 0 6.916-3.117 6.916-6.962C34 3.117 30.904 0 27.084 0c-3.82 0-6.916 3.117-6.916 6.962 0 3.845 3.097 6.962 6.916 6.962Z"
/>
<path
data-logo-part-4
d="M0 24.153c0 3.85 3.09 6.962 6.916 6.962V17.21C3.09 17.21 0 20.322 0 24.153Z"
/>
<path
data-logo-part-5
d="M10.084 24.095c0 3.83 3.091 6.962 6.916 6.962V17.152c-3.806 0-6.916 3.112-6.916 6.943Z"
/>
<path
data-logo-part-6
d="M27.084 31.057c3.82 0 6.916-3.117 6.916-6.962 0-3.845-3.096-6.962-6.916-6.962-3.82 0-6.916 3.117-6.916 6.962 0 3.845 3.097 6.962 6.916 6.962Z"
/>
</svg>
</template>

<script>
import { defineComponent } from '@nuxtjs/composition-api'
import { useReducedMotion } from '~/composables/use-media-query'
import { propTypes } from './VLogoLoader.types'
export default defineComponent({
name: 'VLogoLoader',
props: propTypes,
/**
* @param {import('./VLogoLoader.types').Props} props
* @param {import('@nuxtjs/composition-api').SetupContext} context
*/
setup() {
const defaultWindow = typeof window !== 'undefined' ? window : undefined
const prefersReducedMotion = useReducedMotion({ window: defaultWindow })
return { prefersReducedMotion }
},
})
</script>

<style module>
/**
Openverse Logo Loader
┌───┐ ┌───┐ ┌──────┐
│ 1 │ │ 2 │ │ 3 │
└───┘ └───┘ └──────┘
┌───┐ ┌───┐ ┌──────┐
│ 4 │ │ 5 │ │ 6 │
└───┘ └───┘ └──────┘
The logo, in goofy ascii!
The loader keyframe animation steps (the defined percentage blocks within each animation)
are tightly coupled to the cubic bezier easing curve of the full circles (parts 3 and 6),
as they move from right to left and back to their starting positions.
This easing curve determines at which point in the animation timeline the sphere will be
in a particular position. Changing it or the keyframe step percentages will break the animation.
Things you *can* change are:
1. The duration of the animation
2. The easing of parts 1,2,4, and 5 (this is the animation of the half circles shifting in from
the right after the circles 'sweep' by)
3. The size of the logo (has no bearing on the animation despite the 'magic' pixel values in our
CSS vars)
Things you can't change, without making several additional changes:
1. The --halfcircle-shift and --circle-shift variable values
2. The easing curve of the end-shift animation on parts 3 and 6
3. The keyframe percentage steps (think of these as points on a timeline of the animation, with 0%
being the start and 100% being the end)
*/
.loading {
--halfcircle-shift: 7px;
--circle-shift: -20.1px;
}
.loading > [data-logo-part-1],
.loading > [data-logo-part-4] {
animation: start-shift 2s infinite ease-in-out;
}
.loading > [data-logo-part-2],
.loading > [data-logo-part-5] {
animation: middle-shift 2s infinite ease-in-out;
}
.loading > [data-logo-part-3],
.loading > [data-logo-part-6] {
/**
Changing this cubic-bezier will break the animation and require changes to the
start-shift and middle-shift keyframe steps.
*/
animation: end-shift 2s infinite cubic-bezier(0.79, 0.14, 0.15, 0.86);
}
/* Stagger the second row so it animates slightly after the first */
.loading > [data-logo-part-4],
.loading > [data-logo-part-5],
.loading > [data-logo-part-6] {
animation-delay: 0.35s;
}
@keyframes start-shift {
0% {
visibility: visible;
transform: translateX(0);
}
50% {
visibility: hidden;
transform: translateX(0);
}
73% {
visibility: hidden;
transform: translateX(var(--halfcircle-shift));
}
79% {
visibility: visible;
transform: translateX(0);
}
}
@keyframes middle-shift {
0% {
visibility: visible;
transform: translateX(0);
}
30% {
visibility: hidden;
transform: translateX(0);
}
78.6% {
visibility: hidden;
transform: translateX(var(--halfcircle-shift));
}
86% {
visibility: visible;
transform: translateX(0);
}
}
/**
The stops at 20% and 40% here are used to simulate a 'pause' whenever the spheres
reach the start or end of the logo. CSS's built in animation-delay property only
works on the first cycle of an animation, not between subsequent runs.
*/
@keyframes end-shift {
0%,
20% {
transform: translateX(0);
}
40%,
50% {
transform: translateX(var(--circle-shift));
}
}
</style>
53 changes: 53 additions & 0 deletions src/components/VLogoLoader/meta/VLogoLoader.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import VLogoLoader from '~/components/VLogoLoader/VLogoLoader'

export default {
component: VLogoLoader,
title: 'Components/VLogoLoader',
argTypes: {
status: {
default: 'idle',
options: ['loading', 'idle'],
control: { type: 'radio' },
},
},
}

const SimpleLoaderStory = (_, { argTypes }) => ({
props: Object.keys(argTypes),
template: `
<div>
<VLogoLoader v-bind="$props" />
<p>Remember to test with the <code class="inline p-0"><pre class="inline bg-light-gray p-0 m-0 leading-normal">prefers-reduced-motion</pre></code> media query. You can find instructions for doing so in Firefox <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences">here.</a></p>
</div>
`,
components: { VLogoLoader },
setup() {},
})

export const Default = SimpleLoaderStory.bind({})
Default.args = {
status: 'idle',
}

export const Loading = SimpleLoaderStory.bind({})
Loading.args = {
status: 'loading',
loadingLabel: 'Loading images',
}

const LinkWrappedLoaderStory = (_, { argTypes }) => ({
props: Object.keys(argTypes),
template: `
<a class="focus:outline-none focus:ring focus:ring-offset-2 focus:ring-pink inline-block" href='https://wordpress.org/openverse'>
<VLogoLoader v-bind="$props" />
</a>
`,
components: { VLogoLoader },
setup() {},
})

export const LinkWrapped = LinkWrappedLoaderStory.bind({})
LinkWrappedLoaderStory.args = {
status: 'idle',
loadingLabel: 'Loading images',
}
33 changes: 33 additions & 0 deletions src/composables/use-event-listener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
isRef,
watch,
onMounted,
onBeforeUnmount,
unref,
} from '@nuxtjs/composition-api'

/**
* Use an event listener. Shamelessly stolen from https://logaretm.com/blog/my-favorite-5-vuejs-composables/#useeventlistener
*
* @param {import('@nuxtjs/composition-api').Ref<EventTarget | null> | EventTarget} target The target can be a reactive ref which adds flexibility
* @param {string} event
* @param {(e: Event) => void} handler
*/
export function useEventListener(target, event, handler) {
// if its a reactive ref, use a watcher
if (isRef(target)) {
watch(target, (value, oldValue) => {
oldValue?.removeEventListener(event, handler)
value?.addEventListener(event, handler)
})
} else {
// otherwise use the mounted hook
onMounted(() => {
target.addEventListener(event, handler)
})
}
// clean it up
onBeforeUnmount(() => {
unref(target)?.removeEventListener(event, handler)
})
}
8 changes: 8 additions & 0 deletions src/composables/use-media-query.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function useMediaQuery(query, options = {}) {
if (!window) return ref(false)

const mediaQuery = window.matchMedia(query)
/** @type {import('@nuxtjs/composition-api').Ref<boolean>} */
const matches = ref(mediaQuery.matches)

const handler = (/** @type MediaQueryListEvent */ event) => {
Expand All @@ -36,3 +37,10 @@ export function useMediaQuery(query, options = {}) {

return matches
}

/**
* Check if the user prefers reduced motion or not.
*/
export function useReducedMotion(options = {}) {
return useMediaQuery('(prefers-reduced-motion: reduce)', options)
}
40 changes: 40 additions & 0 deletions test/unit/specs/components/v-logo-loader.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/vue'
import VLogoLoader from '~/components/VLogoLoader/VLogoLoader.vue'
import { useReducedMotion } from '~/composables/use-media-query'

jest.mock('~/utils/warn', () => ({
warn: jest.fn(),
}))

jest.mock('~/composables/use-media-query', () => ({
useReducedMotion: jest.fn(),
}))

describe('VLogoLoader', () => {
it('should render the logo', () => {
render(VLogoLoader)
const element = screen.getByTestId('logo-loader')
expect(element).toBeInTheDocument()
})

describe('accessibility', () => {
it('should render differently when the user prefers reduced motion', () => {
useReducedMotion.mockImplementation(() => true)

render(VLogoLoader, {
props: { status: 'loading' },
})
const element = screen.getByTestId('logo-loader')
expect(element).toHaveAttribute('data-prefers-reduced-motion', 'true')
})
it('should show the default loading style when no motion preference is set', () => {
useReducedMotion.mockImplementation(() => false)

render(VLogoLoader, {
props: { status: 'loading' },
})
const element = screen.getByTestId('logo-loader')
expect(element).not.toHaveAttribute('data-prefers-reduced-motion')
})
})
})

0 comments on commit b8f5a39

Please sign in to comment.