Skip to content

Commit

Permalink
feat(answers): add EXPERIMENTAL_answers widget (#4581)
Browse files Browse the repository at this point in the history
* feat(answers): add widget

* add warning when findAnswers is not supported

* hits fall back to an empty array if not exists

* split into widget + connector + component set

* fix concurrency issue + types

* debounce render

* update stories

* fix types

* add tests

* update storybook

* add comments

* pass query parameters to findAnswers

* disable lint error

* update bundlesize

* Revert "pass query parameters to findAnswers"

This reverts commit 98529c2.

* rename answers widget to EXPERIMENTAL_answers

* remove validation of attributesForPrediction because it can be optional

* remove unnecessary validation

* add __position to hits

* make queryLanguages required

* add "EXPERIMENTAL_"

* Update src/types/algoliasearch.ts

Co-authored-by: Haroen Viaene <hello@haroen.me>

* clean up import & export

* less typing for temporary part

* Update src/connectors/answers/connectAnswers.ts

Co-authored-by: Haroen Viaene <hello@haroen.me>

* add debounceTime

* override x-algolia-agent via requestOptions

* update FindAnswersResponse type

* add test case for queryLanguages missing

* add generic to FindAnswersResponse

* escape answers hits and add queryID

* clear hits when trigger a new search

* Update src/connectors/answers/connectAnswers.ts

Co-authored-by: Clément Vannicatte <20689156+shortcuts@users.noreply.github.com>

* Update src/connectors/answers/__tests__/connectAnswers-test.ts

Co-authored-by: Clément Vannicatte <20689156+shortcuts@users.noreply.github.com>

* do not render list when loading

* fix test cases to include objectID and __escaped

* debounce search call

* fix $$type and $$widgetType

* add comment

* change default render debounce time to 100ms

* catch promise rejects inside the connector

* Update src/components/Answers/Answers.tsx

Co-authored-by: Clément Vannicatte <20689156+shortcuts@users.noreply.github.com>

* clean up `wait` function

* fix wrong closing tag

* add types to debounce functions

* fix eslint rule for EXPERIMENTAL_

* support extraParameters

* remove comma

* update types alphabetically

* remove unused css

* clean up debounce functions

* update bundlesize

* chore: release v4.13.0 (#4635)

* chore: release v4.13.0

* remove unnecessary catch

* remove this from debounce

* Apply suggestions from code review

Co-authored-by: François Chalifour <francoischalifour@users.noreply.github.com>

* fix types for js client v3

* add ts-ignore

* remove the user agent workaround

* update the types to use Partial

* Update src/lib/utils/createConcurrentSafePromise.ts

Co-authored-by: Clément Vannicatte <20689156+shortcuts@users.noreply.github.com>

* update tests

* fix types

* update bundlesize

* Update src/types/algoliasearch.ts

Co-authored-by: Clément Vannicatte <20689156+shortcuts@users.noreply.github.com>

* Update src/connectors/answers/connectAnswers.ts

Co-authored-by: Haroen Viaene <hello@haroen.me>

* fix lint error

* update bundlesize

* update bundlesize

Co-authored-by: Haroen Viaene <hello@haroen.me>
Co-authored-by: Clément Vannicatte <20689156+shortcuts@users.noreply.github.com>
Co-authored-by: InstantSearch <66688561+instantsearch-bot@users.noreply.github.com>
Co-authored-by: François Chalifour <francoischalifour@users.noreply.github.com>
  • Loading branch information
5 people committed Feb 26, 2021
1 parent e7aaa8c commit e4c9070
Show file tree
Hide file tree
Showing 25 changed files with 1,715 additions and 18 deletions.
6 changes: 1 addition & 5 deletions .eslintrc.js
Expand Up @@ -7,11 +7,7 @@ module.exports = {
'new-cap': [
'error',
{
capIsNewExceptions: [
'EXPERIMENTAL_use',
'EXPERIMENTAL_connectConfigureRelatedItems',
'EXPERIMENTAL_configureRelatedItems',
],
capIsNewExceptionPattern: '(\\.|^)EXPERIMENTAL_.+',
},
],
'react/no-string-refs': 'error',
Expand Down
132 changes: 132 additions & 0 deletions .storybook/static/answers.css
@@ -0,0 +1,132 @@
.my-Answers .ais-Answers-loader {
display: none;
}

.my-Answers .ais-Answers-list {
list-style: none;
margin: 0;
padding: 0;
}

.my-Answers .ais-Answers-item {
height: 10rem;
border: 1px solid #ddd;
border-radius: 0.5rem;
}

.my-Answers .title {
padding: 0;
margin: 1rem;
font-size: 1.2rem;
color: #333;
line-height: 1.4rem;
}

.my-Answers .separator {
border-top: 1px solid #ddd;
}

.my-Answers .description {
margin: 1rem;
padding: 0;
color: #333;
line-height: 1.4rem;
}

.my-Answers .description em {
background-color: #ffc168;
}

.one-line {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}

.three-lines {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

/* skeleton loader from https://codepen.io/jordanmsykes/pen/RgPqgV - begin */
@keyframes placeHolderShimmer {
0% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
background-position: -468px 0;
}
to {
-webkit-transform: translateZ(0);
transform: translateZ(0);
background-position: 468px 0;
}
}

.card-skeleton {
margin-left: 1rem;
margin-right: 1rem;
width: calc(100% - 2rem);
height: 10rem;
transition: all 0.3s ease-in-out;
-webkit-backface-visibility: hidden;
background: #fff;
z-index: 10;
opacity: 1;
}

.card-skeleton.hidden {
transition: all 0.3s ease-in-out;
opacity: 0;
height: 0;
padding: 0;
}

.card-skeleton-img {
width: 100%;
height: 120px;
background: #e6e6e6;
display: block;
}

.animated-background {
will-change: transform;
animation: placeHolderShimmer 1s linear infinite forwards;
-webkit-backface-visibility: hidden;
background: #e6e6e6;
background: linear-gradient(90deg, #eee 8%, #ddd 18%, #eee 33%);
background-size: 800px 104px;
height: 100%;
position: relative;
}

.skel-mask-container {
position: relative;
}

.skel-mask {
background: #fff;
position: absolute;
z-index: 200;
}

.skel-mask-1 {
width: 100%;
height: 15px;
top: 0;
}

.skel-mask-2 {
width: 100%;
height: 25px;
top: 45px;
}

.skel-mask-3 {
width: 100%;
height: 15px;
top: 145px;
}
/* skeleton loader - end */
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -143,7 +143,7 @@
"bundlesize": [
{
"path": "./dist/instantsearch.production.min.js",
"maxSize": "67.00 kB"
"maxSize": "68.00 kB"
},
{
"path": "./dist/instantsearch.development.js",
Expand Down
74 changes: 74 additions & 0 deletions src/components/Answers/Answers.tsx
@@ -0,0 +1,74 @@
/** @jsx h */

import { h } from 'preact';
import cx from 'classnames';
import Template from '../Template/Template';
import { AnswersTemplates } from '../../widgets/answers/answers';
import { Hits } from '../../types';

type AnswersCSSClasses = {
root: string;
emptyRoot: string;
header: string;
loader: string;
list: string;
item: string;
};

export type AnswersProps = {
hits: Hits;
isLoading: boolean;
cssClasses: AnswersCSSClasses;
templateProps: {
[key: string]: any;
templates: AnswersTemplates;
};
};

const Answers = ({
hits,
isLoading,
cssClasses,
templateProps,
}: AnswersProps) => (
<div
className={cx(cssClasses.root, {
[cssClasses.emptyRoot]: hits.length === 0,
})}
>
<Template
{...templateProps}
templateKey="header"
rootProps={{ className: cssClasses.header }}
data={{
hits,
isLoading,
}}
/>
{isLoading ? (
<Template
{...templateProps}
templateKey="loader"
rootProps={{ className: cssClasses.loader }}
/>
) : (
<ul className={cssClasses.list}>
{hits.map((hit, position) => (
<Template
{...templateProps}
templateKey="item"
rootTagName="li"
rootProps={{ className: cssClasses.item }}
key={hit.objectID}
data={{
...hit,
__hitIndex: position,
}}
/>
))}
</ul>
)}
</div>
);

export default Answers;
124 changes: 124 additions & 0 deletions src/components/Answers/__tests__/Answers-test.tsx
@@ -0,0 +1,124 @@
/** @jsx h */

import { h } from 'preact';
import { render } from '@testing-library/preact';
import Answers, { AnswersProps } from '../Answers';

const defaultProps: AnswersProps = {
hits: [],
isLoading: false,
cssClasses: {
root: 'root',
header: 'header',
emptyRoot: 'empty',
loader: 'loader',
list: 'list',
item: 'item',
},
templateProps: {
templates: {
header: 'header',
loader: 'loader',
item: 'item',
},
},
};

describe('Answers', () => {
describe('Rendering', () => {
it('renders without anything', () => {
const { container } = render(<Answers {...defaultProps} />);
expect(container.querySelector('.root')).toHaveClass('empty');
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="root empty"
>
<div
class="header"
>
header
</div>
<ul
class="list"
/>
</div>
</div>
`);
});

it('renders the loader', () => {
const { container } = render(
<Answers {...defaultProps} isLoading={true} />
);
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="root empty"
>
<div
class="header"
>
header
</div>
<div
class="loader"
>
loader
</div>
</div>
</div>
`);
});

it('renders the header with data', () => {
const props: AnswersProps = {
...defaultProps,
templateProps: {
templates: {
...defaultProps.templateProps.templates,
header: ({ hits, isLoading }) => {
return `${hits.length} answer(s) ${
isLoading ? 'loading' : 'loaded'
}`;
},
},
},
};
const { container } = render(
<Answers
{...props}
isLoading={false}
hits={[{ objectID: '1', __position: 1 }]}
/>
);
expect(container.querySelector('.header')).toHaveTextContent(
'1 answer(s) loaded'
);
});

it('renders the answers', () => {
const props: AnswersProps = {
...defaultProps,
templateProps: {
templates: {
...defaultProps.templateProps.templates,
item: hit => {
return `answer: ${hit.title}`;
},
},
},
};
const { container } = render(
<Answers
{...props}
isLoading={false}
hits={[{ objectID: '1', title: 'hello!', __position: 1 }]}
/>
);
expect(container.querySelector('.list')).toHaveTextContent(
'answer: hello!'
);
});
});
});

0 comments on commit e4c9070

Please sign in to comment.