Skip to content

Commit

Permalink
Improve multi path component for moblie (#2094)
Browse files Browse the repository at this point in the history
Co-authored-by: Colin Rosati <colin.rosati@commercetools.com>
  • Loading branch information
timonrey and ColinRosati authored Oct 8, 2024
1 parent 07fe8b7 commit 5fe6164
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import styled from '@emotion/styled';
import React, { ReactElement, ReactNode, useEffect, useState } from 'react';
import { designSystem } from '..';
import React, {
forwardRef,
ReactElement,
ReactNode,
useEffect,
useState,
} from 'react';
import { designSystem } from '../..';
import Text from '@commercetools-uikit/text';
import Spacings from '@commercetools-uikit/spacings';
import { Theme, Interpolation, css, SerializedStyles } from '@emotion/react';
import useSelectedPath from '../hooks/use-selected-path';
import useSelectedPath from '../../hooks/use-selected-path';
import { useArrowNavigation } from './useArrowNavigation';
import { AngleRightIcon, AngleLeftIcon } from '@commercetools-uikit/icons';

type OneOrManyChildren = React.ReactElement | React.ReactElement[];
type MultiPathBlockProps = {
Expand Down Expand Up @@ -68,6 +75,10 @@ const getLinkStyles = (isActive: boolean): Interpolation<Theme> => [
padding-left: ${designSystem.dimensions.spacings.m};
}
> p {
font-size: ${designSystem.typography.fontSizes.small};
}
${getBottomBorderStyles('transparent')}
`,
isActive &&
Expand Down Expand Up @@ -137,17 +148,69 @@ const TabHeader = (props: TTabHeaderProps) => {
onClick={props.onClick}
css={getLinkStyles(props.isActive)}
>
<Text.Headline as="h3" truncate={true}>
<Text.Body tone={props.isActive ? 'primary' : 'inherit'} truncate={true}>
{props.label}
</Text.Headline>
</Text.Body>
</span>
);
};

const scrollFocusedTab = (
elem: React.ForwardedRef<HTMLDivElement>,
rightToLeft: boolean
) => {
if (!elem || !('current' in elem)) {
return;
}

if (rightToLeft) {
elem.current?.scrollTo({
left: elem.current.offsetLeft + elem.current.offsetWidth,
behavior: 'smooth',
});
} else {
elem.current?.scrollTo({
left: 0,
behavior: 'smooth',
});
}
};

const ScrollChevronWrapper = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
width: ${designSystem.dimensions.spacings.big};
padding-top: 0.5rem;
`;

type OverFlowScrollProps = {
rightToLeft: boolean;
isVisible: boolean;
};

const OverFlowScroll = forwardRef<HTMLDivElement, OverFlowScrollProps>(
({ rightToLeft, isVisible }, ref) => {
const Chevron = rightToLeft ? (
<AngleRightIcon color="primary" size="small" />
) : (
<AngleLeftIcon size="small" color="primary" />
);

return (
<ScrollChevronWrapper onClick={() => scrollFocusedTab(ref, rightToLeft)}>
{isVisible && Chevron}
</ScrollChevronWrapper>
);
}
);
// forwardRef needs some displayname on the component instance. Otherwise throws some error
OverFlowScroll.displayName = 'OverFlowScroll';

const SelectorsContainer = styled.div`
display: flex;
border-bottom: 1px solid ${designSystem.colors.light.borderPrimary};
padding-top: 8px;
padding-left: 16px;
overflow-x: hidden;
`;

const ComponentWrapper = styled.div`
Expand All @@ -159,6 +222,17 @@ const PathsContainer = styled.div`
padding: 10px;
`;

const ScrollWrapper = styled.span`
display: inline-flex;
align-items: flex-end;
overflow-x: auto;
::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
`;

const MultiPathBlock = (props: MultiPathBlockProps) => {
const labelSyncItems = extractLabelSyncPair(props.children);
const [selected, setSelected] = React.useState<LabelSyncPair>(
Expand All @@ -168,6 +242,9 @@ const MultiPathBlock = (props: MultiPathBlockProps) => {

const { selectedPath, updateSelectedPath } = useSelectedPath();

const { displayStartScroll, displayEndScroll, tabsRef, tabListRef } =
useArrowNavigation(labelSyncItems);

useEffect(() => {
if (selectedPath) {
const matchedSyncItem = labelSyncItems.find(
Expand Down Expand Up @@ -209,8 +286,13 @@ const MultiPathBlock = (props: MultiPathBlockProps) => {

return (
<ComponentWrapper>
<SelectorsContainer role="tablist">
<Spacings.Inline alignItems="flex-end">
<SelectorsContainer role="tablist" ref={tabsRef}>
<OverFlowScroll
rightToLeft={false}
isVisible={displayStartScroll}
ref={tabListRef}
/>
<ScrollWrapper ref={tabListRef}>
{labelSyncItems.map((labelSyncItem, index) => (
<TabHeader
index={index}
Expand All @@ -220,7 +302,12 @@ const MultiPathBlock = (props: MultiPathBlockProps) => {
onClick={(e) => onTabHeaderClick(e)(labelSyncItem)}
/>
))}
</Spacings.Inline>
</ScrollWrapper>
<OverFlowScroll
rightToLeft={true}
isVisible={displayEndScroll}
ref={tabListRef}
/>
</SelectorsContainer>
<PathsContainer>
<TabContentChildren activePathIndex={activePathIndex}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useEffect, useRef, useState } from 'react';
import { LabelSyncPair } from '.';

/**
* Dedicated hook for arrow navigation
* This sets up intersection observers, threshold, display states for navigation
*/
export const useArrowNavigation = (labelSyncItems: LabelSyncPair[]) => {
const [displayStartScroll, setDisplayStartScroll] = useState(false);
const [displayEndScroll, setDisplayEndScroll] = useState(false);
const tabsRef = useRef<HTMLDivElement>(null);
const tabListRef = useRef<HTMLDivElement>(null);

const handleScrollButtonStart = (entries: IntersectionObserverEntry[]) => {
setDisplayStartScroll(!entries[0].isIntersecting);
};
const handleScrollButtonEnd = (entries: IntersectionObserverEntry[]) => {
setDisplayEndScroll(!entries[entries.length - 1].isIntersecting);
};

useEffect(() => {
if (!tabListRef.current?.children) {
return;
}

const tabListChildren = Array.from(tabListRef.current?.children);

if (labelSyncItems.length > 0) {
const firstTab = tabListChildren[0];
const lastTab = tabListChildren[tabListChildren.length - 1];
const observerOptions = {
root: tabsRef.current,
threshold: 0.99,
};

const firstObserver = new IntersectionObserver(
handleScrollButtonStart,
observerOptions
);
firstObserver.observe(firstTab);

const lastObserver = new IntersectionObserver(
handleScrollButtonEnd,
observerOptions
);
lastObserver.observe(lastTab);

return () => {
firstObserver.disconnect();
lastObserver.disconnect();
};
}
}, [labelSyncItems.length]);

return {
displayStartScroll,
displayEndScroll,
tabsRef,
tabListRef,
};
};
68 changes: 55 additions & 13 deletions websites/docs-smoke-test/src/content/views/multi-path-block.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ title: Multi Path Block

import { LoginButton, IfUserLoggedOut, FirstName } from '@commercetools-docs/gatsby-theme-docs'


<MultiPathBlock>
<PathBlock label="Javascript path" syncWith="javascript">

**Markdown content**
<br/>

**Bold text**
*Italic text*
***Bold and Italic text***
_Italic text_
**_Bold and Italic text_**
~~Strikethrough text~~
<br/>

> This is a blockquote. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.
<br/>
> <br/>
1. First ordered list item
2. Second item
Expand All @@ -42,11 +41,11 @@ def hello_world():
print("Hello, World!")
```

Table | Column B | Column C
---------|----------|---------
A1 | B1 | C1
A2 | B2 | C2
A3 | B3 | C3
| Table | Column B | Column C |
| ----- | -------- | -------- |
| A1 | B1 | C1 |
| A2 | B2 | C2 |
| A3 | B3 | C3 |

<br/>

Expand Down Expand Up @@ -76,7 +75,6 @@ Table | Column B | Column C

(Warning) Please note that the health of our delivery infrastructure is independent of the `SubscriptionHealthStatus` and can be checked on our [status page](https://www.google.com) (it is part of the `Background Services`).


</Warning>

<br/>
Expand All @@ -102,17 +100,17 @@ Some custom plan tags: <PlanTag plan="plan1" />, <PlanTag plan="plan2" />.
<Cards narrow>
<Card>

[Documentation](/../documentation)
[Documentation](/../documentation)

</Card>
<Card>

[Docs kit smoke test](/../docs-smoke-test)
[Docs kit smoke test](/../docs-smoke-test)

</Card>
<Card>

[Site Template](/../site-template)
[Site Template](/../site-template)

</Card>
</Cards>
Expand Down Expand Up @@ -170,6 +168,50 @@ Logged in, <FirstName />

Please log in

</IfUserLoggedOut>
</PathBlock>
<PathBlock label="Multi path with more than three tabs">

** More custom components **

<br/>

**Mermaid Diagram**

```mermaid
flowchart LR
subgraph a subgraph
b2-->A
end
A[Start] --> GitHub{GitHub}
GitHub -->|Yes| C[OK]
C --> D[Rethink]
D --> GitHub
GitHub ---->|No| E[End]
click GitHub "http://www.github.com" "B is a link to Github"
```

<br/>

**Video Player**
<Video
url="https://customer-ytbpo1yna9xohg5m.cloudflarestream.com/33709d50562534d2a6f9f2b1766d8ff6/manifest/video.m3u8"
poster="https://customer-ytbpo1yna9xohg5m.cloudflarestream.com/33709d50562534d2a6f9f2b1766d8ff6/thumbnails/thumbnail.jpg?time=10s"
/>

<br />

**Self learning conditionals**

<IfUserLoggedIn>

Logged in, <FirstName />

</IfUserLoggedIn>
<IfUserLoggedOut>

Please log in

</IfUserLoggedOut>
</PathBlock>
</MultiPathBlock>

0 comments on commit 5fe6164

Please sign in to comment.