@os-design/core 1.0.199 → 1.0.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +21 -13
  2. package/src/@types/emotion.d.ts +7 -0
  3. package/src/Alert/index.tsx +112 -0
  4. package/src/Avatar/index.tsx +173 -0
  5. package/src/Avatar/utils/nameToInitials.ts +12 -0
  6. package/src/Avatar/utils/strToHue.ts +13 -0
  7. package/src/AvatarSkeleton/index.tsx +29 -0
  8. package/src/Breadcrumb/index.tsx +93 -0
  9. package/src/BreadcrumbItem/index.tsx +83 -0
  10. package/src/Button/ButtonContent.tsx +91 -0
  11. package/src/Button/index.tsx +225 -0
  12. package/src/Button/utils/useButtonColors.ts +84 -0
  13. package/src/Checkbox/index.tsx +225 -0
  14. package/src/CheckboxSkeleton/index.tsx +50 -0
  15. package/src/DatePicker/DatePickerCalendar.tsx +220 -0
  16. package/src/DatePicker/index.tsx +568 -0
  17. package/src/Drawer/index.tsx +212 -0
  18. package/src/Form/FormConfigContext.ts +16 -0
  19. package/src/Form/index.tsx +49 -0
  20. package/src/FormDivider/index.tsx +74 -0
  21. package/src/FormItem/index.tsx +118 -0
  22. package/src/Gallery/Status.tsx +62 -0
  23. package/src/Gallery/index.tsx +290 -0
  24. package/src/GlobalStyles/index.tsx +17 -0
  25. package/src/GlobalStyles/resetStyles.ts +17 -0
  26. package/src/GlobalStyles/typographyStyles.ts +78 -0
  27. package/src/HeaderSkeleton/index.tsx +64 -0
  28. package/src/Image/index.tsx +104 -0
  29. package/src/ImageSkeleton/index.tsx +22 -0
  30. package/src/Input/index.tsx +330 -0
  31. package/src/Input/utils/getFocusableElements.ts +8 -0
  32. package/src/InputNumber/index.tsx +208 -0
  33. package/src/InputNumber/utils/defaultLocale.ts +9 -0
  34. package/src/InputPassword/index.tsx +201 -0
  35. package/src/InputPassword/utils/defaultLocale.ts +11 -0
  36. package/src/InputSearch/index.tsx +111 -0
  37. package/src/InputSearch/utils/defaultLocale.ts +9 -0
  38. package/src/InputSkeleton/index.tsx +28 -0
  39. package/src/Layout/LayoutContext.ts +21 -0
  40. package/src/Layout/index.tsx +44 -0
  41. package/src/Link/index.tsx +129 -0
  42. package/src/LinkButton/index.tsx +100 -0
  43. package/src/List/WindowScroller.tsx +53 -0
  44. package/src/List/index.tsx +255 -0
  45. package/src/List/utils/bodyPointerEvents.ts +24 -0
  46. package/src/List/utils/frameTimeout.ts +36 -0
  47. package/src/List/utils/useRWLoadNext.ts +38 -0
  48. package/src/ListItem/index.tsx +92 -0
  49. package/src/ListItemActions/index.tsx +207 -0
  50. package/src/ListItemLink/index.tsx +63 -0
  51. package/src/ListSkeleton/index.tsx +115 -0
  52. package/src/LogoLink/index.tsx +93 -0
  53. package/src/LogoLink/logo.example.svg +18 -0
  54. package/src/Menu/index.tsx +128 -0
  55. package/src/Menu/utils/useFocusWithArrows.ts +50 -0
  56. package/src/MenuDivider/index.tsx +22 -0
  57. package/src/MenuGroup/index.tsx +190 -0
  58. package/src/MenuItem/index.tsx +108 -0
  59. package/src/Modal/index.tsx +411 -0
  60. package/src/Modal/utils/defaultLocale.ts +9 -0
  61. package/src/Navigation/index.tsx +214 -0
  62. package/src/Navigation/utils/useScrollFlags.ts +39 -0
  63. package/src/NavigationItem/index.tsx +136 -0
  64. package/src/PageContent/index.tsx +99 -0
  65. package/src/PageHeader/index.tsx +246 -0
  66. package/src/PageHeader/utils/defaultLocale.ts +9 -0
  67. package/src/PageHeaderInputSearch/index.tsx +145 -0
  68. package/src/PageHeaderInputSearch/utils/defaultLocale.ts +16 -0
  69. package/src/PageHeaderSkeleton/index.tsx +33 -0
  70. package/src/ParagraphSkeleton/index.tsx +65 -0
  71. package/src/Popover/index.tsx +243 -0
  72. package/src/Popover/utils/usePopoverPosition.ts +216 -0
  73. package/src/Progress/index.tsx +100 -0
  74. package/src/RadioGroup/index.tsx +165 -0
  75. package/src/RadioGroupSkeleton/index.tsx +36 -0
  76. package/src/Result/index.tsx +109 -0
  77. package/src/ScrollButton/index.tsx +159 -0
  78. package/src/ScrollButton/utils/useContainerPosition.ts +41 -0
  79. package/src/ScrollButton/utils/useVisibility.ts +56 -0
  80. package/src/Select/index.tsx +970 -0
  81. package/src/Select/utils/defaultLocale.ts +11 -0
  82. package/src/Skeleton/index.tsx +52 -0
  83. package/src/Switch/index.tsx +217 -0
  84. package/src/SwitchSkeleton/index.tsx +30 -0
  85. package/src/Tag/index.tsx +75 -0
  86. package/src/TagLink/index.tsx +53 -0
  87. package/src/TagList/index.tsx +95 -0
  88. package/src/TagListSkeleton/index.tsx +38 -0
  89. package/src/TagSkeleton/index.tsx +40 -0
  90. package/src/TextArea/index.tsx +231 -0
  91. package/src/TextAreaSkeleton/index.tsx +20 -0
  92. package/src/ThemeSwitcher/index.tsx +39 -0
  93. package/src/TimePicker/index.tsx +142 -0
  94. package/src/Video/index.tsx +41 -0
  95. package/src/index.ts +125 -0
  96. package/src/message/AlertIcon.tsx +50 -0
  97. package/src/message/Message.tsx +108 -0
  98. package/src/message/index.tsx +64 -0
  99. package/src/message/styles.ts +25 -0
