Skip to content

Commit

Permalink
feat: add search input to chain, bridges and exchanges pages (#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
DNR500 authored Sep 30, 2024
1 parent 7f69cb4 commit 5504d95
Show file tree
Hide file tree
Showing 24 changed files with 500 additions and 200 deletions.
17 changes: 7 additions & 10 deletions packages/widget/src/components/AppContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Box, Container, ScopedCssBaseline, styled } from '@mui/material';
import type { PropsWithChildren } from 'react';
import { defaultMaxHeight } from '../config/constants.js';
import { useHeaderHeight } from '../hooks/useHeaderHeight.js';
import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js';
import { useHeaderHeight } from '../stores/header/useHeaderStore.js';
import type { WidgetVariant } from '../types/widget.js';
import { ElementId, createElementId } from '../utils/elements.js';

Expand Down Expand Up @@ -88,15 +88,12 @@ const CssBaselineContainer = styled(ScopedCssBaseline, {
overflowY: 'auto',
height: theme.container?.display === 'flex' ? 'auto' : '100%',
paddingTop: paddingTopAdjustment,
// The following allows for the token list to always fill the available height
// use of CSS.escape here is to deal with characters such as ':' returned by reacts useId hook
// related issue - https://github.com/facebook/react/issues/26839
[`&:has(#${CSS.escape(createElementId(ElementId.TokenList, elementId))})`]:
{
height: theme.container?.maxHeight
? theme.container?.maxHeight
: theme.container?.height || defaultMaxHeight,
},
// This allows FullPageContainer.tsx to expand and fill the available vertical space in max height and default layout modes
[`&:has(.full-page-container)`]: {
height: theme.container?.maxHeight
? theme.container?.maxHeight
: theme.container?.height || defaultMaxHeight,
},
}),
);

Expand Down
14 changes: 14 additions & 0 deletions packages/widget/src/components/FullPageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { styled } from '@mui/material';
import type { PageContainerProps } from './PageContainer.js';
import { PageContainer } from './PageContainer.js';

// In max height and default layout
// the PageContainer collapses to use the minimum space need to display its child components whereas
// the FullPageContainer expands and fills the available vertical space provide by the max-height
// See the CssBaselineContainer component styles in AppContainer.tsx for usage of full-page-container
export const FullPageContainer = styled((props: PageContainerProps) => (
<PageContainer
{...props}
className={`${props.className} full-page-container`}
/>
))``;
31 changes: 28 additions & 3 deletions packages/widget/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { FC, PropsWithChildren } from 'react';
import { useLayoutEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { useDefaultElementId } from '../../hooks/useDefaultElementId.js';
import { useHeaderHeight } from '../../hooks/useHeaderHeight.js';
import { useSetHeaderHeight } from '../../stores/header/useHeaderStore.js';
import { ElementId, createElementId } from '../../utils/elements.js';
import { stickyHeaderRoutes } from '../../utils/navigationRoutes.js';
import { Container } from './Header.style.js';
Expand All @@ -11,13 +12,37 @@ import { WalletHeader } from './WalletHeader.js';
export const HeaderContainer: FC<PropsWithChildren<{}>> = ({ children }) => {
const { pathname } = useLocation();
const elementId = useDefaultElementId();
const { headerHeight } = useHeaderHeight();
const headerRef = useRef<HTMLDivElement>(null);
const { setHeaderHeight } = useSetHeaderHeight();

useLayoutEffect(() => {
const handleHeaderResize = () => {
const height = headerRef.current?.getBoundingClientRect().height;

if (height) {
setHeaderHeight(height);
}
};

let resizeObserver: ResizeObserver;

if (headerRef.current) {
resizeObserver = new ResizeObserver(handleHeaderResize);
resizeObserver.observe(headerRef.current);
}

return () => {
if (resizeObserver) {
resizeObserver.disconnect();
}
};
}, [headerRef, setHeaderHeight]);

return (
<Container
id={createElementId(ElementId.Header, elementId)}
sticky={stickyHeaderRoutes.some((route) => pathname.includes(route))}
maxHeight={headerHeight}
ref={headerRef}
>
{children}
</Container>
Expand Down
3 changes: 2 additions & 1 deletion packages/widget/src/components/PageContainer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ContainerProps } from '@mui/material';
import { Container, styled } from '@mui/material';

export interface PageContainerProps {
export interface PageContainerProps extends ContainerProps {
halfGutters?: boolean;
topGutters?: boolean;
bottomGutters?: boolean;
Expand Down
46 changes: 46 additions & 0 deletions packages/widget/src/components/Search/SearchInput.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Box, List, styled } from '@mui/material';
import { Input as InputBase } from '../../components/Input.js';

export const Input = styled(InputBase)(({ theme }) => ({
paddingRight: theme.spacing(1.5),
}));

interface SearchStickyContainerProps {
headerHeight: number;
}

export const searchContainerHeight = 64;

// When the widget is in Full Height layout mode in order to appear "sticky the StickySearchInputContainer needs to use
// position fixed in the same way as the header (see Header.tsx). The headerHeight value here is used as the top value
// to ensure that this container positioned correctly beneath the header
export const StickySearchInputContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'headerHeight',
})<SearchStickyContainerProps>(({ theme, headerHeight }) => ({
position: 'sticky',
top: headerHeight,
zIndex: 1,
height: searchContainerHeight,
paddingBottom: theme.spacing(2),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
backgroundColor: theme.palette.background.default,
...(theme.header?.position === 'fixed'
? {
position: 'fixed',
minWidth: theme.breakpoints.values.xs,
maxWidth: theme.breakpoints.values.sm,
width: '100%',
}
: {}),
}));

// When in Full Height layout mode, as the StickySearchInputContainer (see above) uses fixed position, the list element needs to provide
// additional paddingTop in order to be positioned correctly.
export const SearchList = styled(List)(({ theme }) => ({
paddingTop:
theme.header?.position === 'fixed' ? `${searchContainerHeight}px` : 0,
paddingLeft: theme.spacing(1.5),
paddingRight: theme.spacing(1.5),
paddingBottom: theme.spacing(1.5),
}));
57 changes: 57 additions & 0 deletions packages/widget/src/components/Search/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Search } from '@mui/icons-material';
import { FormControl, InputAdornment } from '@mui/material';
import type { FocusEventHandler, FormEventHandler } from 'react';
import { InputCard } from '../../components/Card/InputCard.js';
import { useHeaderHeight } from '../../stores/header/useHeaderStore.js';
import { Input, StickySearchInputContainer } from './SearchInput.style.js';

