Skip to content

Commit

Permalink
83 Show-hide user search bar on scroll (#85)
Browse files Browse the repository at this point in the history
* initial scroll context provider

* hide/show search toolbar on scroll

* tests

* tests

* fixes
  • Loading branch information
mwarman authored Oct 2, 2024
1 parent e46a20d commit 3127f14
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 7 deletions.
9 changes: 6 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { queryClient } from 'common/utils/query-client';
import AuthProvider from 'common/providers/AuthProvider';
import AxiosProvider from 'common/providers/AxiosProvider';
import ToastProvider from 'common/providers/ToastProvider';
import ScrollProvider from 'common/providers/ScrollProvider';
import Toasts from 'common/components/Toast/Toasts';
import AppRouter from 'common/components/Router/AppRouter';

Expand All @@ -26,9 +27,11 @@ const App = (): JSX.Element => (
<AuthProvider>
<AxiosProvider>
<ToastProvider>
<AppRouter />
<Toasts />
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
<ScrollProvider>
<AppRouter />
<Toasts />
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
</ScrollProvider>
</ToastProvider>
</AxiosProvider>
</AuthProvider>
Expand Down
85 changes: 85 additions & 0 deletions src/common/hooks/__tests__/useScrollContext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest';
import { renderHook as renderHookWithoutWrapper } from '@testing-library/react';

import { act, renderHook, waitFor } from 'test/test-utils';

import { useScrollContext } from '../useScrollContext';

describe('useScrollContext', () => {
it('should return context', async () => {
// ARRANGE
const { result } = renderHook(() => useScrollContext());
await waitFor(() => expect(result.current).not.toBeNull());

// ASSERT
expect(result.current).toBeDefined();
expect(result.current.handleIonScroll).toBeDefined();
expect(result.current.scrollDirection).toBeUndefined();
});

it('should return default context', async () => {
// ARRANGE
const { result } = renderHookWithoutWrapper(() => useScrollContext());
await waitFor(() => expect(result.current).not.toBeNull());

// ACT
act(() =>
result.current.handleIonScroll({
// @ts-expect-error required detail attributes only
detail: {
startY: 0,
currentY: 100,
},
}),
);

// ASSERT
expect(result.current).toBeDefined();
expect(result.current.handleIonScroll).toBeDefined();
expect(result.current.scrollDirection).toBeUndefined();
});

it('should set scroll direction down', async () => {
// ARRANGE
const { result } = renderHook(() => useScrollContext());
await waitFor(() => expect(result.current).not.toBeNull());

// ACT
act(() =>
result.current.handleIonScroll({
// @ts-expect-error required detail attributes only
detail: {
startY: 0,
currentY: 100,
},
}),
);

// ASSERT
expect(result.current).toBeDefined();
expect(result.current.handleIonScroll).toBeDefined();
expect(result.current.scrollDirection).toBe('down');
});

it('should set scroll direction up', async () => {
// ARRANGE
const { result } = renderHook(() => useScrollContext());
await waitFor(() => expect(result.current).not.toBeNull());

// ACT
act(() =>
result.current.handleIonScroll({
// @ts-expect-error required detail attributes only
detail: {
startY: 100,
currentY: 0,
},
}),
);

// ASSERT
expect(result.current).toBeDefined();
expect(result.current.handleIonScroll).toBeDefined();
expect(result.current.scrollDirection).toBe('up');
});
});
15 changes: 15 additions & 0 deletions src/common/hooks/useScrollContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useContext } from 'react';

import { ScrollContext, ScrollContextValue } from 'common/providers/ScrollProvider';

/**
* The `useScrollContext` hook returns the current `ScrollContext` value.
* @returns {ScrollContextValue} The current `ScrollContext` value, a
* `ScrollContextValue` object.
* @see {@link ScrollContextValue}
*/
export const useScrollContext = (): ScrollContextValue => {
const context = useContext(ScrollContext);

return context;
};
69 changes: 69 additions & 0 deletions src/common/providers/ScrollProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createContext, PropsWithChildren, useState } from 'react';
import { ScrollCustomEvent } from '@ionic/core';

/**
* The `ScrollDirection` type describes the direction of the scroll.
*/
export type ScrollDirection = 'down' | 'up';

/**
* The `value` provided by the `ScrollContext`.
*/
export type ScrollContextValue = {
scrollDirection?: ScrollDirection;
handleIonScroll: (e: ScrollCustomEvent) => void;
};

/**
* Default value for the `ScrollContext`.
*/
const DEFAULT_VALUE: ScrollContextValue = {
handleIonScroll: () => {},
};

/**
* The `ScrollContext` instance.
*/
export const ScrollContext = createContext<ScrollContextValue>(DEFAULT_VALUE);

