Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1222 Filters (Layered Navigation) - Add keyboard focus and adjust the way tab order should work #3034

Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
be421b7
#1222 - a11y for Layered Navigation
Feb 19, 2021
ddc4bcf
Added focus trap and a11y adjustments for Layered Navigation
Feb 23, 2021
0e96689
run prettier
Mar 3, 2021
d7c04fe
run eslint
Mar 3, 2021
418f750
Merge branch 'develop' into feature/1222-layered-nav-add-keyboard-foc…
Mar 3, 2021
388d4fa
run prettier
Mar 3, 2021
3b3bb44
fixed issue "focus on close button after applying filter item option"
Mar 4, 2021
78b7325
remove filter modal from DOM when drawer closes
Mar 4, 2021
495e109
eslint and prettier fixes
Mar 4, 2021
8baef15
progress: fixing issue - focus not set to "Filters" button after filt…
Apr 5, 2021
dd13da7
Merge remote-tracking branch 'origin/develop' into feature/1222-layer…
sirugh Apr 6, 2021
b205c93
prevent filter button from unmounting when applying uncached filters
sirugh Apr 6, 2021
ba383c5
Merge pull request #1 from magento/feature/1222-layered-nav-add-keybo…
Apr 16, 2021
8b6c5ad
Merge branch 'develop' of github.com:magento/pwa-studio into feature/…
Apr 16, 2021
7fb4dfa
Made the Filter Button and Sorted By Container reusable components an…
Apr 17, 2021
47c3480
Fix issue: filter modal not autofocused and on filter modal close the…
Apr 17, 2021
88e1e40
Codestyle fixes
Apr 17, 2021
fd00536
Made the 'Filters' button on search page focused after applying filte…
Apr 17, 2021
18b6e9c
Added adjustments as per maintainer review
Apr 23, 2021
fb2a156
Codestyle fixes after lint and prettier
Apr 23, 2021
f941c2a
Do not unmount component when filter item is not expanded
Apr 23, 2021
5ffe970
Cover case when data can be falsy
Apr 23, 2021
bb0616b
order translations alphabetically
Apr 23, 2021
e6138e5
Added review adjustments and codestyle fixes
Apr 26, 2021
33ce8d5
Update prop types
Apr 26, 2021
26a6ff6
Fix issues on the search page
Apr 26, 2021
9339835
Fix focus order issue
Apr 29, 2021
dadb3f4
codestyle fixes
Apr 29, 2021
c48a1bb
Fixing failed tests for the Search Page after recent adjustments/refa…
Apr 30, 2021
42b4d9f
fix tests for categoryContent
Apr 30, 2021
7d8eb63
fix failed tests for useCategoryContent.spec.js
Apr 30, 2021
5694212
Merge branch 'develop' into feature/1222-layered-nav-add-keyboard-foc…
sirugh May 6, 2021
e7a6854
Merge branch 'develop' into feature/1222-layered-nav-add-keyboard-foc…
sirugh May 6, 2021
393fae0
Update jsdoc
sirugh May 6, 2021
11679ab
run prettier
sirugh May 6, 2021
d9f81a7
fix tests, remove old loading indicator tests for category component
sirugh May 6, 2021
27adecb
jsdoc fix
sirugh May 6, 2021
3c054f5
Merge branch 'develop' into feature/1222-layered-nav-add-keyboard-foc…
dpatil-magento May 6, 2021
8e581af
Render filter and sort buttons if filters exist
sirugh May 6, 2021
bac614c
Merge branch 'feature/1222-layered-nav-add-keyboard-focus-and-a11y' o…
sirugh May 6, 2021
7ad0d8e
dont render sort button if there are no results
sirugh May 6, 2021
5ed8c3c
SortedBy container should be a div
sirugh May 7, 2021
9661347
different behavior for category and search for rendering sort buttons
sirugh May 7, 2021
369d223
Add tests for categoryContent
sirugh May 7, 2021
81dec33
remote extra whitespace
sirugh May 7, 2021
7752c01
Merge branch 'develop' into feature/1222-layered-nav-add-keyboard-foc…
dpatil-magento May 7, 2021
9aad945
whitespace caused rename
sirugh May 7, 2021
3551640
Only render sort button if there are products
sirugh May 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 52 additions & 19 deletions packages/peregrine/lib/talons/FilterModal/useFilterModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { getSearchFromState, getStateFromSearch, stripHtml } from './helpers';

import DEFAULT_OPERATIONS from './filterModal.gql';

const DRAWER_NAME = 'filter';

