@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.
- package/package.json +21 -13
- package/src/@types/emotion.d.ts +7 -0
- package/src/Alert/index.tsx +112 -0
- package/src/Avatar/index.tsx +173 -0
- package/src/Avatar/utils/nameToInitials.ts +12 -0
- package/src/Avatar/utils/strToHue.ts +13 -0
- package/src/AvatarSkeleton/index.tsx +29 -0
- package/src/Breadcrumb/index.tsx +93 -0
- package/src/BreadcrumbItem/index.tsx +83 -0
- package/src/Button/ButtonContent.tsx +91 -0
- package/src/Button/index.tsx +225 -0
- package/src/Button/utils/useButtonColors.ts +84 -0
- package/src/Checkbox/index.tsx +225 -0
- package/src/CheckboxSkeleton/index.tsx +50 -0
- package/src/DatePicker/DatePickerCalendar.tsx +220 -0
- package/src/DatePicker/index.tsx +568 -0
- package/src/Drawer/index.tsx +212 -0
- package/src/Form/FormConfigContext.ts +16 -0
- package/src/Form/index.tsx +49 -0
- package/src/FormDivider/index.tsx +74 -0
- package/src/FormItem/index.tsx +118 -0
- package/src/Gallery/Status.tsx +62 -0
- package/src/Gallery/index.tsx +290 -0
- package/src/GlobalStyles/index.tsx +17 -0
- package/src/GlobalStyles/resetStyles.ts +17 -0
- package/src/GlobalStyles/typographyStyles.ts +78 -0
- package/src/HeaderSkeleton/index.tsx +64 -0
- package/src/Image/index.tsx +104 -0
- package/src/ImageSkeleton/index.tsx +22 -0
- package/src/Input/index.tsx +330 -0
- package/src/Input/utils/getFocusableElements.ts +8 -0
- package/src/InputNumber/index.tsx +208 -0
- package/src/InputNumber/utils/defaultLocale.ts +9 -0
- package/src/InputPassword/index.tsx +201 -0
- package/src/InputPassword/utils/defaultLocale.ts +11 -0
- package/src/InputSearch/index.tsx +111 -0
- package/src/InputSearch/utils/defaultLocale.ts +9 -0
- package/src/InputSkeleton/index.tsx +28 -0
- package/src/Layout/LayoutContext.ts +21 -0
- package/src/Layout/index.tsx +44 -0
- package/src/Link/index.tsx +129 -0
- package/src/LinkButton/index.tsx +100 -0
- package/src/List/WindowScroller.tsx +53 -0
- package/src/List/index.tsx +255 -0
- package/src/List/utils/bodyPointerEvents.ts +24 -0
- package/src/List/utils/frameTimeout.ts +36 -0
- package/src/List/utils/useRWLoadNext.ts +38 -0
- package/src/ListItem/index.tsx +92 -0
- package/src/ListItemActions/index.tsx +207 -0
- package/src/ListItemLink/index.tsx +63 -0
- package/src/ListSkeleton/index.tsx +115 -0
- package/src/LogoLink/index.tsx +93 -0
- package/src/LogoLink/logo.example.svg +18 -0
- package/src/Menu/index.tsx +128 -0
- package/src/Menu/utils/useFocusWithArrows.ts +50 -0
- package/src/MenuDivider/index.tsx +22 -0
- package/src/MenuGroup/index.tsx +190 -0
- package/src/MenuItem/index.tsx +108 -0
- package/src/Modal/index.tsx +411 -0
- package/src/Modal/utils/defaultLocale.ts +9 -0
- package/src/Navigation/index.tsx +214 -0
- package/src/Navigation/utils/useScrollFlags.ts +39 -0
- package/src/NavigationItem/index.tsx +136 -0
- package/src/PageContent/index.tsx +99 -0
- package/src/PageHeader/index.tsx +246 -0
- package/src/PageHeader/utils/defaultLocale.ts +9 -0
- package/src/PageHeaderInputSearch/index.tsx +145 -0
- package/src/PageHeaderInputSearch/utils/defaultLocale.ts +16 -0
- package/src/PageHeaderSkeleton/index.tsx +33 -0
- package/src/ParagraphSkeleton/index.tsx +65 -0
- package/src/Popover/index.tsx +243 -0
- package/src/Popover/utils/usePopoverPosition.ts +216 -0
- package/src/Progress/index.tsx +100 -0
- package/src/RadioGroup/index.tsx +165 -0
- package/src/RadioGroupSkeleton/index.tsx +36 -0
- package/src/Result/index.tsx +109 -0
- package/src/ScrollButton/index.tsx +159 -0
- package/src/ScrollButton/utils/useContainerPosition.ts +41 -0
- package/src/ScrollButton/utils/useVisibility.ts +56 -0
- package/src/Select/index.tsx +970 -0
- package/src/Select/utils/defaultLocale.ts +11 -0
- package/src/Skeleton/index.tsx +52 -0
- package/src/Switch/index.tsx +217 -0
- package/src/SwitchSkeleton/index.tsx +30 -0
- package/src/Tag/index.tsx +75 -0
- package/src/TagLink/index.tsx +53 -0
- package/src/TagList/index.tsx +95 -0
- package/src/TagListSkeleton/index.tsx +38 -0
- package/src/TagSkeleton/index.tsx +40 -0
- package/src/TextArea/index.tsx +231 -0
- package/src/TextAreaSkeleton/index.tsx +20 -0
- package/src/ThemeSwitcher/index.tsx +39 -0
- package/src/TimePicker/index.tsx +142 -0
- package/src/Video/index.tsx +41 -0
- package/src/index.ts +125 -0
- package/src/message/AlertIcon.tsx +50 -0
- package/src/message/Message.tsx +108 -0
- package/src/message/index.tsx +64 -0
- 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;
|