algolia / instantsearch

⚡️ Libraries for building performant and instant search and recommend experiences with Algolia. Compatible with JavaScript, TypeScript, React and Vue.
https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/
MIT License
3.61k stars 505 forks source link

Color refinement list for InstantSearch.js #5137

Open dhayab opened 1 year ago

dhayab commented 1 year ago

We currently provide a ColorRefinementList widget for React InstantSearch, and we'd like to gather interest in porting this widget to InstantSearch.js.

What's a color refinement list?

A color refinement list is an InstantSearch widget that filters results based on color facet values. The widget only displays the most relevant colors for the current search context.

It shares similarities with a standard refinement list, but it displays color indicators instead of text facet values. This helps users quickly visualize the kind of color that products have in your index.

See Live Demo →

How to show your interest?

Simply add a "👍" reaction on this issue, and optionally subscribe to its notifications. We will share updates here.

aayoubi commented 1 year ago

Commenting on behalf of a prospect, they'd like to implement visual facets through a similar widget with vanilla js (on their custom Magento theme). Today they've built the following:

image
spacecat commented 9 months ago

Hi, it's been a year since this was posted. Just wanted to check if there's been any progress? Is this feature in your roadmap? If so, do you have an ETA? Thanks.

BTW, are there any examples/demos on how to implement this feature using React Instantsearch v7?

Haroenv commented 9 months ago

This is currently not on a short-term roadmap, but it's fairly straightforward to do in a custom widget, as you can see in this example for React InstantSearch Hooks (same as React InstantSearch v7, just change the package names): https://github.com/algolia-samples/storefront-demo-nextjs/blob/main/components/ColorRefinementList.tsx

import { CheckIcon } from '@heroicons/react/24/outline';
import { useRefinementList, type RefinementListProps } from 'react-instantsearch-hooks';

type ColorRefinementListProps = RefinementListProps & {
  classNames: Pick<RefinementListProps, 'classNames'> &
    Partial<{
      swatch: string;
      swatchIcon: string;
    }>;
};

function cx(
  ...classNames: Array<string | number | boolean | undefined | null>
) {
  return classNames.filter(Boolean).join(' ');
}

function extractColorFacet(facet: string) {
  const [label, color] = facet.split(';');

  return { label, color };
}

export function ColorRefinementList({
  searchable,
  searchablePlaceholder,
  attribute,
  operator,
  limit,
  showMore,
  showMoreLimit,
  sortBy,
  escapeFacetValues,
  transformItems,
  classNames = {},
  className,
  ...props
}: ColorRefinementListProps) {
  const { canRefine, items, refine } = useRefinementList({
    attribute,
    operator,
    limit,
    showMore,
    showMoreLimit,
    sortBy,
    escapeFacetValues,
    transformItems,
  });

  return (
    <div
      {...props}
      className={cx(
        'ais-ColorRefinementList',
        classNames.root,
        !canRefine &&
          cx(
            'ais-ColorRefinementList--noRefinement',
            classNames.noRefinementRoot
          ),
        className
      )}
    >
      <ul className={cx('ais-ColorRefinementList-list', classNames.list)}>
        {items.map((item) => {
          const { label, color } = extractColorFacet(item.label);

          return (
            <li
              key={item.value}
              className={cx(
                'ais-ColorRefinementList-item',
                classNames.item,
                item.isRefined &&
                  cx(
                    'ais-ColorRefinementList-item--selected',
                    classNames.selectedItem
                  )
              )}
            >
              <label
                className={cx(
                  'ais-ColorRefinementList-label',
                  classNames.label
                )}
              >
                <input
                  checked={item.isRefined}
                  className={cx(
                    'ais-ColorRefinementList-checkbox',
                    classNames.checkbox
                  )}
                  type="checkbox"
                  value={item.value}
                  onChange={() => refine(item.value)}
                />
                <div
                  style={{
                    background: color.startsWith('#') ? color : `url(${color})`,
                  }}
                  className={cx(
                    'ais-ColorRefinementList-swatch',
                    classNames.swatch
                  )}
                >
                  {item.isRefined && (
                    <CheckIcon
                      className={cx(
                        'ais-ColorRefinementList-swatchIcon',
                        classNames.swatchIcon
                      )}
                    />
                  )}
                </div>
                <span
                  className={cx(
                    'ais-ColorRefinementList-labelText',
                    classNames.labelText
                  )}
                >
                  {label}
                </span>
                <span
                  className={cx(
                    'ais-ColorRefinementList-count',
                    classNames.count
                  )}
                >
                  {item.count}
                </span>
              </label>
            </li>
          );
        })}
      </ul>
    </div>
  );
}