Skip to content

Commit

Permalink
Add custom icon support for SearchAutocomplete/SearchWithin (#3300)
Browse files Browse the repository at this point in the history
* fix(#3299): SearchAutocomplete/SearchWithin support icon prop override

* fix(#3299): SearchField fix padding when there is no icon and isQuiet

* fix(#3299): add chromatic stories

* fix(#3299): update chromatic stories

* fix(#3299): fix lint error

Co-authored-by: Devon Govett <devongovett@gmail.com>
  • Loading branch information
majornista and devongovett committed Aug 3, 2022
1 parent 5414298 commit ec22053
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 95 deletions.
Expand Up @@ -31,7 +31,6 @@ governing permissions and limitations under the License.
}

&.is-quiet .spectrum-Search-input {
padding-inline-start: var(--spectrum-search-quiet-padding-left);
padding-inline-end: var(--spectrum-search-quiet-padding-right);
}

Expand Down
Expand Up @@ -10,6 +10,7 @@
* governing permissions and limitations under the License.
*/

import Filter from '@spectrum-icons/workflow/Filter';
import {generatePowerset} from '@react-spectrum/story-utils';
import {Grid, repeat} from '@react-spectrum/layout';
import {Item, SearchAutocomplete} from '../';
Expand Down Expand Up @@ -128,3 +129,11 @@ PropLabelSide.args = {...PropDefaults.args, labelPosition: 'side'};
export const PropCustomWidth = Template.bind({});
PropCustomWidth.storyName = 'custom width';
PropCustomWidth.args = {...PropDefaults.args, width: 'size-1600'};

export const PropIconFilter = Template.bind({});
PropIconFilter.storyName = 'icon: Filter';
PropIconFilter.args = {...PropDefaults.args, icon: <Filter />};

export const PropIconNull = Template.bind({});
PropIconNull.storyName = 'icon: null';
PropIconNull.args = {...PropDefaults.args, icon: null};
Expand Up @@ -22,4 +22,4 @@ const meta: Meta = {

export default meta;

export {PropDefaults, PropInputValue, PropAriaLabelled, PropLabelEnd, PropLabelSide, PropCustomWidth} from './SearchAutocomplete.chromatic';
export {PropDefaults, PropInputValue, PropAriaLabelled, PropLabelEnd, PropLabelSide, PropCustomWidth, PropIconFilter, PropIconNull} from './SearchAutocomplete.chromatic';
170 changes: 89 additions & 81 deletions packages/@react-spectrum/autocomplete/src/MobileSearchAutocomplete.tsx
Expand Up @@ -19,8 +19,8 @@ import {ComboBoxState, useComboBoxState} from '@react-stately/combobox';
import {DismissButton} from '@react-aria/overlays';
import {Field} from '@react-spectrum/label';
import {FocusableRef, ValidationState} from '@react-types/shared';
import {FocusRing, FocusScope} from '@react-aria/focus';
import {focusSafely} from '@react-aria/focus';
import {FocusScope, useFocusRing} from '@react-aria/focus';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {ListBoxBase, useListBoxLayout} from '@react-spectrum/listbox';
Expand Down Expand Up @@ -99,7 +99,7 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
ref={domRef}
includeNecessityIndicatorInAccessibilityName>
<SearchAutocompleteButton
{...mergeProps(triggerProps, fieldProps, {autoFocus: props.autoFocus})}
{...mergeProps(triggerProps, fieldProps, {autoFocus: props.autoFocus, icon: props.icon})}
ref={buttonRef}
isQuiet={isQuiet}
isDisabled={isDisabled}
Expand All @@ -124,6 +124,7 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
});

interface SearchAutocompleteButtonProps extends AriaButtonProps {
icon?: ReactElement,
isQuiet?: boolean,
isDisabled?: boolean,
isReadOnly?: boolean,
Expand All @@ -137,7 +138,12 @@ interface SearchAutocompleteButtonProps extends AriaButtonProps {
}

const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteButton(props: SearchAutocompleteButtonProps, ref: RefObject<HTMLElement>) {
let searchIcon = (
<Magnifier data-testid="searchicon" />
);

let {
icon = searchIcon,
isQuiet,
isDisabled,
isReadOnly,
Expand All @@ -156,17 +162,15 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
? <AlertMedium id={invalidId} aria-label={stringFormatter.format('invalid')} />
: <CheckmarkMedium />;

let searchIcon = (
<Magnifier data-testid="searchicon" />
);

let icon = React.cloneElement(searchIcon, {
UNSAFE_className: classNames(
textfieldStyles,
'spectrum-Textfield-icon'
),
size: 'S'
});
if (icon) {
icon = React.cloneElement(icon, {
UNSAFE_className: classNames(
textfieldStyles,
'spectrum-Textfield-icon'
),
size: 'S'
});
}

let clearButton = (
<ClearButton
Expand Down Expand Up @@ -199,6 +203,7 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
});

let {hoverProps, isHovered} = useHover({});
let {isFocused, isFocusVisible, focusProps} = useFocusRing();
let {buttonProps} = useButton({
...props,
'aria-labelledby': [
Expand All @@ -211,88 +216,88 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
}, ref);

return (
<FocusRing
focusClass={classNames(styles, 'is-focused')}
focusRingClass={classNames(styles, 'focus-ring')}>
<div
{...mergeProps(hoverProps, focusProps, buttonProps)}
aria-haspopup="dialog"
ref={ref as RefObject<HTMLDivElement>}
style={{...style, outline: 'none'}}
className={
classNames(
styles,
'spectrum-InputGroup',
{
'spectrum-InputGroup--quiet': isQuiet,
'is-disabled': isDisabled,
'spectrum-InputGroup--invalid': validationState === 'invalid',
'is-hovered': isHovered,
'is-focused': isFocused,
'focus-ring': isFocusVisible
},
classNames(
searchAutocompleteStyles,
'mobile-searchautocomplete'
),
className
)
}>
<div
{...mergeProps(hoverProps, buttonProps)}
aria-haspopup="dialog"
ref={ref as RefObject<HTMLDivElement>}
style={{...style, outline: 'none'}}
className={
classNames(
styles,
'spectrum-InputGroup',
textfieldStyles,
'spectrum-Textfield',
{
'spectrum-InputGroup--quiet': isQuiet,
'is-disabled': isDisabled,
'spectrum-InputGroup--invalid': validationState === 'invalid',
'is-hovered': isHovered
'spectrum-Textfield--invalid': validationState === 'invalid',
'spectrum-Textfield--valid': validationState === 'valid',
'spectrum-Textfield--quiet': isQuiet
},
classNames(
searchAutocompleteStyles,
'mobile-searchautocomplete'
),
className
searchStyles,
'spectrum-Search',
{
'is-disabled': isDisabled,
'is-quiet': isQuiet,
'spectrum-Search--invalid': validationState === 'invalid',
'spectrum-Search--valid': validationState === 'valid'
}
)
)
}>
<div
className={
classNames(
textfieldStyles,
'spectrum-Textfield',
'spectrum-Textfield-input',
{
'spectrum-Textfield--invalid': validationState === 'invalid',
'spectrum-Textfield--valid': validationState === 'valid',
'spectrum-Textfield--quiet': isQuiet
'spectrum-Textfield-inputIcon': !!icon,
'is-hovered': isHovered,
'is-placeholder': isPlaceholder,
'is-disabled': isDisabled,
'is-quiet': isQuiet,
'is-focused': isFocused,
'focus-ring': isFocusVisible
},
classNames(
searchStyles,
'spectrum-Search',
{
'is-disabled': isDisabled,
'is-quiet': isQuiet,
'spectrum-Search--invalid': validationState === 'invalid',
'spectrum-Search--valid': validationState === 'valid'
}
'spectrum-Search-input'
)
)
}>
<div
{icon}
<span
id={valueId}
className={
classNames(
textfieldStyles,
'spectrum-Textfield-input',
'spectrum-Textfield-inputIcon',
{
'is-hovered': isHovered,
'is-placeholder': isPlaceholder,
'is-disabled': isDisabled,
'is-quiet': isQuiet
},
classNames(
searchStyles,
'spectrum-Search-input'
)
searchAutocompleteStyles,
'mobile-value'
)
}>
{icon}
<span
id={valueId}
className={
classNames(
searchAutocompleteStyles,
'mobile-value'
)
}>
{children}
</span>
</div>
{validationState ? validation : null}
{(inputValue !== '' || validationState != null) && !isReadOnly && clearButton}
{children}
</span>
</div>
{validationState ? validation : null}
{(inputValue !== '' || validationState != null) && !isReadOnly && clearButton}
</div>
</FocusRing>
</div>
);
});

Expand All @@ -304,9 +309,14 @@ interface SearchAutocompleteTrayProps extends SpectrumSearchAutocompleteProps<un
}

function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
let searchIcon = (
<Magnifier data-testid="searchicon" />
);

let {
// completionMode = 'suggest',
state,
icon = searchIcon,
isDisabled,
validationState,
label,
Expand Down Expand Up @@ -450,17 +460,15 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
}
};

let searchIcon = (
<Magnifier data-testid="searchicon" />
);

let icon = React.cloneElement(searchIcon, {
UNSAFE_className: classNames(
textfieldStyles,
'spectrum-Textfield-icon'
),
size: 'S'
});
if (icon) {
icon = React.cloneElement(icon, {
UNSAFE_className: classNames(
textfieldStyles,
'spectrum-Textfield-icon'
),
size: 'S'
});
}

return (
<FocusScope restoreFocus contain>
Expand Down
11 changes: 6 additions & 5 deletions packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx
Expand Up @@ -198,7 +198,12 @@ interface SearchAutocompleteInputProps extends SpectrumSearchAutocompleteProps<u
}

const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInput(props: SearchAutocompleteInputProps, ref: RefObject<HTMLElement>) {
let searchIcon = (
<Magnifier data-testid="searchicon" />
);

let {
icon = searchIcon,
isQuiet,
isDisabled,
isReadOnly,
Expand Down Expand Up @@ -233,10 +238,6 @@ const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInpu
)} />
);

let searchIcon = (
<Magnifier data-testid="searchicon" />
);

let clearButton = (
<ClearButton
{...clearButtonProps}
Expand Down Expand Up @@ -324,7 +325,7 @@ const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInpu
validationState={validationState}
isLoading={showLoading && (isOpen || menuTrigger === 'manual' || loadingState === 'loading')}
loadingIndicator={loadingState != null && loadingCircle}
icon={searchIcon}
icon={icon}
wrapperChildren={(inputValue !== '' && !isReadOnly) && clearButton} />
</div>
</FocusRing>
Expand Down
Expand Up @@ -12,6 +12,7 @@
*/

import {action} from '@storybook/addon-actions';
import Filter from '@spectrum-icons/workflow/Filter';
import {Flex} from '@react-spectrum/layout';
import {Item, SearchAutocomplete} from '@react-spectrum/autocomplete';
import {mergeProps} from '@react-aria/utils';
Expand Down Expand Up @@ -183,3 +184,9 @@ customWidth6000.storyName = 'custom width: size-6000';

export const customOnSubmit = (props) => <CustomOnSubmit {...props} />;
customOnSubmit.storyName = 'custom onSubmit';

export const iconFilter = (props) => <Default {...props} icon={<Filter />} />;
iconFilter.storyName = 'icon: Filter';

export const iconNull = (props) => <Default {...props} icon={null} />;
iconNull.storyName = 'icon: null';
Expand Up @@ -14,6 +14,7 @@ jest.mock('@react-aria/live-announcer');
import {act, fireEvent, render, screen, triggerPress, typeText, waitFor, within} from '@react-spectrum/test-utils';
import {announce} from '@react-aria/live-announcer';
import {Button} from '@react-spectrum/button';
import Filter from '@spectrum-icons/workflow/Filter';
import {Item, SearchAutocomplete, Section} from '../';
import {Provider} from '@react-spectrum/provider';
import React from 'react';
Expand Down Expand Up @@ -165,6 +166,18 @@ describe('SearchAutocomplete', function () {
expect(label).toBeVisible();
});

it('should support custom icons', function () {
let {getByTestId} = renderSearchAutocomplete({icon: <Filter data-testid="filtericon" />});

expect(getByTestId('filtericon')).toBeTruthy();
});

it('should support no icons', function () {
let {queryByTestId} = renderSearchAutocomplete({icon: null});

expect(queryByTestId('searchicon')).toBeNull();
});

it('renders with placeholder text and shows warning', function () {
let spyWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
let {getByPlaceholderText, getByRole} = renderSearchAutocomplete({placeholder: 'Test placeholder'});
Expand Down
Expand Up @@ -45,6 +45,11 @@ storiesOf('SearchField', module)
() => renderSearchLandmark(render({defaultValue: 'React', icon: <Refresh />})),
{info}
)
.add(
'icon: null',
() => renderSearchLandmark(render({defaultValue: 'React', icon: null})),
{info}
)
.add('custom width',
() => render({defaultValue: 'React', width: 275})
);
Expand Down

0 comments on commit ec22053

Please sign in to comment.