interface SearchInputProps {
name?: string;
value?: string;
placeholder?: string;
onChange?: FormEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
}

export const SearchInput = ({
name,
placeholder,
onChange,
onBlur,
value,
}: SearchInputProps) => {
return (
<InputCard>
<FormControl fullWidth>
<Input
size="small"
placeholder={placeholder}
endAdornment={
<InputAdornment position="end">
<Search />
</InputAdornment>
}
inputProps={{
inputMode: 'search',
onChange,
onBlur,
name,
value,
maxLength: 128,
}}
autoComplete="off"
/>
</FormControl>
</InputCard>
);
};

export const StickySearchInput = ({ ...rest }: SearchInputProps) => {
const { headerHeight } = useHeaderHeight();

return (
<StickySearchInputContainer headerHeight={headerHeight}>
<SearchInput {...rest} />
</StickySearchInputContainer>
);
};
36 changes: 36 additions & 0 deletions packages/widget/src/components/Search/SearchNotFound.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { BoxProps } from '@mui/material';
import { Box, styled, Typography } from '@mui/material';
import { searchContainerHeight } from './SearchInput.style.js';

interface NotFoundContainerProps extends BoxProps {
adjustForStickySearchInput?: boolean;
}

export const NotFoundContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'adjustForStickySearchInput',
})<NotFoundContainerProps>(({ theme, adjustForStickySearchInput }) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
flex: 1,
padding: theme.spacing(3),
...(adjustForStickySearchInput && theme.header?.position === 'fixed'
? { paddingTop: `calc(${searchContainerHeight}px + ${theme.spacing(3)})` }
: {}),
}));

export const NotFoundMessage = styled(Typography)(({ theme }) => ({
fontSize: 14,
color: theme.palette.text.secondary,
textAlign: 'center',
flex: 1,
marginTop: theme.spacing(2),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
}));

export const NotFoundIconContainer = styled(Typography)(({ theme }) => ({
fontSize: 48,
lineHeight: 1,
}));
23 changes: 23 additions & 0 deletions packages/widget/src/components/Search/SearchNotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SearchOff } from '@mui/icons-material';
import {
NotFoundContainer,
NotFoundIconContainer,
NotFoundMessage,
} from './SearchNotFound.style.js';

interface SearchNotFoundProps {
message: string;
adjustForStickySearchInput?: boolean;
}

export const SearchNotFound = ({
message,
adjustForStickySearchInput,
}: SearchNotFoundProps) => (
<NotFoundContainer adjustForStickySearchInput={adjustForStickySearchInput}>
<NotFoundIconContainer>
<SearchOff fontSize="inherit" />
</NotFoundIconContainer>
<NotFoundMessage>{message}</NotFoundMessage>
</NotFoundContainer>
);
22 changes: 21 additions & 1 deletion packages/widget/src/components/Skeleton/WidgetSkeleton.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,27 @@ export const SkeletonSendToWalletButton = styled(Button)({
pointerEvents: 'none',
});

