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
93 changes: 45 additions & 48 deletions src/ProgressBar/index.jsx → src/ProgressBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react/require-default-props */
import React, { useCallback, useEffect } from 'react';
import ProgressBarBase from 'react-bootstrap/ProgressBar';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Annotation from '../Annotation';
import { getOffsetStyles, placeInfoAtZero } from './utils';
Expand All @@ -14,30 +14,53 @@ const VARIANTS = [
'warning',
'success',
'error',
];
] as const;

function ProgressBar(props) {
type Variant = typeof VARIANTS[number];

export interface ProgressBarAnnotatedProps {
/** Current value of progress. */
now?: number;
/** Show label that represents visual percentage. */
label?: React.ReactNode;
/** The `ProgressBar` style variant to use. */
variant?: Variant;
/** Specifies an additional `className` to add to the base element. */
className?: string;
/** Threshold current value. */
threshold?: number;
/** Specifies label for `threshold`. */
thresholdLabel?: React.ReactNode;
/** Variant for threshold value. */
thresholdVariant?: Variant;
/** Text near the progress annotation. */
progressHint?: React.ReactNode;
/** Text near the threshold annotation. */
thresholdHint?: React.ReactNode;
}

function ProgressBar(props: React.ComponentPropsWithoutRef<typeof ProgressBarBase>) {
return <ProgressBarBase {...props} />;
}

function ProgressBarAnnotated({
now,
label,
variant,
variant = PROGRESS_DEFAULT_VARIANT,
threshold,
thresholdLabel,
thresholdVariant,
thresholdVariant = THRESHOLD_DEFAULT_VARIANT,
progressHint,
thresholdHint,
...props
}) {
const progressInfoRef = React.useRef();
const thresholdInfoRef = React.useRef();
}: ProgressBarAnnotatedProps) {
const progressInfoRef = React.useRef<HTMLDivElement>(null);
const thresholdInfoRef = React.useRef<HTMLDivElement>(null);
const thresholdPercent = (threshold || 0) - (now || 0);
const isProgressHintAfter = now < HINT_SWAP_PERCENT;
const isThresholdHintAfter = threshold < HINT_SWAP_PERCENT;
const progressColor = VARIANTS.includes(variant) ? variant : PROGRESS_DEFAULT_VARIANT;
const thresholdColor = VARIANTS.includes(thresholdVariant) ? thresholdVariant : THRESHOLD_DEFAULT_VARIANT;
const isProgressHintAfter = (now as number) < HINT_SWAP_PERCENT;
const isThresholdHintAfter = (threshold as number) < HINT_SWAP_PERCENT;
const progressColor = VARIANTS.includes(variant!) ? variant! : PROGRESS_DEFAULT_VARIANT;
const thresholdColor = VARIANTS.includes(thresholdVariant!) ? thresholdVariant! : THRESHOLD_DEFAULT_VARIANT;
const direction = window.getComputedStyle(document.body).getPropertyValue('direction');

const positionAnnotations = useCallback(() => {
Expand All @@ -51,11 +74,11 @@ function ProgressBarAnnotated({
positionAnnotations();
});
const progressInfoEl = progressInfoRef.current;
observer.observe(progressInfoEl);
return () => progressInfoEl && observer.unobserve(progressInfoEl);
observer.observe(progressInfoEl!);
return () => { if (progressInfoEl) { observer.unobserve(progressInfoEl); } };
}, [positionAnnotations]);

const getHint = (text) => (
const getHint = (text: React.ReactNode) => (
<span className="pgn__progress-hint" data-testid="progress-hint">
{text}
</span>
Expand Down Expand Up @@ -114,38 +137,12 @@ function ProgressBarAnnotated({
);
}

ProgressBarAnnotated.propTypes = {
/** Current value of progress. */
now: PropTypes.number,
/** Show label that represents visual percentage. */
label: PropTypes.node,
/** The `ProgressBar` style variant to use. */
variant: PropTypes.oneOf(VARIANTS),
/** Specifies an additional `className` to add to the base element. */
className: PropTypes.string,
/** Threshold current value. */
threshold: PropTypes.number,
/** Specifies label for `threshold`. */
thresholdLabel: PropTypes.node,
/** Variant for threshold value. */
thresholdVariant: PropTypes.oneOf(VARIANTS),
/** Text near the progress annotation. */
progressHint: PropTypes.node,
/** Text near the threshold annotation. */
thresholdHint: PropTypes.node,
};
interface ProgressBarComponent {
(props: React.ComponentPropsWithoutRef<typeof ProgressBarBase>): React.JSX.Element;
Annotated: React.FC<ProgressBarAnnotatedProps>;
}

ProgressBarAnnotated.defaultProps = {
now: undefined,
label: undefined,
variant: PROGRESS_DEFAULT_VARIANT,
className: undefined,
threshold: undefined,
thresholdLabel: undefined,
thresholdVariant: THRESHOLD_DEFAULT_VARIANT,
progressHint: undefined,
thresholdHint: undefined,
};
const ProgressBarWithAnnotated = ProgressBar as unknown as ProgressBarComponent;
ProgressBarWithAnnotated.Annotated = ProgressBarAnnotated;

ProgressBar.Annotated = ProgressBarAnnotated;
export default ProgressBar;
export default ProgressBarWithAnnotated;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import renderer from 'react-test-renderer';

import ProgressBar, { ANNOTATION_CLASS } from '..';
import ProgressBar, { ANNOTATION_CLASS, ProgressBarAnnotatedProps } from '..';

const ref = {
current: {
Expand All @@ -24,9 +24,9 @@ const ref = {
},
],
},
};
} as any;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've got two files where this type is used. Could we get a type annotation here? Also noticed that this is a bare object but should be created via function (see my other comment on a similar line)


function ProgressBarElement(props) {
function ProgressBarElement(props: ProgressBarAnnotatedProps) {
return (
<ProgressBar.Annotated
now={20}
Expand Down Expand Up @@ -99,13 +99,13 @@ describe('<ProgressBar.Annotated />', () => {
expect(progressHints[1].textContent).toEqual('');
});
it('should apply styles based on direction for threshold', () => {
window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => 'rtl' });
window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => 'rtl' }) as any;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use jest.spyOn with the mock implementation instead of assigning this property and casting it to any?

const { container } = render(<ProgressBarElement />);
const progressInfo = container.querySelector('.pgn__progress-info');
const computedStyles = window.getComputedStyle(progressInfo);
const computedStyles = window.getComputedStyle(progressInfo!);

expect(computedStyles.getPropertyValue('directory')).toBe('rtl');
window.getComputedStyle.mockRestore();
(window.getComputedStyle as jest.Mock).mockRestore();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value of spyOn should be the right type.

});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ref = {
},
],
},
};
} as any;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This structure is used in two places but is cast to any. Can we find a way to type it properly so it's used correctly in both places?

