@os-design/core 1.0.198 → 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 (104) hide show
  1. package/dist/cjs/InputSearch/index.js +2 -2
  2. package/dist/cjs/InputSearch/index.js.map +1 -1
  3. package/dist/esm/InputSearch/index.js +2 -2
  4. package/dist/esm/InputSearch/index.js.map +1 -1
  5. package/dist/types/InputSearch/index.d.ts.map +1 -1
  6. package/package.json +21 -13
  7. package/src/@types/emotion.d.ts +7 -0
  8. package/src/Alert/index.tsx +112 -0
  9. package/src/Avatar/index.tsx +173 -0
  10. package/src/Avatar/utils/nameToInitials.ts +12 -0
  11. package/src/Avatar/utils/strToHue.ts +13 -0
  12. package/src/AvatarSkeleton/index.tsx +29 -0
  13. package/src/Breadcrumb/index.tsx +93 -0
  14. package/src/BreadcrumbItem/index.tsx +83 -0
  15. package/src/Button/ButtonContent.tsx +91 -0
  16. package/src/Button/index.tsx +225 -0
  17. package/src/Button/utils/useButtonColors.ts +84 -0
  18. package/src/Checkbox/index.tsx +225 -0
  19. package/src/CheckboxSkeleton/index.tsx +50 -0
  20. package/src/DatePicker/DatePickerCalendar.tsx +220 -0
  21. package/src/DatePicker/index.tsx +568 -0
  22. package/src/Drawer/index.tsx +212 -0
  23. package/src/Form/FormConfigContext.ts +16 -0
  24. package/src/Form/index.tsx +49 -0
  25. package/src/FormDivider/index.tsx +74 -0
  26. package/src/FormItem/index.tsx +118 -0
  27. package/src/Gallery/Status.tsx +62 -0
  28. package/src/Gallery/index.tsx +290 -0
  29. package/src/GlobalStyles/index.tsx +17 -0
  30. package/src/GlobalStyles/resetStyles.ts +17 -0
  31. package/src/GlobalStyles/typographyStyles.ts +78 -0
  32. package/src/HeaderSkeleton/index.tsx +64 -0
  33. package/src/Image/index.tsx +104 -0
  34. package/src/ImageSkeleton/index.tsx +22 -0
  35. package/src/Input/index.tsx +330 -0
  36. package/src/Input/utils/getFocusableElements.ts +8 -0
  37. package/src/InputNumber/index.tsx +208 -0
  38. package/src/InputNumber/utils/defaultLocale.ts +9 -0
  39. package/src/InputPassword/index.tsx +201 -0
  40. package/src/InputPassword/utils/defaultLocale.ts +11 -0
  41. package/src/InputSearch/index.tsx +111 -0
  42. package/src/InputSearch/utils/defaultLocale.ts +9 -0
  43. package/src/InputSkeleton/index.tsx +28 -0
  44. package/src/Layout/LayoutContext.ts +21 -0
  45. package/src/Layout/index.tsx +44 -0
  46. package/src/Link/index.tsx +129 -0
  47. package/src/LinkButton/index.tsx +100 -0
  48. package/src/List/WindowScroller.tsx +53 -0
  49. package/src/List/index.tsx +255 -0
  50. package/src/List/utils/bodyPointerEvents.ts +24 -0
  51. package/src/List/utils/frameTimeout.ts +36 -0
  52. package/src/List/utils/useRWLoadNext.ts +38 -0
  53. package/src/ListItem/index.tsx +92 -0
  54. package/src/ListItemActions/index.tsx +207 -0
  55. package/src/ListItemLink/index.tsx +63 -0
  56. package/src/ListSkeleton/index.tsx +115 -0
  57. package/src/LogoLink/index.tsx +93 -0
  58. package/src/LogoLink/logo.example.svg +18 -0
  59. package/src/Menu/index.tsx +128 -0
  60. package/src/Menu/utils/useFocusWithArrows.ts +50 -0
  61. package/src/MenuDivider/index.tsx +22 -0
  62. package/src/MenuGroup/index.tsx +190 -0
  63. package/src/MenuItem/index.tsx +108 -0
  64. package/src/Modal/index.tsx +411 -0
  65. package/src/Modal/utils/defaultLocale.ts +9 -0
  66. package/src/Navigation/index.tsx +214 -0
  67. package/src/Navigation/utils/useScrollFlags.ts +39 -0
  68. package/src/NavigationItem/index.tsx +136 -0
  69. package/src/PageContent/index.tsx +99 -0
  70. package/src/PageHeader/index.tsx +246 -0
  71. package/src/PageHeader/utils/defaultLocale.ts +9 -0
  72. package/src/PageHeaderInputSearch/index.tsx +145 -0
  73. package/src/PageHeaderInputSearch/utils/defaultLocale.ts +16 -0
  74. package/src/PageHeaderSkeleton/index.tsx +33 -0
  75. package/src/ParagraphSkeleton/index.tsx +65 -0
  76. package/src/Popover/index.tsx +243 -0
  77. package/src/Popover/utils/usePopoverPosition.ts +216 -0
  78. package/src/Progress/index.tsx +100 -0
  79. package/src/RadioGroup/index.tsx +165 -0
  80. package/src/RadioGroupSkeleton/index.tsx +36 -0
  81. package/src/Result/index.tsx +109 -0
  82. package/src/ScrollButton/index.tsx +159 -0
  83. package/src/ScrollButton/utils/useContainerPosition.ts +41 -0
  84. package/src/ScrollButton/utils/useVisibility.ts +56 -0
  85. package/src/Select/index.tsx +970 -0
  86. package/src/Select/utils/defaultLocale.ts +11 -0
  87. package/src/Skeleton/index.tsx +52 -0
  88. package/src/Switch/index.tsx +217 -0
  89. package/src/SwitchSkeleton/index.tsx +30 -0
  90. package/src/Tag/index.tsx +75 -0
  91. package/src/TagLink/index.tsx +53 -0
  92. package/src/TagList/index.tsx +95 -0
  93. package/src/TagListSkeleton/index.tsx +38 -0
  94. package/src/TagSkeleton/index.tsx +40 -0
  95. package/src/TextArea/index.tsx +231 -0
  96. package/src/TextAreaSkeleton/index.tsx +20 -0
  97. package/src/ThemeSwitcher/index.tsx +39 -0
  98. package/src/TimePicker/index.tsx +142 -0
  99. package/src/Video/index.tsx +41 -0
  100. package/src/index.ts +125 -0
  101. package/src/message/AlertIcon.tsx +50 -0
  102. package/src/message/Message.tsx +108 -0
  103. package/src/message/index.tsx +64 -0
  104. 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;