Skip to content

Commit

Permalink
feat: (sfui2-1089) additional search blocks (#2746)
Browse files Browse the repository at this point in the history
* feat: created react with icon search

* feat: added examples for search with button

* feat: added vue version

* chore: lint improvements

* chore: prettier improvement

* chore: prettier fix

* chore: added changeset

---------

Co-authored-by: Jakub Freisler <jakub@frsource.org>
  • Loading branch information
aniamusial and FRSgit authored May 31, 2023
1 parent 9a06d7b commit 74dfce2
Show file tree
Hide file tree
Showing 8 changed files with 780 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .changeset/odd-trains-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@storefront-ui/react': minor
'@storefront-ui/vue': minor
'@storefront-ui/shared': minor
---

Added new search blocks with customized buttons
30 changes: 30 additions & 0 deletions apps/docs/components/blocks/Search.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,33 @@ Simple search with an autocomplete functionality. Give your users hints of what
<!-- end react -->

</Showcase>

## Search with custom icon

This block enhances search functionality with a custom icon and categorized result suggestions.

<Showcase showcase-name="Search/SearchWithIcon" style="min-height: 500px">

<!-- vue -->
<<<../../preview/nuxt/pages/showcases/Search/SearchWithIcon.vue
<!-- end vue -->
<!-- react -->
<<<../../preview/next/pages/showcases/Search/SearchWithIcon.tsx#source
<!-- end react -->

</Showcase>

## Search with custom button

This block enhances search functionality with a custom button, categorized result suggestions and thumbnails.

<Showcase showcase-name="Search/SearchWithButton" style="min-height: 500px">

<!-- vue -->
<<<../../preview/nuxt/pages/showcases/Search/SearchWithButton.vue
<!-- end vue -->
<!-- react -->
<<<../../preview/next/pages/showcases/Search/SearchWithButton.tsx#source
<!-- end react -->

</Showcase>
2 changes: 1 addition & 1 deletion apps/preview/next/pages/showcases/Search/SearchBasic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default function SearchBasic() {
slotSuffix={
isResetButton && (
<button
type="button"
type="reset"
onClick={handleReset}
aria-label="Reset search"
className="flex rounded-md focus-visible:outline focus-visible:outline-offset"
Expand Down
196 changes: 196 additions & 0 deletions apps/preview/next/pages/showcases/Search/SearchWithButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/* eslint-disable no-alert */
/* eslint-disable no-console */
import { ShowcasePageLayout } from '../../showcases';
// #region source
import { type ChangeEvent, type FormEvent, useState, useRef } from 'react';
import { useDebounce } from 'react-use';
import { offset } from '@floating-ui/react-dom';
import {
SfButton,
SfInput,
SfIconSearch,
SfIconCancel,
useDisclosure,
SfListItem,
SfLoaderCircular,
useTrapFocus,
useDropdown,
SfIconGridView,
} from '@storefront-ui/react';

interface Product {
id: string;
name: string;
thumbnail?: JSX.Element;
image?: string;
}

const mockProducts: Product[] = [
{ id: 'j-avatar', name: 'jack', image: 'http://localhost:3100/@assets/kid.png' },
{ id: 'j-cat', name: 'jackets', thumbnail: <SfIconGridView /> },
{ id: 'j-wom', name: 'jacket women', thumbnail: <SfIconSearch /> },
{ id: 'j-den', name: 'jacket denim', thumbnail: <SfIconSearch /> },
{ id: 'j-dre', name: 'jacket dress', thumbnail: <SfIconSearch /> },
{ id: 'dr-cat', name: 'dresses', thumbnail: <SfIconGridView /> },
{ id: 'dr-cot', name: 'cotton dresses', thumbnail: <SfIconSearch /> },
{ id: 'dr-wom', name: 'dresses women', thumbnail: <SfIconSearch /> },
{ id: 'dr-sum', name: 'summer dresses', thumbnail: <SfIconSearch /> },
];

// Just for presentation purposes. Replace mock request with the actual API call.
// eslint-disable-next-line no-promise-executor-return
const delay = () => new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
const mockAutocompleteRequest = async (phrase: string) => {
await delay();
const results = mockProducts
.filter((product) => product.name.toLowerCase().startsWith(phrase.toLowerCase()))
.map((product) => {
const highlight = product.name.substring(0, phrase.length);
const rest = product.name.substring(phrase.length);
return { highlight, rest, product };
});
return results;
};

export default function SearchWithIcon() {
const inputRef = useRef<HTMLInputElement>(null);
const dropdownListRef = useRef<HTMLUListElement>(null);
const [searchValue, setSearchValue] = useState('');
const [isLoadingSnippets, setIsLoadingSnippets] = useState(false);
const [snippets, setSnippets] = useState<{ highlight: string; rest: string; product: Product }[]>([]);
const { isOpen, close, open } = useDisclosure();
const { refs, style } = useDropdown({
isOpen,
onClose: close,
placement: 'bottom-start',
middleware: [offset(4)],
});
useTrapFocus(dropdownListRef, { arrowKeysOn: true, activeState: isOpen, initialFocus: false });
const isResetButton = Boolean(searchValue);
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
close();
alert(`Search for phrase: ${searchValue}`);
};

const handleFocusInput = () => {
inputRef.current?.focus();
};

const handleReset = () => {
setSearchValue('');
setSnippets([]);
close();
handleFocusInput();
};

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const phrase = event.target.value;
if (phrase) {
setSearchValue(phrase);
} else {
handleReset();
}
};

const handleSelect = (phrase: string) => () => {
setSearchValue(phrase);
close();
handleFocusInput();
};

useDebounce(
() => {
if (searchValue) {
const getSnippets = async () => {
open();
setIsLoadingSnippets(true);
try {
const data = await mockAutocompleteRequest(searchValue);
setSnippets(data);
} catch (error) {
close();
console.error(error);
}
setIsLoadingSnippets(false);
};

getSnippets();
}
},
500,
[searchValue],
);

return (
<form role="search" onSubmit={handleSubmit} ref={refs.setReference} className="relative">
<div className="flex">
<SfInput
ref={inputRef}
value={searchValue}
onChange={handleChange}
onFocus={open}
wrapperClassName="w-full ring-0 active:ring-0 hover:ring-0 focus-within:ring-0 border-y border-l border-neutral-200 rounded-r-none hover:border-primary-800 active:border-primary-700 active:border-y-2 active:border-l-2 focus-within:border-y-2 focus-within:border-l-2 focus-within:border-primary-700"
aria-label="Search"
placeholder="Search 'jackets' or 'dresses'..."
slotPrefix={<SfIconSearch />}
slotSuffix={
isResetButton && (
<button
type="reset"
onClick={handleReset}
aria-label="Reset search"
className="flex rounded-md focus-visible:outline focus-visible:outline-offset"
>
<SfIconCancel />
</button>
)
}
/>
<SfButton type="submit" className="rounded-l-none">
Search
</SfButton>
</div>
{isOpen && (
<div ref={refs.setFloating} style={style} className="left-0 right-0">
{isLoadingSnippets ? (
<div className="flex items-center justify-center bg-white w-full h-screen sm:h-20 py-2 sm:border sm:border-solid sm:rounded-md sm:border-neutral-100 sm:drop-shadow-md">
<SfLoaderCircular />
</div>
) : (
snippets.length > 0 && (
<ul
ref={dropdownListRef}
className="py-2 bg-white sm:border border-solid sm:rounded-md sm:border-neutral-100 sm:drop-shadow-md"
>
{snippets.map(({ highlight, rest, product }) => (
<li key={product.id}>
<SfListItem
as="button"
type="button"
onClick={handleSelect(product.name)}
className="!py-4 sm:!py-2 flex justify-start"
>
<p className="flex items-center text-left text-neutral-500">
{product.image ? (
<img src={product.image} alt={product.name} className="rounded-sm" width={24} height={24} />
) : (
product.thumbnail
)}
<span className="ml-2 text-neutral-900">{highlight}</span>
<span className="font-medium text-neutral-900">{rest}</span>
</p>
</SfListItem>
</li>
))}
</ul>
)
)}
</div>
)}
</form>
);
}

// #endregion source
SearchWithIcon.getLayout = ShowcasePageLayout;
Loading

0 comments on commit 74dfce2

Please sign in to comment.