Skip to content

Commit

Permalink
feat(ui-components): migrate Hits component (#6042)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
  • Loading branch information
3 people authored Feb 16, 2024
1 parent 66ac853 commit 55d550e
Show file tree
Hide file tree
Showing 24 changed files with 676 additions and 884 deletions.
10 changes: 5 additions & 5 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,27 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "76.25 kB"
"maxSize": "76.5 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "167.75 kB"
"maxSize": "168.5 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
"maxSize": "46.25 kB"
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "57.75 kB"
"maxSize": "58 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
"maxSize": "64.5 kB"
"maxSize": "65.25 kB"
},
{
"path": "packages/vue-instantsearch/vue3/umd/index.js",
"maxSize": "65.25 kB"
"maxSize": "65.75 kB"
},
{
"path": "packages/vue-instantsearch/vue2/cjs/index.js",
Expand Down
118 changes: 118 additions & 0 deletions packages/instantsearch-ui-components/__tests__/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import fs from 'fs';
import path from 'path';

import ts from 'typescript';

/**
* Simple linting using the TypeScript compiler API.
*/
function delint(sourceFile: ts.SourceFile) {
const errors: Array<{
file: string;
line: number;
character: number;
message: string;
}> = [];

delintNode(sourceFile);

function delintNode(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.FunctionDeclaration: {
const functionDeclaration = node as ts.FunctionDeclaration;
const fileNameSegment = sourceFile.fileName.replace('.d.ts', '');
const componentName = `create${fileNameSegment}Component`;
if (
functionDeclaration.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
) &&
functionDeclaration.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword
)
) {
const actualName = functionDeclaration.name?.getText();
if (actualName !== componentName) {
report(
node,
`Exported component should be named '${componentName}', but was '${actualName}' instead.`
);
}

const returnType = functionDeclaration.type as ts.FunctionTypeNode;

if (returnType.kind !== ts.SyntaxKind.FunctionType) {
report(
node,
`Exported component's return type should be a function.`
);
}

if (
returnType.kind === ts.SyntaxKind.FunctionType &&
returnType.parameters.length !== 1
) {
report(
node,
`Exported component's return type should have exactly one parameter`
);
}

if (
functionDeclaration.type?.kind === ts.SyntaxKind.FunctionType &&
(
functionDeclaration.type as ts.FunctionTypeNode
).parameters[0].name.getText() !== 'userProps'
) {
report(
node,
`Exported component's return type should be called 'userProps'.`
);
}
}

break;
}
default: {
break;
}
}

ts.forEachChild(node, delintNode);
}

function report(node: ts.Node, message: string) {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
node.getStart()
);
errors.push({
file: sourceFile.fileName,
line: line + 1,
character: character + 1,
message,
});
}

return errors;
}

const files = fs
.readdirSync(path.join(__dirname, '../dist/es/components'))
.filter((file) => file.endsWith('.d.ts') && !file.startsWith('index'));

describe('Exposes correct types', () => {
describe.each(files)('%s', (file) => {
it('has no errors', () => {
const setParentNodes = true;
const sourceFile = ts.createSourceFile(
file,
fs
.readFileSync(path.join(__dirname, '../dist/es/components', file))
.toString(),
ts.ScriptTarget.ES2015,
setParentNodes
);

expect(delint(sourceFile)).toEqual([]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ export function createHighlightComponent({
});

return function Highlight(userProps: HighlightProps) {
// Not destructured in function signature, to make sure it's not exposed in
// the type definition.
const {
parts,
highlightedTagName = 'mark',
Expand Down
97 changes: 97 additions & 0 deletions packages/instantsearch-ui-components/src/components/Hits.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/** @jsx createElement */
import { cx } from '../lib';

import type { ComponentProps, Renderer } from '../types';

// Should be imported from a shared package in the future
type Hit = Record<string, unknown> & {
objectID: string;
};
type SendEventForHits = (...props: unknown[]) => void;

export type HitsProps<THit> = ComponentProps<'div'> & {
hits: THit[];
itemComponent: (props: {
hit: THit;
index: number;
className: string;
onClick: () => void;
onAuxClick: () => void;
}) => JSX.Element;
sendEvent: SendEventForHits;
classNames?: Partial<HitsClassNames>;
emptyComponent?: (props: { className: string }) => JSX.Element;
};

export type HitsClassNames = {
/**
* Class names to apply to the root element
*/
root: string | string[];
/**
* Class names to apply to the root element without results
*/
emptyRoot: string | string[];
/**
* Class names to apply to the list element
*/
list: string | string[];
/**
* Class names to apply to each item element
*/
item: string | string[];
};

export function createHitsComponent({ createElement }: Renderer) {
return function Hits<THit extends Hit>(userProps: HitsProps<THit>) {
const {
classNames = {},
hits,
itemComponent: ItemComponent,
sendEvent,
emptyComponent: EmptyComponent,
...props
} = userProps;

if (hits.length === 0 && EmptyComponent) {
return (
<EmptyComponent
className={cx(
'ais-Hits',
classNames.root,
cx('ais-Hits--empty', classNames.emptyRoot),
props.className
)}
/>
);
}
return (
<div
{...props}
className={cx(
'ais-Hits',
classNames.root,
hits.length === 0 && cx('ais-Hits--empty', classNames.emptyRoot),
props.className
)}
>
<ol className={cx('ais-Hits-list', classNames.list)}>
{hits.map((hit, index) => (
<ItemComponent
key={hit.objectID}
hit={hit}
index={index}
className={cx('ais-Hits-item', classNames.item)}
onClick={() => {
sendEvent('click:internal', hit, 'Hit Clicked');
}}
onAuxClick={() => {
sendEvent('click:internal', hit, 'Hit Clicked');
}}
/>
))}
</ol>
</div>
);
};
}
Loading

0 comments on commit 55d550e

Please sign in to comment.