From b8f5a396a737b341c1f4c2a1914c16383fb4a0ab Mon Sep 17 00:00:00 2001 From: Zack Krida Date: Fri, 3 Dec 2021 12:04:39 -0500 Subject: [PATCH] VLogoLoader (#448) Co-authored-by: Olga Bulat Co-authored-by: Krystle Salazar Co-authored-by: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> --- .../VLogoLoader/VLogoLoader.types.js | 9 + src/components/VLogoLoader/VLogoLoader.vue | 182 ++++++++++++++++++ .../VLogoLoader/meta/VLogoLoader.stories.js | 53 +++++ src/composables/use-event-listener.js | 33 ++++ src/composables/use-media-query.js | 8 + .../specs/components/v-logo-loader.spec.js | 40 ++++ 6 files changed, 325 insertions(+) create mode 100644 src/components/VLogoLoader/VLogoLoader.types.js create mode 100644 src/components/VLogoLoader/VLogoLoader.vue create mode 100644 src/components/VLogoLoader/meta/VLogoLoader.stories.js create mode 100644 src/composables/use-event-listener.js create mode 100644 test/unit/specs/components/v-logo-loader.spec.js diff --git a/src/components/VLogoLoader/VLogoLoader.types.js b/src/components/VLogoLoader/VLogoLoader.types.js new file mode 100644 index 0000000000..64db226166 --- /dev/null +++ b/src/components/VLogoLoader/VLogoLoader.types.js @@ -0,0 +1,9 @@ +export const propTypes = { + status: { + type: /** @type {'loading'|'idle'} */ (String), + default: 'idle', + required: false, + }, +} + +/** @typedef {import('@nuxtjs/composition-api').ExtractPropTypes} Props */ diff --git a/src/components/VLogoLoader/VLogoLoader.vue b/src/components/VLogoLoader/VLogoLoader.vue new file mode 100644 index 0000000000..ce4ddac143 --- /dev/null +++ b/src/components/VLogoLoader/VLogoLoader.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/src/components/VLogoLoader/meta/VLogoLoader.stories.js b/src/components/VLogoLoader/meta/VLogoLoader.stories.js new file mode 100644 index 0000000000..42d40d4107 --- /dev/null +++ b/src/components/VLogoLoader/meta/VLogoLoader.stories.js @@ -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: ` +
+ +

Remember to test with the

prefers-reduced-motion
media query. You can find instructions for doing so in Firefox here.

+
+ `, + 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: ` + + + + `, + components: { VLogoLoader }, + setup() {}, +}) + +export const LinkWrapped = LinkWrappedLoaderStory.bind({}) +LinkWrappedLoaderStory.args = { + status: 'idle', + loadingLabel: 'Loading images', +} diff --git a/src/composables/use-event-listener.js b/src/composables/use-event-listener.js new file mode 100644 index 0000000000..5ce9f30368 --- /dev/null +++ b/src/composables/use-event-listener.js @@ -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} 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) + }) +} diff --git a/src/composables/use-media-query.js b/src/composables/use-media-query.js index b84f73a5ee..4156a34989 100644 --- a/src/composables/use-media-query.js +++ b/src/composables/use-media-query.js @@ -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} */ const matches = ref(mediaQuery.matches) const handler = (/** @type MediaQueryListEvent */ event) => { @@ -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) +} diff --git a/test/unit/specs/components/v-logo-loader.spec.js b/test/unit/specs/components/v-logo-loader.spec.js new file mode 100644 index 0000000000..52d38e8e6b --- /dev/null +++ b/test/unit/specs/components/v-logo-loader.spec.js @@ -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') + }) + }) +})