This repository has been archived by the owner on Feb 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
bd626d2
commit b8f5a39
Showing
6 changed files
with
325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) | ||
}) |