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

Move filter UI into a toggle-able panel to improve experience on narrow viewports/containers #63203

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 37 additions & 21 deletions packages/dataviews/src/components/dataviews-filters/add-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,18 @@ interface AddFilterProps {
setOpenedFilter: ( filter: string | null ) => void;
}

function AddFilter(
{ filters, view, onChangeView, setOpenedFilter }: AddFilterProps,
ref: Ref< HTMLButtonElement >
) {
if ( ! filters.length || filters.every( ( { isPrimary } ) => isPrimary ) ) {
return null;
}
export function AddFilterDropdownMenu( {
filters,
view,
onChangeView,
setOpenedFilter,
trigger,
}: AddFilterProps & {
trigger: React.ReactNode;
} ) {
const inactiveFilters = filters.filter( ( filter ) => ! filter.isVisible );
return (
<DropdownMenu
trigger={
<Button
accessibleWhenDisabled
size="compact"
className="dataviews-filters__button"
variant="tertiary"
disabled={ ! inactiveFilters.length }
ref={ ref }
>
{ __( 'Add filter' ) }
</Button>
}
>
<DropdownMenu trigger={ trigger }>
{ inactiveFilters.map( ( filter ) => {
return (
<DropdownMenuItem
Expand Down Expand Up @@ -85,4 +74,31 @@ function AddFilter(
);
}

function AddFilter(
{ filters, view, onChangeView, setOpenedFilter }: AddFilterProps,
ref: Ref< HTMLButtonElement >
) {
if ( ! filters.length || filters.every( ( { isPrimary } ) => isPrimary ) ) {
return null;
}
const inactiveFilters = filters.filter( ( filter ) => ! filter.isVisible );
return (
<AddFilterDropdownMenu
trigger={
<Button
accessibleWhenDisabled
size="compact"
className="dataviews-filters-button"
variant="tertiary"
disabled={ ! inactiveFilters.length }
ref={ ref }
>
{ __( 'Add filter' ) }
</Button>
}
{ ...{ filters, view, onChangeView, setOpenedFilter } }
/>
);
}

export default forwardRef( AddFilter );
210 changes: 149 additions & 61 deletions packages/dataviews/src/components/dataviews-filters/index.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,150 @@
/**
* WordPress dependencies
*/
import { memo, useContext, useRef } from '@wordpress/element';
import { __experimentalHStack as HStack } from '@wordpress/components';
import {
memo,
useContext,
useRef,
useMemo,
useCallback,
} from '@wordpress/element';
import { __experimentalHStack as HStack, Button } from '@wordpress/components';
import { funnel } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import FilterSummary from './filter-summary';
import AddFilter from './add-filter';
import { default as AddFilter, AddFilterDropdownMenu } from './add-filter';
import ResetFilters from './reset-filters';
import DataViewsContext from '../dataviews-context';
import { sanitizeOperators } from '../../utils';
import { ALL_OPERATORS, OPERATOR_IS, OPERATOR_IS_NOT } from '../../constants';
import type { NormalizedFilter } from '../../types';
import type { NormalizedFilter, NormalizedField, View } from '../../types';

function Filters() {
const { fields, view, onChangeView, openedFilter, setOpenedFilter } =
useContext( DataViewsContext );
const addFilterRef = useRef< HTMLButtonElement >( null );
const filters: NormalizedFilter[] = [];
fields.forEach( ( field ) => {
if ( ! field.elements?.length ) {
return;
}
export function useFilters( fields: NormalizedField< any >[], view: View ) {
return useMemo( () => {
const filters: NormalizedFilter[] = [];
fields.forEach( ( field ) => {
if ( ! field.elements?.length ) {
return;
}

const operators = sanitizeOperators( field );
if ( operators.length === 0 ) {
return;
}
const operators = sanitizeOperators( field );
if ( operators.length === 0 ) {
return;
}

const isPrimary = !! field.filterBy?.isPrimary;
filters.push( {
field: field.id,
name: field.label,
elements: field.elements,
singleSelection: operators.some( ( op ) =>
[ OPERATOR_IS, OPERATOR_IS_NOT ].includes( op )
),
operators,
isVisible:
isPrimary ||
!! view.filters?.some(
( f ) =>
f.field === field.id &&
ALL_OPERATORS.includes( f.operator )
const isPrimary = !! field.filterBy?.isPrimary;
filters.push( {
field: field.id,
name: field.label,
elements: field.elements,
singleSelection: operators.some( ( op ) =>
[ OPERATOR_IS, OPERATOR_IS_NOT ].includes( op )
),
isPrimary,
operators,
isVisible:
isPrimary ||
!! view.filters?.some(
( f ) =>
f.field === field.id &&
ALL_OPERATORS.includes( f.operator )
),
isPrimary,
} );
} );
} );
// Sort filters by primary property. We need the primary filters to be first.
// Then we sort by name.
filters.sort( ( a, b ) => {
if ( a.isPrimary && ! b.isPrimary ) {
return -1;
}
if ( ! a.isPrimary && b.isPrimary ) {
return 1;
}
return a.name.localeCompare( b.name );
} );
// Sort filters by primary property. We need the primary filters to be first.
// Then we sort by name.
filters.sort( ( a, b ) => {
if ( a.isPrimary && ! b.isPrimary ) {
return -1;
}
if ( ! a.isPrimary && b.isPrimary ) {
return 1;
}
return a.name.localeCompare( b.name );
} );
return filters;
}, [ fields, view ] );
}

export function FilterVisibilityToggle( {
filters,
view,
onChangeView,
setOpenedFilter,
isShowingFilter,
setIsShowingFilter,
}: {
filters: NormalizedFilter[];
view: View;
onChangeView: ( view: View ) => void;
setOpenedFilter: ( filter: string | null ) => void;
isShowingFilter: boolean;
setIsShowingFilter: React.Dispatch< React.SetStateAction< boolean > >;
} ) {
const onChangeViewWithFilterVisibility = useCallback(
( _view: View ) => {
onChangeView( _view );
setIsShowingFilter( true );
},
[ onChangeView, setIsShowingFilter ]
);
const visibleFilters = filters.filter( ( filter ) => filter.isVisible );

const hasVisibleFilters = !! visibleFilters.length;
if ( ! hasVisibleFilters ) {
return (
<AddFilterDropdownMenu
filters={ filters }
view={ view }
onChangeView={ onChangeViewWithFilterVisibility }
setOpenedFilter={ setOpenedFilter }
trigger={
<Button
className="dataviews-filters__visibility-toggle"
size="compact"
icon={ funnel }
label={ __( 'Add filter' ) }
isPressed={ false }
aria-expanded={ false }
/>
}
/>
);
}
return (
<div className="dataviews-filters__container-visibility-toggle">
<Button
className="dataviews-filters__visibility-toggle"
size="compact"
icon={ funnel }
label={ __( 'Toggle filter display' ) }
onClick={ () => {
if ( ! isShowingFilter ) {
setOpenedFilter( null );
}
setIsShowingFilter( ! isShowingFilter );
} }
isPressed={ isShowingFilter }
aria-expanded={ isShowingFilter }
/>
{ hasVisibleFilters && !! view.filters?.length && (
<span className="dataviews-filters-toggle__count">
{ view.filters?.length }
</span>
) }
</div>
);
}

function Filters() {
const { fields, view, onChangeView, openedFilter, setOpenedFilter } =
useContext( DataViewsContext );
const addFilterRef = useRef< HTMLButtonElement >( null );
const filters = useFilters( fields, view );
const addFilter = (
<AddFilter
key="add-filter"
Expand All @@ -70,12 +155,12 @@ function Filters() {
setOpenedFilter={ setOpenedFilter }
/>
);
const visibleFilters = filters.filter( ( filter ) => filter.isVisible );
if ( visibleFilters.length === 0 ) {
return null;
}
const filterComponents = [
...filters.map( ( filter ) => {
if ( ! filter.isVisible ) {
return null;
}

...visibleFilters.map( ( filter ) => {
return (
<FilterSummary
key={ filter.field }
Expand All @@ -90,19 +175,22 @@ function Filters() {
addFilter,
];

if ( filterComponents.length > 1 ) {
filterComponents.push(
<ResetFilters
key="reset-filters"
filters={ filters }
view={ view }
onChangeView={ onChangeView }
/>
);
}
filterComponents.push(
<ResetFilters
key="reset-filters"
filters={ filters }
view={ view }
onChangeView={ onChangeView }
/>
);

return (
<HStack justify="flex-start" style={ { width: 'fit-content' } } wrap>
<HStack
justify="flex-start"
style={ { width: 'fit-content' } }
className="dataviews-filters__container"
wrap
>
{ filterComponents }
</HStack>
);
Expand Down
30 changes: 30 additions & 0 deletions packages/dataviews/src/components/dataviews-filters/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
position: relative;
}

.dataviews-filters__container {
padding-top: 0;
}

.dataviews-filters__reset-button.dataviews-filters__reset-button[aria-disabled="true"] {
&,
&:hover {
Expand Down Expand Up @@ -250,3 +254,29 @@
width: $icon-size;
}
}

.dataviews-filters__container-visibility-toggle {
position: relative;
flex-shrink: 0;
}

.dataviews-filters-toggle__count {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
background: var(--wp-admin-theme-color, #3858e9);
height: $grid-unit-20;
min-width: $grid-unit-20;
line-height: $grid-unit-20;
padding: 0 $grid-unit-05;
text-align: center;
border-radius: $grid-unit-10;
font-size: 11px;
outline: var(--wp-admin-border-width-focus) solid $white;
color: $white;
}

.dataviews-search {
width: fit-content;
}
13 changes: 8 additions & 5 deletions packages/dataviews/src/components/dataviews-search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@ const DataViewsSearch = memo( function Search( { label }: SearchProps ) {
viewRef.current = view;
}, [ onChangeView, view ] );
useEffect( () => {
onChangeViewRef.current( {
...viewRef.current,
page: 1,
search: debouncedSearch,
} );
if ( debouncedSearch !== viewRef.current?.search ) {
onChangeViewRef.current( {
...viewRef.current,
page: 1,
search: debouncedSearch,
} );
}
}, [ debouncedSearch ] );
const searchLabel = label || __( 'Search' );
return (
<SearchControl
className="dataviews-search"
__nextHasNoMarginBottom
onChange={ setSearch }
value={ search }
Expand Down
Loading
Loading