From cd468d0800bd8e0ef50e2e40b6d58fd365f262d3 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Wed, 19 Jun 2024 12:53:24 -0600 Subject: [PATCH] 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'; +}