feat(progress): initial structure created

This commit is contained in:
Junior Garcia 2023-03-29 00:23:52 -03:00
parent 2b5ac90a22
commit 187e8dde83
15 changed files with 691 additions and 18 deletions

View File

@ -0,0 +1,24 @@
# @nextui-org/progress
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/progress
# or
npm i @nextui-org/progress
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,19 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {Progress} from "../src";
describe("Progress", () => {
it("should render correctly", () => {
const wrapper = render(<Progress />);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Progress ref={ref} />);
expect(ref.current).not.toBeNull();
});
});

View File

@ -0,0 +1,63 @@
{
"name": "@nextui-org/progress",
"version": "2.0.0-beta.1",
"description": "Progress bars show either determinate or indeterminate progress of an operation over time.",
"keywords": [
"progress"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/components/progress"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18"
},
"dependencies": {
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@react-aria/i18n": "^3.7.0",
"@react-aria/progress": "^3.4.0",
"@react-aria/utils": "^3.15.0"
},
"devDependencies": {
"@react-types/progress": "^3.3.0",
"clean-package": "2.2.0",
"react": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json",
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}

View File

@ -0,0 +1,10 @@
import Progress from "./progress";
// export types
export type {ProgressProps} from "./progress";
// export hooks
export {useProgress} from "./use-progress";
// export component
export {Progress};

View File

@ -0,0 +1,45 @@
import {forwardRef} from "@nextui-org/system";
import {UseProgressProps, useProgress} from "./use-progress";
export interface ProgressProps extends Omit<UseProgressProps, "ref"> {}
const Progress = forwardRef<ProgressProps, "div">((props, ref) => {
const {
Component,
slots,
styles,
label,
showValueLabel,
barWidth,
getProgressBarProps,
getLabelProps,
} = useProgress({ref, ...props});
const progressBarProps = getProgressBarProps();
return (
<Component {...progressBarProps}>
<div className={slots.labelWrapper({class: styles?.labelWrapper})}>
{label && <span {...getLabelProps()}>{label}</span>}
{showValueLabel && (
<span className={slots.value({class: styles?.value})}>
{progressBarProps["aria-valuetext"]}
</span>
)}
</div>
<div className={slots.wrapper({class: styles?.wrapper})}>
<div
className={slots.filler({class: styles?.filler})}
style={{
width: barWidth,
}}
/>
</div>
</Component>
);
});
Progress.displayName = "NextUI.Progress";
export default Progress;

View File

@ -0,0 +1,60 @@
import {AriaProgressBarProps} from "@react-types/progress";
import {clamp, filterDOMProps, mergeProps} from "@react-aria/utils";
import {DOMAttributes} from "@react-types/shared";
import {useLabel} from "@nextui-org/aria-utils";
import {useNumberFormatter} from "@react-aria/i18n";
export interface ProgressBarAria {
/** Props for the progress bar container element. */
progressBarProps: DOMAttributes;
/** Props for the progress bar's visual label element (if any). */
labelProps: DOMAttributes;
}
/**
* Provides the accessibility implementation for a progress bar component.
* Progress bars show either determinate or indeterminate progress of an operation
* over time.
*/
export function useProgressBar(props: AriaProgressBarProps): ProgressBarAria {
let {
value = 0,
minValue = 0,
maxValue = 100,
valueLabel,
isIndeterminate,
formatOptions = {
style: "percent",
},
} = props;
const domProps = filterDOMProps(props, {labelable: true});
const {labelProps, fieldProps} = useLabel({
...props,
// Progress bar is not an HTML input element so it
// shouldn't be labeled by a <label> element.
labelElementType: "span",
});
value = clamp(value, minValue, maxValue);
const percentage = (value - minValue) / (maxValue - minValue);
const formatter = useNumberFormatter(formatOptions);
if (!isIndeterminate && !valueLabel) {
const valueToFormat = formatOptions.style === "percent" ? percentage : value;
valueLabel = formatter.format(valueToFormat);
}
return {
progressBarProps: mergeProps(domProps, {
...fieldProps,
"aria-valuenow": isIndeterminate ? undefined : value,
"aria-valuemin": minValue,
"aria-valuemax": maxValue,
"aria-valuetext": isIndeterminate ? undefined : (valueLabel as string),
role: "progressbar",
}),
labelProps,
};
}

View File

@ -0,0 +1,128 @@
import type {ProgressVariantProps, SlotsToClasses, ProgressSlots} from "@nextui-org/theme";
import type {PropGetter} from "@nextui-org/system";
import type {AriaProgressBarProps} from "@react-types/progress";
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
import {progress} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, ReactRef} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";
import {useMemo, useCallback} from "react";
import {useProgressBar as useAriaProgress} from "./use-aria-progress";
export interface Props extends HTMLNextUIProps<"div"> {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLElement | null>;
/**
* Classname or List of classes to change the styles of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Progress styles={{
* base:"base-classes",
* wrapper: "wrapper-classes",
* filler: "filler-classes",
* labelWrapper: "labelWrapper-classes",
* label: "label-classes",
* value: "value-classes",
* }} />
* ```
*/
styles?: SlotsToClasses<ProgressSlots>;
}
export type UseProgressProps = Props & AriaProgressBarProps & ProgressVariantProps;
export function useProgress(originalProps: UseProgressProps) {
const [props, variantProps] = mapPropsVariants(originalProps, progress.variantKeys);
const {
ref,
as,
id,
className,
styles,
label,
valueLabel,
value = 0,
minValue = 0,
maxValue = 100,
showValueLabel = false,
isIndeterminate = false,
formatOptions = {
style: "percent",
},
...otherProps
} = props;
const Component = as || "div";
const domRef = useDOMRef(ref);
const baseStyles = clsx(styles?.base, className);
const {progressBarProps, labelProps} = useAriaProgress({
id,
label,
value,
minValue,
maxValue,
valueLabel,
isIndeterminate,
formatOptions,
"aria-labelledby": originalProps["aria-labelledby"],
"aria-label": originalProps["aria-label"],
});
const slots = useMemo(
() =>
progress({
...variantProps,
}),
[...Object.values(variantProps)],
);
// Calculate the width of the progress bar as a percentage
const percentage = useMemo(
() => (isIndeterminate ? undefined : ((value - minValue) / (maxValue - minValue)) * 100),
[isIndeterminate, value, minValue, maxValue],
);
const barWidth = typeof percentage === "number" ? `${Math.round(percentage)}%` : undefined;
const getProgressBarProps = useCallback<PropGetter>(
(props = {}) => ({
ref: domRef,
className: slots.base({class: baseStyles}),
...mergeProps(progressBarProps, otherProps, props),
}),
[domRef, slots, baseStyles, progressBarProps, otherProps],
);
const getLabelProps = useCallback<PropGetter>(
(props = {}) => ({
className: slots.label({class: styles?.label}),
...mergeProps(labelProps, props),
}),
[slots, styles, labelProps],
);
return {
Component,
domRef,
slots,
styles,
label,
barWidth,
percentage,
showValueLabel,
getProgressBarProps,
getLabelProps,
};
}
export type UseProgressReturn = ReturnType<typeof useProgress>;