/**
* The `ScrollProvider` component creates and provides access to the `ScrollContext`
* value. Provides information regarding scroll events.
*
* Useful when integrated with scroll events emitted by components such as `IonContent`.
*
* *Example:*
* ```
* const { handleIonScroll, scrollDirection } = useScrollContext();
* ...
* <IonContent scrollEvents onIonScroll={handleIonScroll}>
* <IonText className={classNames('ion-hide', scrollDirection==='down')}>
* I am hidden when scrolling down!
* </IonText>
* </IonContent>
* ```
* @param {PropsWithChildren} props - Component properties.
* @returns {JSX.Element} JSX
*/
const ScrollProvider = ({ children }: PropsWithChildren): JSX.Element => {
const [scrollDirection, setScrollDirection] = useState<ScrollDirection | undefined>(undefined);

const handleIonScroll = (event: ScrollCustomEvent) => {
const { currentY, startY } = event.detail;
const scrollY = currentY - startY;
if (scrollY > 0) {
setScrollDirection('down');
} else if (scrollY < 0) {
setScrollDirection('up');
}
};

const contextValue: ScrollContextValue = {
scrollDirection: scrollDirection,
handleIonScroll: handleIonScroll,
};

return <ScrollContext.Provider value={contextValue}>{children}</ScrollContext.Provider>;
};

export default ScrollProvider;
2 changes: 1 addition & 1 deletion src/common/providers/__tests__/ProgressProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest';
import userEvent from '@testing-library/user-event';

import { render, screen, waitFor } from 'test/test-utils';

import ProgressProvider from '../ProgressProvider';
import userEvent from '@testing-library/user-event';

describe('ProgressProvider', () => {
it('should render successfully', async () => {
Expand Down
20 changes: 20 additions & 0 deletions src/common/providers/__tests__/ScrollProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';

import { render, screen } from 'test/test-utils';

import ScrollProvider from '../ScrollProvider';

describe('ScrollProvider', () => {
it('should render successfully', async () => {
// ARRANGE
render(
<ScrollProvider>
<div data-testid="scroll-provider-children"></div>
</ScrollProvider>,
);
await screen.findByTestId('scroll-provider-children');

// ASSERT
expect(screen.getByTestId('scroll-provider-children')).toBeDefined();
});
});
9 changes: 7 additions & 2 deletions src/pages/Users/components/UserList/UserListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import {
} from '@ionic/react';
import { useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import classNames from 'classnames';

import './UserListPage.scss';
import { PropsWithTestId } from 'common/components/types';
import { QueryKey } from 'common/utils/constants';
import { useScrollContext } from 'common/hooks/useScrollContext';
import Header from 'common/components/Header/Header';
import Searchbar from 'common/components/Searchbar/Searchbar';
import Container from 'common/components/Content/Container';
Expand All @@ -34,6 +36,7 @@ export const UserListPage = ({ testid = 'page-user-list' }: PropsWithTestId): JS
const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
const [search, setSearch] = useState<string>('');
const queryClient = useQueryClient();
const { handleIonScroll, scrollDirection } = useScrollContext();

/**
* Handle pull to refresh events.
Expand All @@ -60,12 +63,14 @@ export const UserListPage = ({ testid = 'page-user-list' }: PropsWithTestId): JS
toolbars={[
{
children: <Searchbar debounce={500} onIonInput={handleInputSearch} />,
className: 'ls-user-list-page__searchbar',
className: classNames('ls-user-list-page__searchbar', {
'ion-hide': scrollDirection === 'down',
}),
},
]}
/>

<IonContent>
<IonContent scrollEvents onIonScroll={handleIonScroll}>
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
<IonRefresherContent></IonRefresherContent>
</IonRefresher>
Expand Down
5 changes: 4 additions & 1 deletion src/test/wrappers/WithAllProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ConfigContextProvider from 'common/providers/ConfigProvider';
import ToastProvider from 'common/providers/ToastProvider';
import AxiosProvider from 'common/providers/AxiosProvider';
import AuthProvider from 'common/providers/AuthProvider';
import ScrollProvider from 'common/providers/ScrollProvider';

const WithAllProviders = ({ children }: PropsWithChildren): JSX.Element => {
return (
Expand All @@ -15,7 +16,9 @@ const WithAllProviders = ({ children }: PropsWithChildren): JSX.Element => {
<AuthProvider>
<AxiosProvider>
<ToastProvider>
<MemoryRouter>{children}</MemoryRouter>
<ScrollProvider>
<MemoryRouter>{children}</MemoryRouter>
</ScrollProvider>
</ToastProvider>
</AxiosProvider>
</AuthProvider>
Expand Down

0 comments on commit 3127f14

Please sign in to comment.