Skip to content

Commit 5fb2bfe

Browse files
committed
feat: added virualization and added search hook
1 parent d58f76e commit 5fb2bfe

18 files changed

+470
-99
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ function App() {
3838
}
3939
```
4040

41+
### `useEmojiSearch`
42+
This will allow the user to search on top of the native emojis avaiable ( this will not return custom emojis )
43+
44+
```jsx
45+
const SearchBox = () => {
46+
const emojiSearch = useEmojiSearch({});
47+
return <div>
48+
<h1>Search</h1>
49+
<input onChange={event => {
50+
const filteredEmojis = emojiSearch(event.target.value)
51+
console.log(filteredEmojis);
52+
}}
53+
/>
54+
</div>;
55+
}
56+
```
57+
4158
## Shout Outs
4259

4360
| Component Design 🎨 |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "4.11.1",
2+
"version": "4.11.2",
33
"license": "MIT",
44
"main": "dist/index.js",
55
"typings": "dist/index.d.ts",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { cx } from 'flairup';
2+
import React, { useState, useCallback, useMemo } from 'react';
3+
4+
import { stylesheet } from '../../Stylesheet/stylesheet';
5+
import useMeasure from '../../hooks/useMeasure';
6+
7+
const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0);
8+
9+
const Virtualise = React.memo(({ children, itemHeights, overscan = 0, className }: {
10+
children: React.ReactNode[];
11+
itemHeights: number[];
12+
overscan?: number;
13+
className?: string;
14+
}) => {
15+
const [containerMeasure, { height: containerHeight }] = useMeasure<HTMLDivElement>();
16+
const [scrollOffset, setScrollOffset] = useState(0);
17+
const totalHeight = useMemo(() => sum(itemHeights), [itemHeights]);
18+
19+
const calculateVisibleItems = useCallback(() => {
20+
let start = 0;
21+
let currentHeight = 0;
22+
23+
// Find the first item in the viewport
24+
for (let i = 0; i < itemHeights.length; i++) {
25+
if (currentHeight + itemHeights[i] > scrollOffset) {
26+
start = Math.max(i - overscan, 0);
27+
break;
28+
}
29+
currentHeight += itemHeights[i];
30+
}
31+
32+
let end = start;
33+
currentHeight = sum(itemHeights.slice(0, start));
34+
35+
// Find the last item in the viewport
36+
for (let i = start; i < itemHeights.length; i++) {
37+
currentHeight += itemHeights[i];
38+
if (currentHeight > scrollOffset + containerHeight || i === itemHeights.length - 1) {
39+
end = Math.min(i + overscan + 1, itemHeights.length);
40+
break;
41+
}
42+
}
43+
44+
// Adjust end to include the last element if we're near the bottom
45+
if (scrollOffset + containerHeight >= totalHeight) {
46+
end = itemHeights.length;
47+
}
48+
49+
return itemHeights.slice(start, end).map((_, index) => {
50+
const itemIndex = start + index;
51+
const top = sum(itemHeights.slice(0, itemIndex));
52+
return {
53+
index: itemIndex,
54+
top,
55+
height: itemHeights[itemIndex],
56+
element: children[itemIndex],
57+
};
58+
});
59+
}, [scrollOffset, containerHeight, itemHeights, overscan, children, totalHeight]);
60+
61+
const visibleItems = useMemo(() => calculateVisibleItems(), [calculateVisibleItems]);
62+
63+
const handleScroll: React.UIEventHandler<HTMLDivElement> = useCallback((e) => {
64+
setScrollOffset(e.currentTarget.scrollTop);
65+
}, []);
66+
67+
return (
68+
<div
69+
ref={containerMeasure}
70+
onScroll={handleScroll}
71+
style={{ position: 'relative', height: '100%', overflowY: 'auto' }}
72+
className={cx(styles.virtualizeWrapper)}
73+
>
74+
<div className={cx(styles.virtualise)} style={{ height: totalHeight, position: 'relative' }}>
75+
<ul className={className} style={{ margin: 0, padding: 0 }}>
76+
{visibleItems.map(({ index, top, height, element }) => (
77+
<div
78+
key={index}
79+
style={{
80+
position: 'absolute',
81+
top: `${top}px`,
82+
height: `${height}px`,
83+
width: '100%',
84+
}}
85+
>
86+
{element}
87+
</div>
88+
))}
89+
</ul>
90+
</div>
91+
</div>
92+
);
93+
});
94+
95+
const styles = stylesheet.create({
96+
virtualizeWrapper: {
97+
'.': 'epr-virutalise-wrapper',
98+
},
99+
virtualise: {
100+
'.': 'epr-virtualise',
101+
},
102+
});
103+
104+
Virtualise.displayName = 'Virtualise';
105+
export default Virtualise;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const convertToPixel = (value: number, unit: string, fontSize: number, rootFontSize: number) => {
2+
switch (unit) {
3+
case 'px':
4+
return value; // already in pixels
5+
case 'rem':
6+
return value * rootFontSize; // root font size is default 16px
7+
case 'em':
8+
return value * fontSize; // font size of the element
9+
default:
10+
return value; // assuming value is in pixels if unit is unknown
11+
}
12+
}
13+
14+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { parsePadding } from "./parse-padding";
2+
3+
export const getCategoriesHeight = (totalEmojis: number, width?: number) => {
4+
const mainContent = document.querySelector('.epr-main')
5+
if (!width || !mainContent) return 0;
6+
7+
const categoryPadding = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-category-padding"))
8+
const emojiSize = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-emoji-size"))
9+
const emojiPadding = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-emoji-padding"))
10+
const categoryLabelHeight = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-category-label-height"))
11+
const totalEmojiWidth = emojiSize.left + emojiPadding.left + emojiPadding.right;
12+
const totalEmojiHeight = emojiSize.top + emojiPadding.top + emojiPadding.bottom;
13+
14+
if (!totalEmojis) return 0;
15+
16+
const noOfEmojisInARow = Math.floor((width - categoryPadding.left - categoryPadding.right) / totalEmojiWidth);
17+
const noOfRows = Math.ceil(totalEmojis / noOfEmojisInARow);
18+
return (noOfRows * totalEmojiHeight) + categoryLabelHeight.left;
19+
};
20+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { getCategoriesHeight } from "./get-category-height";
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { convertToPixel } from "./convert-value-to-pixel";
2+
3+
type PaddingValue = {
4+
value: number;
5+
unit: string;
6+
};
7+
8+
type Padding = {
9+
top: number;
10+
right: number;
11+
bottom: number;
12+
left: number;
13+
};
14+
15+
// Function to extract value and unit
16+
export const parseValue = (value: string) => {
17+
const match = value.match(/^([\d.]+)(\D+)$/);
18+
return match ? { value: parseFloat(match[1]), unit: match[2] } : { value: parseFloat(value), unit: '' };
19+
}
20+
21+
export const parsePadding = (paddingString: string, fontSize: number = 16, rootFontSize: number = 16): Padding => {
22+
const values: PaddingValue[] = paddingString.split(' ').map(value => parseValue(value));
23+
24+
let top: number, right: number, bottom: number, left: number;
25+
26+
if (values.length === 1) {
27+
// If only one value is provided, it applies to all sides
28+
top = right = bottom = left = convertToPixel(values[0].value, values[0].unit, fontSize, rootFontSize);
29+
} else if (values.length === 2) {
30+
// If two values are provided: [top-bottom, left-right]
31+
top = bottom = convertToPixel(values[0].value, values[0].unit, fontSize, rootFontSize);
32+
right = left = convertToPixel(values[1].value, values[1].unit, fontSize, rootFontSize);
33+
} else if (values.length === 3) {
34+
// If three values are provided: [top, left-right, bottom]
35+
top = convertToPixel(values[0].value, values[0].unit, fontSize, rootFontSize);
36+
right = left = convertToPixel(values[1].value, values[1].unit, fontSize, rootFontSize);
37+
bottom = convertToPixel(values[2].value, values[2].unit, fontSize, rootFontSize);
38+
} else if (values.length === 4) {
39+
// If four values are provided: [top, right, bottom, left]
40+
[top, right, bottom, left] = values.map(v => convertToPixel(v.value, v.unit, fontSize, rootFontSize));
41+
} else {
42+
// Handle unexpected cases
43+
top = right = bottom = left = 0;
44+
}
45+
46+
return { top, right, bottom, left };
47+
}

src/components/body/EmojiCategory.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515

1616
type Props = Readonly<{
1717
categoryConfig: CategoryConfig;
18-
children?: React.ReactNode;
18+
children: React.ReactNode;
1919
hidden?: boolean;
2020
hiddenOnSearch?: boolean;
2121
}>;
@@ -26,6 +26,7 @@ export function EmojiCategory({
2626
hidden,
2727
hiddenOnSearch
2828
}: Props) {
29+
2930
const category = categoryFromCategoryConfig(categoryConfig);
3031
const categoryName = categoryNameFromCategoryConfig(categoryConfig);
3132

0 commit comments

Comments
 (0)