From f4bd37600723f86af6e56b8f591d1c4471b0398a Mon Sep 17 00:00:00 2001 From: Prabhu Murthy Date: Wed, 22 Feb 2023 12:47:44 +0530 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Adding=20a=20new=20loading=20indica?= =?UTF-8?q?tor=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../components/loading-indicator/index.tsx | 87 +++++++++++++ .../loading-indicator/widget-variants.tsx | 15 +++ .../components/loading-indicator/widgets.tsx | 117 ++++++++++++++++++ .../documentation/home/sidebar-home-data.ts | 75 +++++------ .../route-configs/route-configs-1.tsx | 5 + .../__snapshots__/accordion.test.tsx.snap | 8 +- .../lib/components/button/button-model.ts | 25 ++++ packages/lib/components/button/button.tsx | 5 +- packages/lib/components/card/card-model.ts | 16 +++ .../lib/components/carousel/carousel-model.ts | 27 ++++ packages/lib/components/index.ts | 1 + .../__tests__/loading-indicator.test.tsx | 46 +++++++ .../loading-indicator.model.ts | 9 ++ .../loading-indicator.module.scss | 49 ++++++++ .../loading-indicator/loading-indicator.tsx | 88 +++++++++++++ .../__snapshots__/tabs.test.tsx.snap | 12 +- packages/lib/react-creme.ts | 2 + .../stories/LoadingIndicator.stories.tsx | 34 +++++ 19 files changed, 574 insertions(+), 49 deletions(-) create mode 100644 packages/documentation/components/loading-indicator/index.tsx create mode 100644 packages/documentation/components/loading-indicator/widget-variants.tsx create mode 100644 packages/documentation/components/loading-indicator/widgets.tsx create mode 100644 packages/lib/components/loading-indicator/__tests__/loading-indicator.test.tsx create mode 100644 packages/lib/components/loading-indicator/loading-indicator.model.ts create mode 100644 packages/lib/components/loading-indicator/loading-indicator.module.scss create mode 100644 packages/lib/components/loading-indicator/loading-indicator.tsx create mode 100644 packages/storybook/stories/LoadingIndicator.stories.tsx diff --git a/.gitignore b/.gitignore index b662bfc1f..0c9b76177 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ bundle-analysis** storybook-static build-storybook.log + +debug.log \ No newline at end of file diff --git a/packages/documentation/components/loading-indicator/index.tsx b/packages/documentation/components/loading-indicator/index.tsx new file mode 100644 index 000000000..5d21d8125 --- /dev/null +++ b/packages/documentation/components/loading-indicator/index.tsx @@ -0,0 +1,87 @@ +import { faBarsProgress } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; +import DemoPageRenderer from '../../common/demo-page-renderer'; + +const Widgets = React.lazy(() => import('./widgets')); + +const Description = ( +
+

+ A loading indicator component is a visual element that is commonly used in + user interfaces to indicate that a process or action is taking place in + the background, and the user needs to wait for the process to complete + before they can interact with the interface. +

+
+); + +function menu() { + return ( + } + features={[ + 'Customizable shape,speed and size', + 'Customizable number of items', + 'RTL support', + ]} + properties={[ + { + default: 3, + description: 'Number of items in the loading indicator', + name: 'count', + optional: true, + type: 'Number', + }, + { + default: 'square', + description: + 'Shape of the loading indicator. can be square or circle', + name: 'shape', + optional: true, + type: 'String', + }, + { + default: 'false', + description: 'Direction of the loading indicator', + name: 'rtl', + optional: true, + type: 'Boolean', + }, + { + default: 'slow', + description: + 'Speed of the loading indicator. can be slow, normal or fast', + name: 'speed', + optional: true, + type: 'String', + }, + { + default: 'sm', + description: + 'Size of the loading indicator. can be sm, md or lg', + name: 'size', + optional: true, + type: 'String', + }, + { + default: '0', + description: + 'Custom size of the loading indicator. Allows to set a custom size in pixels', + name: 'customSize', + optional: true, + type: 'Number', + }, + ]} + demoWidget={} + > + ); +} + +export default menu; diff --git a/packages/documentation/components/loading-indicator/widget-variants.tsx b/packages/documentation/components/loading-indicator/widget-variants.tsx new file mode 100644 index 000000000..e0a54ce92 --- /dev/null +++ b/packages/documentation/components/loading-indicator/widget-variants.tsx @@ -0,0 +1,15 @@ +import { LoadingIndicator } from '../../../lib/components'; + +export const Default = ; + +export const RTL = ; + +export const CircleShape = ; + +export const CustomSpeed = ; + +export const CustomSize = ; + +export const FineGrainedSize = ; + +export const LoadingIndicatorCount = ; diff --git a/packages/documentation/components/loading-indicator/widgets.tsx b/packages/documentation/components/loading-indicator/widgets.tsx new file mode 100644 index 000000000..e94b1213b --- /dev/null +++ b/packages/documentation/components/loading-indicator/widgets.tsx @@ -0,0 +1,117 @@ +import { BlockQuote, Section, Text } from '../../../lib/components'; +import { DemoWidget } from '../../common/demo-widget'; +import { + CircleShape, + CustomSize, + CustomSpeed, + Default, + FineGrainedSize, + LoadingIndicatorCount, + RTL, +} from './widget-variants'; + +function Widgets() { + return ( +
+
+ + {Default} + +
+
+ + The shape of the loading indicator can be changed to circle by passing + the + shape prop with value circle. The default + shape is square. + + + {CircleShape} + +
+
+ + The speed of the loading indicator can be changed by passing the + speed prop with value slow,{' '} + normal or fast. The default speed is normal. + + + {CustomSpeed} + +
+
+ + The number of items in the loading indicator can be changed by passing + the count prop with a number value. The default count is{' '} + 3. + + + {LoadingIndicatorCount} + +
+
+ + The loading indicator can be displayed from right to left by passing + the rtl. The default direction is left to right. + + + {RTL} + +
+
+ + Customize the size of the loading indicator by passing the{' '} + size prop with value sm, md or{' '} + lg. The default size is sm. + +
+ The sizes can be managed by adjusting the iconSizes settings in the + ThemeProvider +
+ + {CustomSize} + +
+
+ + If you want to take complete control over the size of the loading + indicator, you can pass the customSize prop with a number + value (pixels). This will override the size prop. + + + {FineGrainedSize} + +
+
+ ); +} + +export default Widgets; diff --git a/packages/documentation/home/sidebar-home-data.ts b/packages/documentation/home/sidebar-home-data.ts index 91595ca07..0fd7c527e 100644 --- a/packages/documentation/home/sidebar-home-data.ts +++ b/packages/documentation/home/sidebar-home-data.ts @@ -7,14 +7,6 @@ export default [ name: 'Installation', value: 'home', }, - // { - // name: 'Dependencies', - // value: 'home', - // }, - // { - // name: 'Browser Support', - // value: 'home', - // }, { name: 'Usage', value: 'home', @@ -23,75 +15,84 @@ export default [ name: 'Theme', value: 'home', }, + // { + // name: 'Dependencies', + // value: 'home', + // }, + // { + // name: 'Browser Support', + // value: 'home', + // }, ], title: 'Getting started', }, { items: [ - { name: 'Splitter' }, - { name: 'Accordion' }, - { name: 'Tabs' }, + // { name: 'Reveal' }, { name: 'Accordion Group' }, + { name: 'Accordion' }, + { name: 'Carousel' }, { name: 'Sidebar' }, + { name: 'Splitter' }, + { name: 'Tabs' }, { name: 'image comparer' }, - { name: 'Carousel' }, - // { name: 'Reveal' }, { name: 'scroll spy' }, ], title: 'Layout', }, { items: [ - { name: 'section' }, + { name: 'Avatar' }, { name: 'Card' }, - { name: 'page header' }, - { name: 'Image' }, { name: 'Gallery' }, - { name: 'Avatar' }, + { name: 'Image' }, { name: 'Read More' }, + { name: 'page header' }, + { name: 'section' }, ], title: 'content', }, { items: [ - { name: 'Input Text' }, - { name: 'Input Number' }, - { name: 'Tags' }, - { name: 'Radio Group' }, - { name: 'Checkbox' }, + { name: 'Auto Suggest' }, + { name: 'Button' }, { name: 'Checkbox Group' }, - { name: 'Switch' }, + { name: 'Checkbox' }, { name: 'Dropdown' }, - { name: 'Rate' }, - { name: 'Button' }, - { name: 'Slider' }, - { name: 'Auto Suggest' }, - { name: 'Menu Button' }, { name: 'Form Field' }, { name: 'Form Group' }, - { name: 'Pin' }, + { name: 'Input Number' }, + { name: 'Input Text' }, + { name: 'Menu Button' }, { name: 'Password' }, + { name: 'Pin' }, + { name: 'Radio Group' }, + { name: 'Rate' }, + { name: 'Slider' }, + { name: 'Switch' }, + { name: 'Tags' }, ], title: 'Inputs', }, { items: [ + { name: 'Alerts' }, + { name: 'Global Notification' }, + { name: 'Notification' }, { name: 'Progress' }, { name: 'Skeleton' }, - { name: 'Notification' }, - { name: 'Global Notification' }, - { name: 'Alerts' }, { name: 'Spinner' }, + { name: 'Loading Indicator' }, ], title: 'Feedback', }, { items: [ - { name: 'Tree' }, - { name: 'List' }, { name: 'Data Grid' }, - { name: 'Transfer' }, { name: 'Kbd' }, + { name: 'List' }, + { name: 'Transfer' }, + { name: 'Tree' }, ], title: 'Data', }, @@ -103,9 +104,9 @@ export default [ items: [ { name: 'Dialog' }, { name: 'Drawer' }, - { name: 'Tooltip' }, - { name: 'Menu' }, { name: 'Menu Bar' }, + { name: 'Menu' }, + { name: 'Tooltip' }, ], title: 'Overlay', }, diff --git a/packages/documentation/route-configs/route-configs-1.tsx b/packages/documentation/route-configs/route-configs-1.tsx index 63ba6149a..c07fce962 100644 --- a/packages/documentation/route-configs/route-configs-1.tsx +++ b/packages/documentation/route-configs/route-configs-1.tsx @@ -133,6 +133,11 @@ const routes = [ key: 'avatar', path: '/avatar', }, + { + component: lazy(() => import('../components/loading-indicator')), + key: 'loading-indicator', + path: '/loading-indicator', + }, ]; export { routes }; diff --git a/packages/lib/components/accordion/__tests__/__snapshots__/accordion.test.tsx.snap b/packages/lib/components/accordion/__tests__/__snapshots__/accordion.test.tsx.snap index 283a4205e..05d7bdc46 100644 --- a/packages/lib/components/accordion/__tests__/__snapshots__/accordion.test.tsx.snap +++ b/packages/lib/components/accordion/__tests__/__snapshots__/accordion.test.tsx.snap @@ -5,10 +5,10 @@ exports[`Accordion > should render snapshot 1`] = ` class="_accordion_f93c7f _no-border_f93c7f" >
void; + + // primary changes the color of the button to emphasize it being selected (optional) primary?: boolean; + + // size sets the sizeof the button (optional) size?: 'sm' | 'md' | 'lg'; + + // style provides styling options at runtime (optional) style?: CSSProperties; + + // type is related to the design theme of the buttons (primary, default, danger, icon, progress) (optional) type?: 'primary' | 'default' | 'danger' | 'icon' | 'progress'; } diff --git a/packages/lib/components/button/button.tsx b/packages/lib/components/button/button.tsx index 455758c28..8565a96bf 100644 --- a/packages/lib/components/button/button.tsx +++ b/packages/lib/components/button/button.tsx @@ -20,6 +20,7 @@ const Button = React.forwardRef((props, ref) => { size = 'sm', style = {}, accent = 'rounded', + isBusy = false, } = props; const isDarkMode = useMemo(() => isDark(), []); @@ -30,13 +31,13 @@ const Button = React.forwardRef((props, ref) => { { [styles[`default`]]: type === 'progress', [styles.no_border]: !border, - [styles.disabled]: disabled, + [styles.disabled]: disabled || isBusy, [styles.dark]: isDarkMode, [styles[accent]]: true, }, [styles[size], styles[type], styles.btn] ), - [disabled, isDarkMode] + [disabled, isDarkMode, isBusy] ); // setup for focus diff --git a/packages/lib/components/card/card-model.ts b/packages/lib/components/card/card-model.ts index e40d1fae7..240345c36 100644 --- a/packages/lib/components/card/card-model.ts +++ b/packages/lib/components/card/card-model.ts @@ -1,12 +1,28 @@ import { ReactNode } from 'react'; +// CardProps interface export interface CardProps { + // alignFooter sets the alignment of the footer element alignFooter?: 'left' | 'center' | 'right'; + + // alignHeader sets the alignment of the header element alignHeader?: 'left' | 'center' | 'right'; + + // border determines whether or not to show a border border?: boolean; + + // children can be either a single ReactNode or an array of ReactNode elements children?: ReactNode | ReactNode[]; + + // footer allows adding optional footer into the card footer?: ReactNode; + + // header allows adding a header into the card header?: ReactNode; + + // height defines the height of the card (default is auto) height?: number; + + // shadow sets a drop shadow on the card shadow?: boolean; } diff --git a/packages/lib/components/carousel/carousel-model.ts b/packages/lib/components/carousel/carousel-model.ts index e27d7d835..396a6283f 100644 --- a/packages/lib/components/carousel/carousel-model.ts +++ b/packages/lib/components/carousel/carousel-model.ts @@ -1,14 +1,31 @@ import React from 'react'; export type CarouselProps = { + // AutoPlay (in millis seconds) controls the speed at which each item is shown before moving to the next. autoPlay?: number; + + // Border defines whether or not to draw a border around the component. border?: boolean; + + //Children are the React elements/components displayed inside the Carousel component. children: React.ReactNode | React.ReactNode[]; + + // The direction determines if the items move horizontally or vertically in the carousel. direction?: 'horizontal' | 'vertical'; + + // enableSwipe enable swipe gestures for moving forward and backward. Defaults to true. enableSwipe?: boolean; + + //focusable defines whether or not the node can be focused, false by default. focusable?: boolean; + + //height sets the height of the element. height?: number; + + //size sets the size of the element. size?: 'sm' | 'md' | 'lg'; + + //transition configures the transition effect between slides. transition?: string; }; @@ -21,14 +38,24 @@ export type CarouselItemProps = { width: number; }; +// The CarouselItemsProps type is a combination of props from the CarouselProps type and additional props export type CarouselItemsProps = Pick< CarouselProps, 'children' | 'direction' | 'size' > & { + // The activePage prop keeps tracks of which page/item is currently in view activePage: number; + + // The carouselItems prop contains an array of CarouselItemProps carouselItems: CarouselItemProps[]; + + // The height prop is used to determine the size of the carousel height: number; + + // The totalItems prop stores the total items/pages in the carousel totalItems: number; + + // The width prop determines the width of the carousel width: number; }; diff --git a/packages/lib/components/index.ts b/packages/lib/components/index.ts index 0fc065699..9ae8392c3 100644 --- a/packages/lib/components/index.ts +++ b/packages/lib/components/index.ts @@ -29,6 +29,7 @@ export { Kbd } from './kbd/kbd'; export { KbdCombination } from './kbd/kbd-combination'; export { Link } from './link/link'; export { List } from './list/list'; +export { LoadingIndicator } from './loading-indicator/loading-indicator'; export { MenuBar } from './menu-bar/menu-bar'; export { Menu } from './menu/menu'; export { Notification } from './notification/notification'; diff --git a/packages/lib/components/loading-indicator/__tests__/loading-indicator.test.tsx b/packages/lib/components/loading-indicator/__tests__/loading-indicator.test.tsx new file mode 100644 index 000000000..602bb9070 --- /dev/null +++ b/packages/lib/components/loading-indicator/__tests__/loading-indicator.test.tsx @@ -0,0 +1,46 @@ +import { render } from '@testing-library/react'; +import { LoadingIndicator } from '../loading-indicator'; +import styles from '../loading-indicator.module.scss'; + +describe('Loading Indicator', () => { + it('should render loading indicator', () => { + const { getByRole } = render(); + expect(getByRole('progressbar')).toBeInTheDocument(); + }); + + it('should render square shape', () => { + const { getByRole } = render(); + expect(getByRole('progressbar').children[0]).toHaveClass(styles.square); + }); + + it('should render a large sized loading indicator', () => { + const { getByRole } = render(); + expect(getByRole('progressbar').children[0]).toHaveClass(styles.lg); + }); + + // should render a custom sized loading indicator and check if the height and width are equal to the custom size + it('should render a custom sized loading indicator', () => { + const { getByRole } = render(); + expect(getByRole('progressbar').children[0]).toHaveStyle( + 'height: 100px; width: 100px;' + ); + }); + + it('should render in rtl mode', () => { + const { getByRole } = render(); + expect(getByRole('progressbar')).toHaveClass(styles.rtl); + }); + + it('should render custom number of loading indicators', () => { + const { getByRole } = render(); + expect(getByRole('progressbar').children).toHaveLength(5); + }); + + //check if the loading indicator is rendered with the normal speed and check is the css transition is applied + it('should render loading indicator with normal speed', () => { + const { getByRole } = render(); + expect(getByRole('progressbar').children[0]).toHaveStyle( + 'transition: background 500ms ease-in-out;' + ); + }); +}); diff --git a/packages/lib/components/loading-indicator/loading-indicator.model.ts b/packages/lib/components/loading-indicator/loading-indicator.model.ts new file mode 100644 index 000000000..0c6e6467c --- /dev/null +++ b/packages/lib/components/loading-indicator/loading-indicator.model.ts @@ -0,0 +1,9 @@ +export type LoadingIndicatorProps = { + animationType?: 'jump' | 'pulse'; + count?: number; + customSize?: number; + rtl?: boolean; + shape?: 'circle' | 'square'; + size?: 'sm' | 'md' | 'lg'; + speed?: 'slow' | 'normal' | 'fast'; +}; diff --git a/packages/lib/components/loading-indicator/loading-indicator.module.scss b/packages/lib/components/loading-indicator/loading-indicator.module.scss new file mode 100644 index 000000000..3ccf8a103 --- /dev/null +++ b/packages/lib/components/loading-indicator/loading-indicator.module.scss @@ -0,0 +1,49 @@ +@use '@design/list.scss'; +@use '@design/core.scss'; +@use '@design/theme.scss'; +@use '@design/icon.scss'; + +$sizes: (sm, md, lg); + +.wrapper { + @extend %list-horizontal-left; + + &.rtl { + direction: rtl; + } +} + +.indicator { + margin-right: 0.5rem; + position: relative; + + &:not(.square) { + border-radius: 50%; + } + + &.square { + border-radius: 1px; + } + + &.dark { + background: theme.$black; + } + + &:not(.dark) { + background: theme.$white; + } + + &:not(.custom_size) { + @each $size in $sizes { + &.#{$size} { + @extend %icon-#{$size}; + } + } + } +} + +.animate { + &.flash { + background: theme.$primary; + } +} diff --git a/packages/lib/components/loading-indicator/loading-indicator.tsx b/packages/lib/components/loading-indicator/loading-indicator.tsx new file mode 100644 index 000000000..1aebd4068 --- /dev/null +++ b/packages/lib/components/loading-indicator/loading-indicator.tsx @@ -0,0 +1,88 @@ +import cls from 'classnames'; +import { FunctionComponent, useEffect, useMemo, useRef, useState } from 'react'; +import { isDark } from '../common/utils'; +import { LoadingIndicatorProps } from './loading-indicator.model'; +import styles from './loading-indicator.module.scss'; + +const speeds = { + fast: 250, + normal: 500, + slow: 750, +}; + +const LoadingIndicator: FunctionComponent = ({ + count = 3, + shape = 'square', + rtl = false, + speed = 'slow', + size = 'sm', + customSize = 0, +}) => { + const [activeIndex, setActiveIndex] = useState(0); + + const timerRef = useRef(0); + + useEffect(() => { + timerRef.current = window.setInterval(() => { + setActiveIndex(prev => { + if (prev >= 0 && prev < count - 1) { + return prev + 1; + } else { + return 0; + } + }); + }, speeds[speed]); + }, []); + + useEffect(() => { + return () => clearInterval(timerRef.current); + }, []); + + const isDarkMode = useMemo(() => isDark(), []); + + const wrapperClass = useMemo(() => { + return cls(styles.wrapper, rtl ? styles.rtl : ''); + }, [rtl]); + + const transition = useMemo(() => { + return { + transition: `background ${speeds[speed]}ms ease-in-out`, + }; + }, [speed]); + + const itemStyle = useMemo(() => { + if (customSize) { + return { + ...transition, + height: `${customSize}px`, + width: `${customSize}px`, + }; + } else { + return transition; + } + }, [customSize, transition]); + + return ( +
+ {Array(count) + .fill(0) + .map((_, index) => ( + + ))} +
+ ); +}; + +export { LoadingIndicator }; diff --git a/packages/lib/components/tabs/__tests__/__snapshots__/tabs.test.tsx.snap b/packages/lib/components/tabs/__tests__/__snapshots__/tabs.test.tsx.snap index 8819a247c..423e239a9 100644 --- a/packages/lib/components/tabs/__tests__/__snapshots__/tabs.test.tsx.snap +++ b/packages/lib/components/tabs/__tests__/__snapshots__/tabs.test.tsx.snap @@ -6,10 +6,10 @@ exports[`Tabs > should render snapshot 1`] = ` role="tablist" >