/**
* Filter Modal talon.
*
Expand All @@ -27,10 +29,10 @@ export const useFilterModal = props => {
const { getFilterInputsQuery } = operations;

const [isApplying, setIsApplying] = useState(false);
const [{ drawer }, { closeDrawer }] = useAppContext();
const [{ drawer }, { toggleDrawer, closeDrawer }] = useAppContext();
const [filterState, filterApi] = useFilterState();
const prevDrawer = useRef(null);
const isOpen = drawer === 'filter';
const isOpen = drawer === DRAWER_NAME;

const history = useHistory();
const { pathname, search } = useLocation();
Expand Down Expand Up @@ -123,11 +125,48 @@ export const useFilterModal = props => {
}
}, [filterKeys, filterState, history, isApplying, pathname, search]);

// on drawer toggle, read filter state from location
const handleOpen = useCallback(() => {
toggleDrawer(DRAWER_NAME);
}, [toggleDrawer]);

const handleClose = useCallback(() => {
closeDrawer();
}, [closeDrawer]);

const handleApply = useCallback(() => {
setIsApplying(true);
handleClose();
}, [handleClose]);

const handleReset = useCallback(() => {
filterApi.clear();
setIsApplying(true);
}, [filterApi, setIsApplying]);

const handleKeyDownActions = useCallback(
event => {
// do not handle keyboard actions when the modal is closed
if (!isOpen) {
return;
}

switch (event.keyCode) {
// when "Esc" key fired -> close the modal
case 27:
handleClose();
break;
}
},
[isOpen, handleClose]
);

useEffect(() => {
const justOpened = prevDrawer.current === null && drawer === 'filter';
const justClosed = prevDrawer.current === 'filter' && drawer === null;
const justOpened =
prevDrawer.current === null && drawer === DRAWER_NAME;
const justClosed =
prevDrawer.current === DRAWER_NAME && drawer === null;

// on drawer toggle, read filter state from location
if (justOpened || justClosed) {
const nextState = getStateFromSearch(
search,
Expand All @@ -137,22 +176,14 @@ export const useFilterModal = props => {

filterApi.setItems(nextState);
}
prevDrawer.current = drawer;
}, [drawer, filterApi, filterItems, filterKeys, search]);

const handleApply = useCallback(() => {
setIsApplying(true);
closeDrawer();
}, [closeDrawer]);

const handleClose = useCallback(() => {
closeDrawer();
}, [closeDrawer]);
// on drawer close, update the modal visibility state
if (justClosed) {
handleClose();
}

const handleReset = useCallback(() => {
filterApi.clear();
setIsApplying(true);
}, [filterApi, setIsApplying]);
prevDrawer.current = drawer;
}, [drawer, filterApi, filterItems, filterKeys, search, handleClose]);

return {
filterApi,
Expand All @@ -161,8 +192,10 @@ export const useFilterModal = props => {
filterNames,
filterState,
handleApply,
handleOpen,
handleClose,
handleReset,
handleKeyDownActions,
isApplying,
isOpen
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

exports[`handles no data prop 1`] = `
Object {
"categoryDescription": null,
"categoryName": null,
"categoryDescription": "Jewelry category",
"categoryName": "Jewelry",
"filters": null,
"handleLoadFilters": [Function],
"handleOpenFilters": [Function],
"items": Array [
null,
null,
Expand All @@ -18,7 +16,6 @@ Object {
null,
null,
],
"loadFilters": false,
"totalPagesFromData": null,
}
`;
Expand All @@ -32,8 +29,6 @@ Object {
"label": "Label",
},
],
"handleLoadFilters": [Function],
"handleOpenFilters": [Function],
"items": Array [
Object {
"id": 1,
Expand All @@ -44,7 +39,6 @@ Object {
"name": "Necklace",
},
],
"loadFilters": false,
"totalPagesFromData": 1,
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import React from 'react';
import { createTestInstance } from '@magento/peregrine';

import { useCategoryContent } from '../useCategoryContent';
import { useLazyQuery } from '@apollo/client';
import { act } from 'react-test-renderer';

import { useAppContext } from '../../../../context/app';
import { useLazyQuery, useQuery } from '@apollo/client';

global.STORE_NAME = 'Venia';

Expand All @@ -23,7 +20,8 @@ jest.mock('@apollo/client', () => {
const apolloClient = jest.requireActual('@apollo/client');
return {
...apolloClient,
useLazyQuery: jest.fn()
useLazyQuery: jest.fn(),
useQuery: jest.fn()
};
});
const Component = props => {
Expand All @@ -35,10 +33,6 @@ const Component = props => {
const mockProps = {
categoryId: 3,
data: {
category: {
name: 'Jewelry',
description: 'Jewelry category'
},
products: {
page_info: {
total_pages: 1
Expand Down Expand Up @@ -66,6 +60,12 @@ const mockProductFiltersByCategoryData = {
]
}
};
const mockCategoryData = {
category: {
name: 'Jewelry',
description: 'Jewelry category'
}
}

const mockGetFilters = jest.fn();

Expand All @@ -74,60 +74,20 @@ useLazyQuery.mockReturnValue([
{ data: mockProductFiltersByCategoryData }
]);

useQuery.mockReturnValue(
{ data: mockCategoryData }
);

it('returns the proper shape', () => {
const rendered = createTestInstance(<Component {...mockProps} />);

const talonProps = rendered.root.findByType('i').props;

expect(mockGetFilters).toHaveBeenCalled();
expect(useQuery).toHaveBeenCalled();
expect(talonProps).toMatchSnapshot();
});

it('sets the filter loading state', () => {
const rendered = createTestInstance(<Component {...mockProps} />);

const talonProps = rendered.root.findByType('i').props;

const { loadFilters, handleLoadFilters } = talonProps;

expect(loadFilters).toBeFalsy();

act(() => {
handleLoadFilters();
});

const updatedProps = rendered.root.findByType('i').props;

expect(updatedProps.loadFilters).toBeTruthy();
});

it('toggles drawer when opening filters', () => {
const mockToggleDrawer = jest.fn();
useAppContext.mockReturnValue([
{},
{
toggleDrawer: mockToggleDrawer
}
]);

const rendered = createTestInstance(<Component {...mockProps} />);

const talonProps = rendered.root.findByType('i').props;

const { loadFilters, handleOpenFilters } = talonProps;

expect(loadFilters).toBeFalsy();

act(() => {
handleOpenFilters();
});

const updatedProps = rendered.root.findByType('i').props;

expect(updatedProps.loadFilters).toBeTruthy();
expect(mockToggleDrawer).toHaveBeenCalled();
});

it('handles default category id', () => {
const testProps = Object.assign({}, mockProps, {
categoryId: 0
Expand All @@ -153,6 +113,10 @@ it('handles no data prop', () => {
data: null,
pageSize: 9
};
useLazyQuery.mockReturnValue([
mockGetFilters,
{ data: null }
]);

const rendered = createTestInstance(<Component {...testProps} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ export const GET_PRODUCT_FILTERS_BY_CATEGORY = gql`
}
`;

export const GET_CATEGORY_CONTENT = gql`
query getCategoryData($id: Int!) {
category(id: $id) {
id
name
description
}
}
`;

export default {
getCategoryContentQuery: GET_CATEGORY_CONTENT,
getProductFiltersByCategoryQuery: GET_PRODUCT_FILTERS_BY_CATEGORY
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import { gql } from '@apollo/client';
export const CategoryFragment = gql`
fragment CategoryFragment on CategoryTree {
id
description
name
product_count
meta_title
meta_keywords
meta_description
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { useCallback, useEffect, useState } from 'react';
import { useLazyQuery } from '@apollo/client';
import { useEffect } from 'react';
import { useLazyQuery, useQuery } from '@apollo/client';

import mergeOperations from '../../../util/shallowMerge';
import { useAppContext } from '../../../context/app';

import DEFAULT_OPERATIONS from './categoryContent.gql';

const DRAWER_NAME = 'filter';

/**
* Returns props necessary to render the categoryContent component.
*
Expand All @@ -17,28 +14,20 @@ const DRAWER_NAME = 'filter';
* @returns {number} result.categoryId - This category's ID.
* @returns {string} result.categoryName - This category's name.
* @returns {object} result.filters - The filters object.
* @returns {func} result.handleLoadFilters - A callback function to signal the user's intent to interact with the filters.
* @returns {func} result.handleOpenFilters - A callback function that actually opens the filter drawer.
* @returns {object} result.items - The items in this category.
* @returns {bool} result.loadFilters - Whether or not the user has signalled their intent to interact with the filters.
*/
export const useCategoryContent = props => {
const { categoryId, data, pageSize = 6 } = props;

const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
const { getProductFiltersByCategoryQuery } = operations;

const placeholderItems = Array.from({ length: pageSize }).fill(null);
const [loadFilters, setLoadFilters] = useState(false);
const [, { toggleDrawer }] = useAppContext();
const {
getCategoryContentQuery,
getProductFiltersByCategoryQuery
} = operations;

const handleLoadFilters = useCallback(() => {
setLoadFilters(true);
}, [setLoadFilters]);
const handleOpenFilters = useCallback(() => {
setLoadFilters(true);
toggleDrawer(DRAWER_NAME);
}, [setLoadFilters, toggleDrawer]);
const placeholderItems = Array.from({ length: pageSize }).fill(null);

const [getFilters, { data: filterData }] = useLazyQuery(
getProductFiltersByCategoryQuery,
Expand All @@ -48,6 +37,15 @@ export const useCategoryContent = props => {
}
);

const { data: categoryData } = useQuery(getCategoryContentQuery, {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
skip: !categoryId,
variables: {
id: categoryId
}
});

useEffect(() => {
if (categoryId) {
getFilters({
Expand All @@ -65,17 +63,16 @@ export const useCategoryContent = props => {
const totalPagesFromData = data
? data.products.page_info.total_pages
: null;
const categoryName = data ? data.category.name : null;
const categoryDescription = data ? data.category.description : null;
const categoryName = categoryData ? categoryData.category.name : null;
const categoryDescription = categoryData
? categoryData.category.description
: null;

return {
categoryName,
categoryDescription,
filters,
handleLoadFilters,
handleOpenFilters,
items,
loadFilters,
totalPagesFromData
};
};
Loading