diff --git a/.changeset/scrollarea-fade-mask.md b/.changeset/scrollarea-fade-mask.md new file mode 100644 index 00000000..d7606e03 --- /dev/null +++ b/.changeset/scrollarea-fade-mask.md @@ -0,0 +1,5 @@ +--- +"reshaped": minor +--- + +ScrollArea: Added fade prop that displays a fade mask on the sides of the area that can be scrolled towards diff --git a/.changeset/table-fade-scroll-driven.md b/.changeset/table-fade-scroll-driven.md new file mode 100644 index 00000000..24f65bbb --- /dev/null +++ b/.changeset/table-fade-scroll-driven.md @@ -0,0 +1,5 @@ +--- +"reshaped": patch +--- + +Table: Fade mask for the horizontally scrollable content is now rendered with CSS scroll-driven animations instead of JS scroll tracking diff --git a/README.md b/README.md index e2d2e4fd..f0bdd1e3 100644 --- a/README.md +++ b/README.md @@ -36,5 +36,3 @@ Read our [contribution guide](CONTRIBUTING.md) to learn about our principles, de ## License This project is licensed under the terms of the MIT license. - - diff --git a/packages/reshaped/src/components/ScrollArea/ScrollArea.module.css b/packages/reshaped/src/components/ScrollArea/ScrollArea.module.css index 6e96ae0c..288ca5e9 100644 --- a/packages/reshaped/src/components/ScrollArea/ScrollArea.module.css +++ b/packages/reshaped/src/components/ScrollArea/ScrollArea.module.css @@ -1,6 +1,33 @@ +/* stylelint-disable plugin/browser-compat -- graceful degradation for Firefox */ +@property --rs-scroll-area-fade-block-start { + syntax: ""; + initial-value: 0; + inherits: false; +} + +@property --rs-scroll-area-fade-block-end { + syntax: ""; + initial-value: 0; + inherits: false; +} + +@property --rs-scroll-area-fade-inline-start { + syntax: ""; + initial-value: 0; + inherits: false; +} + +@property --rs-scroll-area-fade-inline-end { + syntax: ""; + initial-value: 0; + inherits: false; +} +/* stylelint-enable plugin/browser-compat */ + .root { --rs-scroll-area-thumb-size: var(--rs-unit-x1); --rs-scroll-area-thumb-offset: var(--rs-unit-x1); + --rs-scroll-area-fade-size: var(--rs-unit-x8); overflow: hidden; height: 100%; @@ -26,6 +53,99 @@ } } +@keyframes fade-block-start { + from { + --rs-scroll-area-fade-block-start: 0px; + } + + to { + --rs-scroll-area-fade-block-start: var(--rs-scroll-area-fade-size); + } +} + +@keyframes fade-block-end { + from { + --rs-scroll-area-fade-block-end: var(--rs-scroll-area-fade-size); + } + + to { + --rs-scroll-area-fade-block-end: 0px; + } +} + +@keyframes fade-inline-start { + from { + --rs-scroll-area-fade-inline-start: 0px; + } + + to { + --rs-scroll-area-fade-inline-start: var(--rs-scroll-area-fade-size); + } +} + +@keyframes fade-inline-end { + from { + --rs-scroll-area-fade-inline-end: var(--rs-scroll-area-fade-size); + } + + to { + --rs-scroll-area-fade-inline-end: 0px; + } +} + +@supports (animation-timeline: scroll(self block)) { + .--fade { + --rs-scroll-area-fade-inline-direction: to right; + + mask-image: + linear-gradient( + to bottom, + rgb(0 0 0 / 0%) 0, + rgb(0 0 0 / 10%) calc(var(--rs-scroll-area-fade-block-start) * 0.2), + rgb(0 0 0 / 35%) calc(var(--rs-scroll-area-fade-block-start) * 0.4), + rgb(0 0 0 / 65%) calc(var(--rs-scroll-area-fade-block-start) * 0.6), + rgb(0 0 0 / 90%) calc(var(--rs-scroll-area-fade-block-start) * 0.8), + rgb(0 0 0 / 100%) var(--rs-scroll-area-fade-block-start), + rgb(0 0 0 / 100%) calc(100% - var(--rs-scroll-area-fade-block-end)), + rgb(0 0 0 / 90%) calc(100% - var(--rs-scroll-area-fade-block-end) * 0.8), + rgb(0 0 0 / 65%) calc(100% - var(--rs-scroll-area-fade-block-end) * 0.6), + rgb(0 0 0 / 35%) calc(100% - var(--rs-scroll-area-fade-block-end) * 0.4), + rgb(0 0 0 / 10%) calc(100% - var(--rs-scroll-area-fade-block-end) * 0.2), + rgb(0 0 0 / 0%) 100% + ), + linear-gradient( + var(--rs-scroll-area-fade-inline-direction), + rgb(0 0 0 / 0%) 0, + rgb(0 0 0 / 10%) calc(var(--rs-scroll-area-fade-inline-start) * 0.2), + rgb(0 0 0 / 35%) calc(var(--rs-scroll-area-fade-inline-start) * 0.4), + rgb(0 0 0 / 65%) calc(var(--rs-scroll-area-fade-inline-start) * 0.6), + rgb(0 0 0 / 90%) calc(var(--rs-scroll-area-fade-inline-start) * 0.8), + rgb(0 0 0 / 100%) var(--rs-scroll-area-fade-inline-start), + rgb(0 0 0 / 100%) calc(100% - var(--rs-scroll-area-fade-inline-end)), + rgb(0 0 0 / 90%) calc(100% - var(--rs-scroll-area-fade-inline-end) * 0.8), + rgb(0 0 0 / 65%) calc(100% - var(--rs-scroll-area-fade-inline-end) * 0.6), + rgb(0 0 0 / 35%) calc(100% - var(--rs-scroll-area-fade-inline-end) * 0.4), + rgb(0 0 0 / 10%) calc(100% - var(--rs-scroll-area-fade-inline-end) * 0.2), + rgb(0 0 0 / 0%) 100% + ); + mask-composite: intersect; + animation-name: fade-block-start, fade-block-end, fade-inline-start, fade-inline-end; + animation-timing-function: linear; + animation-fill-mode: both; + animation-timeline: + scroll(self block), scroll(self block), scroll(self inline), scroll(self inline); + animation-range: + 0 var(--rs-scroll-area-fade-size), + calc(100% - var(--rs-scroll-area-fade-size)) 100%, + 0 var(--rs-scroll-area-fade-size), + calc(100% - var(--rs-scroll-area-fade-size)) 100%; + } + + [dir="rtl"] .--fade { + --rs-scroll-area-fade-inline-direction: to left; + } +} + .--overscroll-auto { overscroll-behavior: auto; } diff --git a/packages/reshaped/src/components/ScrollArea/ScrollArea.tsx b/packages/reshaped/src/components/ScrollArea/ScrollArea.tsx index 5aae02cd..18202892 100644 --- a/packages/reshaped/src/components/ScrollArea/ScrollArea.tsx +++ b/packages/reshaped/src/components/ScrollArea/ScrollArea.tsx @@ -104,6 +104,7 @@ const ScrollArea = forwardRef((props, ref) => { maxHeight, scrollbarDisplay = "hover", overscrollBehavior = "auto", + fade, onScroll, className, attributes, @@ -124,7 +125,8 @@ const ScrollArea = forwardRef((props, ref) => { ); const scrollableClassNames = classNames( s.scrollable, - overscrollBehavior && s[`--overscroll-${overscrollBehavior}`] + overscrollBehavior && s[`--overscroll-${overscrollBehavior}`], + fade && s["--fade"] ); const updateScroll = React.useCallback(() => { diff --git a/packages/reshaped/src/components/ScrollArea/ScrollArea.types.ts b/packages/reshaped/src/components/ScrollArea/ScrollArea.types.ts index f3e3275d..d8b293c3 100644 --- a/packages/reshaped/src/components/ScrollArea/ScrollArea.types.ts +++ b/packages/reshaped/src/components/ScrollArea/ScrollArea.types.ts @@ -12,6 +12,8 @@ export type Props = { scrollbarDisplay?: "visible" | "hover" | "hidden"; /** Control whether scroll can chain to parent scrollable containers */ overscrollBehavior?: "auto" | "contain"; + /** Display a fade mask on the sides of the area that can be scrolled towards */ + fade?: boolean; /** Callback when the scroll area is scrolled */ onScroll?: (args: Coordinates) => void; /** Height of the scroll area, literal css value or unit token multiplier */ diff --git a/packages/reshaped/src/components/ScrollArea/tests/ScrollArea.stories.tsx b/packages/reshaped/src/components/ScrollArea/tests/ScrollArea.stories.tsx index fc8e6227..5e81419c 100644 --- a/packages/reshaped/src/components/ScrollArea/tests/ScrollArea.stories.tsx +++ b/packages/reshaped/src/components/ScrollArea/tests/ScrollArea.stories.tsx @@ -154,6 +154,58 @@ export const overscrollBehavior = { ), }; +export const fade = { + name: "fade", + render: () => ( + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum + has been the industry's standard dummy text ever since the 1500s, when an unknown + printer took a galley of type and scrambled it to make a type specimen book. It has + survived not only five centuries, but also the leap into electronic typesetting, + remaining essentially unchanged. It was popularised in the 1960s with the release of + Letraset sheets containing Lorem Ipsum passages, and more recently with desktop + publishing software like Aldus PageMaker including versions of Lorem Ipsum. + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum + has been the industry's standard dummy text ever since the 1500s + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum + has been the industry's standard dummy text ever since the 1500s, when an unknown + printer took a galley of type and scrambled it to make a type specimen book. It has + survived not only five centuries, but also the leap into electronic typesetting, + remaining essentially unchanged. It was popularised in the 1960s with the release of + Letraset sheets containing Lorem Ipsum passages, and more recently with desktop + publishing software like Aldus PageMaker including versions of Lorem Ipsum. + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. + + + + + ), +}; + export const height = { name: "height, maxHeight", render: () => ( diff --git a/packages/reshaped/src/components/Table/Table.module.css b/packages/reshaped/src/components/Table/Table.module.css index 1d24c2f8..a6e0224c 100644 --- a/packages/reshaped/src/components/Table/Table.module.css +++ b/packages/reshaped/src/components/Table/Table.module.css @@ -13,16 +13,62 @@ /* stylelint-enable plugin/browser-compat */ .root { + --rs-table-fade-size: var(--rs-unit-x8); + overflow: auto; - mask-image: linear-gradient( - to right, - rgb(0 0 0 / 0%) 0, - rgb(0 0 0 / 100%) var(--rs-table-fade-start), - rgb(0 0 0 / 100%) calc(100% - var(--rs-table-fade-end)), - rgb(0 0 0 / 0%) 100% - ); - transition: var(--rs-duration-fast) var(--rs-easing-decelerate); - transition-property: --rs-table-fade-start, --rs-table-fade-end; +} + +@keyframes fade-start { + from { + --rs-table-fade-start: 0px; + } + + to { + --rs-table-fade-start: var(--rs-table-fade-size); + } +} + +@keyframes fade-end { + from { + --rs-table-fade-end: var(--rs-table-fade-size); + } + + to { + --rs-table-fade-end: 0px; + } +} + +@supports (animation-timeline: scroll(self inline)) { + .root { + --rs-table-fade-direction: to right; + + mask-image: linear-gradient( + var(--rs-table-fade-direction), + rgb(0 0 0 / 0%) 0, + rgb(0 0 0 / 10%) calc(var(--rs-table-fade-start) * 0.2), + rgb(0 0 0 / 35%) calc(var(--rs-table-fade-start) * 0.4), + rgb(0 0 0 / 65%) calc(var(--rs-table-fade-start) * 0.6), + rgb(0 0 0 / 90%) calc(var(--rs-table-fade-start) * 0.8), + rgb(0 0 0 / 100%) var(--rs-table-fade-start), + rgb(0 0 0 / 100%) calc(100% - var(--rs-table-fade-end)), + rgb(0 0 0 / 90%) calc(100% - var(--rs-table-fade-end) * 0.8), + rgb(0 0 0 / 65%) calc(100% - var(--rs-table-fade-end) * 0.6), + rgb(0 0 0 / 35%) calc(100% - var(--rs-table-fade-end) * 0.4), + rgb(0 0 0 / 10%) calc(100% - var(--rs-table-fade-end) * 0.2), + rgb(0 0 0 / 0%) 100% + ); + animation-name: fade-start, fade-end; + animation-timing-function: linear; + animation-fill-mode: both; + animation-timeline: scroll(self inline), scroll(self inline); + animation-range: + 0 var(--rs-table-fade-size), + calc(100% - var(--rs-table-fade-size)) 100%; + } + + [dir="rtl"] .root { + --rs-table-fade-direction: to left; + } } .table { @@ -98,13 +144,3 @@ padding-inline-end: calc(var(--rs-unit-x1) * var(--rs-table-p-horizontal-s)); } } - -.--fade-start, -[dir="rtl"] .--fade-end { - --rs-table-fade-start: var(--rs-unit-x4); -} - -.--fade-end, -[dir="rtl"] .--fade-start { - --rs-table-fade-end: var(--rs-unit-x4); -} diff --git a/packages/reshaped/src/components/Table/Table.tsx b/packages/reshaped/src/components/Table/Table.tsx index 308bfe3e..b0c86853 100644 --- a/packages/reshaped/src/components/Table/Table.tsx +++ b/packages/reshaped/src/components/Table/Table.tsx @@ -3,7 +3,6 @@ import React, { isValidElement } from "react"; import { classNames } from "@reshaped/utilities"; -import useFadeSide from "@/hooks/_internal/useFadeSide"; import { responsiveVariables } from "@/utilities/props"; import { resolveMixin } from "@/styles/mixin"; import type * as T from "./Table.types"; @@ -100,15 +99,11 @@ export const TableHead: React.FC = (props) => { const Table: React.FC = (props) => { const { children, border, columnBorder, className, attributes } = props; - const rootRef = React.useRef(null); - const fadeSide = useFadeSide(rootRef); const rootClassNames = classNames( s.root, className, border && s["--border-outer"], - columnBorder && s["--border-column"], - (fadeSide === "start" || fadeSide === "both") && s["--fade-start"], - (fadeSide === "end" || fadeSide === "both") && s["--fade-end"] + columnBorder && s["--border-column"] ); const [firstChild] = React.Children.toArray(children); const isElement = isValidElement(firstChild); @@ -116,7 +111,7 @@ const Table: React.FC = (props) => { const isHead = isElement && firstChild.type === TableHead; return ( -
+
{isBody || isHead ? children : {children}}
diff --git a/packages/reshaped/src/components/Table/tests/Table.stories.tsx b/packages/reshaped/src/components/Table/tests/Table.stories.tsx index 524fe7f1..0ec4a634 100644 --- a/packages/reshaped/src/components/Table/tests/Table.stories.tsx +++ b/packages/reshaped/src/components/Table/tests/Table.stories.tsx @@ -281,8 +281,8 @@ export const edgeCases = { - Column 1 - Column 2 + Column 1 + Column 2 Cell 1 diff --git a/packages/theming/src/cli/run.ts b/packages/theming/src/cli/run.ts index 71727646..4456075a 100644 --- a/packages/theming/src/cli/run.ts +++ b/packages/theming/src/cli/run.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; import process from "node:process"; -import { createRequire } from "node:module"; import chalk from "chalk"; import { Command } from "commander";