Skip to content

Commit

Permalink
Move filter UI into a toggle-able panel to improve experience on narr…
Browse files Browse the repository at this point in the history
…ow viewports/containers (#63203)

Co-authored-by: jorgefilipecosta <[email protected]>
Co-authored-by: ntsekouras <[email protected]>
Co-authored-by: jameskoster <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: jasmussen <[email protected]>
Co-authored-by: SaxonF <[email protected]>
  • Loading branch information
7 people authored Aug 5, 2024
1 parent 645ae89 commit 4be4912
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 107 deletions.
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

0 comments on commit 4be4912

Please sign in to comment.