@@ -0,0 +1,243 @@
1
+ import { css, keyframes } from '@emotion/react';
2
+ import styled from '@emotion/styled';
3
+ import Portal from '@os-design/portal';
4
+ import { sizeStyles, WithSize } from '@os-design/styles';
5
+ import { clr, useTheme } from '@os-design/theming';
6
+ import {
7
+ omitEmotionProps,
8
+ useBrowserLayoutEffect,
9
+ useClickOutside,
10
+ useClosable,
11
+ useEvent,
12
+ useForwardedRef,
13
+ useResizeObserver,
14
+ } from '@os-design/utils';
15
+ import React, {
16
+ forwardRef,
17
+ RefCallback,
18
+ RefObject,
19
+ useCallback,
20
+ useEffect,
21
+ useMemo,
22
+ useState,
23
+ } from 'react';
24
+ import usePopoverPosition, {
25
+ Placement,
26
+ Rect,
27
+ } from './utils/usePopoverPosition';
28
+
29
+ type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
30
+ export interface PopoverProps extends JsxDivProps, WithSize {
31
+ /**
32
+ * The element next to which the popover appears.
33
+ * @default undefined
34
+ */
35
+ trigger?: RefObject<Element> | Rect;
36
+ /**
37
+ * On which side of the element the popover will appear.
38
+ * @default top
39
+ */
40
+ placement?: Placement;
41
+ /**
42
+ * The gap between the element and the popover in em.
43
+ * @default 0.2
44
+ */
45
+ gap?: number;
46
+ /**
47
+ * Whether the popover to flip if it does not fit in the window.
48
+ * @default true
49
+ */
50
+ flip?: boolean;
51
+ /**
52
+ * Whether the popover is visible.
53
+ * @default false
54
+ */
55
+ visible?: boolean;
56
+ /**
57
+ * The close event handler.
58
+ * @default undefined
59
+ */
60
+ onClose?: () => void;
61
+ }
62
+
63
+ const fadeIn = keyframes`
64
+ from { opacity: 0; }
65
+ to { opacity: 1; }
66
+ `;
67
+
68
+ const fadeOut = keyframes`
69
+ from { opacity: 1; }
70
+ to { opacity: 0; }
71
+ `;
72
+
73
+ const visibleStyles = (p) =>
74
+ p.visible &&
75
+ css`
76
+ animation: ${fadeIn} ${p.theme.transitionDelay}ms forwards;
77
+ `;
78
+
79
+ const invisibleStyles = (p) =>
80
+ !p.visible &&
81
+ css`
82
+ animation: ${fadeOut} ${p.theme.transitionDelay}ms forwards;
83
+ `;
84
+
85
+ interface ContainerProps extends Pick<PopoverProps, 'visible' | 'size'> {
86
+ top: number;
87
+ left: number;
88
+ }
89
+ const Container = styled(
90
+ 'div',
91
+ omitEmotionProps('top', 'left', 'visible', 'size')
92
+ )<ContainerProps>`
93
+ position: absolute;
94
+ top: ${(p) => p.top}px;
95
+ left: ${(p) => p.left}px;
96
+
97
+ border-radius: ${(p) => p.theme.borderRadius}em;
98
+ background-color: ${(p) => clr(p.theme.popoverColorBg)};
99
+ color: ${(p) => clr(p.theme.popoverColorText)};
100
+ border: 1px solid ${(p) => clr(p.theme.popoverColorBorder)};
101
+ box-shadow: 0 0.15em 0.8em ${(p) => clr(p.theme.popoverColorBoxShadow)};
102
+ z-index: 1000; // Greater than the z-index of the Drawer
103
+
104
+ ${visibleStyles};
105
+ ${invisibleStyles};
106
+ ${sizeStyles};
107
+ `;
108
+
109
+ const emptyRect: Rect = {
110
+ top: 0,
111
+ left: 0,
112
+ width: 0,
113
+ height: 0,
114
+ };
115
+
116
+ /**
117
+ * The pop-up window located next to the element.
118
+ */
119
+ const Popover = forwardRef<HTMLDivElement, PopoverProps>(
120
+ (
121
+ {
122
+ trigger,
123
+ placement = 'top',
124
+ gap = 0.2,
125
+ flip = true,
126
+ visible = false,
127
+ onClose = () => {},
128
+ id,
129
+ children,
130
+ ...rest
131
+ },
132
+ ref
133
+ ) => {
134
+ const [popoverRef, mergedPopoverRef] = useForwardedRef(ref);
135
+ const [popoverRect, setPopoverRect] = useState(emptyRect);
136
+ const [triggerRect, setTriggerRect] = useState(emptyRect);
137
+ const { theme } = useTheme();
138
+ const mounted = useClosable(visible, theme.transitionDelay);
139
+
140
+ // Init the rect of the popover and update it when the popover size changes
141
+ const popoverResizeListener = useCallback(() => {
142
+ if (!popoverRef.current) return;
143
+ setPopoverRect(popoverRef.current.getBoundingClientRect());
144
+ }, [popoverRef]);
145
+ useResizeObserver(
146
+ popoverRef.current as HTMLDivElement,
147
+ popoverResizeListener
148
+ );
149
+
150
+ const measuredPopoverRef = useCallback<RefCallback<HTMLDivElement>>(
151
+ (node) => {
152
+ if (node === null) return;
153
+ setPopoverRect(node.getBoundingClientRect());
154
+ mergedPopoverRef(node);
155
+ },
156
+ [mergedPopoverRef]
157
+ );
158
+
159
+ // Init the rect of the trigger and update it when the window was resized
160
+ // or scrolled
161
+ const triggerResizeListener = useCallback(() => {
162
+ window.requestAnimationFrame(() => {
163
+ if (!trigger) return;
164
+ const { current } = trigger as RefObject<Element>;
165
+ if (!current) return;
166
+ setTriggerRect(current.getBoundingClientRect());
167
+ });
168
+ }, [trigger]);
169
+ useBrowserLayoutEffect(() => {
170
+ if (!visible) return;
171
+ triggerResizeListener();
172
+ }, [triggerResizeListener, visible]);
173
+ useResizeObserver(trigger as never, triggerResizeListener);
174
+ useEvent(
175
+ (typeof window === 'undefined' ? null : window) as never,
176
+ 'resize',
177
+ triggerResizeListener
178
+ );
179
+ useEvent(document, 'scroll', triggerResizeListener);
180
+
181
+ useEffect(() => {
182
+ if (!trigger || (trigger as RefObject<Element>).current !== undefined)
183
+ return;
184
+ setTriggerRect(trigger as Rect);
185
+ }, [trigger]);
186
+
187
+ const popoverId = useMemo(
188
+ () => id || `popover-${Math.random().toString(36).slice(2, 11)}`,
189
+ [id]
190
+ );
191
+
192
+ // Set the aria tags to support accessibility features
193
+ useBrowserLayoutEffect(() => {
194
+ if (!trigger) return;
195
+ const { current } = trigger as RefObject<Element>;
196
+ if (!current) return;
197
+ if (current.getAttribute('aria-haspopup') === null) {
198
+ current.setAttribute('aria-haspopup', 'dialog');
199
+ }
200
+ current.setAttribute('aria-controls', popoverId);
201
+ }, []);
202
+ useBrowserLayoutEffect(() => {
203
+ if (!trigger) return;
204
+ const { current } = trigger as RefObject<Element>;
205
+ if (!current) return;
206
+ current.setAttribute('aria-expanded', visible.toString());
207
+ }, [visible]);
208
+
209
+ // Get the popover coordinates
210
+ const { top, left } = usePopoverPosition({
211
+ elementRect: triggerRect,
212
+ popoverRect,
213
+ placement,
214
+ gap,
215
+ flip,
216
+ });
217
+
218
+ // Close the popover when the user clicks outside of it
219
+ useClickOutside(popoverRef, onClose);
220
+
221
+ if (!mounted) return null;
222
+
223
+ return (
224
+ <Portal>
225
+ <Container
226
+ top={top}
227
+ left={left}
228
+ visible={visible}
229
+ id={popoverId}
230
+ role='dialog'
231
+ {...rest}
232
+ ref={measuredPopoverRef}
233
+ >
234
+ {children}
235
+ </Container>
236
+ </Portal>
237
+ );
238
+ }
239
+ );
240
+
241
+ Popover.displayName = 'Popover';
242
+
243
+ export default Popover;
@@ -0,0 +1,216 @@
1
+ import { useFontSize } from '@os-design/utils';
2
+ import { useMemo } from 'react';
3
+
4
+ type Side = 'top' | 'left' | 'right' | 'bottom';
5
+ type Alignment = 'start' | 'end';
6
+ type AlignedPlacement = `${Side}-${Alignment}`;
7
+
8
+ export type Placement = Side | AlignedPlacement;
9
+
10
+ export interface Rect {
11
+ top: number;
12
+ left: number;
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ interface UsePopoverPositionProps {
18
+ /**
19
+ * The rect of the element.
20
+ */
21
+ elementRect: Rect;
22
+ /**
23
+ * The rect of the popover.
24
+ */
25
+ popoverRect: Rect;
26
+ /**
27
+ * On which side of the element the popover will appear.
28
+ * @default top
29
+ */
30
+ placement?: Placement;
31
+ /**
32
+ * The gap between the element and the popover in em.
33
+ * @default 0.5
34
+ */
35
+ gap?: number;
36
+ /**
37
+ * Whether to flip the popover if it does not fit in the window.
38
+ * @default true
39
+ */
40
+ flip?: boolean;
41
+ }
42
+
43
+ interface PopoverPositionGetterOptions {
44
+ elementRect: Rect;
45
+ popoverRect: Rect;
46
+ gap: number;
47
+ flip: boolean;
48
+ }
49
+ type PositionKeys = 'before' | 'after' | 'start' | 'end' | 'center';
50
+ type PopoverPositionGetterFn = (
51
+ options: PopoverPositionGetterOptions
52
+ ) => number;
53
+ type PopoverPositionGetters = Record<PositionKeys, PopoverPositionGetterFn>;
54
+ type FitToWindow = Pick<
55
+ PopoverPositionGetterOptions,
56
+ 'elementRect' | 'popoverRect'
57
+ >;
58
+
59
+ const popoverPositionGetters = (
60
+ rectKey: 'top' | 'left'
61
+ ): PopoverPositionGetters => {
62
+ const sizeKey = rectKey === 'top' ? 'height' : 'width';
63
+ const windowSizeKey = rectKey === 'top' ? 'innerHeight' : 'innerWidth';
64
+ const windowOffsetKey = rectKey === 'top' ? 'pageYOffset' : 'pageXOffset';
65
+
66
+ const fitToWindow = (
67
+ start: number,
68
+ { elementRect, popoverRect }: FitToWindow
69
+ ): number => {
70
+ let popoverStart = start;
71
+ const windowStart = window[windowOffsetKey];
72
+ const windowEnd = windowStart + window[windowSizeKey];
73
+ const elementStart = windowStart + elementRect[rectKey];
74
+ const elementEnd = elementStart + elementRect[sizeKey];
75
+ const popoverEnd = popoverStart + popoverRect[sizeKey];
76
+
77
+ // Fit the popover to the end of the window
78
+ if (popoverEnd > windowEnd) {
79
+ if (elementEnd < windowEnd)
80
+ popoverStart = windowEnd - popoverRect[sizeKey];
81
+ else if (popoverRect[sizeKey] > elementRect[sizeKey])
82
+ popoverStart = elementEnd - popoverRect[sizeKey];
83
+ else if (windowEnd - elementStart > popoverRect[sizeKey])
84
+ popoverStart = windowEnd - popoverRect[sizeKey];
85
+ else popoverStart = elementStart;
86
+ }
87
+
88
+ // Fit the popover to the beginning of the window
89
+ if (popoverStart < windowStart) {
90
+ if (elementStart > windowStart) popoverStart = windowStart;
91
+ else if (popoverRect[sizeKey] > elementRect[sizeKey])
92
+ popoverStart = elementStart;
93
+ else if (elementEnd - windowStart > popoverRect[sizeKey])
94
+ popoverStart = windowStart;
95
+ else popoverStart = elementEnd - popoverRect[sizeKey];
96
+ }
97
+
98
+ return popoverStart;
99
+ };
100
+
101
+ return {
102
+ before(options) {
103
+ const { elementRect, popoverRect, gap, flip } = options;
104
+ const windowStart = window[windowOffsetKey];
105
+ const windowEnd = windowStart + window[windowSizeKey];
106
+ const elementStart = windowStart + elementRect[rectKey];
107
+ const popoverStart = elementStart - popoverRect[sizeKey] - gap;
108
+ if (flip && popoverStart < windowStart) {
109
+ const afterPopoverStart = this.after({ ...options, flip: false });
110
+ const afterPopoverEnd = afterPopoverStart + popoverRect[sizeKey];
111
+ const diffStart = windowStart - popoverStart;
112
+ const diffEnd = afterPopoverEnd - windowEnd;
113
+ if (afterPopoverEnd <= windowEnd || diffStart > diffEnd)
114
+ return afterPopoverStart;
115
+ }
116
+ return popoverStart;
117
+ },
118
+ after(options) {
119
+ const { elementRect, popoverRect, gap, flip } = options;
120
+ const windowStart = window[windowOffsetKey];
121
+ const windowEnd = windowStart + window[windowSizeKey];
122
+ const elementStart = windowStart + elementRect[rectKey];
123
+ const elementEnd = elementStart + elementRect[sizeKey];
124
+ const popoverStart = elementEnd + gap;
125
+ const popoverEnd = popoverStart + popoverRect[sizeKey];
126
+ if (flip && popoverEnd > windowEnd) {
127
+ const beforePopoverStart = this.before({ ...options, flip: false });
128
+ const diffStart = windowStart - beforePopoverStart;
129
+ const diffEnd = popoverEnd - windowEnd;
130
+ if (beforePopoverStart >= windowStart || diffEnd > diffStart)
131
+ return beforePopoverStart;
132
+ }
133
+ return popoverStart;
134
+ },
135
+ start: ({ elementRect, popoverRect }) => {
136
+ const windowStart = window[windowOffsetKey];
137
+ const elementStart = windowStart + elementRect[rectKey];
138
+ return fitToWindow(elementStart, { elementRect, popoverRect });
139
+ },
140
+ end: ({ elementRect, popoverRect }) => {
141
+ const windowStart = window[windowOffsetKey];
142
+ const elementStart = windowStart + elementRect[rectKey];
143
+ const elementEnd = elementStart + elementRect[sizeKey];
144
+ const popoverStart = elementEnd - popoverRect[sizeKey];
145
+ return fitToWindow(popoverStart, { elementRect, popoverRect });
146
+ },
147
+ center: ({ elementRect, popoverRect }) => {
148
+ const windowStart = window[windowOffsetKey];
149
+ const elementStart = windowStart + elementRect[rectKey];
150
+ const popoverStart =
151
+ elementStart + (elementRect[sizeKey] - popoverRect[sizeKey]) / 2;
152
+ return fitToWindow(popoverStart, { elementRect, popoverRect });
153
+ },
154
+ };
155
+ };
156
+
157
+ const getPopoverPosition = (
158
+ top: PositionKeys,
159
+ left: PositionKeys,
160
+ options: PopoverPositionGetterOptions
161
+ ) => ({
162
+ top: popoverPositionGetters('top')[top](options),
163
+ left: popoverPositionGetters('left')[left](options),
164
+ });
165
+
166
+ type PlacementPositionKeysMap = Record<Placement, [PositionKeys, PositionKeys]>;
167
+ const placementPositionKeysMap: PlacementPositionKeysMap = {
168
+ top: ['before', 'center'],
169
+ bottom: ['after', 'center'],
170
+ left: ['center', 'before'],
171
+ right: ['center', 'after'],
172
+ 'top-start': ['before', 'start'],
173
+ 'top-end': ['before', 'end'],
174
+ 'bottom-start': ['after', 'start'],
175
+ 'bottom-end': ['after', 'end'],
176
+ 'left-start': ['start', 'before'],
177
+ 'left-end': ['end', 'before'],
178
+ 'right-start': ['start', 'after'],
179
+ 'right-end': ['end', 'after'],
180
+ };
181
+
182
+ interface PopoverPosition {
183
+ top: number;
184
+ left: number;
185
+ }
186
+
187
+ /**
188
+ * Computes the position of the popover.
189
+ * Note that you must change the elementRect when the scroll and resize events of the parent scrollable container occur.
190
+ * In most cases, it will be the window.
191
+ */
192
+ const usePopoverPosition = ({
193
+ elementRect,
194
+ popoverRect,
195
+ placement = 'top',
196
+ gap = 0.5,
197
+ flip = true,
198
+ }: UsePopoverPositionProps): PopoverPosition => {
199
+ const bodyFontSize = useFontSize(document.body);
200
+ const gapPx = useMemo(() => gap * bodyFontSize, [gap, bodyFontSize]);
201
+
202
+ const positionKeys = useMemo(() => {
203
+ if (typeof placement === 'string' && !!placementPositionKeysMap[placement])
204
+ return placementPositionKeysMap[placement];
205
+ return placementPositionKeysMap.top;
206
+ }, [placement]);
207
+
208
+ return getPopoverPosition(...positionKeys, {
209
+ elementRect,
210
+ popoverRect,
211
+ gap: gapPx,
212
+ flip,
213
+ });
214
+ };
215
+
216
+ export default usePopoverPosition;
@@ -0,0 +1,100 @@
1
+ import styled from '@emotion/styled';
2
+ import { WithSize, sizeStyles, transitionStyles } from '@os-design/styles';
3
+ import { clr } from '@os-design/theming';
4
+
5
+ import { omitEmotionProps } from '@os-design/utils';
6
+ import React, { forwardRef, useMemo } from 'react';
7
+
8
+ type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
9
+ export interface ProgressProps extends JsxDivProps, WithSize {
10
+ /**
11
+ * The percentage of completion.
12
+ * @default 0
13
+ */
14
+ percent?: number;
15
+ /**
16
+ * The text that is displayed to the right of the progress bar.
17
+ * @default undefined
18
+ */
19
+ text?: string;
20
+ /**
21
+ * The height of the progress bar.
22
+ * @default 0.5em
23
+ */
24
+ height?: string;
25
+ }
26
+
27
+ const Container = styled('div', omitEmotionProps('size'))<WithSize>`
28
+ display: flex;
29
+ align-items: center;
30
+ ${sizeStyles};
31
+ `;
32
+
33
+ type TrailProps = Required<Pick<ProgressProps, 'height'>>;
34
+ const Trail = styled('div', omitEmotionProps('height'))<TrailProps>`
35
+ width: 100%;
36
+ background-color: ${(p) => clr(p.theme.progressColorTrail)};
37
+ border-radius: ${(p) => `calc(${p.height} / 2)`};
38
+ overflow: hidden; // To hide the border of the progress bar
39
+ `;
40
+
41
+ type StrokeProps = Required<Pick<ProgressProps, 'percent' | 'height'>>;
42
+ const Stroke = styled(
43
+ 'div',
44
+ omitEmotionProps('percent', 'height')
45
+ )<StrokeProps>`
46
+ width: ${(p) => p.percent}%;
47
+ height: ${(p) => p.height};
48
+ border-radius: 0 ${(p) => `calc(${p.height} / 2)`}
49
+ ${(p) => `calc(${p.height} / 2)`} 0;
50
+ background-color: ${(p) =>
51
+ p.percent < 100
52
+ ? clr(p.theme.progressColorStroke)
53
+ : clr(p.theme.progressColorStrokeSuccess)};
54
+ ${transitionStyles('width', 'background-color')};
55
+ `;
56
+
57
+ const Text = styled.div`
58
+ margin-left: 0.5em;
59
+ color: ${(p) => clr(p.theme.progressColorPercentage)};
60
+ line-height: 1;
61
+ `;
62
+
63
+ /**
64
+ * The progress bar.
65
+ */
66
+ const Progress = forwardRef<HTMLDivElement, ProgressProps>(
67
+ ({ percent = 0, text, height = '0.5em', ...rest }, ref) => {
68
+ const validPercent = useMemo(() => {
69
+ if (percent < 0) return 0;
70
+ if (percent > 100) return 100;
71
+ return percent;
72
+ }, [percent]);
73
+
74
+ const textId = useMemo(
75
+ () => `progress-bar-text-${Math.random().toString(36).slice(2, 11)}`,
76
+ []
77
+ );
78
+
79
+ return (
80
+ <Container
81
+ role='progressbar'
82
+ aria-valuenow={validPercent}
83
+ aria-valuemin={0}
84
+ aria-valuemax={100}
85
+ aria-describedby={textId}
86
+ {...rest}
87
+ ref={ref}
88
+ >
89
+ <Trail height={height}>
90
+ <Stroke percent={validPercent} height={height} />
91
+ </Trail>
92
+ {text && <Text id={textId}>{text}</Text>}
93
+ </Container>
94
+ );
95
+ }
96
+ );
97
+
98
+ Progress.displayName = 'Progress';
99
+
100
+ export default Progress;