Skip to content

Commit

Permalink
[IndexFilters] Prototype "add as filter" search input UX
Browse files Browse the repository at this point in the history
[IndexFilters] Remove search from FilterButton

[IndexFilters] Add a SearchField subcomponent

[IndexFilters] Move search input beside tabs; prototype add as filter UX

[IndexFilters] Update stories to change search placeholder tab reference when active tab changes

[Storybook][Test Pages] Create a new ProductsPage story for prototype

WIP mock saved filter logic (have to fake outside of admin)

switch prototype to tabless filterless orders index

ignore commas when filtering by query filter

Improve order data with David's feedback

Implement fully working tabs and filters to flesh out the table

[Key dev dependencies] Bumped TS and Storybook packages

Ensure special chars like '#' don't break search
  • Loading branch information
chloerice committed Aug 26, 2024
1 parent a52c476 commit e933d8d
Show file tree
Hide file tree
Showing 25 changed files with 2,588 additions and 179 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.pressed > button {
/* stylelint-disable polaris/conventions/polaris/custom-property-allowed-list -- Filter section activator pressed state */
background: var(--pc-button-bg_active);
color: var(--pc-button-color_active);
box-shadow: var(--pc-button-box-shadow_active);
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import React from 'react';
import type {CSSProperties} from 'react';
import {SearchIcon, FilterIcon} from '@shopify/polaris-icons';
import {FilterIcon} from '@shopify/polaris-icons';

<<<<<<<< HEAD:polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx
import {Icon} from '../../../Icon';
========
>>>>>>>> ee64715c9 ([IndexFilters] Prototype "add as filter" search input UX):polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx
import {Tooltip} from '../../../Tooltip';
import {Text} from '../../../Text';
import {InlineStack} from '../../../InlineStack';
import {Button} from '../../../Button';

export interface SearchFilterButtonProps {
import styles from './FilterButton.module.css';

export interface FilterButtonProps {
onClick: () => void;
label: string;
disabled?: boolean;
pressed?: boolean;
tooltipContent: string;
disclosureZIndexOverride?: number;
hideFilters?: boolean;
hideQueryField?: boolean;
style: CSSProperties;
}

export function SearchFilterButton({
export function FilterButton({
onClick,
label,
disabled,
pressed,
tooltipContent,
disclosureZIndexOverride,
<<<<<<<< HEAD:polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx
style,
hideFilters,
hideQueryField,
Expand All @@ -35,14 +38,19 @@ export function SearchFilterButton({
{hideFilters ? null : <Icon source={FilterIcon} tone="base" />}
</InlineStack>
);
========
}: FilterButtonProps) {
const className = pressed ? styles.pressed : undefined;
>>>>>>>> ee64715c9 ([IndexFilters] Prototype "add as filter" search input UX):polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx

const activator = (
<div style={style}>
<div className={className}>
<Button
size="slim"
onClick={onClick}
disabled={disabled}
icon={iconMarkup}
pressed={pressed}
icon={FilterIcon}
accessibilityLabel={label}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {FilterButton} from './FilterButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, {useId, useState} from 'react';
import {SearchIcon, ReturnIcon} from '@shopify/polaris-icons';

import {Box} from '../../../Box';
import {Icon} from '../../../Icon';
import {TextField} from '../../../TextField';
import {useBreakpoints} from '../../../../utilities/breakpoints';
import {useI18n} from '../../../../utilities/i18n';
import {InlineStack} from '../../../InlineStack';

export interface SearchFieldProps {
focused?: boolean;
value?: string;
placeholder?: string;
disabled?: boolean;
/** Shows a loading spinner to the right of the input */
loading?: boolean;
onChange: (value: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onClear?: () => void;
onKeyDownEnter?(): void;
}

export function SearchField({
focused: forceFocus = false,
value,
placeholder,
disabled,
loading,
onChange,
onClear,
onFocus,
onBlur,
onKeyDownEnter,
}: SearchFieldProps) {
const id = useId();
const i18n = useI18n();
const {mdUp} = useBreakpoints();
const [focused, setFocused] = useState(forceFocus);

function handleChange(eventValue: string) {
onChange(eventValue ?? value);
}

function handleClear() {
if (onClear) {
onClear();
} else {
onChange('');
}
}

function handleKeyDown(event: React.KeyboardEvent) {
if (event.key === 'Enter') onKeyDownEnter?.();
}

function handleFocus() {
onFocus?.();
setFocused(true);
}

function handleBlur() {
onBlur?.();
setFocused(false);
}

const addAsFilterText =
value && focused ? (
<InlineStack gap="200" blockAlign="center" align="end">
{i18n.translate('Polaris.IndexFilters.SearchField.action.addAsFilter')}
<Box borderRadius="100" background="bg-surface-tertiary-hover">
<Icon source={ReturnIcon} tone="inherit" />
</Box>
</InlineStack>
) : undefined;

return (
<div
onKeyDown={handleKeyDown}
style={{width: '100%', minWidth: mdUp ? '22.5rem' : 'none'}}
>
<TextField
id={id}
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onClearButtonClick={handleClear}
autoComplete="off"
placeholder={placeholder}
disabled={disabled}
variant="borderless"
size="slim"
prefix={mdUp ? <Icon source={SearchIcon} /> : undefined}
focused={focused}
label={
placeholder ??
i18n.translate('Polaris.IndexFilters.SearchField.defaultPlaceholder')
}
labelHidden
clearButton
loading={loading}
suffix={addAsFilterText}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {SearchField} from './SearchField';
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import type {ComponentProps} from 'react';
import {mountWithApp} from 'tests/utilities';

import {SearchField} from '..';
import {TextField} from '../../../../TextField';

jest.mock('../../../../../utilities/breakpoints', () => ({
...(jest.requireActual('../../../../../utilities/breakpoints') as any),
useBreakpoints: jest.fn(),
}));

function mockUseBreakpoints(mdUp: boolean) {
const useBreakpoints: jest.Mock = jest.requireMock(
'../../../../../utilities/breakpoints',
).useBreakpoints;

useBreakpoints.mockReturnValue({
mdUp,
});
}

describe('SearchField', () => {
const defaultProps: ComponentProps<typeof SearchField> = {
onChange: jest.fn(),
value: 'foo',
placeholder: 'bar',
};

beforeEach(() => {
jest.clearAllMocks();
mockUseBreakpoints(false);
});

it('will call onChange when changed', () => {
const props = {...defaultProps};
const spy = jest.fn();
const wrapper = mountWithApp(<SearchField {...props} onChange={spy} />, {});

wrapper.act(() => {
wrapper.find(TextField)!.trigger('onChange');
});

expect(spy).toHaveBeenCalledTimes(1);
});

it('will call onChange correctly when clear button clicked', () => {
const props = {...defaultProps};
const wrapper = mountWithApp(<SearchField {...props} />, {});

wrapper.act(() => {
wrapper.find(TextField)?.trigger('onClearButtonClick');
});

expect(props.onChange).toHaveBeenCalledWith('');
});

it('will call onFocus', () => {
const props = {...defaultProps, onFocus: jest.fn()};
const wrapper = mountWithApp(<SearchField {...props} />);

wrapper.act(() => {
wrapper.findAll('input')[0]?.trigger('onFocus');
});

expect(props.onFocus).toHaveBeenCalledTimes(1);
});

it('will call onBlur', () => {
const props = {...defaultProps, onBlur: jest.fn()};
const wrapper = mountWithApp(<SearchField {...props} />);

wrapper.act(() => {
wrapper.findAll('input')[0]?.trigger('onBlur');
});

expect(props.onBlur).toHaveBeenCalledTimes(1);
});

it('will pass the placeholder', () => {
const wrapper = mountWithApp(<SearchField {...defaultProps} />);

expect(wrapper).toContainReactComponent('input', {
placeholder: defaultProps.placeholder,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {PropsWithChildren} from 'react';
import React from 'react';
import type {PropsWithChildren} from 'react';

interface Data {
id: string;
Expand Down
14 changes: 11 additions & 3 deletions polaris-react/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,17 @@
"unsavedChanges": "Unsaved changes - {label}"
},
"IndexFilters": {
"searchFilterTooltip": "Search and filter",
"searchFilterTooltipWithShortcut": "Search and filter (F)",
"searchFilterAccessibilityLabel": "Search and filter results",
"searchFilterTooltip": "Filter",
"searchFilterTooltipWithShortcut": "Filter (F)",
"searchFilterAccessibilityLabel": "Filter results",
"editSearchFilter": "Edit search filter",
"SearchField": {
"defaultPlaceholder": "Search",
"action": {
"addAsFilter": "add as filter",
"addToFilter": "add to filter"
}
},
"sort": "Sort your results",
"addView": "Add a new view",
"newView": "Custom search",
Expand Down
Loading

0 comments on commit e933d8d

Please sign in to comment.