Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ function App() {
}
```

### `useEmojiSearch`
This will allow the user to search on top of the native emojis avaiable ( this will not return custom emojis )

```jsx
const SearchBox = () => {
const emojiSearch = useEmojiSearch({});
return <div>
<h1>Search</h1>
<input onChange={event => {
const filteredEmojis = emojiSearch(event.target.value)
console.log(filteredEmojis);
}}
/>
</div>;
}
```

## Shout Outs

| Component Design 🎨 |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "4.12.1",
"version": "4.13.0",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
2 changes: 2 additions & 0 deletions src/DomUtils/keyboardNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,11 @@ function visibleEmojiOneRowDown(element: HTMLElement) {
const indexInRow = elementIndexInRow(categoryContent, element);
const row = rowNumber(categoryContent, element);
const countInRow = elementCountInRow(categoryContent, element);

if (!hasNextRow(categoryContent, element)) {
const nextVisibleCategory = nextCategory(category);


if (!nextVisibleCategory) {
return null;
}
Expand Down
60 changes: 60 additions & 0 deletions src/DomUtils/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VIRTUALIZE_CLASS_NAMES } from '../components/Layout/Virtualise';
import { DataEmoji } from '../dataUtils/DataTypes';
import {
emojiByUnified,
Expand Down Expand Up @@ -267,7 +268,37 @@ export function firstVisibleEmoji(parent: NullableElement) {
return firstVisibleElementInContainer(parent, allEmojis, 0.1);
}


const getNameBasedPrevCategory = (element: HTMLElement): HTMLElement | null => {
const emojiList = document.querySelector<HTMLElement>(asSelectors(ClassNames.emojiList));
if (!emojiList) return null;

const currentName = element?.getAttribute('data-name');
const categories = Array.from(emojiList.children);
const currentIndex = categories.findIndex(
child => child.firstElementChild?.getAttribute('data-name') === currentName
);

const prevIndex = Math.max(currentIndex - 1, 0);
const prevName = categories[prevIndex]?.firstElementChild?.getAttribute('data-name');

return prevName
? emojiList.querySelector(`[data-name="${prevName}"]`) as HTMLElement
: null;
};


export function prevCategory(element: NullableElement): NullableElement {
if (!element) {
return null;
}

const currentPrevCategory = getNameBasedPrevCategory(element);

if(currentPrevCategory){
return currentPrevCategory;
}

const category = closestCategory(element);

if (!category) {
Expand All @@ -287,7 +318,36 @@ export function prevCategory(element: NullableElement): NullableElement {
return prev;
}

const getNameBasedNextCategory = (element: HTMLElement): HTMLElement | null => {
const emojiList = document.querySelector<HTMLElement>(asSelectors(ClassNames.emojiList));
if (!emojiList) return null;

const currentName = element?.getAttribute('data-name');
const categories = Array.from(emojiList.children);
const currentIndex = categories.findIndex(
child => child.firstElementChild?.getAttribute('data-name') === currentName
);

const nextIndex = Math.min(currentIndex + 1, categories.length - 1);
const nextName = categories[nextIndex]?.firstElementChild?.getAttribute('data-name');

return nextName
? emojiList.querySelector(`[data-name="${nextName}"]`)
: null;
};


export function nextCategory(element: NullableElement): NullableElement {
if (!element) {
return null;
}

const currentNextCategory = getNameBasedNextCategory(element);

if(currentNextCategory){
return currentNextCategory;
}

const category = closestCategory(element);

if (!category) {
Expand Down
116 changes: 116 additions & 0 deletions src/components/Layout/Virtualise.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { cx } from 'flairup';
import React, { useState, useCallback, useMemo, useEffect } from 'react';

import { stylesheet } from '../../Stylesheet/stylesheet';
import useMeasure from '../../hooks/useMeasure';
import { useActiveCategoryState } from '../context/PickerContext';

const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0);

const Virtualise = React.memo(({ children, itemHeights, overscan = 0, className }: {
children: React.ReactElement[];
itemHeights: number[];
overscan?: number;
className?: string;
}) => {
const [_,setActiveCategory] = useActiveCategoryState();
const [containerMeasure, { height: containerHeight, width }] = useMeasure<HTMLDivElement>();
const [scrollOffset, setScrollOffset] = useState(0);
const totalHeight = useMemo(() => sum(itemHeights), [itemHeights]);

const calculateVisibleItems = useCallback(() => {
let start = 0;
let currentHeight = 0;

// Find the first item in the viewport
for (let i = 0; i < itemHeights.length; i++) {
if (currentHeight + itemHeights[i] > scrollOffset) {
start = Math.max(i - overscan, 0);
break;
}
currentHeight += itemHeights[i];
}

let end = start;
currentHeight = sum(itemHeights.slice(0, start));

// Find the last item in the viewport
for (let i = start; i < itemHeights.length; i++) {
currentHeight += itemHeights[i];
if (currentHeight > scrollOffset + containerHeight || i === itemHeights.length - 1) {
end = Math.min(i + overscan + 1, itemHeights.length);
break;
}
}

// Adjust end to include the last element if we're near the bottom
if (scrollOffset + containerHeight >= totalHeight) {
end = itemHeights.length;
}

return itemHeights.slice(start, end).map((_, index) => {
const itemIndex = start + index;
const top = sum(itemHeights.slice(0, itemIndex));
return {
index: itemIndex,
top,
height: itemHeights[itemIndex],
element: children[itemIndex],
};
});
}, [scrollOffset, containerHeight, itemHeights, overscan, children, totalHeight]);

const visibleItems = useMemo(() => calculateVisibleItems(), [calculateVisibleItems]);

const handleScroll: React.UIEventHandler<HTMLDivElement> = useCallback((e) => {
setScrollOffset(e.currentTarget.scrollTop);
}, []);

useEffect(() => {
if(!visibleItems.length) return;
const lastItem = visibleItems[visibleItems.length - 1];
const key = lastItem.element.key;
if(typeof key === 'string') {
setActiveCategory(key);
}
}, [setActiveCategory, visibleItems]);

return (
<div
ref={containerMeasure}
onScroll={handleScroll}
style={{ position: 'relative', height: '100%', overflowY: 'auto' }}
className={cx(styles.virtualizeWrapper)}
>
<div className={cx(styles.virtualise)} style={{ height: totalHeight, position: 'relative' }}>
<ul className={className} style={{ margin: 0, padding: 0 }}>
{visibleItems.map(({ index, top, height, element }) => (
<div
key={index}
style={{
position: 'absolute',
top: `${top}px`,
height: `${height}px`,
width: '100%',
}}
>
{element}
</div>
))}
</ul>
</div>
</div>
);
});

const styles = stylesheet.create({
virtualizeWrapper: {
'.': 'epr-virutalise-wrapper',
},
virtualise: {
'.': 'epr-virtualise',
},
});

Virtualise.displayName = 'Virtualise';
export default Virtualise;
14 changes: 14 additions & 0 deletions src/components/Layout/helpers/convert-value-to-pixel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const convertToPixel = (value: number, unit: string, fontSize: number, rootFontSize: number) => {
switch (unit) {
case 'px':
return value; // already in pixels
case 'rem':
return value * rootFontSize; // root font size is default 16px
case 'em':
return value * fontSize; // font size of the element
default:
return value; // assuming value is in pixels if unit is unknown
}
}


20 changes: 20 additions & 0 deletions src/components/Layout/helpers/get-category-height.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { parsePadding } from "./parse-padding";

export const getCategoriesHeight = (totalEmojis: number, width?: number) => {
const mainContent = document.querySelector('.epr-main')
if (!width || !mainContent) return 0;

const categoryPadding = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-category-padding"))
const emojiSize = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-emoji-size"))
const emojiPadding = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-emoji-padding"))
const categoryLabelHeight = parsePadding(getComputedStyle(mainContent).getPropertyValue("--epr-category-label-height"))
const totalEmojiWidth = emojiSize.left + emojiPadding.left + emojiPadding.right;
const totalEmojiHeight = emojiSize.top + emojiPadding.top + emojiPadding.bottom;

if (!totalEmojis) return 0;

const noOfEmojisInARow = Math.floor((width - categoryPadding.left - categoryPadding.right) / totalEmojiWidth);
const noOfRows = Math.ceil(totalEmojis / noOfEmojisInARow);
return (noOfRows * totalEmojiHeight) + categoryLabelHeight.left;
};

26 changes: 26 additions & 0 deletions src/components/Layout/helpers/get-scrollbar-width.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
let cachedScrollWidth: number | null = null;

export function getScrollbarWidth() {
if (cachedScrollWidth !== null) {
return cachedScrollWidth;
}

// Create a temporary div to measure
const outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.overflow = 'scroll';
document.body.appendChild(outer);

// Create inner div
const inner = document.createElement('div');
outer.appendChild(inner);

// Calculate the width difference
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;

// Clean up
outer.parentNode?.removeChild(outer);

cachedScrollWidth = scrollbarWidth;
return scrollbarWidth;
}
1 change: 1 addition & 0 deletions src/components/Layout/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getCategoriesHeight } from "./get-category-height";
47 changes: 47 additions & 0 deletions src/components/Layout/helpers/parse-padding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { convertToPixel } from "./convert-value-to-pixel";

type PaddingValue = {
value: number;
unit: string;
};

type Padding = {
top: number;
right: number;
bottom: number;
left: number;
};

// Function to extract value and unit
export const parseValue = (value: string) => {
const match = value.match(/^([\d.]+)(\D+)$/);
return match ? { value: parseFloat(match[1]), unit: match[2] } : { value: parseFloat(value), unit: '' };
}

export const parsePadding = (paddingString: string, fontSize: number = 16, rootFontSize: number = 16): Padding => {
const values: PaddingValue[] = paddingString.split(' ').map(value => parseValue(value));

let top: number, right: number, bottom: number, left: number;

if (values.length === 1) {
// If only one value is provided, it applies to all sides
top = right = bottom = left = convertToPixel(values[0].value, values[0].unit, fontSize, rootFontSize);
} else if (values.length === 2) {
// If two values are provided: [top-bottom, left-right]
top = bottom = convertToPixel(values[0].value, values[0].unit, fontSize, rootFontSize);
right = left = convertToPixel(values[1].value, values[1].unit, fontSize, rootFontSize);
} else if (values.length === 3) {
// If three values are provided: [top, left-right, bottom]
top = convertToPixel(values[0].value, values[0].unit, fontSize, rootFontSize);
right = left = convertToPixel(values[1].value, values[1].unit, fontSize, rootFontSize);
bottom = convertToPixel(values[2].value, values[2].unit, fontSize, rootFontSize);
} else if (values.length === 4) {
// If four values are provided: [top, right, bottom, left]
[top, right, bottom, left] = values.map(v => convertToPixel(v.value, v.unit, fontSize, rootFontSize));
} else {
// Handle unexpected cases
top = right = bottom = left = 0;
}

return { top, right, bottom, left };
}
2 changes: 1 addition & 1 deletion src/components/body/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const styles = stylesheet.create({
body: {
'.': ClassNames.scrollBody,
flex: '1',
overflowY: 'scroll',
overflowY: 'hidden',
overflowX: 'hidden',
position: 'relative'
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/body/EmojiCategory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

type Props = Readonly<{
categoryConfig: CategoryConfig;
children?: React.ReactNode;
children: React.ReactNode;
hidden?: boolean;
hiddenOnSearch?: boolean;
}>;
Expand All @@ -26,6 +26,7 @@ export function EmojiCategory({
hidden,
hiddenOnSearch
}: Props) {

const category = categoryFromCategoryConfig(categoryConfig);
const categoryName = categoryNameFromCategoryConfig(categoryConfig);

Expand Down
Loading