Other concern-- this isn't part of your PR but seems like something we should clean up while we're here:

The object is created once but is used in multiple tests. That means it could change between tests and any subtle errors from this would be difficult to detect. Could you create a function which returns this object instead and use that for each test?


describe('utils', () => {
describe('placeInfoAtZero', () => {
Expand Down Expand Up @@ -86,8 +86,8 @@ describe('utils', () => {
expect(actualMarginRight).toEqual(expectedHorizontalMargin);
});
it('returns false if reference is wrong', () => {
const wrongRef1 = {};
const wrongRef2 = { current: {} };
const wrongRef1 = {} as any;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing these are any because we're literally testing garbage data? OK if so.

const wrongRef2 = { current: {} } as any;
expect(placeInfoAtZero(wrongRef1)).toEqual(false);
expect(placeInfoAtZero(wrongRef2)).toEqual(false);
});
Expand Down
18 changes: 10 additions & 8 deletions src/ProgressBar/utils.js → src/ProgressBar/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

/**
* Gets the current annotation styles and calculates the left margin so
* that the annotation pointer indicates on zero of the ProgressBar.
Expand All @@ -8,11 +10,11 @@
* @param {string} annotationClass is used to identify the annotation element
*/
export const placeInfoAtZero = (
ref,
direction = 'ltr',
annotationOnly = true,
annotationClass = 'pgn__annotation',
) => {
ref: React.RefObject<HTMLElement | null>,
direction: string = 'ltr',
annotationOnly: boolean = true,
annotationClass: string = 'pgn__annotation',
): boolean => {
if (!ref.current || !ref.current.style) { return false; }
const { children } = ref.current;
let horizontalMargin = 0.0;
Expand All @@ -38,6 +40,6 @@ export const placeInfoAtZero = (
* depending on the direction.
*/
export const getOffsetStyles = (
value,
direction,
) => (direction === 'rtl' ? { right: `${value}%` } : { left: `${value}%` });
value: number | undefined,
direction: string,
): { left: string } | { right: string } => (direction === 'rtl' ? { right: `${value}%` } : { left: `${value}%` });
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ export {
} from './Pagination';
// @ts-ignore: has yet to be converted to TypeScript
export { default as Popover, PopoverTitle, PopoverContent } from './Popover';
// @ts-ignore: has yet to be converted to TypeScript
export { default as ProgressBar } from './ProgressBar';
export type { ProgressBarAnnotatedProps } from './ProgressBar';
// @ts-ignore: has yet to be converted to TypeScript
export { default as ProductTour } from './ProductTour';
// @ts-ignore: has yet to be converted to TypeScript
Expand Down