From cd468d0800bd8e0ef50e2e40b6d58fd365f262d3 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Wed, 19 Jun 2024 12:53:24 -0600 Subject: [PATCH 1/3] feat: progress bar --- src/progress/ProgressBar.tsx | 31 +++++++++++ src/progress/ProgressBarBase.tsx | 91 ++++++++++++++++++++++++++++++++ src/progress/index.tsx | 1 + src/progress/styles.ts | 80 ++++++++++++++++++++++++++++ src/provider/GlobalStyles.tsx | 22 ++++++++ src/types/progress.ts | 58 ++++++++++++++++++++ 6 files changed, 283 insertions(+) create mode 100644 src/progress/ProgressBar.tsx create mode 100644 src/progress/ProgressBarBase.tsx diff --git a/src/progress/ProgressBar.tsx b/src/progress/ProgressBar.tsx new file mode 100644 index 00000000..5a5dc3e8 --- /dev/null +++ b/src/progress/ProgressBar.tsx @@ -0,0 +1,31 @@ +import { DOMRef } from '@react-types/shared'; +import React from 'react'; +import { useProgressBar } from '@react-aria/progress'; +import { ACProgressBarProps } from '../types'; +import { classNames } from '../utils'; +import { ProgressBarBase } from './ProgressBarBase'; + +function ProgressBar(props: ACProgressBarProps, ref: DOMRef) { + let { staticColor, ...otherProps } = props; + const { progressBarProps, labelProps } = useProgressBar(props); + + return ( + + ); +} + +/** + * ProgressBars show the progression of a system operation: downloading, uploading, processing, etc., in a visual way. + * They can represent either determinate or indeterminate progress. + */ +let _ProgressBar = React.forwardRef(ProgressBar); +export { _ProgressBar as ProgressBar }; diff --git a/src/progress/ProgressBarBase.tsx b/src/progress/ProgressBarBase.tsx new file mode 100644 index 00000000..d1b630c7 --- /dev/null +++ b/src/progress/ProgressBarBase.tsx @@ -0,0 +1,91 @@ +import { clamp } from '@react-aria/utils'; +import React, { CSSProperties, HTMLAttributes } from 'react'; +import { DOMRef, ProgressBarProps, ACProgressBarBaseProps } from '../types'; +import { classNames, useDOMRef, useStyleProps } from '../utils'; +import { progressBarCSS } from './styles'; +interface ProgressBarBaseProps + extends ACProgressBarBaseProps, + ProgressBarProps { + barClassName?: string; + barProps?: HTMLAttributes; + labelProps?: HTMLAttributes; +} + +// Base ProgressBar component shared with Meter. +function ProgressBarBase( + props: ProgressBarBaseProps, + ref: DOMRef +) { + let { + value = 0, + minValue = 0, + maxValue = 100, + size = 'L', + label, + barClassName, + showValueLabel = !!label, + labelPosition = 'top', + isIndeterminate = false, + barProps, + labelProps, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + ...otherProps + } = props; + let domRef = useDOMRef(ref); + let { styleProps } = useStyleProps(otherProps); + + value = clamp(value, minValue, maxValue); + + let barStyle: CSSProperties = {}; + if (!isIndeterminate) { + let percentage = (value - minValue) / (maxValue - minValue); + barStyle.width = `${Math.round(percentage * 100)}%`; + } + + // Ideally this should be in useProgressBar, but children + // are not supported in ProgressCircle which shares that hook... + if (!label && !ariaLabel && !ariaLabelledby) { + // eslint-disable-next-line no-console + console.warn( + 'If you do not provide a visible label via children, you must specify an aria-label or aria-labelledby attribute for accessibility' + ); + } + // use inline style for fit-content because cssnano is too smart for us and will strip out the -moz prefix in css files + return ( +
+ {label && ( + + {label} + + )} + {showValueLabel && barProps && ( +
+ {barProps['aria-valuetext']} +
+ )} +
+
+
+
+ ); +} + +let _ProgressBarBase = React.forwardRef(ProgressBarBase); +export { _ProgressBarBase as ProgressBarBase }; diff --git a/src/progress/index.tsx b/src/progress/index.tsx index 882ee3c5..11a3dd4f 100644 --- a/src/progress/index.tsx +++ b/src/progress/index.tsx @@ -1 +1,2 @@ export * from './ProgressCircle'; +export * from './ProgressBar'; diff --git a/src/progress/styles.ts b/src/progress/styles.ts index 39f8d458..b52eceab 100644 --- a/src/progress/styles.ts +++ b/src/progress/styles.ts @@ -122,3 +122,83 @@ export const progressCircleCSS = css` } } `; + +export const progressBarCSS = css` + --ac-barloader-large-border-radius: 3px; + --ac-barloader-track-color-default: var(--ac-global-color-grey-300); + &.ac-barloader { + --ac-barloader-large-track-fill-color: var(--ac-global-color-primary); + --ac-barloader-static-black-track-color: #00000040; + --ac-barloader-static-black-fill-color: var( + --ac-global-static-color-black-900 + ); + + min-inline-size: var(--ac-global-dimension-static-size-600, 48px); + inline-size: var(--ac-global-dimension-size-2400); + vertical-align: top; + isolation: isolate; + flex-flow: wrap; + justify-content: space-between; + align-items: center; + display: inline-flex; + position: relative; + } + + &.ac-barloader--static-white { + --mod-barloader-label-and-value-color: var( + --ac-global-static-color-white-900 + ); + --mod-barloader-fill-color: var(--ac-global-color-white-900); + } + &.ac-barloader--static-black { + --mod-barloader-label-and-value-color: var( + --ac-global-static-color-black-900 + ); + --mod-barloader-fill-color: var(--ac-global-static-color-black-900); + --mod-barloader-track-color: var(--ac-barloader-static-black-track-color); + } + + .ac-barloader-label, + .ac-barloader-percentage { + color: var( + --mod-barloader-label-and-value-color, + var(--ac-global-text-color-900) + ); + font-size: var(--spectrum-global-dimension-font-size-75); + font-weight: var(--spectrum-global-font-weight-regular); + line-height: var(--spectrum-global-font-line-height-small); + text-align: start; + text-align: start; + margin-bottom: var(--ac-global-dimension-size-115); + } + + .ac-barloader-label { + flex: 1; + } + + .ac-barloader-percentage { + align-self: flex-start; + margin-inline-start: var(--ac-global-dimension-size-150); + } + + .ac-barloader-track { + background-color: var( + --mod-barloader-track-color, + var(--ac-barloader-track-color-default) + ); + min-inline-size: var(--ac-global-dimension-static-size-600); + height: var(--ac-global-dimension-size-75); + border-radius: var(--ac-barloader-large-border-radius); + z-index: 1; + inline-size: 100%; + overflow: hidden; + } + + .ac-barloader-fill { + background: var(--mod-barloader-fill-color, var(--ac-global-color-primary)); + height: var(--ac-global-dimension-size-75); + + border: none; + transition: width 1s; + } +`; diff --git a/src/provider/GlobalStyles.tsx b/src/provider/GlobalStyles.tsx index 39a38e8c..e9851186 100644 --- a/src/provider/GlobalStyles.tsx +++ b/src/provider/GlobalStyles.tsx @@ -69,6 +69,9 @@ const staticCSS = css` --ac-global-static-color-white-900: rgba(255, 255, 255, 0.9); --ac-global-static-color-white-700: rgba(255, 255, 255, 0.7); --ac-global-static-color-white-300: rgba(255, 255, 255, 0.3); + --ac-global-static-color-black-900: rgba(0, 0, 0, 0.9); + --ac-global-static-color-black-700: rgba(0, 0, 0, 0.7); + --ac-global-static-color-black-300: rgba(0, 0, 0, 0.3); } `; @@ -132,6 +135,25 @@ const dimensionsCSS = css` --ac-global-dimension-static-grid-columns: 12; --ac-global-dimension-static-grid-fluid-width: 100%; --ac-global-dimension-static-grid-fixed-max-width: 1280px; + + /* Font sizing */ + --ac-global-dimension-font-size-25: 10px; + --ac-global-dimension-font-size-50: 11px; + --ac-global-dimension-font-size-75: 12px; + --ac-global-dimension-font-size-100: 14px; + --ac-global-dimension-font-size-150: 15px; + --ac-global-dimension-font-size-200: 16px; + --ac-global-dimension-font-size-300: 18px; + --ac-global-dimension-font-size-400: 20px; + --ac-global-dimension-font-size-500: 22px; + --ac-global-dimension-font-size-600: 25px; + --ac-global-dimension-font-size-700: 28px; + --ac-global-dimension-font-size-800: 32px; + --ac-global-dimension-font-size-900: 36px; + --ac-global-dimension-font-size-1000: 40px; + --ac-global-dimension-font-size-1100: 45px; + --ac-global-dimension-font-size-1200: 50px; + --ac-global-dimension-font-size-1300: 60px; } `; diff --git a/src/types/progress.ts b/src/types/progress.ts index db3755dd..394d845f 100644 --- a/src/types/progress.ts +++ b/src/types/progress.ts @@ -1,3 +1,8 @@ +import { ReactNode } from 'react'; +import { AriaLabelingProps, DOMProps } from './dom'; +import { LabelPosition } from './labelable'; +import { StyleProps } from './style'; + export interface ProgressBaseProps { /** * The current value (controlled). @@ -15,3 +20,56 @@ export interface ProgressBaseProps { */ maxValue?: number; } + +export interface ProgressBarBaseProps extends ProgressBaseProps { + /** The content to display as the label. */ + label?: ReactNode; + /** + * The display format of the value label. + * @default {style: 'percent'} + */ + formatOptions?: Intl.NumberFormatOptions; + /** The content to display as the value's label (e.g. 1 of 4). */ + valueLabel?: ReactNode; +} + +export interface AriaProgressBarBaseProps + extends ProgressBarBaseProps, + DOMProps, + AriaLabelingProps {} + +export interface ProgressBarProps extends ProgressBarBaseProps { + /** + * Whether presentation is indeterminate when progress isn't known. + */ + isIndeterminate?: boolean; +} + +export interface AriaProgressBarProps + extends ProgressBarProps, + DOMProps, + AriaLabelingProps {} + +export interface ACProgressBarBaseProps + extends AriaProgressBarBaseProps, + StyleProps { + /** + * How thick the bar should be. + * @default 'L' + */ + size?: 'S' | 'L'; + /** + * The label's overall position relative to the element it is labeling. + * @default 'top' + */ + labelPosition?: LabelPosition; + /** Whether the value's label is displayed. True by default if there's a label, false by default if not. */ + showValueLabel?: boolean; +} + +export interface ACProgressBarProps + extends ACProgressBarBaseProps, + ProgressBarProps { + /** The static color style to apply. Useful when the button appears over a color background. */ + staticColor?: 'white' | 'black'; +} From 8d5eb73d37714f57fe9fdd6624a3a68cc4fe35d8 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Wed, 19 Jun 2024 13:00:05 -0600 Subject: [PATCH 2/3] progress bar --- stories/ProgressBar.stories.tsx | 293 ++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 stories/ProgressBar.stories.tsx diff --git a/stories/ProgressBar.stories.tsx b/stories/ProgressBar.stories.tsx new file mode 100644 index 00000000..7e602849 --- /dev/null +++ b/stories/ProgressBar.stories.tsx @@ -0,0 +1,293 @@ +import React, { CSSProperties } from 'react'; +import { css } from '@emotion/react'; +import { ProgressBar, Provider } from '../src'; + +const formatOptions = { + style: 'currency', + currency: 'JPY', +}; + +const grayedBoxStyle: CSSProperties = { + width: '250px', + height: '60px', + backgroundColor: 'rgba(0,0,0,0.4)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +export default { + title: 'ProgressBar', + providerSwitcher: { status: 'positive' }, + args: { value: 32 }, + argTypes: { + value: { + control: { + type: 'range', + min: 0, + max: 100, + }, + }, + }, +}; + +export const Default = args => render(args); +export const Value50 = () => render({ value: 50 }); + +Value50.story = { + name: 'value: 50', +}; + +export const Value100 = () => render({ value: 100 }); + +Value100.story = { + name: 'value: 100', +}; + +export const SizeS = args => render({ size: 'S', ...args }); + +SizeS.story = { + name: 'size: S', +}; + +export const ShowValueLabelTrue = args => + render({ showValueLabel: true, ...args }); + +ShowValueLabelTrue.story = { + name: 'showValueLabel: true', +}; + +export const ShowValueLabelFalse = args => + render({ showValueLabel: false, ...args }); + +ShowValueLabelFalse.story = { + name: 'showValueLabel: false', +}; + +export const ValueLabel1Of4 = () => render({ value: 25, valueLabel: '1 of 4' }); + +ValueLabel1Of4.story = { + name: 'valueLabel: 1 of 4', +}; + +export const UsingNumberFormatOptionsWithCurrencyStyle = args => + render({ + ...args, + showValueLabel: true, + formatOptions, + }); + +UsingNumberFormatOptionsWithCurrencyStyle.story = { + name: 'Using number formatOptions with currency style', +}; + +export const NoVisibleLabel = args => + render({ label: null, 'aria-label': 'Loading…', ...args }); + +NoVisibleLabel.story = { + name: 'no visible label', +}; + +export const LabelPositionSide = args => + render({ labelPosition: 'side', ...args }); + +LabelPositionSide.story = { + name: 'labelPosition: side', +}; + +export const LabelPositionTop = args => + render({ labelPosition: 'top', ...args }); + +LabelPositionTop.story = { + name: 'labelPosition: top', +}; + +export const LongLabel = args => + render({ + label: 'Super long progress bar label. Sample label copy. Loading...', + ...args, + }); + +LongLabel.story = { + name: 'long label', +}; + +export const LongLabelLabelPositionSide = args => + render({ + labelPosition: 'side', + label: 'Super long progress bar label. Sample label copy. Loading...', + ...args, + }); + +LongLabelLabelPositionSide.story = { + name: 'long label, labelPosition: side', +}; + +export const IsIndeterminateTrue = args => + render({ isIndeterminate: true, ...args }); + +IsIndeterminateTrue.story = { + name: 'isIndeterminate: true', +}; + +export const IsIndeterminateTrueSizeS = () => + render({ isIndeterminate: true, size: 'S' }); + +IsIndeterminateTrueSizeS.story = { + name: 'isIndeterminate: true, size: S', +}; + +export const VariantOverBackground = args => ( +
+ {render({ variant: 'overBackground', ...args })} +
+); + +VariantOverBackground.story = { + name: 'variant: overBackground', +}; + +export const StaticColorWhite = args => ( +
{render({ staticColor: 'white', ...args })}
+); + +export const CustomColor = args => ( +
+ {render({ ...args, width: '30px' })} +
+); + +StaticColorWhite.story = { + name: 'staticColor: white', +}; + +export const StaticColorBlack = args => ( +
+ {render({ staticColor: 'black', ...args })} +
+); + +StaticColorBlack.story = { + name: 'staticColor: black', +}; + +export const ParentWidth100 = () => ( + {render()} +); + +ParentWidth100.story = { + name: 'parent width 100%', +}; + +export const ParentWidth100Px = () => ( + {render()} +); + +ParentWidth100Px.story = { + name: 'parent width 100px', +}; + +export const Width300Px = () => render({ width: '300px', value: 100 }); + +Width300Px.story = { + name: 'width: 300px', +}; + +export const Width300PxIsIndeterminateTrue = () => + render({ width: '300px', isIndeterminate: true }); + +Width300PxIsIndeterminateTrue.story = { + name: 'width: 300px, isIndeterminate: true', +}; + +export const Width300PxLabelPositionSide = () => + render({ width: '300px', labelPosition: 'side' }); + +Width300PxLabelPositionSide.story = { + name: 'width: 300px, labelPosition: side', +}; + +export const Width300PxLabelPositionSideIsIndeterminateTrue = () => + render({ width: '300px', labelPosition: 'side', isIndeterminate: true }); + +Width300PxLabelPositionSideIsIndeterminateTrue.story = { + name: 'width: 300px, labelPosition: side, isIndeterminate: true', +}; + +export const Width30Px = () => render({ width: '30px' }); + +Width30Px.story = { + name: 'width: 30px', +}; + +export const Width30PxSizeS = () => render({ width: '30px', size: 'S' }); + +Width30PxSizeS.story = { + name: 'width: 30px, size: S', +}; + +export const Width30PxLabelPositionSideLongLabel = () => + render({ + width: '30px', + labelPosition: 'side', + label: 'Super long progress bar label. Sample label copy. Loading...', + }); + +Width30PxLabelPositionSideLongLabel.story = { + name: 'width: 30px, labelPosition: side, long label', +}; + +export const Width30PxLabelPositionSideIsIndeterminateTrueLongLabelButtonOnRight = () => ( + <> + {render({ + width: '30px', + labelPosition: 'side', + isIndeterminate: true, + label: 'Super long progress bar label. Sample label copy. Loading...', + })} + + +); + +Width30PxLabelPositionSideIsIndeterminateTrueLongLabelButtonOnRight.story = { + name: + 'width: 30px, labelPosition: side, isIndeterminate: true, long label, button on right', +}; + +export const UsingRawValuesForMinValueMaxValueAndValue = () => + render({ + showValueLabel: true, + labelPosition: 'top', + maxValue: 2147483648, + value: 715827883, + }); + +UsingRawValuesForMinValueMaxValueAndValue.story = { + name: 'Using raw values for minValue, maxValue, and value', +}; + +export const UsingRawValuesWithNumberFormatter = () => + render({ + showValueLabel: true, + labelPosition: 'top', + maxValue: 2147483648, + value: 715827883, + formatOptions, + }); + +UsingRawValuesWithNumberFormatter.story = { + name: 'Using raw values with number formatter', +}; + +function render(props: any = {}) { + return ( + + + + ); +} From b29b48ace90d618fc4e9220536e647069351916b Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Wed, 19 Jun 2024 13:01:20 -0600 Subject: [PATCH 3/3] v1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f563f16..cd93e1bf 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.3.7", + "version": "1.4.0", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts",