Skip to content

Commit c28ef0b

Browse files
authored
Optional sticky headers (#19)
1 parent 24156fe commit c28ef0b

File tree

5 files changed

+56
-2
lines changed

5 files changed

+56
-2
lines changed

site/src/components/sections/docs.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,13 @@ export function Docs({
308308
<PropertiesListRow defaultValue="10" name="columns" type="number">
309309
<p>The number of columns in the list.</p>
310310
</PropertiesListRow>
311+
<PropertiesListRow
312+
defaultValue="true"
313+
name="sticky"
314+
type="boolean"
315+
>
316+
<p>Whether to enable the sticky position of the category headers.</p>
317+
</PropertiesListRow>
311318
<PropertiesListRow
312319
defaultValue="the most recent version supported by the current browser"
313320
name="emojiVersion"

src/components/__tests__/emoji-picker.test.browser.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function DefaultPage({
2727
searchOnChange,
2828
rootChildren,
2929
emptyChildren = <div data-testid="empty">No emojis found</div>,
30+
sticky = true,
3031
}: {
3132
children?: ReactNode;
3233
locale?: EmojiPickerRootProps["locale"];
@@ -40,6 +41,7 @@ function DefaultPage({
4041
searchValue?: EmojiPickerSearchProps["value"];
4142
rootChildren?: EmojiPickerRootProps["children"];
4243
emptyChildren?: EmojiPickerEmptyProps["children"];
44+
sticky?: EmojiPickerRootProps["sticky"];
4345
}) {
4446
const [selectedEmoji, setSelectedEmoji] = useState<Emoji | null>(null);
4547

@@ -57,6 +59,7 @@ function DefaultPage({
5759
locale={locale}
5860
onEmojiSelect={setSelectedEmoji}
5961
skinTone={skinTone}
62+
sticky={sticky}
6063
>
6164
<EmojiPicker.Search
6265
data-testid="search"
@@ -431,6 +434,28 @@ describe("EmojiPicker.Root", () => {
431434
.element(page.getByRole("gridcell").nth(7))
432435
.toHaveAttribute("aria-colindex", "7");
433436
});
437+
438+
it("should support disabling sticky category headers", async () => {
439+
page.render(
440+
<DefaultPage
441+
sticky={false}
442+
listComponents={{
443+
CategoryHeader: ({ category, ...props }) => (
444+
<div
445+
data-testid="category-header"
446+
{...props}
447+
>
448+
{category.label}
449+
</div>
450+
),
451+
}}
452+
/>,
453+
);
454+
455+
await expect.element(page.getByTestId("category-header").nth(1)).not.toHaveStyle({
456+
position: "sticky",
457+
});
458+
});
434459
});
435460

436461
describe("EmojiPicker.Search", () => {

src/components/emoji-picker.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const EmojiPickerRoot = forwardRef<HTMLDivElement, EmojiPickerRootProps>(
149149
onBlurCapture,
150150
children,
151151
style,
152+
sticky = true,
152153
...props
153154
},
154155
forwardedRef,
@@ -159,6 +160,7 @@ const EmojiPickerRoot = forwardRef<HTMLDivElement, EmojiPickerRootProps>(
159160
stableOnEmojiSelect,
160161
validateLocale(locale),
161162
columns,
163+
sticky,
162164
validateSkinTone(skinTone),
163165
),
164166
);
@@ -179,6 +181,10 @@ const EmojiPickerRoot = forwardRef<HTMLDivElement, EmojiPickerRootProps>(
179181
store.set({ columns });
180182
}, [columns]);
181183

184+
useLayoutEffect(() => {
185+
store.set({ sticky });
186+
}, [sticky]);
187+
182188
useLayoutEffect(() => {
183189
store.set({ skinTone: validateSkinTone(skinTone) });
184190
}, [skinTone]);
@@ -769,6 +775,7 @@ function listCategoryProps(
769775
function listCategoryHeaderProps(
770776
category: EmojiPickerCategory,
771777
sizer = false,
778+
sticky = true,
772779
): WithAttributes<EmojiPickerListCategoryHeaderProps> {
773780
return {
774781
category,
@@ -777,7 +784,7 @@ function listCategoryHeaderProps(
777784
contain: !sizer ? "layout paint" : undefined,
778785
height: !sizer ? "var(--frimousse-category-header-height)" : undefined,
779786
pointerEvents: "auto",
780-
position: "sticky",
787+
position: sticky ? "sticky" : undefined,
781788
top: 0,
782789
},
783790
};
@@ -913,6 +920,7 @@ const EmojiPickerListCategory = memo(
913920
(state) => state.data?.categories[categoryIndex],
914921
shallow,
915922
);
923+
const sticky = useSelectorKey(store, "sticky");
916924

917925
/* v8 ignore next 3 */
918926
if (!category) {
@@ -922,7 +930,11 @@ const EmojiPickerListCategory = memo(
922930
return (
923931
<div {...listCategoryProps(categoryIndex, category)}>
924932
<CategoryHeader
925-
{...listCategoryHeaderProps({ label: category.label })}
933+
{...listCategoryHeaderProps(
934+
{ label: category.label },
935+
false,
936+
sticky,
937+
)}
926938
/>
927939
</div>
928940
);

src/store.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Interaction = "keyboard" | "pointer" | "none";
1616
export type EmojiPickerStore = {
1717
locale: Locale;
1818
columns: number;
19+
sticky: boolean;
1920
skinTone: SkinTone;
2021
onEmojiSelect: NonNullable<EmojiPickerRootProps["onEmojiSelect"]>;
2122

@@ -59,13 +60,15 @@ export function createEmojiPickerStore(
5960
onEmojiSelect: NonNullable<EmojiPickerRootProps["onEmojiSelect"]>,
6061
initialLocale: Locale,
6162
initialColumns: number,
63+
initialSticky: boolean,
6264
initialSkinTone: SkinTone,
6365
) {
6466
let viewportScrollY = 0;
6567

6668
return createStore<EmojiPickerStore>((set, get) => ({
6769
locale: initialLocale,
6870
columns: initialColumns,
71+
sticky: initialSticky,
6972
skinTone: initialSkinTone,
7073
onEmojiSelect,
7174

src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ export interface EmojiPickerRootProps extends ComponentProps<"div"> {
195195
* @default "https://cdn.jsdelivr.net/npm/emojibase-data"
196196
*/
197197
emojibaseUrl?: string;
198+
199+
/**
200+
* Whether to enable the sticky position of the category headers.
201+
*
202+
* @default true
203+
*/
204+
sticky?: boolean;
198205
}
199206

200207
export type EmojiPickerViewportProps = ComponentProps<"div">;

0 commit comments

Comments
 (0)