Skip to content

Commit

Permalink
feat: Filter options in Content Display Options (#2644)
Browse files Browse the repository at this point in the history
Co-authored-by: Ernst Kaese <ernstka@amazon.com>
Co-authored-by: Andrei Zhaleznichenka <zhalea@amazon.de>
Co-authored-by: guptbhum <guptbhum@amazon.com>
Co-authored-by: Cansu Aksu <45541797+cansuaa@users.noreply.github.com>
Co-authored-by: fralongo <88311273+fralongo@users.noreply.github.com>
Co-authored-by: Francesco Longo <flongo@amazon.de>
Co-authored-by: Boris Serdiuk <serdiuk@amazon.com>
Co-authored-by: Aleksandra Danilina <akdanili@amazon.com>
Co-authored-by: at-susie <atsuboy@gmail.com>
Co-authored-by: Johannes Weber <569011+johannes-weber@users.noreply.github.com>
Co-authored-by: Johannes Weber <jowejowe@amazon.com>
Co-authored-by: Michael <michaeldowseza@users.noreply.github.com>
Co-authored-by: Connor Lanigan <dev@connorlanigan.com>
Co-authored-by: Dennis Pitcock <14841504+dpitcock@users.noreply.github.com>
  • Loading branch information
15 people authored Sep 27, 2024
1 parent f824612 commit a5defd9
Show file tree
Hide file tree
Showing 32 changed files with 418 additions and 51 deletions.
1 change: 1 addition & 0 deletions pages/collection-preferences/reorder-content.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default function App() {
contentDensityPreference={contentDensityPreference}
customPreference={customPreference}
contentDisplayPreference={{
enableColumnFiltering: true,
title: 'Column preferences',
description: 'Customize the columns visibility and order.',
options: longOptionsList,
Expand Down
7 changes: 7 additions & 0 deletions pages/common/i18n-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ export const contentDisplayPreferenceI18nStrings: Partial<CollectionPreferencesP
dragHandleAriaDescription:
"Use Space or Enter to activate drag for an item, then use the arrow keys to move the item's position. To complete the position move, use Space or Enter, or to discard the move, use Escape.",
dragHandleAriaLabel: 'Drag handle',
i18nStrings: {
columnFilteringPlaceholder: 'Filter columns',
columnFilteringAriaLabel: 'Filter columns',
columnFilteringNoMatchText: 'No matches',
columnFilteringCountText: count => (count > 1 || count === 0 ? `${count} matches` : `${count} match`),
columnFilteringClearFilterText: 'Clear filter',
},
};
10 changes: 10 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5204,6 +5204,16 @@ You must provide an ordered list of the items to display in the \`preferences.co
"optional": true,
"type": "string",
},
{
"name": "enableColumnFiltering",
"optional": true,
"type": "false | true",
},
{
"name": "i18nStrings",
"optional": true,
"type": "CollectionPreferencesProps.ContentDisplayPreferenceI18nStrings",
},
{
"name": "liveAnnouncementDndDiscarded",
"optional": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,13 @@ exports[`test-utils selectors 1`] = `
"awsui_confirm-button_tc96w",
"awsui_content-density_tc96w",
"awsui_content-display-description_tc96w",
"awsui_content-display-no-match_tc96w",
"awsui_content-display-option-content_tc96w",
"awsui_content-display-option-label_tc96w",
"awsui_content-display-option-list_tc96w",
"awsui_content-display-option-toggle_tc96w",
"awsui_content-display-option_tc96w",
"awsui_content-display-text-filter_tc96w",
"awsui_content-display-title_tc96w",
"awsui_content-display_tc96w",
"awsui_custom_tc96w",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ const setupTest = (testFn: (page: CollectionPreferencesPageObject) => Promise<vo

describe('Collection preferences', () => {
test(
'renders no columns if there is only custom content',
'renders one column if there is only custom content',
setupTest(async page => {
page.wrapper = createWrapper().findCollectionPreferences('.cp-3');

await page.openCollectionPreferencesModal();

// The content is small enough so that it doesn't need column layout
// The content is small enough so that it only needs one column
const columnLayout = page.wrapper.findModal().findContent().findColumnLayout();
await expect(page.isExisting(columnLayout.toSelector())).resolves.toBe(false);
await expect(page.isExisting(columnLayout.findColumn(1).toSelector())).resolves.toBe(true);
await expect(page.isExisting(columnLayout.findColumn(2).toSelector())).resolves.toBe(false);

await expect(page.isExisting(page.wrapper.findModal().findWrapLinesPreference().toSelector())).resolves.toBe(
true
Expand All @@ -33,14 +34,15 @@ describe('Collection preferences', () => {
);

test(
'renders no columns if there is only visible content preferences',
'renders one column if there is only visible content preferences',
setupTest(async page => {
page.wrapper = createWrapper().findCollectionPreferences('.cp-4');
await page.openCollectionPreferencesModal();

// The content is small enough so that it doesn't need column layout
// The content is small enough so that it only needs one column
const columnLayout = page.wrapper.findModal().findContent().findColumnLayout();
await expect(page.isExisting(columnLayout.toSelector())).resolves.toBe(false);
await expect(page.isExisting(columnLayout.findColumn(1).toSelector())).resolves.toBe(true);
await expect(page.isExisting(columnLayout.findColumn(2).toSelector())).resolves.toBe(false);

await expect(page.isExisting(page.wrapper.findModal().findVisibleContentPreference().toSelector())).resolves.toBe(
true
Expand Down
28 changes: 26 additions & 2 deletions src/collection-preferences/__tests__/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,34 @@ import * as React from 'react';
import { render } from '@testing-library/react';

import CollectionPreferences, { CollectionPreferencesProps } from '../../../lib/components/collection-preferences';
import TestI18nProvider from '../../../lib/components/i18n/testing';
import createWrapper from '../../../lib/components/test-utils/dom';
import { CollectionPreferencesWrapper } from '../../../lib/components/test-utils/dom';

export function renderCollectionPreferences(props: Partial<CollectionPreferencesProps>): CollectionPreferencesWrapper {
render(<CollectionPreferences title="Preferences title" confirmLabel="Confirm" cancelLabel="Cancel" {...props} />);
const i18nMessages = {
'collection-preferences': {
'contentDisplayPreference.i18nStrings.columnFilteringPlaceholder': 'Filter columns',
'contentDisplayPreference.i18nStrings.columnFilteringAriaLabel': 'Filter columns',
'contentDisplayPreference.i18nStrings.columnFilteringNoMatchText': 'No matches found',
'contentDisplayPreference.i18nStrings.columnFilteringClearFilterText': 'Clear filter',
'contentDisplayPreference.i18nStrings.columnFilteringCountText':
'{count, select, zero {0 matches} one {1 match} other {{count} matches}}',
},
};

export function renderCollectionPreferences(
props: Partial<CollectionPreferencesProps>,
withI18nProvider = false
): CollectionPreferencesWrapper {
let contentToRender = (
<CollectionPreferences title="Preferences title" confirmLabel="Confirm" cancelLabel="Cancel" {...props} />
);

if (withI18nProvider) {
contentToRender = <TestI18nProvider messages={i18nMessages}>{contentToRender}</TestI18nProvider>;
}

render(contentToRender);
return createWrapper().findCollectionPreferences()!;
}

Expand Down Expand Up @@ -101,4 +124,5 @@ export const contentDisplayPreference: CollectionPreferencesProps.ContentDisplay
initialPosition === finalPosition
? `Item moved back to its original position ${initialPosition} of ${total}`
: `Item moved from position ${initialPosition} to position ${finalPosition} of ${total}`,
enableColumnFiltering: true,
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ export default class ContentDisplayPageObject extends CollectionPreferencesPageO
return this.wrapper.findModal().findContentDisplayPreference().findOptions();
}

focusDragHandle(index = 0) {
return this.keys(new Array(5 + index * 2).fill('Tab'));
async focusDragHandle(index = 0) {
const isSearchable = await this.isExisting(
this.wrapper.findModal().findContentDisplayPreference().findTextFilter().toSelector()
);
const offset = isSearchable ? 6 : 5;

return this.keys(new Array(offset + index * 2).fill('Tab'));
}

async openCollectionPreferencesModal() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,120 @@ describe('Content Display preference', () => {
});
});

describe.each<boolean>([false, true])('Filtering (with i18n = %s)', withI18nProvider => {
it('filters options', () => {
const wrapper = renderContentDisplay(
withI18nProvider
? undefined
: {
contentDisplayPreference: {
...contentDisplayPreference,
i18nStrings: {
columnFilteringPlaceholder: 'Filter columns',
columnFilteringAriaLabel: 'Filter columns',
columnFilteringClearFilterText: 'Clear filter',
columnFilteringNoMatchText: 'No matches found',
columnFilteringCountText: count => (count > 1 || count === 0 ? `${count} matches` : `${count} match`),
},
},
},
withI18nProvider
);
const filterInput = wrapper.findTextFilter();
expect(filterInput).not.toBeNull();
filterInput!.findInput().setInputValue('Item 1');

expect(filterInput?.findInput().findNativeInput().getElement()).toHaveAttribute('placeholder', 'Filter columns');
expect(filterInput?.findInput().findNativeInput().getElement()).toHaveAttribute('aria-label', 'Filter columns');
expect(filterInput?.findInput().findClearButton()?.getElement()).toHaveAccessibleName('Clear filter');

expect(filterInput!.findResultsCount().getElement()).toHaveTextContent('1 match');

const options = wrapper.findOptions();
expect(options).toHaveLength(1);
expect(options[0].findLabel().getElement()).toHaveTextContent('Item 1');
});

it('shows empty state when no options match and clears filter', () => {
const wrapper = renderContentDisplay(
withI18nProvider
? undefined
: {
contentDisplayPreference: {
...contentDisplayPreference,
i18nStrings: {
columnFilteringPlaceholder: 'Filter columns',
columnFilteringAriaLabel: 'Filter columns',
columnFilteringClearFilterText: 'Clear filter',
columnFilteringNoMatchText: 'No matches found',
columnFilteringCountText: count => (count > 1 || count === 0 ? `${count} matches` : `${count} match`),
},
},
},
withI18nProvider
);
const filterInput = wrapper.findTextFilter();
expect(filterInput).not.toBeNull();

filterInput!.findInput().setInputValue('Item 100');
expect(filterInput!.findResultsCount().getElement()).toHaveTextContent('0 matches');

const options = wrapper.findOptions();
expect(options).toHaveLength(0);

const emptyState = wrapper.findNoMatch();
expect(emptyState).not.toBeNull();
expect(emptyState!.getElement()).toHaveTextContent('No matches found');

const emptyStateButton = emptyState?.findButton();
expect(emptyStateButton).not.toBeNull();
expect(emptyStateButton!.getElement()).toHaveTextContent('Clear filter');
emptyStateButton?.click();

expect(filterInput!.findInput().getInputValue()).toBe('');
expect(filterInput!.findResultsCount()).toBeNull();
});
});

describe('Filtering - continued', () => {
it('does not render the text filter with searchable columns turned off', () => {
const wrapper = renderContentDisplay({
contentDisplayPreference: {
...contentDisplayPreference,
enableColumnFiltering: false,
},
});
const filterInput = wrapper.findTextFilter();
expect(filterInput).toBeNull();
});

it('clears filter and shows all options', () => {
const wrapper = renderContentDisplay();
const filterInput = wrapper.findTextFilter();
expect(filterInput).not.toBeNull();

filterInput!.findInput().setInputValue('Item 1');
filterInput!.findInput().findClearButton()?.click();

const options = wrapper.findOptions();
expect(options).toHaveLength(4);
});

it('sets the drag-handle to a disabled state when filtering', () => {
const wrapper = renderContentDisplay();
const filterInput = wrapper.findTextFilter();
expect(filterInput).not.toBeNull();

const dragHandleBefore = wrapper.findOptionByIndex(1)!.findDragHandle().getElement();
expect(dragHandleBefore.getAttribute('aria-disabled')).toBe('false');

filterInput!.findInput().setInputValue('Item 1');

const dragHandleAfter = wrapper.findOptionByIndex(1)!.findDragHandle().getElement();
expect(dragHandleAfter.getAttribute('aria-disabled')).toBe('true');
});
});

describe('State management', () => {
const initialStateWithCustomVisibility = [
{
Expand Down Expand Up @@ -320,8 +434,11 @@ describe('Content Display preference', () => {
});
});

function renderContentDisplay(props: Partial<CollectionPreferencesProps> = {}) {
const collectionPreferencesWrapper = renderCollectionPreferences({ contentDisplayPreference, ...props });
function renderContentDisplay(props: Partial<CollectionPreferencesProps> = {}, withI18nProvider = false) {
const collectionPreferencesWrapper = renderCollectionPreferences(
{ contentDisplayPreference, ...props },
withI18nProvider
);
collectionPreferencesWrapper.findTriggerButton().click();
return collectionPreferencesWrapper.findModal()!.findContentDisplayPreference()!;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ export interface ContentDisplayOptionProps {
listeners?: SyntheticListenerMap;
onToggle?: (option: OptionWithVisibility) => void;
option: OptionWithVisibility;
disabled?: boolean;
}

const ContentDisplayOption = forwardRef(
(
{ dragHandleAriaLabel, listeners, onToggle, option }: ContentDisplayOptionProps,
{ dragHandleAriaLabel, listeners, onToggle, option, disabled }: ContentDisplayOptionProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const idPrefix = useUniqueId(componentPrefix);
Expand All @@ -34,7 +35,7 @@ const ContentDisplayOption = forwardRef(

return (
<div ref={ref} className={getClassName('content')}>
<DragHandle attributes={dragHandleAttributes} listeners={listeners} />
<DragHandle disabled={disabled} attributes={dragHandleAttributes} listeners={listeners} />

<label className={getClassName('label')} htmlFor={controlId}>
{option.label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function DraggableOption({
onToggle: (option: OptionWithVisibility) => void;
option: OptionWithVisibility;
}) {
const { isDragging, isSorting, listeners, setNodeRef, transform } = useSortable({
const { isDragging, isSorting, listeners, setNodeRef, transform, attributes } = useSortable({
id: option.id,
});
const style = {
Expand All @@ -48,6 +48,7 @@ export default function DraggableOption({
dragHandleAriaLabel={dragHandleAriaLabel}
onToggle={onToggle}
option={option}
disabled={attributes['aria-disabled']}
/>
</li>
);
Expand Down
Loading

0 comments on commit a5defd9

Please sign in to comment.