@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,255 @@
|
|
|
1
|
+
import { useIsMinWidth } from '@os-design/media';
|
|
2
|
+
import { useTheme } from '@os-design/theming';
|
|
3
|
+
import {
|
|
4
|
+
useFontSize,
|
|
5
|
+
useForwardedRef,
|
|
6
|
+
useSafeAreaInset,
|
|
7
|
+
useSize,
|
|
8
|
+
} from '@os-design/utils';
|
|
9
|
+
|
|
10
|
+
import React, {
|
|
11
|
+
CSSProperties,
|
|
12
|
+
forwardRef,
|
|
13
|
+
useCallback,
|
|
14
|
+
useContext,
|
|
15
|
+
useMemo,
|
|
16
|
+
} from 'react';
|
|
17
|
+
import {
|
|
18
|
+
FixedSizeList,
|
|
19
|
+
ListChildComponentProps,
|
|
20
|
+
ListProps as RWListProps,
|
|
21
|
+
} from 'react-window';
|
|
22
|
+
import LayoutContext from '../Layout/LayoutContext';
|
|
23
|
+
|
|
24
|
+
import WindowScroller, { ScrollPosition } from './WindowScroller';
|
|
25
|
+
import useRWLoadNext from './utils/useRWLoadNext';
|
|
26
|
+
|
|
27
|
+
export interface ListProps extends Partial<RWListProps> {
|
|
28
|
+
/**
|
|
29
|
+
* Total number of items in the list.
|
|
30
|
+
*/
|
|
31
|
+
itemCount: number;
|
|
32
|
+
/**
|
|
33
|
+
* The item height in em.
|
|
34
|
+
* @default 4
|
|
35
|
+
*/
|
|
36
|
+
itemHeight?: number;
|
|
37
|
+
/**
|
|
38
|
+
* A threshold N means that the onLoadNext function calls when a user scrolls all items except N.
|
|
39
|
+
* @default 10
|
|
40
|
+
*/
|
|
41
|
+
threshold?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Defines how many items outside of the visible "window" to render at all times.
|
|
44
|
+
* @default 10
|
|
45
|
+
*/
|
|
46
|
+
overscanCount?: number;
|
|
47
|
+
/**
|
|
48
|
+
* The height of the list in px.
|
|
49
|
+
* @default undefined
|
|
50
|
+
*/
|
|
51
|
+
height?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The top padding in em.
|
|
54
|
+
* @default 0
|
|
55
|
+
*/
|
|
56
|
+
paddingTop?: number;
|
|
57
|
+
/**
|
|
58
|
+
* The bottom padding in em.
|
|
59
|
+
* @default 0
|
|
60
|
+
*/
|
|
61
|
+
paddingBottom?: number;
|
|
62
|
+
/**
|
|
63
|
+
* The inline css style.
|
|
64
|
+
* @default undefined
|
|
65
|
+
*/
|
|
66
|
+
style?: CSSProperties;
|
|
67
|
+
/**
|
|
68
|
+
* The component that renders if the list is empty.
|
|
69
|
+
* @default undefined
|
|
70
|
+
*/
|
|
71
|
+
empty?: React.ReactNode;
|
|
72
|
+
/**
|
|
73
|
+
* The callback to load more items.
|
|
74
|
+
* @default undefined
|
|
75
|
+
*/
|
|
76
|
+
onLoadNext?: () => void;
|
|
77
|
+
/**
|
|
78
|
+
* The callback to render items.
|
|
79
|
+
* @default () => null
|
|
80
|
+
*/
|
|
81
|
+
itemRenderer?: React.FC<ListChildComponentProps>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Virtualized list.
|
|
86
|
+
* Used the react-window library.
|
|
87
|
+
*/
|
|
88
|
+
const List = forwardRef<FixedSizeList, ListProps>(
|
|
89
|
+
(
|
|
90
|
+
{
|
|
91
|
+
itemCount,
|
|
92
|
+
itemHeight = 4,
|
|
93
|
+
threshold = 10,
|
|
94
|
+
overscanCount = 10,
|
|
95
|
+
height: heightProp,
|
|
96
|
+
paddingTop = 0,
|
|
97
|
+
paddingBottom = 0,
|
|
98
|
+
style = {},
|
|
99
|
+
empty,
|
|
100
|
+
onLoadNext = () => {},
|
|
101
|
+
itemRenderer = () => null,
|
|
102
|
+
...rest
|
|
103
|
+
},
|
|
104
|
+
ref
|
|
105
|
+
) => {
|
|
106
|
+
const [listRef, mergedListRef] = useForwardedRef(ref);
|
|
107
|
+
const size = useSize();
|
|
108
|
+
const fontSize = useFontSize(document.body);
|
|
109
|
+
const { hasNavigation, hasPageHeader } = useContext(LayoutContext);
|
|
110
|
+
const { theme } = useTheme();
|
|
111
|
+
const isMinMd = useIsMinWidth('md');
|
|
112
|
+
const safeAreaInset = useSafeAreaInset();
|
|
113
|
+
|
|
114
|
+
const itemSize = useMemo(
|
|
115
|
+
() => itemHeight * fontSize,
|
|
116
|
+
[itemHeight, fontSize]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const paddingTopSize = useMemo(
|
|
120
|
+
() =>
|
|
121
|
+
((hasPageHeader ? theme.pageHeaderHeight[isMinMd ? 1 : 0] : 0) +
|
|
122
|
+
paddingTop) *
|
|
123
|
+
fontSize,
|
|
124
|
+
[hasPageHeader, theme.pageHeaderHeight, isMinMd, paddingTop, fontSize]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const paddingBottomSize = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
((hasNavigation && !isMinMd ? theme.navigationTabHeight : 0) +
|
|
130
|
+
paddingBottom) *
|
|
131
|
+
fontSize +
|
|
132
|
+
safeAreaInset.bottom,
|
|
133
|
+
[
|
|
134
|
+
hasNavigation,
|
|
135
|
+
theme.navigationTabHeight,
|
|
136
|
+
isMinMd,
|
|
137
|
+
paddingBottom,
|
|
138
|
+
fontSize,
|
|
139
|
+
safeAreaInset.bottom,
|
|
140
|
+
]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const height = useMemo(
|
|
144
|
+
() => heightProp || size.height,
|
|
145
|
+
[heightProp, size.height]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const loadNextHandler = useRWLoadNext({
|
|
149
|
+
itemCount,
|
|
150
|
+
threshold,
|
|
151
|
+
itemSize,
|
|
152
|
+
paddingTop: paddingTopSize,
|
|
153
|
+
height,
|
|
154
|
+
onLoadNext,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const windowScrollHandler = useCallback(
|
|
158
|
+
({ top }: ScrollPosition) => {
|
|
159
|
+
// Set the scroll position to the list
|
|
160
|
+
if (listRef.current) listRef.current.scrollTo(top);
|
|
161
|
+
loadNextHandler(top);
|
|
162
|
+
},
|
|
163
|
+
[listRef, loadNextHandler]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
167
|
+
const InnerElement = useCallback(
|
|
168
|
+
({ style: innerStyle, ...innerRest }) => (
|
|
169
|
+
<div
|
|
170
|
+
style={{
|
|
171
|
+
...innerStyle,
|
|
172
|
+
height: `${
|
|
173
|
+
parseFloat(innerStyle.height) + paddingTopSize + paddingBottomSize
|
|
174
|
+
}px`,
|
|
175
|
+
}}
|
|
176
|
+
{...innerRest}
|
|
177
|
+
/>
|
|
178
|
+
),
|
|
179
|
+
[paddingBottomSize, paddingTopSize]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const listChildren = useCallback<RWListProps['children']>(
|
|
183
|
+
({ style: childrenStyle, ...childrenRest }) =>
|
|
184
|
+
itemRenderer({
|
|
185
|
+
style: {
|
|
186
|
+
...childrenStyle,
|
|
187
|
+
top: `${
|
|
188
|
+
parseFloat(
|
|
189
|
+
childrenStyle.top ? childrenStyle.top.toString() : '0'
|
|
190
|
+
) + paddingTopSize
|
|
191
|
+
}px`,
|
|
192
|
+
},
|
|
193
|
+
...childrenRest,
|
|
194
|
+
}),
|
|
195
|
+
[itemRenderer, paddingTopSize]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
199
|
+
const EmptyComponent = useCallback(() => <>{empty}</>, [empty]);
|
|
200
|
+
|
|
201
|
+
const renderList = useCallback(
|
|
202
|
+
() => (
|
|
203
|
+
<FixedSizeList
|
|
204
|
+
ref={mergedListRef}
|
|
205
|
+
itemCount={itemCount}
|
|
206
|
+
width='100%'
|
|
207
|
+
height={height}
|
|
208
|
+
itemSize={itemSize}
|
|
209
|
+
style={heightProp ? style : { ...style, height: '100% important!' }}
|
|
210
|
+
overscanCount={overscanCount}
|
|
211
|
+
onScroll={
|
|
212
|
+
heightProp
|
|
213
|
+
? ({ scrollOffset }) => loadNextHandler(scrollOffset)
|
|
214
|
+
: undefined
|
|
215
|
+
}
|
|
216
|
+
innerElementType={itemCount > 0 ? InnerElement : EmptyComponent}
|
|
217
|
+
{...rest}
|
|
218
|
+
>
|
|
219
|
+
{listChildren}
|
|
220
|
+
</FixedSizeList>
|
|
221
|
+
),
|
|
222
|
+
[
|
|
223
|
+
EmptyComponent,
|
|
224
|
+
InnerElement,
|
|
225
|
+
height,
|
|
226
|
+
heightProp,
|
|
227
|
+
itemCount,
|
|
228
|
+
itemSize,
|
|
229
|
+
listChildren,
|
|
230
|
+
loadNextHandler,
|
|
231
|
+
mergedListRef,
|
|
232
|
+
overscanCount,
|
|
233
|
+
rest,
|
|
234
|
+
style,
|
|
235
|
+
]
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (heightProp) {
|
|
239
|
+
return renderList();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<WindowScroller onScroll={windowScrollHandler}>
|
|
244
|
+
{renderList()}
|
|
245
|
+
</WindowScroller>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
List.displayName = 'List';
|
|
251
|
+
|
|
252
|
+
export { default as WindowScroller } from './WindowScroller';
|
|
253
|
+
export { default as useRWLoadNext } from './utils/useRWLoadNext';
|
|
254
|
+
|
|
255
|
+
export default List;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { clearFrameTimeout, Frame, setFrameTimeout } from './frameTimeout';
|
|
2
|
+
|
|
3
|
+
let originalBodyPointerEvents: string | null = null;
|
|
4
|
+
let timeoutId: Frame | null = null;
|
|
5
|
+
|
|
6
|
+
export const disableBodyPointerEvents = (): void => {
|
|
7
|
+
if (originalBodyPointerEvents === null) {
|
|
8
|
+
originalBodyPointerEvents = document.body.style.pointerEvents;
|
|
9
|
+
document.body.style.pointerEvents = 'none';
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const enableBodyPointerEvents = (): void => {
|
|
14
|
+
timeoutId = null;
|
|
15
|
+
if (originalBodyPointerEvents !== null) {
|
|
16
|
+
document.body.style.pointerEvents = originalBodyPointerEvents;
|
|
17
|
+
originalBodyPointerEvents = null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const enableBodyPointerEventsAfterDelay = (delay: number): void => {
|
|
22
|
+
if (timeoutId) clearFrameTimeout(timeoutId);
|
|
23
|
+
timeoutId = setFrameTimeout(enableBodyPointerEvents, delay);
|
|
24
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Frame {
|
|
2
|
+
id: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cancels a timeout previously created by setFrameTimeout.
|
|
7
|
+
*/
|
|
8
|
+
export const clearFrameTimeout = (frame: Frame): void => {
|
|
9
|
+
window.cancelAnimationFrame(frame.id);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sets a timeout using requestAnimationFrame.
|
|
14
|
+
*/
|
|
15
|
+
export const setFrameTimeout = (
|
|
16
|
+
handler: () => void,
|
|
17
|
+
timeout: number
|
|
18
|
+
): Frame => {
|
|
19
|
+
let start = 0;
|
|
20
|
+
const frame = { id: 0 };
|
|
21
|
+
|
|
22
|
+
const frameRequestCallback = (timestamp: number) => {
|
|
23
|
+
if (!start) start = timestamp;
|
|
24
|
+
const timePassed = timestamp - start;
|
|
25
|
+
|
|
26
|
+
if (timePassed >= timeout) {
|
|
27
|
+
clearFrameTimeout(frame);
|
|
28
|
+
handler();
|
|
29
|
+
} else {
|
|
30
|
+
frame.id = window.requestAnimationFrame(frameRequestCallback);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
frame.id = window.requestAnimationFrame(frameRequestCallback);
|
|
35
|
+
return frame;
|
|
36
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseRWLoadNextProps {
|
|
4
|
+
itemCount: number;
|
|
5
|
+
threshold: number;
|
|
6
|
+
itemSize: number;
|
|
7
|
+
paddingTop: number;
|
|
8
|
+
height: number;
|
|
9
|
+
onLoadNext: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type OnScroll = (scrollOffset: number) => void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates the onScroll callback for react-window to implement
|
|
16
|
+
* the onLoadNext function.
|
|
17
|
+
*/
|
|
18
|
+
const useRWLoadNext = (props: UseRWLoadNextProps): OnScroll => {
|
|
19
|
+
const propsRef = useRef(props);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
propsRef.current = props;
|
|
23
|
+
}, [props]);
|
|
24
|
+
|
|
25
|
+
return useCallback<OnScroll>((scrollOffset) => {
|
|
26
|
+
const { itemCount, threshold, itemSize, paddingTop, height, onLoadNext } =
|
|
27
|
+
propsRef.current;
|
|
28
|
+
|
|
29
|
+
const maxItemBeforeLoad = itemCount - threshold;
|
|
30
|
+
const maxScrollOffsetBeforeLoad =
|
|
31
|
+
maxItemBeforeLoad * itemSize + paddingTop - height;
|
|
32
|
+
|
|
33
|
+
if (scrollOffset < maxScrollOffsetBeforeLoad) return;
|
|
34
|
+
onLoadNext();
|
|
35
|
+
}, []);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default useRWLoadNext;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { ellipsisStyles, horizontalPaddingStyles } from '@os-design/styles';
|
|
3
|
+
import { clr } from '@os-design/theming';
|
|
4
|
+
import React, { forwardRef } from 'react';
|
|
5
|
+
import ListItemActions, { ListItemActionsProps } from '../ListItemActions';
|
|
6
|
+
|
|
7
|
+
export interface ListItemProps extends ListItemActionsProps {
|
|
8
|
+
/**
|
|
9
|
+
* The title of the item.
|
|
10
|
+
*/
|
|
11
|
+
title: string;
|
|
12
|
+
/**
|
|
13
|
+
* The description of the item.
|
|
14
|
+
* @default undefined
|
|
15
|
+
*/
|
|
16
|
+
description?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The component located on the left side.
|
|
19
|
+
* For example, avatar, button, checkbox.
|
|
20
|
+
* @default undefined
|
|
21
|
+
*/
|
|
22
|
+
left?: React.ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* The component located on the right side.
|
|
25
|
+
* For example, date.
|
|
26
|
+
*/
|
|
27
|
+
right?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const Container = styled(ListItemActions)`
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
min-height: 4em;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
|
|
38
|
+
&:not(:last-of-type) {
|
|
39
|
+
border-bottom: 1px solid ${(p) => clr(p.theme.listItemColorBorder)};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
${horizontalPaddingStyles()};
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const Content = styled.div`
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const Title = styled.div`
|
|
50
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
51
|
+
font-weight: 500;
|
|
52
|
+
${ellipsisStyles};
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const Description = styled.div`
|
|
56
|
+
color: ${(p) => clr(p.theme.listItemColorDescription)};
|
|
57
|
+
font-size: ${(p) => p.theme.sizes.small}em;
|
|
58
|
+
${ellipsisStyles};
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const LeftAddon = styled.div`
|
|
62
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
63
|
+
padding-right: ${(p) => p.theme.listItemAddonPaddingHorizontal}em;
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const RightAddon = styled.div`
|
|
67
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
68
|
+
margin-left: auto;
|
|
69
|
+
padding-left: ${(p) => p.theme.listItemAddonPaddingHorizontal}em;
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The base list item.
|
|
74
|
+
*/
|
|
75
|
+
const ListItem = forwardRef<HTMLDivElement, ListItemProps>(
|
|
76
|
+
({ title, description, left, right, ...rest }, ref) => (
|
|
77
|
+
<Container {...rest} ref={ref}>
|
|
78
|
+
{left && <LeftAddon>{left}</LeftAddon>}
|
|
79
|
+
|
|
80
|
+
<Content>
|
|
81
|
+
<Title>{title}</Title>
|
|
82
|
+
{description && <Description>{description}</Description>}
|
|
83
|
+
</Content>
|
|
84
|
+
|
|
85
|
+
{right && <RightAddon>{right}</RightAddon>}
|
|
86
|
+
</Container>
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
ListItem.displayName = 'ListItem';
|
|
91
|
+
|
|
92
|
+
export default ListItem;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { sizeStyles, transitionStyles, WithSize } from '@os-design/styles';
|
|
4
|
+
import { clr, Color } from '@os-design/theming';
|
|
5
|
+
import {
|
|
6
|
+
isTouchDevice,
|
|
7
|
+
omitEmotionProps,
|
|
8
|
+
useForwardedRef,
|
|
9
|
+
useSwipe,
|
|
10
|
+
} from '@os-design/utils';
|
|
11
|
+
import React, {
|
|
12
|
+
forwardRef,
|
|
13
|
+
useCallback,
|
|
14
|
+
useLayoutEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useState,
|
|
17
|
+
} from 'react';
|
|
18
|
+
import Button, { ButtonProps } from '../Button';
|
|
19
|
+
|
|
20
|
+
export interface ListItemAction extends Omit<ButtonProps, 'type' | 'wide'> {
|
|
21
|
+
icon: React.ReactElement;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
25
|
+
export interface ListItemActionsProps extends JsxDivProps, WithSize {
|
|
26
|
+
/**
|
|
27
|
+
* Buttons to control the item.
|
|
28
|
+
* For example, edit, delete.
|
|
29
|
+
* @default undefined
|
|
30
|
+
*/
|
|
31
|
+
actions?: ListItemAction[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Container = styled.div`
|
|
35
|
+
position: relative;
|
|
36
|
+
|
|
37
|
+
@media (hover: hover) {
|
|
38
|
+
// Hide actions by default
|
|
39
|
+
& > nav {
|
|
40
|
+
opacity: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Display actions either on focus, or on hover
|
|
44
|
+
&:hover,
|
|
45
|
+
&:focus-within {
|
|
46
|
+
& > nav {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const swipeableStyles = (p) =>
|
|
54
|
+
p.swipeable &&
|
|
55
|
+
css`
|
|
56
|
+
transform: translateX(100%);
|
|
57
|
+
${transitionStyles('transform')(p)};
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const openedStyles = (p) =>
|
|
61
|
+
p.opened &&
|
|
62
|
+
css`
|
|
63
|
+
transform: translateX(0);
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
interface ActionsProps extends WithSize {
|
|
67
|
+
swipeable: boolean;
|
|
68
|
+
opened: boolean;
|
|
69
|
+
paddingRight: number;
|
|
70
|
+
}
|
|
71
|
+
const Actions = styled(
|
|
72
|
+
'nav',
|
|
73
|
+
omitEmotionProps('swipeable', 'opened', 'paddingRight', 'size')
|
|
74
|
+
)<ActionsProps>`
|
|
75
|
+
position: absolute;
|
|
76
|
+
top: 0;
|
|
77
|
+
right: 0;
|
|
78
|
+
bottom: 0;
|
|
79
|
+
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
|
|
83
|
+
padding-left: ${(p) => p.theme.listItemActionsPaddingLeft}em;
|
|
84
|
+
padding-right: ${(p) => p.paddingRight}px;
|
|
85
|
+
|
|
86
|
+
background: linear-gradient(
|
|
87
|
+
to right,
|
|
88
|
+
${(p) => clr([...p.theme.colorBg.slice(0, 3), 0] as Color)},
|
|
89
|
+
${(p) => clr(p.theme.colorBg)}
|
|
90
|
+
${(p) => p.theme.listItemActionsPaddingLeft}em
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
${swipeableStyles};
|
|
94
|
+
${openedStyles};
|
|
95
|
+
${sizeStyles}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
let actionIndex = 0;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Shows the menu in the list item.
|
|
102
|
+
*/
|
|
103
|
+
const ListItemActions = forwardRef<HTMLDivElement, ListItemActionsProps>(
|
|
104
|
+
(
|
|
105
|
+
{
|
|
106
|
+
actions = [],
|
|
107
|
+
onTouchStart = () => {},
|
|
108
|
+
onTouchMove = () => {},
|
|
109
|
+
onTouchEnd = () => {},
|
|
110
|
+
children,
|
|
111
|
+
size,
|
|
112
|
+
...rest
|
|
113
|
+
},
|
|
114
|
+
ref
|
|
115
|
+
) => {
|
|
116
|
+
const [containerRef, mergedContainerRef] = useForwardedRef(ref);
|
|
117
|
+
const { opened, handlers } = useSwipe();
|
|
118
|
+
const swipeable = useMemo(() => isTouchDevice(), []);
|
|
119
|
+
|
|
120
|
+
const touchStartHandler = useCallback(
|
|
121
|
+
(e) => {
|
|
122
|
+
handlers.onTouchStart(e);
|
|
123
|
+
onTouchStart(e);
|
|
124
|
+
},
|
|
125
|
+
[handlers, onTouchStart]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const touchMoveHandler = useCallback(
|
|
129
|
+
(e) => {
|
|
130
|
+
handlers.onTouchMove(e);
|
|
131
|
+
onTouchMove(e);
|
|
132
|
+
},
|
|
133
|
+
[handlers, onTouchMove]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const touchEndHandler = useCallback(
|
|
137
|
+
(e) => {
|
|
138
|
+
handlers.onTouchEnd(e);
|
|
139
|
+
onTouchEnd(e);
|
|
140
|
+
},
|
|
141
|
+
[handlers, onTouchEnd]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const [paddingRight, setPaddingRight] = useState(0);
|
|
145
|
+
useLayoutEffect(() => {
|
|
146
|
+
if (!containerRef.current) return;
|
|
147
|
+
const value = Number(
|
|
148
|
+
window
|
|
149
|
+
.getComputedStyle(containerRef.current)
|
|
150
|
+
.getPropertyValue('padding-right')
|
|
151
|
+
.replace('px', '')
|
|
152
|
+
);
|
|
153
|
+
setPaddingRight(value);
|
|
154
|
+
}, [containerRef]);
|
|
155
|
+
|
|
156
|
+
const actionsComponent = useMemo(() => {
|
|
157
|
+
if (actions.length === 0) return null;
|
|
158
|
+
const items = actions.map(
|
|
159
|
+
({ icon, onClick = () => {}, ...actionRest }) => {
|
|
160
|
+
actionIndex += 1;
|
|
161
|
+
return (
|
|
162
|
+
<Button
|
|
163
|
+
key={actionIndex}
|
|
164
|
+
type='ghost'
|
|
165
|
+
wide='never'
|
|
166
|
+
size={size}
|
|
167
|
+
onClick={(e) => {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
onClick(e);
|
|
170
|
+
}}
|
|
171
|
+
{...actionRest}
|
|
172
|
+
>
|
|
173
|
+
{icon}
|
|
174
|
+
</Button>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
return (
|
|
179
|
+
<Actions
|
|
180
|
+
swipeable={swipeable}
|
|
181
|
+
opened={opened}
|
|
182
|
+
paddingRight={paddingRight}
|
|
183
|
+
size={size}
|
|
184
|
+
>
|
|
185
|
+
{items}
|
|
186
|
+
</Actions>
|
|
187
|
+
);
|
|
188
|
+
}, [actions, opened, paddingRight, size, swipeable]);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<Container
|
|
192
|
+
onTouchStart={touchStartHandler}
|
|
193
|
+
onTouchMove={touchMoveHandler}
|
|
194
|
+
onTouchEnd={touchEndHandler}
|
|
195
|
+
{...rest}
|
|
196
|
+
ref={mergedContainerRef}
|
|
197
|
+
>
|
|
198
|
+
{children}
|
|
199
|
+
{actionsComponent}
|
|
200
|
+
</Container>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
ListItemActions.displayName = 'ListItemActions';
|
|
206
|
+
|
|
207
|
+
export default ListItemActions;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { resetFocusStyles } from '@os-design/styles';
|
|
3
|
+
import { clr, Color } from '@os-design/theming';
|
|
4
|
+
import React, { forwardRef } from 'react';
|
|
5
|
+
import { LinkProps, ReactRouterLinkProps } from '../Link';
|
|
6
|
+
import ListItem, { ListItemProps } from '../ListItem';
|
|
7
|
+
|
|
8
|
+
type JsxAProps = Omit<JSX.IntrinsicElements['a'], 'ref'>;
|
|
9
|
+
export type ListItemLinkProps = JsxAProps &
|
|
10
|
+
ReactRouterLinkProps &
|
|
11
|
+
Pick<LinkProps, 'as'> &
|
|
12
|
+
ListItemProps;
|
|
13
|
+
|
|
14
|
+
const Link = styled.a`
|
|
15
|
+
${resetFocusStyles};
|
|
16
|
+
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
text-decoration: none;
|
|
19
|
+
|
|
20
|
+
&:not(:last-of-type) {
|
|
21
|
+
border-bottom: 1px solid ${(p) => clr(p.theme.listItemColorBorder)};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@media (hover: hover) {
|
|
25
|
+
&:hover,
|
|
26
|
+
&:focus-within {
|
|
27
|
+
& > div {
|
|
28
|
+
background-color: ${(p) => clr(p.theme.listItemColorBgHover)};
|
|
29
|
+
|
|
30
|
+
& > nav {
|
|
31
|
+
background: linear-gradient(
|
|
32
|
+
to right,
|
|
33
|
+
${(p) =>
|
|
34
|
+
clr([...p.theme.listItemColorBgHover.slice(0, 3), 0] as Color)},
|
|
35
|
+
${(p) => clr(p.theme.listItemColorBgHover)}
|
|
36
|
+
${(p) => p.theme.listItemActionsPaddingLeft}em
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The list item with a link.
|
|
46
|
+
*/
|
|
47
|
+
const ListItemLink = forwardRef<HTMLAnchorElement, ListItemLinkProps>(
|
|
48
|
+
({ title, description, left, right, actions, ...rest }, ref) => (
|
|
49
|
+
<Link {...rest} ref={ref}>
|
|
50
|
+
<ListItem
|
|
51
|
+
title={title}
|
|
52
|
+
description={description}
|
|
53
|
+
left={left}
|
|
54
|
+
right={right}
|
|
55
|
+
actions={actions}
|
|
56
|
+
/>
|
|
57
|
+
</Link>
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
ListItemLink.displayName = 'ListItemLink';
|
|
62
|
+
|
|
63
|
+
export default ListItemLink;
|