Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(responsive-tabs): allow scroll on components with tabs #4067

Merged
merged 63 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f7b4cf3
feat(tabs): make non-fitted tabs responsive
TheSisb Apr 11, 2024
2d43153
chore: fixes
TheSisb Apr 11, 2024
65e4fdc
fix: add missing 4px under scrollbar
TheSisb Apr 12, 2024
e27187c
chore: fixes to codeblock overflow tabs
TheSisb Apr 18, 2024
c143028
fix(codeblock): improve shadow behavior
TheSisb Apr 22, 2024
98d431f
fix(tabs): fitted tab styling issue
TheSisb Apr 22, 2024
8d5353a
chore: fix build
TheSisb Apr 23, 2024
19498cd
chore: fixes
TheSisb Apr 23, 2024
544a312
Merge branch 'tabs/responsive' of github.com:twilio-labs/paste into f…
krisantrobus Sep 9, 2024
ba0fba9
feat(tabs): a11y + code cleanup
krisantrobus Sep 11, 2024
daec83f
feat(in-page-navigation): added styled scroll
krisantrobus Sep 11, 2024
de2afa0
feat(in-page-navigation): remove unnecessary padding
krisantrobus Sep 12, 2024
79de443
feat(code-block): add shadows to scroll
krisantrobus Sep 12, 2024
1606fb4
feat(code-block): a11y
krisantrobus Sep 12, 2024
6bcc5f7
chore(pr): changeset for codemod
krisantrobus Sep 12, 2024
c22f620
chore(testing): testing for responsive tabs
krisantrobus Sep 12, 2024
1e86f45
chore(code-block): code cleanup
krisantrobus Sep 12, 2024
4b60b6f
chore(pr): cleanup
krisantrobus Sep 12, 2024
bcbaafa
feat(in-page-navigation): add scroll to active on mount
krisantrobus Sep 12, 2024
b3def55
Merge branch 'main' of github.com:twilio-labs/paste into feat/respons…
krisantrobus Sep 12, 2024
e77bcf8
chore(pr): fix lint issues
krisantrobus Sep 12, 2024
721bbe3
chore(pr): fix failing tests & lint
krisantrobus Sep 12, 2024
8e022e0
chore(pr): lint fix
krisantrobus Sep 12, 2024
18b0ae9
chore(pr): address comments
krisantrobus Sep 13, 2024
0f3db32
feat(tokens): added box shadow for scroll inverse
krisantrobus Sep 13, 2024
2e563d9
chore(pr): update to use boudning client
krisantrobus Sep 13, 2024
5846f9c
chore(pr): clenaup
krisantrobus Sep 13, 2024
009bfe9
chore(pr): clenaup
krisantrobus Sep 13, 2024
5476271
chore(pr): typedocs
krisantrobus Sep 13, 2024
1c6d8bb
chore(pr): fix tests
krisantrobus Sep 13, 2024
c9194a5
chore(pr): story responsive fixed widths
krisantrobus Sep 13, 2024
a043a30
Update packages/paste-design-tokens/tokens/global/box-shadow.yml
krisantrobus Sep 13, 2024
1ea0648
chore(pr): fix flex issue
krisantrobus Sep 13, 2024
88224a0
feat(tabs): inital scroll arrow impl
krisantrobus Oct 1, 2024
54ab4cc
feat(tokens): udpate for shadows
krisantrobus Oct 2, 2024
7cea9a2
feat(tabs): unrefined overflow buttons
krisantrobus Oct 2, 2024
d65f586
Merge branch 'main' of github.com:twilio-labs/paste into feat/respons…
krisantrobus Oct 2, 2024
10583a6
feat(tabs): unrefined overflow buttons
krisantrobus Oct 4, 2024
2dbfcab
feat(tabs): customization to elements
krisantrobus Oct 8, 2024
f968ee6
chore(tabs): fix tests
krisantrobus Oct 8, 2024
b5b1f68
chore(tabs): add customization examples
krisantrobus Oct 8, 2024
15cf036
chore(tabs): comments and eventsncleanup
krisantrobus Oct 8, 2024
7734717
chore(tabs): code optimization
krisantrobus Oct 8, 2024
7f80ad1
chore(tabs): code inverse styles
krisantrobus Oct 8, 2024
4e34ccf
feat(tabs): shadow on scroll
krisantrobus Oct 8, 2024
8fa1495
chore(tabs): linting issues
krisantrobus Oct 8, 2024
85064f5
feat(tabs): update button sizes
krisantrobus Oct 9, 2024
1b31a36
feat(code-block): remove line under overflow buttons
krisantrobus Oct 9, 2024
0cb5277
chore(tabs): fix test
krisantrobus Oct 10, 2024
bf5ee1c
feat(tabs): handle sinfle tab in view issue
krisantrobus Oct 10, 2024
7b026fc
feat(tabs): overflow button sizings
krisantrobus Oct 10, 2024
360993e
feat(tabs): overflow button sizings
krisantrobus Oct 10, 2024
454e045
chore(tabs): ful width and border
krisantrobus Oct 11, 2024
c3734dc
chore(in-page-navigation): columnGap
krisantrobus Oct 11, 2024
c3e3aa7
chore(tabs): add columnGap back
krisantrobus Oct 11, 2024
b8f034a
chore(code-block): add columnGap back
krisantrobus Oct 11, 2024
5aa23e6
chore(code-block): overflow button width
krisantrobus Oct 11, 2024
a98b9c6
feat(code-block): fix underline issue
krisantrobus Oct 14, 2024
3ce59a9
fix: inverse colors
krisantrobus Oct 15, 2024
e1d11f9
chore(tabs): refactor to pull scroll logic into hooks
krisantrobus Oct 15, 2024
1f5dc4a
Merge branch 'feat/responsive-tabs' of github.com:twilio-labs/paste i…
krisantrobus Oct 15, 2024
a0fac32
chore(ci): lint fixes
krisantrobus Oct 15, 2024
ecdbdd4
Merge branch 'main' of github.com:twilio-labs/paste into feat/respons…
krisantrobus Oct 16, 2024
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
6 changes: 6 additions & 0 deletions .changeset/fresh-points-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/design-tokens": minor
"@twilio-paste/core": minor
---

