diff --git a/package.json b/package.json index e28b2526..2a40a8db 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@storybook/addon-essentials": "^6.3.11", "@storybook/addon-links": "^6.3.11", "@storybook/addon-storysource": "^6.5.12", + "@storybook/api": "^6.3.11", "@storybook/react": "^6.3.11", "@vitejs/plugin-react": "^4.0.3", "antd": "^3.6.5", diff --git a/src/components/Scrollbar.tsx b/src/components/Scrollbar.tsx index ae18816b..b94de093 100644 --- a/src/components/Scrollbar.tsx +++ b/src/components/Scrollbar.tsx @@ -3,72 +3,65 @@ import Measure from 'react-measure'; export interface ScrollbarProperties { /** - * A number which represents the maximum scroll position + * E.g. 0 */ minScrollPosition: number; /** - * A number which represents the minimum scroll position + * E.g. 100 */ maxScrollPosition: number; /** - * The number of lines equivalent to one page. + * E.g. 10 */ pageSize: number; /** - * The initial position of the scroll thumb + * E.g. W/ the previous examples, can have values between 0 and 90 */ - scrollPosition?: number; + initialScrollPosition?: number; /** * Can be both `Direction.HORIZONTAL`(the default value) or `Direction.VERTICAL` */ direction?: Direction; - - /** - * By default Semantic ui hides the increase/decrease arrow buttons of the scrollbars on Webkit based browsers. So it default to false. - * This should be set to true if the scrollbar needs to display the arrow buttons. - */ - hasArrows?: boolean; /** - * This handler is called when the scrollbar is scrolled. - * It receives as parameter the current scrollPosition * This handler can be used for example to synchronize the scrollbar position with a virtual scroll of another component * - * @param scrollPosition - * @returns + * E.g. W/ the previous examples, the scrollPosition can be between 0 and 90 */ onScroll?: (scrollPosition: number) => void; - /** - * If the configuration of the scrollbar doesn't require scrolling (i.e. the min-max interval is smaller than the pageSize) - * the Scrollbar component doesn't display nothing. - * - * This handler can be used to detect when the scrollbar is not required to be displayed - * - * @param isScrollbarVisible - * @returns - */ - onVisibilityChange?: (isScrollbarVisible: boolean) => void; + /* + * The scrollbar component already measures its content. + * So the parent component should use this handler + * if it needs to detect scrollbar size changes (e.g. when the component becomes invisible + * because the current configuration needs no scrollbar) + */ + onResize?: (scrollbarContentRect: boolean) => void; } export enum Direction { - HORIZONTAL, - VERTICAL + HORIZONTAL = "horizontal", + VERTICAL = "vertical" } -export const SCROLLBAR_SIZE = 10; const ROUND_FACTOR = 4; +/** + * If the configuration of the scrollbar doesn't require scrolling + * (i.e. the min-max interval is smaller than the pageSize) + * the Scrollbar component doesn't display nothing. + * + * @author Daniela Buzatu + */ export class Scrollbar extends React.Component { static defaultProps = { scrollPosition: 0, - direction: Direction.HORIZONTAL, - hasArrows: false + direction: Direction.HORIZONTAL } _outterDiv: HTMLDivElement; @@ -101,21 +94,11 @@ export class Scrollbar extends React.Component): void { - if (nextProps.onVisibilityChange && this.isScrollbarNeeded(nextProps) != this.isScrollbarNeeded(this.props)) { - nextProps.onVisibilityChange(this.isScrollbarNeeded(nextProps)); - } - if (nextProps.minScrollPosition != this.props.minScrollPosition || nextProps.maxScrollPosition != this.props.maxScrollPosition || nextProps.pageSize != this.props.pageSize || - nextProps.scrollPosition != this.props.scrollPosition) { + nextProps.initialScrollPosition != this.props.initialScrollPosition) { this.setScrollPosition(nextProps); } } @@ -128,7 +111,7 @@ export class Scrollbar extends React.Component { + if (value == 0) { + return value; + } + let roundFactor = Math.pow(10, ROUND_FACTOR); + return Math.round(value * roundFactor) / roundFactor; } render(): React.ReactNode { @@ -179,13 +169,13 @@ export class Scrollbar extends React.Component {({ measureRef }) => { return ( this.isScrollbarNeeded(this.props) ?
{ measureRef(node); this._outterDiv = node; @@ -202,12 +192,4 @@ export class Scrollbar extends React.Component } -} - -const roundDoubleToDecimals = (value: number) => { - if (value == 0) { - return value; - } - let roundFactor = Math.pow(10, ROUND_FACTOR); - return Math.round(value * roundFactor) / roundFactor; } \ No newline at end of file diff --git a/src/resources/horizontal-decrement-arrow.png b/src/resources/horizontal-decrement-arrow.png deleted file mode 100644 index 791c396d..00000000 Binary files a/src/resources/horizontal-decrement-arrow.png and /dev/null differ diff --git a/src/resources/horizontal-increment-arrow.png b/src/resources/horizontal-increment-arrow.png deleted file mode 100644 index 22d86b39..00000000 Binary files a/src/resources/horizontal-increment-arrow.png and /dev/null differ diff --git a/src/resources/vertical-decrement-arrow.png b/src/resources/vertical-decrement-arrow.png deleted file mode 100644 index df271b65..00000000 Binary files a/src/resources/vertical-decrement-arrow.png and /dev/null differ diff --git a/src/resources/vertical-increment-arrow.png b/src/resources/vertical-increment-arrow.png deleted file mode 100644 index 6d486266..00000000 Binary files a/src/resources/vertical-increment-arrow.png and /dev/null differ diff --git a/src/stories/basic/Basic.stories.tsx b/src/stories/basic/Basic.stories.tsx index 17e95da3..22f10750 100644 --- a/src/stories/basic/Basic.stories.tsx +++ b/src/stories/basic/Basic.stories.tsx @@ -101,33 +101,15 @@ AlternativeRowColoring.parameters = { }; export const HorizontalScroll = () => { -const tasks: Item[] = [ - ...someTasks, // Tasks that are outside the display interval - { key: 11, row: 4, title: 'Task GW1', start: d('2018-09-19 7:00'), end: d('2018-09-19 8:00') }, - { key: 12, row: 4, title: 'Task GW2', start: d('2018-09-19 17:00'), end: d('2018-09-19 19:00') }, - { key: 14, row: 1, title: 'Task GW3', start: d('2018-09-21 12:00'), end: d('2018-09-21 14:00') }, - { key: 15, row: 1, title: 'Task GW4', start: d('2018-09-21 15:00'), end: d('2018-09-21 17:00') }, - { key: 16, row: 1, title: 'Task GW5', start: d('2018-09-19 12:00'), end: d('2018-09-19 14:00') }, - { key: 17, row: 1, title: 'Task GW6', start: d('2018-09-19 9:00'), end: d('2018-09-19 12:00') }, - ]; - return ( - <> - {/* This is a trivial example to illustrate how Timeline "glues" to its "flex" parent. Notes: */} - {/* 1/ In other stories we don't have this, because we have a Storybook decorator that wraps w/ a div + CSS class. */} - {/* 2/ You'll probably have a better flex-box layout, i.e. not hardcoded. 3/ Use CSS classes and not styles. */} -
- - Title} - cell={({rowIndex}) => {rowIndex < someHumanResources.length ? someHumanResources[rowIndex].title : ""}}/> - } - /> -
- + + Title} + cell={({rowIndex}) => {rowIndex < someHumanResources.length ? someHumanResources[rowIndex].title : ""}}/> + }/> ); }; diff --git a/src/stories/sampleData.ts b/src/stories/sampleData.ts index 5779d5f9..2436daea 100644 --- a/src/stories/sampleData.ts +++ b/src/stories/sampleData.ts @@ -40,7 +40,14 @@ export const someTasks: Item[] = [ {key: 8, row: 2, title: 'Task MD5', start: d('2018-09-20 20:00'), end: d('2018-09-20 21:00')}, {key: 9, row: 2, title: 'Task MD2', start: d('2018-09-20 5:00'), end: d('2018-09-20 7:00')}, {key: 10, row: 2, title: 'Task MD3', start: d('2018-09-20 13:00'), end: d('2018-09-20 14:00')}, - {key: 11, row: 2, title: 'Task MD3', start: d('2018-09-20 22:00'), end: d('2018-09-20 24:00')} + {key: 11, row: 2, title: 'Task MD3', start: d('2018-09-20 22:00'), end: d('2018-09-20 24:00')}, + // Tasks that are outside the display interval + { key: 12, row: 4, title: 'Task GW1', start: d('2018-09-19 7:00'), end: d('2018-09-19 8:00') }, + { key: 13, row: 4, title: 'Task GW2', start: d('2018-09-19 17:00'), end: d('2018-09-19 19:00') }, + { key: 14, row: 1, title: 'Task GW3', start: d('2018-09-21 12:00'), end: d('2018-09-21 14:00') }, + { key: 15, row: 1, title: 'Task GW4', start: d('2018-09-21 15:00'), end: d('2018-09-21 17:00') }, + { key: 16, row: 1, title: 'Task GW5', start: d('2018-09-19 12:00'), end: d('2018-09-19 14:00') }, + { key: 17, row: 1, title: 'Task GW6', start: d('2018-09-19 9:00'), end: d('2018-09-19 12:00') } ]; export const manyHumanResources: Employee[] = [...someHumanResources, { id: 4, title: 'George Walsh', job: 'Developer' }, diff --git a/src/stories/scrollbar/Scrollbar.stories.tsx b/src/stories/scrollbar/Scrollbar.stories.tsx index 3a9b5315..01d29ba5 100644 --- a/src/stories/scrollbar/Scrollbar.stories.tsx +++ b/src/stories/scrollbar/Scrollbar.stories.tsx @@ -3,12 +3,69 @@ import { Direction, Scrollbar } from "../../components/Scrollbar"; import { useRef, useState } from "react"; import { Alert } from "antd"; import Measure from 'react-measure'; +import { createTestids } from "@famiprog-foundation/tests-are-demo"; +import { useArgs } from '@storybook/client-api'; + export default { - title: 'Components/Scrollbar' + title: 'Components/Scrollbar', + component: Scrollbar, + includeStories: /^[A-Z]/, + parameters: { + controls: { + disable: false + } + }, + args: { + pageSize: 10, + minScrollPosition: 0, + maxScrollPosition: 100, + initialScrollPosition: 0, + scrollPosition: 0 + }, + argTypes: { + onScroll: { + table: { + disable: true, + } + }, + onVisibilityChange: { + table: { + disable: true, + } + } + } }; -export const HorizontalScrollBar = () => { +export const scrollbarTestIds = createTestids('ScrollbarStory', { + scrollbar: '' +}); + +export const HorizontalScrollBar = (args) => { + const [{}, updateArgs] = useArgs(); + return( +
+ updateArgs({scrollPosition: scrollPosition})} {...args}/> +
+ ); +} +HorizontalScrollBar.args = { + direction: Direction.HORIZONTAL +} + +export const VerticalScrollBar = (args) => { + const [{}, updateArgs] = useArgs(); + return ( +
+ updateArgs({scrollPosition: scrollPosition})} {...args}/> +
+ ); +} +VerticalScrollBar.args = { + direction: Direction.VERTICAL +} + +export const HorizontalScrollBarScrollingADiv = () => { const divContentWidth1 = 700; const divContentWidth2 = 700; const totalWidth = 800; @@ -62,14 +119,20 @@ export const HorizontalScrollBar = () => { div1.current.scrollLeft = scrollPosition}/> div2.current.scrollLeft = scrollPosition} hasArrows={true}/> + onScroll={(scrollPosition) => div2.current.scrollLeft = scrollPosition}/>
); } -export const VerticalScrollBar = () => { +HorizontalScrollBarScrollingADiv.parameters = { + controls: { + disable: true + } +}; + +export const VerticalScrollBarScrollingADiv = () => { const divContentHeight1 = 500; const divContentHeight2 = 500; const totalHeight = 500; @@ -124,10 +187,16 @@ export const VerticalScrollBar = () => { div1.current.scrollTop = scrollPosition}/> div2.current.scrollTop = scrollPosition} hasArrows={true}/> + onScroll={(scrollPosition) => div2.current.scrollTop = scrollPosition}/> ); -} \ No newline at end of file +} + +VerticalScrollBarScrollingADiv.parameters = { + controls: { + disable: true + } +}; \ No newline at end of file diff --git a/src/style.css b/src/style.css index 81be6b45..9239cc93 100644 --- a/src/style.css +++ b/src/style.css @@ -76,28 +76,8 @@ height: 1px; } -.rct9k-horizontal-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:horizontal:decrement { - background: url(resources/horizontal-decrement-arrow.png), #acacac; - background-repeat: no-repeat, repeat-x; - background-position: 3px 2px, 0 0; -} - -.rct9k-horizontal-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:horizontal:decrement:hover { - background: url(resources/horizontal-decrement-arrow.png), #919699; - background-repeat: no-repeat, repeat-x; - background-position: 3px 2px, 0 0; -} - -.rct9k-horizontal-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:horizontal:increment { - background: url(resources/horizontal-increment-arrow.png), #acacac; - background-repeat: no-repeat, repeat-x; - background-position: 5px 2px, 0 0; -} - -.rct9k-horizontal-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:horizontal:increment:hover { - background: url(resources/horizontal-increment-arrow.png), #919699; - background-repeat: no-repeat, repeat-x; - background-position: 5px 2px, 0 0; +.rct9k-vertical-scrollbar-inner { + width: 1px; } .rct9k-vertical-scrollbar-outter { @@ -106,32 +86,52 @@ height: 100%; } -.rct9k-vertical-scrollbar-inner { - width: 1px; +/* WEBKIT scrollbar buttons */ +.rct9k-horizontal-scrollbar-outter::-webkit-scrollbar-button:single-button, +.rct9k-vertical-scrollbar-outter::-webkit-scrollbar-button:single-button { + background-color: rgb(229, 229, 229); + + display: block; + background-size: 8px; + background-repeat: no-repeat; + margin: 1px; +} + +.rct9k-horizontal-scrollbar-outter::-webkit-scrollbar-button:single-button:hover, +.rct9k-vertical-scrollbar-outter::-webkit-scrollbar-button:single-button:hover { + background-color: rgb(148, 154, 157); } -.rct9k-vertical-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:vertical:decrement { - background: url(resources/vertical-decrement-arrow.png), #acacac; - background-repeat: no-repeat, repeat-x; - background-position: 2px 4px, 0 0; +/* Left */ +.rct9k-horizontal-scrollbar-outter::-webkit-scrollbar-button:single-button:horizontal:decrement { + height: 10px; + width: 13px; + background-position: center right 1px; + background-image: url("data:image/svg+xml;utf8,"); } -.rct9k-vertical-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:vertical:decrement:hover { - background: url(resources/vertical-decrement-arrow.png), #919699; - background-repeat: no-repeat, repeat-x; - background-position: 2px 4px, 0 0; +/* Right */ +.rct9k-horizontal-scrollbar-outter::-webkit-scrollbar-button:single-button:horizontal:increment { + height: 10px; + width: 13px; + background-position: center left 5px; + background-image: url("data:image/svg+xml;utf8,"); } -.rct9k-vertical-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:vertical:increment { - background: url(resources/vertical-increment-arrow.png), #acacac; - background-repeat: no-repeat, repeat-x; - background-position: 2px 5px, 0 0; +/* Up */ +.rct9k-vertical-scrollbar-outter::-webkit-scrollbar-button:single-button:vertical:decrement { + height: 13px; + width: 10px; + background-position: center bottom 1px; + background-image: url("data:image/svg+xml;utf8,"); } -.rct9k-vertical-scrollbar-outter.rct9k-scrollbar-with-arrows::-webkit-scrollbar-button:vertical:increment:hover { - background: url(resources/vertical-increment-arrow.png), #919699; - background-repeat: no-repeat, repeat-x; - background-position: 2px 5px, 0 0; +/* Down */ +.rct9k-vertical-scrollbar-outter::-webkit-scrollbar-button:single-button:vertical:increment { + height: 13px; + width: 10px; + background-position: center top 5px; + background-image: url("data:image/svg+xml;utf8,"); } .rct9k-timebar-item:first-child { diff --git a/src/timeline.js b/src/timeline.js index 10657e78..dc7d6359 100644 --- a/src/timeline.js +++ b/src/timeline.js @@ -47,7 +47,7 @@ import {SelectionHolder} from './utils/SelectionHolder'; import {IGanttAction} from './types'; import {ContextMenu} from './components/ContextMenu/ContextMenu'; import moment from 'moment'; -import {SCROLLBAR_SIZE, Scrollbar} from './components/Scrollbar'; +import {Scrollbar} from './components/Scrollbar'; const testids = createTestids('Timeline', { menuButton: '', @@ -65,8 +65,6 @@ const testids = createTestids('Timeline', { export const timelineTestids = testids; const EMPTY_GROUP_KEY = 'empty-group'; -// This was added by bogdan. From my understanding it reprezents the table vertical scrollbar width -// If we don't take in consideration this, a horizontal scrollbar appears export const TABLE_OFFSET = 15; export const DEFAULT_ITEM_HEIGHT = 40; export const DEFAULT_ROW_CLASS = 'rct9k-row'; @@ -567,7 +565,7 @@ export default class Timeline extends React.Component { openedContextMenuTime: undefined, startDate: this.props.startDate, endDate: this.props.endDate, - hasHorizontalScrollbar: false, + horizontalScrollbarHeight: 0, touchPositionX: undefined }; @@ -1698,7 +1696,7 @@ export default class Timeline extends React.Component { var tableRowHeight = this.rowHeight({index}); let group = _.find(this.state.groups, g => g.id == index); if (group.rowHeight && group.key.startsWith(EMPTY_GROUP_KEY)) { - tableRowHeight = Math.round(tableRowHeight) + (this.state.hasHorizontalScrollbar ? SCROLLBAR_SIZE : 0) - 2; + tableRowHeight = Math.round(tableRowHeight) + this.state.horizontalScrollbarHeight - 2; } return tableRowHeight; } @@ -2097,7 +2095,7 @@ export default class Timeline extends React.Component { {markers.map(m => ( this.state.gridWidth} - height={bodyHeight - (this.state.hasHorizontalScrollbar ? SCROLLBAR_SIZE : 0)} + height={bodyHeight - this.state.horizontalScrollbarHeight} rowHeight={this.rowHeight} rowCount={this.state.groups.length} columnCount={1} @@ -2121,18 +2119,18 @@ export default class Timeline extends React.Component { onScroll={this.handleScrollGantt} /> (this._scrollbar = node)} minScrollPosition={this.getMinDate().valueOf()} maxScrollPosition={this.getMaxDate().valueOf()} - scrollPosition={this.getStartDate().valueOf()} + initialScrollPosition={this.getStartDate().valueOf()} pageSize={this.getEndDate().valueOf() - this.getStartDate().valueOf()} - hasArrows={true} onScroll={scrollPosition => { this.onHorizontalScroll(scrollPosition); }} - onVisibilityChange={isScrollbarVisible => - this.setState({hasHorizontalScrollbar: isScrollbarVisible}) + onResize={contentRect => + this.setState({horizontalScrollbarHeight: contentRect.bounds ? contentRect.bounds.height : 0}) } - ref={node => (this._scrollbar = node)}> + /> {this.renderContextMenu()} {backgroundLayer && React.cloneElement(backgroundLayer, { @@ -2153,7 +2151,7 @@ export default class Timeline extends React.Component { ), width: this.state.gridWidth - ganttVerticalScrollbarWidth, leftOffset: 0, - height: bodyHeight - (this.state.hasHorizontalScrollbar ? SCROLLBAR_SIZE : 0), + height: bodyHeight - this.state.horizontalScrollbarHeight, topOffset: timebarHeight, verticalGridLines: this.state.verticalGridLines })} diff --git a/yarn.lock b/yarn.lock index b621f89f..636654a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1965,7 +1965,7 @@ global "^4.4.0" regenerator-runtime "^0.13.7" -"@storybook/api@6.5.16": +"@storybook/api@6.5.16", "@storybook/api@^6.3.11": version "6.5.16" resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.5.16.tgz#897915b76de05587fd702951d5d836f708043662" integrity sha512-HOsuT8iomqeTMQJrRx5U8nsC7lJTwRr1DhdD0SzlqL4c80S/7uuCy4IZvOt4sYQjOzW5fOo/kamcoBXyLproTA== @@ -2884,19 +2884,10 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "18.2.17" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.17.tgz#baa565b17ddb649c2dac85b5eaf9e9a1fe0f3b4e" - integrity sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@^17": - version "17.0.62" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.62.tgz#2efe8ddf8533500ec44b1334dd1a97caa2f860e3" - integrity sha512-eANCyz9DG8p/Vdhr0ZKST8JV12PhH2ACCDYlFw6DIO+D+ca+uP4jtEDEpVqXZrh/uZdXQGwk7whJa3ah5DtyLw== +"@types/react@*", "@types/react@^17", "@types/react@^17.0.0": + version "17.0.65" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.65.tgz#95f6a2ab61145ffb69129d07982d047f9e0870cd" + integrity sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ== dependencies: "@types/prop-types" "*" "@types/scheduler" "*"