export const SkeletonPoweredByContainer = styled(Box)({
export const SkeletonPoweredByContainer = styled(Box)(({ theme }) => ({
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-end',
alignItems: 'flex-end',
paddingBottom: theme.spacing(2),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
}));

export const SkeletonHeaderContainer = styled(Box)(({ theme }) => {
return {
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
backdropFilter: 'blur(12px)',
position: 'relative',
top: 0,
zIndex: 1200,
gap: theme.spacing(0.5),
padding: theme.spacing(1.5, 3, 1.5, 3),
overflow: 'auto',
};
});
19 changes: 10 additions & 9 deletions packages/widget/src/components/Skeleton/WidgetSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
FlexContainer,
RelativeContainer,
} from '../AppContainer.js';
import { Container as HeaderContainer } from '../Header/Header.style.js';
import {
SkeletonAmountContainer,
SkeletonCard,
SkeletonCardRow,
SkeletonHeaderAppBar,
SkeletonHeaderContainer,
SkeletonInputCard,
SkeletonPoweredByContainer,
SkeletonReviewButton,
Expand Down Expand Up @@ -87,8 +87,8 @@ export const WidgetSkeleton = ({ config }: WidgetConfigPartialProps) => {
return (
<ThemeProvider theme={theme}>
<AppExpandedContainer>
<RelativeContainer>
<HeaderContainer>
<RelativeContainer sx={{ display: 'flex', flexDirection: 'column' }}>
<SkeletonHeaderContainer>
{!hiddenUI.includes('walletMenu') ? (
<SkeletonHeaderAppBar>
<SkeletonWalletMenuButton />
Expand All @@ -100,7 +100,8 @@ export const WidgetSkeleton = ({ config }: WidgetConfigPartialProps) => {
<Skeleton width={126} height={34} variant="text" />
<SkeletonIcon />
</SkeletonHeaderAppBar>
</HeaderContainer>
</SkeletonHeaderContainer>

<FlexContainer
sx={{
gap: 2,
Expand All @@ -123,12 +124,12 @@ export const WidgetSkeleton = ({ config }: WidgetConfigPartialProps) => {
</SkeletonSendToWalletButton>
) : null}
</SkeletonReviewButtonContainer>
{!hiddenUI.includes('poweredBy') ? (
<SkeletonPoweredByContainer>
<Skeleton width={96} height={18} variant="text" />
</SkeletonPoweredByContainer>
) : null}
</FlexContainer>
{!hiddenUI.includes('poweredBy') ? (
<SkeletonPoweredByContainer>
<Skeleton width={96} height={18} variant="text" />
</SkeletonPoweredByContainer>
) : null}
</RelativeContainer>
</AppExpandedContainer>
</ThemeProvider>
Expand Down
9 changes: 1 addition & 8 deletions packages/widget/src/components/TokenList/TokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import { useChain } from '../../hooks/useChain.js';
import { useDebouncedWatch } from '../../hooks/useDebouncedWatch.js';
import { useTokenBalances } from '../../hooks/useTokenBalances.js';
import { useTokenSearch } from '../../hooks/useTokenSearch.js';
import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js';
import { FormKeyHelper } from '../../stores/form/types.js';
import { useFieldValues } from '../../stores/form/useFieldValues.js';
import type { TokenAmount } from '../../types/token.js';
import { createElementId, ElementId } from '../../utils/elements.js';
import { TokenNotFound } from './TokenNotFound.js';
import { VirtualizedTokenList } from './VirtualizedTokenList.js';
import type { TokenListProps } from './types.js';
Expand All @@ -27,7 +25,6 @@ export const TokenList: FC<TokenListProps> = ({
320,
'tokenSearchFilter',
);
const { elementId } = useWidgetConfig();

const { chain, isLoading: isChainLoading } = useChain(selectedChainId);
const { account } = useAccount({ chainType: chain?.chainType });
Expand Down Expand Up @@ -84,11 +81,7 @@ export const TokenList: FC<TokenListProps> = ({
!tokenSearchFilter;

return (
<Box
ref={parentRef}
style={{ height, overflow: 'auto' }}
id={createElementId(ElementId.TokenList, elementId)}
>
<Box ref={parentRef} style={{ height, overflow: 'auto' }}>
{!tokens.length && !isLoading ? (
<TokenNotFound formType={formType} />
) : null}
Expand Down
Loading

0 comments on commit 5504d95

Please sign in to comment.