[Design Token] added new box shadows to support scrollable styling on inverse colored components
6 changes: 6 additions & 0 deletions .changeset/grumpy-dryers-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/code-block": patch
"@twilio-paste/core": patch
---

[CodeBlock] make tabs responsive
6 changes: 6 additions & 0 deletions .changeset/loud-items-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/in-page-navigation": patch
"@twilio-paste/core": patch
---

[In Page Navigation] make items scrollable
7 changes: 7 additions & 0 deletions .changeset/strong-eagles-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@twilio-paste/tabs": minor
"@twilio-paste/core": minor
"@twilio-paste/codemods": minor
---

[Tabs] make the non-fitted variant Tabs responsive. Export the context provider `TabsContext`.
1 change: 1 addition & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@
"TabPanel": "@twilio-paste/core/tabs",
"TabPanels": "@twilio-paste/core/tabs",
"Tabs": "@twilio-paste/core/tabs",
"TabsContext": "@twilio-paste/core/tabs",
"useTabState": "@twilio-paste/core/tabs",
"TextArea": "@twilio-paste/core/textarea",
"TimePicker": "@twilio-paste/core/time-picker",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const CustomizationWrapper: React.FC<React.PropsWithChildren> = ({ children }) =
CODE_BLOCK_TAB: { borderRadius: "borderRadius0" },
CODE_BLOCK_WRAPPER: { width: "size50" },
CODE_BLOCK: { width: "size50" },
CODE_BLOCK_TAB_LIST_CHILD: { backgroundColor: "colorBackgroundError" },
CODE_BLOCK_TAB_LIST_CHILD_SCROLL_WRAPPER: { backgroundColor: "colorBackgroundAvailable" },
}}
>
{children}
Expand All @@ -47,6 +49,8 @@ const CustomizationMyWrapper: React.FC<React.PropsWithChildren> = ({ children })
MY_CODE_BLOCK_TAB: { borderRadius: "borderRadius0" },
MY_CODE_BLOCK_WRAPPER: { width: "size50" },
MY_CODE_BLOCK: { width: "size50" },
MY_CODE_BLOCK_TAB_LIST_CHILD: { backgroundColor: "colorBackgroundError" },
MY_CODE_BLOCK_TAB_LIST_CHILD_SCROLL_WRAPPER: { backgroundColor: "colorBackgroundAvailable" },
}}
>
{children}
Expand Down Expand Up @@ -85,6 +89,10 @@ describe("Customization", () => {
expect(content?.getAttribute("data-paste-element")).toBe("CODE_BLOCK_CONTENT");
expect(tabList.getAttribute("data-paste-element")).toBe("CODE_BLOCK_TAB_LIST");
expect(tab.getAttribute("data-paste-element")).toBe("CODE_BLOCK_TAB");
expect(tab.parentElement?.getAttribute("data-paste-element")).toBe("CODE_BLOCK_TAB_LIST_CHILD");
expect(tab.parentElement?.parentElement?.getAttribute("data-paste-element")).toBe(
"CODE_BLOCK_TAB_LIST_CHILD_SCROLL_WRAPPER",
);
expect(tabPanel?.getAttribute("data-paste-element")).toBe("CODE_BLOCK_TAB_PANEL");
expect(codeBlock.getAttribute("data-paste-element")).toBe("CODE_BLOCK");
expect(heading.getAttribute("data-paste-element")).toBe("CODE_BLOCK_HEADER");
Expand Down Expand Up @@ -128,6 +136,10 @@ describe("Customization", () => {
expect(content?.getAttribute("data-paste-element")).toBe("MY_CODE_BLOCK_CONTENT");
expect(tabList.getAttribute("data-paste-element")).toBe("MY_CODE_BLOCK_TAB_LIST");
expect(tab.getAttribute("data-paste-element")).toBe("MY_CODE_BLOCK_TAB");
expect(tab.parentElement?.getAttribute("data-paste-element")).toBe("MY_CODE_BLOCK_TAB_LIST_CHILD");
expect(tab.parentElement?.parentElement?.getAttribute("data-paste-element")).toBe(
"MY_CODE_BLOCK_TAB_LIST_CHILD_SCROLL_WRAPPER",
);
expect(tabPanel?.getAttribute("data-paste-element")).toBe("MY_CODE_BLOCK_TAB_PANEL");
expect(codeBlock.getAttribute("data-paste-element")).toBe("MY_CODE_BLOCK");
expect(heading.getAttribute("data-paste-element")).toBe("MY_CODE_BLOCK_HEADER");
Expand Down Expand Up @@ -167,6 +179,8 @@ describe("Customization", () => {
expect(heading).toHaveStyleRule("border-top-right-radius", "8px");
expect(tabList).toHaveStyleRule("column-gap", "0");
expect(tab).toHaveStyleRule("border-radius", "0");
expect(tab.parentElement).toHaveStyleRule("background-color", "rgb(214, 31, 31)");
expect(tab.parentElement?.parentElement).toHaveStyleRule("background-color", "rgb(20, 176, 83)");
expect(tabPanel).toHaveStyleRule("border-bottom-right-radius", "8px");
expect(copyButton).toHaveStyleRule("background-color", "rgb(254, 236, 236)");
expect(externalLink).toHaveStyleRule("background-color", "rgb(254, 236, 236)");
Expand Down Expand Up @@ -210,6 +224,8 @@ describe("Customization", () => {
expect(heading).toHaveStyleRule("border-top-right-radius", "8px");
expect(tabList).toHaveStyleRule("column-gap", "0");
expect(tab).toHaveStyleRule("border-radius", "0");
expect(tab.parentElement).toHaveStyleRule("background-color", "rgb(214, 31, 31)");
expect(tab.parentElement?.parentElement).toHaveStyleRule("background-color", "rgb(20, 176, 83)");
expect(tabPanel).toHaveStyleRule("border-bottom-right-radius", "8px");
expect(copyButton).toHaveStyleRule("background-color", "rgb(254, 236, 236)");
expect(externalLink).toHaveStyleRule("background-color", "rgb(254, 236, 236)");
Expand Down
117 changes: 109 additions & 8 deletions packages/paste-core/components/code-block/src/CodeBlockTabList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import { Box } from "@twilio-paste/box";
import { Box, safelySpreadBoxProps } from "@twilio-paste/box";
import type { BoxProps } from "@twilio-paste/box";
import { TabList } from "@twilio-paste/tabs";
import type { TabListProps } from "@twilio-paste/tabs";
import { css, styled } from "@twilio-paste/styling-library";
import { type TabListProps, TabsContext } from "@twilio-paste/tabs";
import { TabPrimitiveList } from "@twilio-paste/tabs-primitive";
import * as React from "react";

import { OverflowButton } from "./OverflowButton";
import { handleScrollDirection, useElementsOutOfBounds, useShowShadow } from "./utlis";

/**
* This wrapper applies styles that customize the scrollbar and its track.
*/
const StyledTabList = styled.div(() => {
return css({
overflowX: "auto",
overflowY: "hidden",
overflowScrolling: "touch",
/* Firefox scrollbar */
"@supports (-moz-appearance:none)": {
paddingBottom: "0px",
scrollbarWidth: "none",
},
/* Chrome + Safari scrollbar */
"::-webkit-scrollbar": {
height: 0,
},
"::-webkit-scrollbar-track": {
background: "transparent",
},
});
});

export interface CodeBlockTabListProps extends Omit<TabListProps, "aria-label"> {
/**
* Overrides the default element name to apply unique styles with the Customization Provider
Expand All @@ -17,12 +44,86 @@ export interface CodeBlockTabListProps extends Omit<TabListProps, "aria-label">

export const CodeBlockTabList = React.forwardRef<HTMLDivElement, CodeBlockTabListProps>(
({ children, element = "CODE_BLOCK_TAB_LIST", ...props }, ref) => {
const tabContext = React.useContext(TabsContext);
// ref to the scrollable element
const scrollableRef = React.useRef<HTMLDivElement>(null);
const listRef = React.useRef<HTMLDivElement>(null);

const { determineElementsOutOfBounds, elementOutOBoundsLeft, elementOutOBoundsRight } = useElementsOutOfBounds();
const { handleShadow, showShadow } = useShowShadow();

const handleScrollEvent = (): void => {
handleShadow();
determineElementsOutOfBounds(scrollableRef.current, listRef.current);
};

React.useEffect(() => {
if (scrollableRef.current && listRef.current) {
scrollableRef.current.addEventListener("scroll", handleScrollEvent);
window.addEventListener("resize", handleScrollEvent);
determineElementsOutOfBounds(scrollableRef.current, listRef.current);
}
}, [scrollableRef.current, listRef.current]);

// Cleanup event listeners on destroy
React.useEffect(() => {
return () => {
if (scrollableRef.current) {
scrollableRef.current.removeEventListener("scroll", handleScrollEvent);
window.removeEventListener("resize", handleScrollEvent);
}
};
}, []);

return (
<Box paddingX="space70">
<TabList {...props} aria-label="label" ref={ref} element={element}>
{children}
</TabList>
</Box>
<TabPrimitiveList {...(tabContext as any)} as={Box} {...props} element={element} ref={ref}>
<Box element={`${element}_CHILD_WRAPPER`} display="flex">
<OverflowButton
position="left"
onClick={() =>
handleScrollDirection("left", elementOutOBoundsLeft, elementOutOBoundsRight, listRef.current)
}
visible={Boolean(elementOutOBoundsLeft)}
element={element}
showShadow={showShadow}
/>
<Box
{...safelySpreadBoxProps(props)}
as={StyledTabList as any}
ref={scrollableRef}
display="flex"
flexWrap="nowrap"
element={`${element}_CHILD_SCROLL_WRAPPER`}
overflowX="auto"
overflowY="hidden"
flexGrow={1}
width="calc(100% - 60px)"
>
<Box
whiteSpace="nowrap"
element={`${element}_CHILD`}
display="flex"
borderBottomStyle="solid"
borderBottomWidth="borderWidth10"
borderBottomColor="colorBorderInverseWeaker"
ref={listRef}
flexGrow={1}
columnGap="space20"
>
{children}
</Box>
</Box>
<OverflowButton
position="right"
onClick={() =>
handleScrollDirection("right", elementOutOBoundsLeft, elementOutOBoundsRight, listRef.current)
}
visible={Boolean(elementOutOBoundsRight)}
element={element}
showShadow={showShadow}
/>
</Box>
</TabPrimitiveList>
);
},
);
Expand Down
55 changes: 55 additions & 0 deletions packages/paste-core/components/code-block/src/OverflowButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Box, BoxProps, BoxStyleProps } from "@twilio-paste/box";
import { ChevronLeftIcon } from "@twilio-paste/icons/esm/ChevronLeftIcon";
import { ChevronRightIcon } from "@twilio-paste/icons/esm/ChevronRightIcon";
import { useTheme } from "@twilio-paste/theme";
import React from "react";

interface OverflowButtonProps {
onClick: () => void;
position: "left" | "right";
visible?: boolean;
element?: BoxProps["element"];
showShadow?: boolean;
}

const Styles: BoxStyleProps = {
color: "colorTextIconInverse",
_hover: {
color: "colorTextInverse",
cursor: "pointer",
},
};

export const OverflowButton: React.FC<OverflowButtonProps> = ({
onClick,
position,
visible,
element = "CODE_BLOCK_TAB_LIST",
showShadow,
}) => {
const theme = useTheme();
const Chevron = position === "left" ? ChevronLeftIcon : ChevronRightIcon;
if (!visible && position === "right") return null;

return (
<Box
onClick={onClick}
aria-hidden={true}
display="flex"
alignItems="center"
justifyContent="center"
width="sizeIcon40"
padding="space20"
position="relative"
boxShadow={visible && showShadow ? theme.shadows.shadowScrollInverse : undefined}
element={`${element}_OVERFLOW_BUTTON_${position.toUpperCase()}`}
cursor={visible ? "pointer" : "none"}
{...Styles}
>
{/* For left button to align with spacing of header we hide icon */}
{visible && <Chevron decorative={true} />}
</Box>
);
};

OverflowButton.displayName = "OverflowButton";
87 changes: 87 additions & 0 deletions packages/paste-core/components/code-block/src/utlis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "react";

export const useElementsOutOfBounds = (): {
elementOutOBoundsLeft: HTMLDivElement | null;
elementOutOBoundsRight: HTMLDivElement | null;
determineElementsOutOfBounds: (scrollContainer: HTMLDivElement | null, listContainer: HTMLElement | null) => void;
} => {
// Keep track of first elements that are paritally or completely out of view in either direction
const [elementOutOBoundsLeft, setElementOutOfBoundsLeft] = React.useState<HTMLDivElement | null>(null);
const [elementOutOBoundsRight, setElementOutOfBoundsRight] = React.useState<HTMLDivElement | null>(null);

// called on load and resize and on scroll to set the elements that are out of view
const determineElementsOutOfBounds = (
scrollContainer: HTMLDivElement | null,
listContainer: HTMLElement | null,
): void => {
if (scrollContainer && listContainer) {
const currentScrollContainerRightPosition = (scrollContainer as HTMLDivElement)?.getBoundingClientRect().right;
const currentScrollContainerXOffset = (scrollContainer as HTMLDivElement)?.getBoundingClientRect().x;

let leftOutOfBounds: HTMLDivElement | null = null;
let rightOutOfBounds: HTMLDivElement | null = null;

(listContainer.childNodes as NodeListOf<HTMLDivElement>).forEach((tab) => {
const { x, right } = tab.getBoundingClientRect();
// Check if the tab is spanning the view if text is really long on smaller devices, wont skip to next element
const isSpanningView = x < currentScrollContainerXOffset && right > currentScrollContainerRightPosition;
if (!isSpanningView) {
/**
* Compares the left side of the tab with the left side of the scrollable container position
* as the x value will not be 0 due to being offset in the screen.
*/
if (x < currentScrollContainerXOffset) {
leftOutOfBounds = tab;
}
/**
* Compares the right side to the end of container with some buffer. Also ensure there are
* no value set as it loops through the array we don't want it to override the first value out of bounds.
*/
if (right > currentScrollContainerRightPosition + 10 && !rightOutOfBounds) {
rightOutOfBounds = tab;
}
}

setElementOutOfBoundsLeft(leftOutOfBounds);
setElementOutOfBoundsRight(rightOutOfBounds);
});
}
};

return { elementOutOBoundsLeft, elementOutOBoundsRight, determineElementsOutOfBounds };
};

export const useShowShadow = (): { showShadow: boolean; handleShadow: () => void } => {
const [showShadow, setShowShadow] = React.useState(false);
let showShadowTimer: number;

const handleShadow = (): void => {
if (showShadowTimer) {
window.clearTimeout(showShadowTimer);
}
setShowShadow(true);
showShadowTimer = window.setTimeout(() => {
setShowShadow(false);
}, 500);
};

return { showShadow, handleShadow };
};

/**
* Scrolls to the element that is out of bounds (from React State), centering it in the scrollable container
* Logic to handle scrolling also replicated in CodeBlock and InPageNavigation. If changing here, consider reviewing those components too.
*/
export const handleScrollDirection = (
direction: "left" | "right",
elementOutOBoundsLeft: HTMLDivElement | null,
elementOutOBoundsRight: HTMLDivElement | null,
listContainer: HTMLElement | null,
): void => {
if (listContainer) {
const elementToScrollTo = direction === "left" ? elementOutOBoundsLeft : elementOutOBoundsRight;
if (elementToScrollTo) {
elementToScrollTo.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
}
}
};
Loading
Loading