View File

@ -0,0 +1,60 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {progress} from "@nextui-org/theme";
import {Progress, ProgressProps} from "../src";
export default {
title: "Components/Progress",
component: Progress,
argTypes: {
color: {
control: {
type: "select",
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
isDisabled: {
control: {
type: "boolean",
},
},
},
} as ComponentMeta<typeof Progress>;
const defaultProps = {
...progress.defaultVariants,
value: 55,
};
const Template: ComponentStory<typeof Progress> = (args: ProgressProps) => <Progress {...args} />;
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};
export const WithLabel = Template.bind({});
WithLabel.args = {
...defaultProps,
label: "Loading...",
};
export const WithValueLabel = Template.bind({});
WithValueLabel.args = {
...defaultProps,
label: "Loading...",
showValueLabel: true,
};

View File

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -9,7 +9,7 @@ import {ringClasses} from "../utils";
* ```js
* const {base, header, body, footer} = card({...})
*
* <div className={card()}>
* <div className={base()}>
* <div className={header()}>Header</div>
* <div className={body()}>Body</div>
* <div className={footer()}>Footer</div>

View File

@ -20,3 +20,4 @@ export * from "./pagination";
export * from "./toggle";
export * from "./accordion-item";
export * from "./accordion";
export * from "./progress";

View File

@ -0,0 +1,119 @@
import {tv, type VariantProps} from "tailwind-variants";
/**
* Card **Tailwind Variants** component
*
* @example
* ```js
* const {base, wrapper, filler, labelWrapper, label, value} = progress({...})
*
* <div className={base()} aria-label="progress" role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max}>
* <div className={labelWrapper()}>
* <span className={label()}>{label}</span>
* <span className={value()}>{value}</span>
* </div>
* <div className={wrapper()}>
* <div className={filler()} style={{width: `${value}%`}} />
* </div>
* </div>
* ```
*/
const progress = tv({
slots: {
base: "flex flex-col gap-2",
label: "",
labelWrapper: "flex justify-between",
value: "",
wrapper: "flex flex-1 bg-neutral",
filler: "",
},
variants: {
color: {
neutral: {
filler: "bg-neutral-500",
},
primary: {
filler: "bg-primary",
},
secondary: {
filler: "bg-secondary",
},
success: {
filler: "bg-success",
},
warning: {
filler: "bg-warning",
},
danger: {
filler: "bg-danger",
},
},
size: {
xs: {
filler: "h-1",
},
sm: {
filler: "h-2",
},
md: {
filler: "h-4",
},
lg: {
filler: "h-6",
},
xl: {
filler: "h-7",
},
},
radius: {
none: {
wrapper: "rounded-none",
filler: "rounded-none",
},
wrapper: {
wrapper: "rounded",
filler: "rounded",
},
sm: {
wrapper: "rounded-sm",
filler: "rounded-sm",
},
md: {
wrapper: "rounded-md",
filler: "rounded-md",
},
lg: {
wrapper: "rounded-lg",
filler: "rounded-lg",
},
xl: {
wrapper: "rounded-xl",
filler: "rounded-xl",
},
full: {
wrapper: "rounded-full",
filler: "rounded-full",
},
},
isDisabled: {
true: {
base: "opacity-50 cursor-not-allowed",
},
},
disableAnimation: {
true: {},
},
},
defaultVariants: {
color: "primary",
size: "md",
radius: "full",
isDisabled: false,
disableAnimation: false,
},
});
export type ProgressVariantProps = VariantProps<typeof progress>;
export type ProgressSlots = keyof ReturnType<typeof progress>;
export {progress};

View File

@ -4,3 +4,4 @@ export * from "./utils";
export * from "./interactions";
export * from "./type-utils";
export * from "./overlays";
export * from "./label";

View File

@ -0,0 +1,76 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {useId} from "react";
import {AriaLabelingProps, DOMAttributes, DOMProps, LabelableProps} from "@react-types/shared";
import {ElementType, LabelHTMLAttributes} from "react";
import {useLabels} from "@react-aria/utils";
export interface LabelAriaProps extends LabelableProps, DOMProps, AriaLabelingProps {
/**
* The HTML element used to render the label, e.g. 'label', or 'span'.
* @default 'label'
*/
labelElementType?: ElementType;
}
export interface LabelAria {
/** Props to apply to the label container element. */
labelProps: DOMAttributes | LabelHTMLAttributes<HTMLLabelElement>;
/** Props to apply to the field container element being labeled. */
fieldProps: AriaLabelingProps & DOMProps;
}
/**
* Provides the accessibility implementation for labels and their associated elements.
* Labels provide context for user inputs.
* @param props - The props for labels and fields.
*/
export function useLabel(props: LabelAriaProps): LabelAria {
let {
label,
id: idProp,
"aria-labelledby": ariaLabelledby,
"aria-label": ariaLabel,
labelElementType = "label",
} = props;
const reactId = useId();
const id = idProp || reactId;
const labelId = useId();
let labelProps = {};
if (label) {
ariaLabelledby = ariaLabelledby ? `${ariaLabelledby} ${labelId}` : labelId;
labelProps = {
id: labelId,
htmlFor: labelElementType === "label" ? id : undefined,
};
} else if (!ariaLabelledby && !ariaLabel) {
// eslint-disable-next-line no-console
console.warn(
"If you do not provide a visible label, you must specify an aria-label or aria-labelledby attribute for accessibility",
);
}
const fieldProps = useLabels({
id,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
});
return {
labelProps,
fieldProps,
};
}

91
pnpm-lock.yaml generated
View File

@ -622,6 +622,33 @@ importers:
clean-package: 2.2.0
react: 18.2.0
packages/components/progress:
specifiers:
'@nextui-org/aria-utils': workspace:*
'@nextui-org/dom-utils': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@react-aria/i18n': ^3.7.0
'@react-aria/progress': ^3.4.0
'@react-aria/utils': ^3.15.0
'@react-types/progress': ^3.3.0
clean-package: 2.2.0
react: ^18.2.0
dependencies:
'@nextui-org/aria-utils': link:../../utilities/aria-utils
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
'@react-aria/i18n': 3.7.0_react@18.2.0
'@react-aria/progress': 3.4.0_react@18.2.0
'@react-aria/utils': 3.15.0_react@18.2.0
devDependencies:
'@react-types/progress': 3.3.0_react@18.2.0
clean-package: 2.2.0
react: 18.2.0
packages/components/radio:
specifiers:
'@nextui-org/button': workspace:*
@ -847,11 +874,8 @@ importers:
packages/core/system:
specifiers:
'@react-aria/ssr': ^3.5.0
clean-package: 2.2.0
react: ^18.2.0
dependencies:
'@react-aria/ssr': 3.5.0_react@18.2.0
devDependencies:
clean-package: 2.2.0
react: 18.2.0
@ -5151,7 +5175,7 @@ packages:
react-refresh: 0.11.0
schema-utils: 3.1.1
source-map: 0.7.4
webpack: 5.76.3
webpack: 5.76.3_pntylvuxq2ri2ypsmihtt46hyq
dev: true
/@polka/url/1.0.0-next.21:
@ -5314,6 +5338,20 @@ packages:
react: 18.2.0
dev: false
/@react-aria/progress/3.4.0_react@18.2.0:
resolution: {integrity: sha512-G8Wew/EjgzoBM6OOAtVinA0hEng/DXWZF7luCoMPuqSKOakFzzazHBMpmjcXku0GGoTGzLsqqOK1M5o3kDn4FA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/i18n': 3.7.0_react@18.2.0
'@react-aria/label': 3.5.0_react@18.2.0
'@react-aria/utils': 3.15.0_react@18.2.0
'@react-types/progress': 3.3.0_react@18.2.0
'@react-types/shared': 3.17.0_react@18.2.0
'@swc/helpers': 0.4.14
react: 18.2.0
dev: false
/@react-aria/radio/3.5.0_react@18.2.0:
resolution: {integrity: sha512-d/Hxdu+jUdi3wmyaWYRLTyN16vZxr2MOdkmw8tojTOIMLQj7zTaCFc0D4LR4KZEn3E0F5buGCUHL6o4CTqBeBA==}
peerDependencies:
@ -5613,6 +5651,14 @@ packages:
'@react-types/shared': 3.17.0_react@18.2.0
react: 18.2.0
/@react-types/progress/3.3.0_react@18.2.0:
resolution: {integrity: sha512-k4xivWiEYogsROLyNqVDVjl33rm+X9RKNbKQXg5pqjGikaC8T9hmIwE4tJr26XkMIIvQS6duEBCcuQN/U1+dGQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-types/shared': 3.17.0_react@18.2.0
react: 18.2.0
/@react-types/radio/3.4.0_react@18.2.0:
resolution: {integrity: sha512-klgEU+987xVUCRqBoxXJiPJvy0upfo76dFnK5eF7U7BzcxhuiFeLDUcqUHtK92Cy5QOiDAF2ML0vUYGIabKMPA==}
peerDependencies:
@ -9843,7 +9889,7 @@ packages:
loader-utils: 2.0.4
make-dir: 3.1.0
schema-utils: 2.7.1
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/babel-plugin-add-module-exports/1.0.4:
@ -10431,7 +10477,7 @@ packages:
mississippi: 3.0.0
mkdirp: 0.5.6
move-concurrently: 1.0.1
promise-inflight: 1.0.1
promise-inflight: 1.0.1_bluebird@3.7.2
rimraf: 2.6.3
ssri: 6.0.2
unique-filename: 1.1.1
@ -11456,7 +11502,7 @@ packages:
postcss-value-parser: 4.2.0
schema-utils: 2.7.1
semver: 6.3.0
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/css-loader/5.2.7_webpack@5.76.3:
@ -13643,7 +13689,7 @@ packages:
dependencies:
loader-utils: 2.0.4
schema-utils: 3.1.1
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/file-system-cache/1.1.0:
@ -14882,7 +14928,7 @@ packages:
pretty-error: 2.1.2
tapable: 1.1.3
util.promisify: 1.0.0
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/html-webpack-plugin/5.5.0_webpack@5.76.3:
@ -18891,7 +18937,7 @@ packages:
postcss: 7.0.39
schema-utils: 3.1.1
semver: 7.3.8
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/postcss-loader/4.3.0_postcss@7.0.39:
@ -19258,6 +19304,17 @@ packages:
optional: true
dev: true
/promise-inflight/1.0.1_bluebird@3.7.2:
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
bluebird: '*'
peerDependenciesMeta:
bluebird:
optional: true
dependencies:
bluebird: 3.7.2
dev: true
/promise.allsettled/1.0.6:
resolution: {integrity: sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg==}
engines: {node: '>= 0.4'}
@ -19466,7 +19523,7 @@ packages:
dependencies:
loader-utils: 2.0.4
schema-utils: 3.1.1
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/react-autosuggest/10.1.0_react@18.2.0:
@ -21307,7 +21364,7 @@ packages:
dependencies:
loader-utils: 2.0.4
schema-utils: 2.7.1
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/style-loader/2.0.0_webpack@5.76.3:
@ -21601,7 +21658,7 @@ packages:
serialize-javascript: 4.0.0
source-map: 0.6.1
terser: 4.8.1
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
webpack-sources: 1.4.3
worker-farm: 1.7.0
dev: true
@ -21620,7 +21677,7 @@ packages:
serialize-javascript: 5.0.1
source-map: 0.6.1
terser: 5.16.8
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
webpack-sources: 1.4.3
transitivePeerDependencies:
- bluebird
@ -22443,7 +22500,7 @@ packages:
loader-utils: 2.0.4
mime-types: 2.1.35
schema-utils: 3.1.1
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/url-parse/1.5.10:
@ -22757,7 +22814,7 @@ packages:
mime: 2.6.0
mkdirp: 0.5.6
range-parser: 1.2.1
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
webpack-log: 2.0.0
dev: true
@ -22782,7 +22839,7 @@ packages:
peerDependencies:
webpack: ^2.0.0 || ^3.0.0 || ^4.0.0
dependencies:
webpack: 4.46.0
webpack: 4.46.0_webpack-cli@3.3.12
dev: true
/webpack-hot-middleware/2.25.3: