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

Components: Introduce a basic ProgressBar component #53030

Merged
merged 26 commits into from Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fec0532
Components: Introduce a basic ProgressBar
tyxla Jul 26, 2023
5452f8a
Add a CHANGELOG entry
tyxla Jul 27, 2023
cafb1d8
Expose as a private API
tyxla Jul 27, 2023
43d2dc2
Add missing className in type
tyxla Jul 27, 2023
f1d5c80
Render progressbar element invisibly
tyxla Jul 27, 2023
c6273f4
Update appearance
jameskoster Jul 27, 2023
b28b7b5
Mark the component story as experimental
tyxla Jul 28, 2023
ac38177
Add an experimental alert in the component README
tyxla Jul 28, 2023
a4d5de9
Remove usage from README
tyxla Jul 28, 2023
5065164
Remove README from manifest
tyxla Jul 28, 2023
bbfbfcc
Add border radius to indicator as well
tyxla Jul 28, 2023
166ae8d
Refactor from SCSS to Emotion
tyxla Jul 28, 2023
e00dc4f
Remove color props
tyxla Jul 28, 2023
05245ac
Update tests
tyxla Jul 28, 2023
d6d7044
Add an aria-label to the underlying progress element
tyxla Jul 28, 2023
fef81d2
Update snapshots
tyxla Jul 28, 2023
3b2b625
Intentionally ignore ProgressBar README from docs manifest
tyxla Jul 28, 2023
2bda78e
Use the components gray color variable
tyxla Jul 31, 2023
453f5d1
Add TODO for using the :indeterminate pseudo-class
tyxla Jul 31, 2023
7b63099
Remove default className
tyxla Jul 31, 2023
4c1c86e
Add a max-width
tyxla Jul 31, 2023
2e16c12
Update snapshots
tyxla Jul 31, 2023
f772032
Add support for ID and ref
tyxla Jul 31, 2023
664a4de
Fix docs
tyxla Jul 31, 2023
2df2d48
Use emotion for indeterminate and value-based styling
tyxla Aug 1, 2023
f0c3be6
Remove ID, pass rest props down to the progress element
tyxla Aug 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/tool/manifest.js
Expand Up @@ -15,6 +15,7 @@ const componentPaths = glob( 'packages/components/src/*/**/README.md', {
'packages/components/src/theme/README.md',
'packages/components/src/view/README.md',
'packages/components/src/dropdown-menu-v2/README.md',
'packages/components/src/progress-bar/README.md',
],
} );
const packagePaths = glob( 'packages/*/package.json' )
Expand Down
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Feature

- Add a new `ProgressBar` component. ([#53030](https://github.com/WordPress/gutenberg/pull/53030)).

### Enhancements

- `ColorPalette`, `BorderControl`: Don't hyphenate hex value in `aria-label` ([#52932](https://github.com/WordPress/gutenberg/pull/52932)).
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/private-apis.ts
Expand Up @@ -8,6 +8,7 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri
*/
import { default as CustomSelectControl } from './custom-select-control';
import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } from './popover/utils';
import { default as ProgressBar } from './progress-bar';
import { createPrivateSlotFill } from './slot-fill';
import {
DropdownMenu as DropdownMenuV2,
Expand Down Expand Up @@ -45,4 +46,5 @@ lock( privateApis, {
DropdownMenuSeparatorV2,
DropdownSubMenuV2,
DropdownSubMenuTriggerV2,
ProgressBar,
} );
30 changes: 30 additions & 0 deletions packages/components/src/progress-bar/README.md
@@ -0,0 +1,30 @@
# ProgressBar

<div class="callout callout-alert">
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>

A simple horizontal progress bar component.
tyxla marked this conversation as resolved.
Show resolved Hide resolved

Supports two modes: determinate and indeterminate. A progress bar is determinate when a specific progress value has been specified (from 0 to 100), and indeterminate when a value hasn't been specified.

### Props

The component accepts the following props:

#### `value`: `number`

The progress value, a number from 0 to 100.
If a `value` is not specified, the progress bar will be considered indeterminate.

- Required: No

##### `className`: `string`

A CSS class to apply to the underlying `div` element, serving as a progress bar track.

- Required: No

#### Inherited props

Any additional props will be passed the underlying `<progress/>` element.
45 changes: 45 additions & 0 deletions packages/components/src/progress-bar/index.tsx
@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import type { ForwardedRef } from 'react';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { forwardRef } from '@wordpress/element';

/**
* Internal dependencies
*/
import * as ProgressBarStyled from './styles';
import type { ProgressBarProps } from './types';
import type { WordPressComponentProps } from '../ui/context';

function UnforwardedProgressBar(
props: WordPressComponentProps< ProgressBarProps, 'progress', false >,
ref: ForwardedRef< HTMLProgressElement >
) {
const { className, value, ...progressProps } = props;
const isIndeterminate = ! Number.isFinite( value );

return (
<ProgressBarStyled.Track className={ className }>
<ProgressBarStyled.Indicator
isIndeterminate={ isIndeterminate }
value={ value }
/>
<ProgressBarStyled.ProgressElement
max={ 100 }
value={ value }
aria-label={ __( 'Loading …' ) }
ref={ ref }
{ ...progressProps }
/>
</ProgressBarStyled.Track>
);
}

export const ProgressBar = forwardRef( UnforwardedProgressBar );

export default ProgressBar;
33 changes: 33 additions & 0 deletions packages/components/src/progress-bar/stories/index.tsx
@@ -0,0 +1,33 @@
/**
* External dependencies
*/
import type { ComponentMeta, ComponentStory } from '@storybook/react';

/**
* Internal dependencies
*/
import { ProgressBar } from '..';

const meta: ComponentMeta< typeof ProgressBar > = {
component: ProgressBar,
title: 'Components (Experimental)/ProgressBar',
argTypes: {
value: { control: { type: 'number', min: 0, max: 100, step: 1 } },
},
parameters: {
controls: {
expanded: true,
},
docs: { source: { state: 'open' } },
},
};
export default meta;

const Template: ComponentStory< typeof ProgressBar > = ( { ...args } ) => {
return <ProgressBar { ...args } />;
};

export const Default: ComponentStory< typeof ProgressBar > = Template.bind(
{}
);
Default.args = {};
67 changes: 67 additions & 0 deletions packages/components/src/progress-bar/styles.ts
@@ -0,0 +1,67 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';
import { css, keyframes } from '@emotion/react';

/**
* Internal dependencies
*/
import { COLORS, CONFIG } from '../utils';

const animateProgressBar = keyframes( {
'0%': {
left: '-50%',
},
'100%': {
left: '100%',
},
} );

// Width of the indicator for the indeterminate progress bar
export const INDETERMINATE_TRACK_WIDTH = 50;

export const Track = styled.div`
position: relative;
overflow: hidden;
width: 100%;
max-width: 160px;
height: ${ CONFIG.borderWidthFocus };
background-color: var(
--wp-components-color-gray-100,
${ COLORS.gray[ 100 ] }
);
border-radius: ${ CONFIG.radiusBlockUi };
`;

export const Indicator = styled.div< {
isIndeterminate: boolean;
value?: number;
} >`
display: inline-block;
position: absolute;
top: 0;
height: 100%;
border-radius: ${ CONFIG.radiusBlockUi };
background-color: ${ COLORS.ui.theme };

${ ( { isIndeterminate, value } ) =>
isIndeterminate
? css( {
animationDuration: '1.5s',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
animationName: animateProgressBar,
width: `${ INDETERMINATE_TRACK_WIDTH }%`,
} )
: css( { width: `${ value }%` } ) };
`;

export const ProgressElement = styled.progress`
position: absolute;
top: 0;
left: 0;
opacity: 0;
width: 100%;
height: 100%;
`;
79 changes: 79 additions & 0 deletions packages/components/src/progress-bar/test/index.tsx
@@ -0,0 +1,79 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';

/**
* Internal dependencies
*/
import { ProgressBar } from '..';
import { INDETERMINATE_TRACK_WIDTH } from '../styles';

describe( 'ProgressBar', () => {
it( 'should render an indeterminate semantic progress bar element', () => {
render( <ProgressBar /> );

const progressBar = screen.getByRole( 'progressbar' );

expect( progressBar ).toBeInTheDocument();
expect( progressBar ).not.toBeVisible();
expect( progressBar ).not.toHaveValue();
} );

it( 'should render a determinate semantic progress bar element', () => {
render( <ProgressBar value={ 55 } /> );

const progressBar = screen.getByRole( 'progressbar' );

expect( progressBar ).toBeInTheDocument();
expect( progressBar ).not.toBeVisible();
expect( progressBar ).toHaveValue( 55 );
} );

it( 'should use `INDETERMINATE_TRACK_WIDTH`% as track width for indeterminate progress bar', () => {
const { container } = render( <ProgressBar /> );

/**
* We're intentionally not using an accessible selector, because
* the track is an intentionally non-interactive presentation element.
*/
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const indicator = container.firstChild?.firstChild;

expect( indicator ).toHaveStyle( {
width: `${ INDETERMINATE_TRACK_WIDTH }%`,
} );
} );

it( 'should use `value`% as width for determinate progress bar', () => {
const { container } = render( <ProgressBar value={ 55 } /> );

/**
* We're intentionally not using an accessible selector, because
* the track is an intentionally non-interactive presentation element.
*/
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const indicator = container.firstChild?.firstChild;

expect( indicator ).toHaveStyle( {
width: '55%',
} );
} );

it( 'should pass any additional props down to the underlying `progress` element', () => {
const id = 'foo-bar-123';
const ariaLabel = 'in progress...';
const style = { opacity: 1 };

render(
<ProgressBar id={ id } aria-label={ ariaLabel } style={ style } />
);

expect( screen.getByRole( 'progressbar' ) ).toHaveAttribute( 'id', id );
expect( screen.getByRole( 'progressbar' ) ).toHaveAttribute(
'aria-label',
ariaLabel
);
expect( screen.getByRole( 'progressbar' ) ).toHaveStyle( style );
} );
} );
11 changes: 11 additions & 0 deletions packages/components/src/progress-bar/types.ts
@@ -0,0 +1,11 @@
export type ProgressBarProps = {
/**
* Value of the progress bar.
*/
value?: number;

/**
* A CSS class to apply to the progress bar wrapper (track) element.
*/
className?: string;
};