diff --git a/packages/peregrine/lib/talons/CategoryList/useCategoryList.js b/packages/peregrine/lib/talons/CategoryList/useCategoryList.js
new file mode 100644
index 0000000000..033e1060e2
--- /dev/null
+++ b/packages/peregrine/lib/talons/CategoryList/useCategoryList.js
@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+import { useQuery } from '../../hooks/useQuery';
+
+/**
+ * Returns props necessary to render a CategoryList component.
+ *
+ * @param {object} props
+ * @param {object} props.query - category data
+ * @param {string} props.id - category id
+ * @return {{ childCategories: array, error: object }}
+ */
+export const useCategoryList = props => {
+ const { query, id } = props;
+ const [queryResult, queryApi] = useQuery(query);
+ const { data, error, loading } = queryResult;
+ const { runQuery, setLoading } = queryApi;
+
+ useEffect(() => {
+ const fetchCategories = async () => {
+ setLoading(true);
+
+ await runQuery({
+ variables: {
+ id
+ }
+ });
+
+ setLoading(false);
+ };
+
+ fetchCategories();
+ }, [runQuery, setLoading, id]);
+
+ return {
+ childCategories:
+ (data && data.category && data.category.children) || null,
+ error,
+ loading
+ };
+};
diff --git a/packages/peregrine/lib/talons/CategoryList/useCategoryTile.js b/packages/peregrine/lib/talons/CategoryList/useCategoryTile.js
new file mode 100644
index 0000000000..76ca21fab3
--- /dev/null
+++ b/packages/peregrine/lib/talons/CategoryList/useCategoryTile.js
@@ -0,0 +1,49 @@
+import { useMemo } from 'react';
+
+// TODO: get categoryUrlSuffix from graphql storeOptions when it is ready
+const categoryUrlSuffix = '.html';
+const previewImageSize = 480;
+
+/**
+ * Returns props necessary to render a CategoryTile component.
+ *
+ * @returns {Object} props necessary to render a category tile
+ * @returns {Object} .image - an object containing url, type and width for the category image
+ * @returns {Object} .item - an object containing name and url for the category tile
+ */
+export const useCategoryTile = props => {
+ const { item } = props;
+ const { image, productImagePreview } = item;
+
+ const imageObj = useMemo(() => {
+ const previewProduct = productImagePreview.items[0];
+ if (image) {
+ return {
+ url: image,
+ type: 'image-category',
+ width: previewImageSize
+ };
+ } else if (previewProduct) {
+ return {
+ url: previewProduct.small_image,
+ type: 'image-product',
+ width: previewImageSize
+ };
+ } else {
+ return null;
+ }
+ }, [image, productImagePreview]);
+
+ const itemObject = useMemo(
+ () => ({
+ name: item.name,
+ url: `/${item.url_key}${categoryUrlSuffix}`
+ }),
+ [item]
+ );
+
+ return {
+ image: imageObj,
+ item: itemObject
+ };
+};
diff --git a/packages/venia-ui/lib/components/CategoryList/__tests__/categoryList.spec.js b/packages/venia-ui/lib/components/CategoryList/__tests__/categoryList.spec.js
index 7c2f8ef038..13ea173bc1 100644
--- a/packages/venia-ui/lib/components/CategoryList/__tests__/categoryList.spec.js
+++ b/packages/venia-ui/lib/components/CategoryList/__tests__/categoryList.spec.js
@@ -1,31 +1,44 @@
import React from 'react';
-import waitForExpect from 'wait-for-expect';
-import TestRenderer from 'react-test-renderer';
-import { MemoryRouter } from 'react-router-dom';
-import { MockedProvider } from 'react-apollo/test-utils';
+import { createTestInstance } from '@magento/peregrine';
import LoadingIndicator from '../../LoadingIndicator';
import CategoryTile from '../categoryTile';
import CategoryList from '../categoryList';
-import getCategoryList from '../../../queries/getCategoryList.graphql';
+import { useCategoryList } from '@magento/peregrine/lib/talons/CategoryList/useCategoryList';
+import { useCategoryTile } from '@magento/peregrine/lib/talons/CategoryList/useCategoryTile';
+jest.mock('@magento/venia-drivers');
jest.mock('../../../classify');
+jest.mock('@magento/peregrine/lib/talons/CategoryList/useCategoryTile', () => {
+ return {
+ useCategoryTile: jest.fn()
+ };
+});
+
+jest.mock('@magento/peregrine/lib/talons/CategoryList/useCategoryList', () => {
+ return {
+ useCategoryList: jest.fn()
+ };
+});
-const withRouterAndApolloClient = (mocks, renderFn) => (
-
-
- {renderFn()}
-
-
-);
+useCategoryTile.mockReturnValue({
+ image: {},
+ item: {}
+});
+
+useCategoryList.mockReturnValue({
+ data: {
+ category: {
+ children: []
+ }
+ },
+ loading: false,
+ error: false
+});
test('renders a header', () => {
const title = 'foo';
- const { root } = TestRenderer.create(
- withRouterAndApolloClient([], () => (
-
- ))
- );
+ const { root } = createTestInstance();
const list = root.findByProps({ className: 'root' });
const header = list.findByProps({ className: 'header' });
@@ -35,91 +48,83 @@ test('renders a header', () => {
});
test('omits the header if there is no title', () => {
- const { root } = TestRenderer.create(
- withRouterAndApolloClient([], () => )
- );
+ const { root } = createTestInstance();
expect(root.findAllByProps({ className: 'header' })).toHaveLength(0);
});
-test('renders category tiles', async () => {
- const mocks = [
- {
- request: {
- query: getCategoryList,
- variables: {
- id: 2
- }
- },
- result: {
- data: {
- category: {
- id: 2,
- children: [
- {
- id: 15,
- name: 'foo',
- url_key: 'foo-url.html',
- url_path: '/foo-url.html',
- children_count: 0,
- path: '1/2/15',
- image: 'media/foo.png',
- productImagePreview: {
- items: [
- {
- small_image: {
- url: 'media/foo-product.jpg'
- }
- }
- ]
- }
- },
+test('renders a loading indicator', () => {
+ useCategoryList.mockReturnValueOnce({
+ loading: true
+ });
+
+ const { root } = createTestInstance();
+
+ expect(root.findAllByType(LoadingIndicator)).toBeTruthy();
+});
+
+test('renders category tiles', () => {
+ const data = {
+ category: {
+ id: 2,
+ children: [
+ {
+ id: 15,
+ name: 'foo',
+ url_key: 'foo-url.html',
+ url_path: '/foo-url.html',
+ children_count: 0,
+ path: '1/2/15',
+ image: 'media/foo.png',
+ productImagePreview: {
+ items: [
{
- id: 16,
- name: 'bar',
- url_key: 'bar-url.html',
- url_path: '/bar-url.html',
- children_count: 0,
- path: '1/2/16',
- image: null,
- productImagePreview: {
- items: [
- {
- small_image: {
- url: 'media/bar-product.jpg'
- }
- }
- ]
+ small_image: {
+ url: 'media/foo-product.jpg'
}
- },
+ }
+ ]
+ }
+ },
+ {
+ id: 16,
+ name: 'bar',
+ url_key: 'bar-url.html',
+ url_path: '/bar-url.html',
+ children_count: 0,
+ path: '1/2/16',
+ image: null,
+ productImagePreview: {
+ items: [
{
- id: 17,
- name: 'baz',
- url_key: 'baz-url.html',
- url_path: '/baz-url.html',
- children_count: 0,
- path: '1/2/17',
- image: null,
- productImagePreview: {
- items: []
+ small_image: {
+ url: 'media/bar-product.jpg'
}
}
]
}
+ },
+ {
+ id: 17,
+ name: 'baz',
+ url_key: 'baz-url.html',
+ url_path: '/baz-url.html',
+ children_count: 0,
+ path: '1/2/17',
+ image: null,
+ productImagePreview: {
+ items: []
+ }
}
- }
+ ]
}
- ];
+ };
- const { root } = TestRenderer.create(
- withRouterAndApolloClient(mocks, () => (
-
- ))
- );
+ useCategoryList.mockReturnValueOnce({
+ childCategories: data.category.children
+ });
- expect(root.findByType(LoadingIndicator)).toBeTruthy();
+ const { root } = createTestInstance();
- await waitForExpect(() => {
- expect(root.findAllByType(CategoryTile)).toHaveLength(3);
- });
+ expect(root.findAllByType(CategoryTile)).toHaveLength(3);
});
diff --git a/packages/venia-ui/lib/components/CategoryList/categoryList.js b/packages/venia-ui/lib/components/CategoryList/categoryList.js
index a8722530a3..6fa2e9d6df 100644
--- a/packages/venia-ui/lib/components/CategoryList/categoryList.js
+++ b/packages/venia-ui/lib/components/CategoryList/categoryList.js
@@ -1,96 +1,90 @@
-import React, { Component } from 'react';
+import React from 'react';
import { string, number, shape } from 'prop-types';
-import { Query } from '@magento/venia-drivers';
-import classify from '../../classify';
+import { mergeClasses } from '../../classify';
import { fullPageLoadingIndicator } from '../LoadingIndicator';
import defaultClasses from './categoryList.css';
import CategoryTile from './categoryTile';
import categoryListQuery from '../../queries/getCategoryList.graphql';
+import { useCategoryList } from '@magento/peregrine/lib/talons/CategoryList/useCategoryList';
-class CategoryList extends Component {
- static propTypes = {
- id: number,
- title: string,
- classes: shape({
- root: string,
- header: string,
- content: string
- })
+// map Magento 2.3.1 schema changes to Venia 2.0.0 proptype shape to maintain backwards compatibility
+const mapCategory = categoryItem => {
+ const { items } = categoryItem.productImagePreview;
+ return {
+ ...categoryItem,
+ productImagePreview: {
+ items: items.map(item => {
+ const { small_image } = item;
+ return {
+ ...item,
+ small_image:
+ typeof small_image === 'object'
+ ? small_image.url
+ : small_image
+ };
+ })
+ }
};
+};
- get header() {
- const { title, classes } = this.props;
+const CategoryList = props => {
+ const { id, title } = props;
+ const talonProps = useCategoryList({
+ query: categoryListQuery,
+ id
+ });
- return title ? (
-
-
- {title}
-
-
- ) : null;
- }
+ const { childCategories, error, loading } = talonProps;
- // map Magento 2.3.1 schema changes to Venia 2.0.0 proptype shape to maintain backwards compatibility
- mapCategory(categoryItem) {
- const { items } = categoryItem.productImagePreview;
- return {
- ...categoryItem,
- productImagePreview: {
- items: items.map(item => {
- const { small_image } = item;
- return {
- ...item,
- small_image:
- typeof small_image === 'object'
- ? small_image.url
- : small_image
- };
- })
- }
- };
- }
+ const classes = mergeClasses(defaultClasses, props.classes);
- render() {
- const { id, classes } = this.props;
+ const header = title ? (
+
+
+ {title}
+
+
+ ) : null;
- return (
-
- {this.header}
-
- {({ loading, error, data }) => {
- if (error) {
- return (
-
- Data Fetch Error:
{error.message}
-
- );
- }
- if (loading) {
- return fullPageLoadingIndicator;
- }
- if (data.category.children.length === 0) {
- return (
-
- No child categories found.
-
- );
- }
-
- return (
-
- {data.category.children.map(item => (
-
- ))}
-
- );
- }}
-
+ let child;
+ if (error) {
+ child = (
+
+ Data Fetch Error:
{error.message}
);
}
-}
+ if (loading || !childCategories) {
+ child = fullPageLoadingIndicator;
+ } else if (childCategories.length === 0) {
+ child = (
+
No child categories found.
+ );
+ } else {
+ child = (
+
+ {childCategories.map(item => (
+
+ ))}
+
+ );
+ }
+ return (
+
+ {header}
+ {child}
+
+ );
+};
+
+CategoryList.propTypes = {
+ id: number,
+ title: string,
+ classes: shape({
+ root: string,
+ header: string,
+ content: string
+ })
+};
-export default classify(defaultClasses)(CategoryList);
+export default CategoryList;
diff --git a/packages/venia-ui/lib/components/CategoryList/categoryTile.js b/packages/venia-ui/lib/components/CategoryList/categoryTile.js
index 6f439e30a8..354b53c2c3 100644
--- a/packages/venia-ui/lib/components/CategoryList/categoryTile.js
+++ b/packages/venia-ui/lib/components/CategoryList/categoryTile.js
@@ -1,80 +1,62 @@
-import React, { Component } from 'react';
+import React from 'react';
import { arrayOf, string, shape } from 'prop-types';
-import classify from '../../classify';
+import { mergeClasses } from '../../classify';
import { Link, resourceUrl } from '@magento/venia-drivers';
import defaultClasses from './categoryTile.css';
-
-// TODO: get categoryUrlSuffix from graphql storeOptions when it is ready
-const categoryUrlSuffix = '.html';
-
-const previewImageSize = 480;
-
-class CategoryTile extends Component {
- static propTypes = {
- item: shape({
- image: string,
- name: string.isRequired,
- productImagePreview: shape({
- items: arrayOf(
- shape({
- small_image: string
- })
- )
- }),
- url_key: string.isRequired
- }).isRequired,
- classes: shape({
- item: string,
- image: string,
- imageWrapper: string,
- name: string
- }).isRequired
- };
-
- get imagePath() {
- const { image, productImagePreview } = this.props.item;
- const previewProduct = productImagePreview.items[0];
- if (image) {
- return resourceUrl(image, {
- type: 'image-category',
- width: previewImageSize
- });
- } else if (previewProduct) {
- return resourceUrl(previewProduct.small_image, {
- type: 'image-product',
- width: previewImageSize
- });
- } else {
- return null;
- }
- }
-
- render() {
- const { imagePath, props } = this;
- const { classes, item } = props;
-
- // interpolation doesn't work inside `url()` for legacy reasons
- // so a custom property should wrap its value in `url()`
- const imageUrl = imagePath ? `url(${imagePath})` : 'none';
- const style = { '--venia-image': imageUrl };
-
- // render an actual image element for accessibility
- const imagePreview = imagePath ? (
-
- ) : null;
-
- return (
-
-
- {imagePreview}
-
-
{item.name}
-
- );
- }
-}
-
-export default classify(defaultClasses)(CategoryTile);
+import { useCategoryTile } from '@magento/peregrine/lib/talons/CategoryList/useCategoryTile';
+
+const CategoryTile = props => {
+ const mixinProps = useCategoryTile({
+ item: props.item
+ });
+
+ const { image, item } = mixinProps;
+
+ const imagePath = resourceUrl(image.url, {
+ type: image.type,
+ width: image.width
+ });
+
+ // interpolation doesn't work inside `url()` for legacy reasons
+ // so a custom property should wrap its value in `url()`
+ const imageUrl = imagePath ? `url(${imagePath})` : 'none';
+ const imageWrapperStyle = { '--venia-image': imageUrl };
+
+ const classes = mergeClasses(defaultClasses, props.classes);
+
+ // render an actual image element for accessibility
+ const imagePreview = imagePath ? (
+
+ ) : null;
+
+ return (
+
+
+ {imagePreview}
+
+
{item.name}
+
+ );
+};
+
+CategoryTile.propTypes = {
+ item: shape({
+ image: string,
+ name: string.isRequired,
+ productImagePreview: shape({
+ items: arrayOf(
+ shape({
+ small_image: string
+ })
+ )
+ }),
+ url_key: string.isRequired
+ }).isRequired,
+ classes: shape({
+ item: string,
+ image: string,
+ imageWrapper: string,
+ name: string
+ })
+};
+export default CategoryTile;