Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 16 additions & 19 deletions src/Scrollable/index.jsx → src/Scrollable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,57 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import useIsVisible from '../hooks/useIsVisibleHook';

export const CLASSNAME_SCROLL_TOP = 'pgn__scrollable-body-scroll-top';
export const CLASSNAME_SCROLL_BOTTOM = 'pgn__scrollable-body-scroll-bottom';

function Scrollable({ children, ...props }) {
export interface ScrollableProps extends React.HTMLAttributes<HTMLDivElement> {
/** Specifies the content of the `Scrollable`. */
children: React.ReactNode;
/** Additional classnames for this component. */
className?: string;
}

function Scrollable({ children, className, ...props }: ScrollableProps) {
const [isScrolledToTop, topSentinelRef] = useIsVisible();
const [isScrolledToBottom, bottomSentinelRef] = useIsVisible();
const [valueNow, setValueNow] = useState(0);
const className = classNames(
const scrollableClassName = classNames(
'pgn__scrollable-body',
props.className,
className,
{
[CLASSNAME_SCROLL_TOP]: isScrolledToTop,
[CLASSNAME_SCROLL_BOTTOM]: isScrolledToBottom,
},
);

const handleScroll = (e) => {
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const maxScrollHeight = e.currentTarget.scrollHeight - e.currentTarget.clientHeight;
setValueNow(Math.ceil((100 * e.currentTarget.scrollTop) / maxScrollHeight));
};

return (
<div
{...props}
className={className}
className={scrollableClassName}
role="scrollbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={valueNow}
aria-controls="scrollbar"
tabIndex="0"
tabIndex={0}
onScroll={handleScroll}
>
<div ref={topSentinelRef} />
<div ref={topSentinelRef as React.RefObject<HTMLDivElement>} />
<div className="pgn__scrollable-body-content">
{children}
</div>
<div ref={bottomSentinelRef} />
<div ref={bottomSentinelRef as React.RefObject<HTMLDivElement>} />
</div>
);
}

Scrollable.propTypes = {
/** Specifies the content of the `Scrollable`. */
children: PropTypes.node.isRequired,
/** Additional classnames for this component. */
className: PropTypes.string,
};

Scrollable.defaultProps = {
className: undefined,
};
Scrollable.displayName = 'Scrollable';

export default Scrollable;
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { default as ModalDialog } from './Modal/ModalDialog';
export { default as ModalLayer } from './Modal/ModalLayer';
export { default as Overlay, OverlayTrigger } from './Overlay';
export { default as Portal } from './Modal/Portal';
export { default as Scrollable } from './Scrollable';
export { default as Spinner } from './Spinner';
export { default as Stack } from './Stack';
export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
Expand Down Expand Up @@ -83,8 +84,6 @@ export { default as Layout, Col, Row } from './Layout';
export { default as Collapse } from './Collapse';
// @ts-ignore: has yet to be converted to TypeScript
export { default as Collapsible } from './Collapsible';
// @ts-ignore: has yet to be converted to TypeScript
export { default as Scrollable } from './Scrollable';
export {
default as Dropdown,
DropdownToggle,
Expand Down