@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,52 @@
|
|
|
1
|
+
import { keyframes } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { clr } from '@os-design/theming';
|
|
4
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
5
|
+
import React, { forwardRef } from 'react';
|
|
6
|
+
|
|
7
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
8
|
+
export interface SkeletonProps extends JsxDivProps {
|
|
9
|
+
/**
|
|
10
|
+
* The width of the skeleton.
|
|
11
|
+
* @default 100%
|
|
12
|
+
*/
|
|
13
|
+
width?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SkeletonAnimation = keyframes`
|
|
17
|
+
from { background-position: 100% 50%; }
|
|
18
|
+
to { background-position: 0 50%; }
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
type StyledSkeletonProps = Required<Pick<SkeletonProps, 'width'>>;
|
|
22
|
+
const StyledSkeleton = styled(
|
|
23
|
+
'div',
|
|
24
|
+
omitEmotionProps('width', 'size')
|
|
25
|
+
)<StyledSkeletonProps>`
|
|
26
|
+
width: ${(p) => p.width};
|
|
27
|
+
height: 1em;
|
|
28
|
+
|
|
29
|
+
background: linear-gradient(
|
|
30
|
+
90deg,
|
|
31
|
+
${(p) => clr(p.theme.skeletonColorBgFrom)} 25%,
|
|
32
|
+
${(p) => clr(p.theme.skeletonColorBgTo)} 37%,
|
|
33
|
+
${(p) => clr(p.theme.skeletonColorBgFrom)} 63%
|
|
34
|
+
);
|
|
35
|
+
background-size: 400% 100%;
|
|
36
|
+
|
|
37
|
+
border-radius: ${(p) => p.theme.borderRadius}em;
|
|
38
|
+
animation: ${SkeletonAnimation} 1.4s ease infinite;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Provides a basic placeholder while a user waits for the content to load.
|
|
43
|
+
*/
|
|
44
|
+
const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
|
|
45
|
+
({ width = '100%', ...rest }, ref) => (
|
|
46
|
+
<StyledSkeleton width={width} aria-busy {...rest} ref={ref} />
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
Skeleton.displayName = 'Skeleton';
|
|
51
|
+
|
|
52
|
+
export default Skeleton;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import {
|
|
4
|
+
resetButtonStyles,
|
|
5
|
+
sizeStyles,
|
|
6
|
+
transitionStyles,
|
|
7
|
+
WithSize,
|
|
8
|
+
} from '@os-design/styles';
|
|
9
|
+
import { clr } from '@os-design/theming';
|
|
10
|
+
import { omitEmotionProps, useForwardedState } from '@os-design/utils';
|
|
11
|
+
import React, { forwardRef, useCallback } from 'react';
|
|
12
|
+
|
|
13
|
+
type JsxButtonProps = Omit<
|
|
14
|
+
JSX.IntrinsicElements['button'],
|
|
15
|
+
'value' | 'defaultValue' | 'onChange' | 'onClick' | 'ref'
|
|
16
|
+
>;
|
|
17
|
+
export interface SwitchProps extends JsxButtonProps, WithSize {
|
|
18
|
+
/**
|
|
19
|
+
* Whether the switch is disabled.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the switch is checked.
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
value?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The default value.
|
|
30
|
+
* @default undefined
|
|
31
|
+
*/
|
|
32
|
+
defaultValue?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The change event handler.
|
|
35
|
+
* @default undefined
|
|
36
|
+
*/
|
|
37
|
+
onChange?: (value: boolean) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FRACTION_DIGITS = 4;
|
|
41
|
+
|
|
42
|
+
const uncheckedStyles = (p) =>
|
|
43
|
+
!p.checked &&
|
|
44
|
+
css`
|
|
45
|
+
background-color: ${clr(p.theme.switchUncheckedColorBg)};
|
|
46
|
+
&::after {
|
|
47
|
+
left: ${+((p.theme.switchHeight - p.theme.switchCircleSize) / 2).toFixed(
|
|
48
|
+
FRACTION_DIGITS
|
|
49
|
+
)}em;
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const checkedStyles = (p) =>
|
|
54
|
+
p.checked &&
|
|
55
|
+
css`
|
|
56
|
+
background-color: ${clr(p.theme.switchCheckedColorBg)};
|
|
57
|
+
&::after {
|
|
58
|
+
left: ${+(
|
|
59
|
+
p.theme.switchWidth -
|
|
60
|
+
p.theme.switchCircleSize -
|
|
61
|
+
(p.theme.switchHeight - p.theme.switchCircleSize) / 2
|
|
62
|
+
).toFixed(FRACTION_DIGITS)}em;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const hoverUncheckedStyles = (p) =>
|
|
67
|
+
!p.checked &&
|
|
68
|
+
css`
|
|
69
|
+
background-color: ${clr(p.theme.switchUncheckedColorBgHover)};
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const hoverCheckedStyles = (p) =>
|
|
73
|
+
p.checked &&
|
|
74
|
+
css`
|
|
75
|
+
background-color: ${clr(p.theme.switchCheckedColorBgHover)};
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const hoverStyles = (p) =>
|
|
79
|
+
!p.disabled &&
|
|
80
|
+
css`
|
|
81
|
+
@media (hover: hover) {
|
|
82
|
+
&:hover,
|
|
83
|
+
&:focus {
|
|
84
|
+
${hoverUncheckedStyles(p)};
|
|
85
|
+
${hoverCheckedStyles(p)};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const disabledUncheckedStyles = (p) =>
|
|
91
|
+
!p.checked &&
|
|
92
|
+
css`
|
|
93
|
+
background-color: ${clr(p.theme.switchDisabledUncheckedColorBg)};
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
const disabledCheckedStyles = (p) =>
|
|
97
|
+
p.checked &&
|
|
98
|
+
css`
|
|
99
|
+
background-color: ${clr(p.theme.switchDisabledCheckedColorBg)};
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const disabledStyles = (p) =>
|
|
103
|
+
p.disabled &&
|
|
104
|
+
css`
|
|
105
|
+
cursor: not-allowed;
|
|
106
|
+
${disabledUncheckedStyles(p)};
|
|
107
|
+
${disabledCheckedStyles(p)};
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
interface StyledSwitchProps extends Pick<SwitchProps, 'disabled' | 'size'> {
|
|
111
|
+
checked: SwitchProps['value'];
|
|
112
|
+
}
|
|
113
|
+
const StyledSwitch = styled(
|
|
114
|
+
'button',
|
|
115
|
+
omitEmotionProps('size', 'checked')
|
|
116
|
+
)<StyledSwitchProps>`
|
|
117
|
+
${resetButtonStyles};
|
|
118
|
+
position: relative;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
user-select: none;
|
|
121
|
+
display: block;
|
|
122
|
+
|
|
123
|
+
width: ${(p) => p.theme.switchWidth}em;
|
|
124
|
+
height: ${(p) => p.theme.switchHeight}em;
|
|
125
|
+
border-radius: ${(p) => p.theme.switchHeight / 2}em;
|
|
126
|
+
|
|
127
|
+
&::after {
|
|
128
|
+
position: absolute;
|
|
129
|
+
top: ${(p) =>
|
|
130
|
+
+((p.theme.switchHeight - p.theme.switchCircleSize) / 2).toFixed(
|
|
131
|
+
FRACTION_DIGITS
|
|
132
|
+
)}em;
|
|
133
|
+
|
|
134
|
+
width: ${(p) => p.theme.switchCircleSize}em;
|
|
135
|
+
height: ${(p) => p.theme.switchCircleSize}em;
|
|
136
|
+
border-radius: 50%;
|
|
137
|
+
|
|
138
|
+
background-color: ${(p) => clr(p.theme.switchColorCircleBg)};
|
|
139
|
+
content: ' ';
|
|
140
|
+
|
|
141
|
+
${transitionStyles('left')};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
${uncheckedStyles};
|
|
145
|
+
${checkedStyles};
|
|
146
|
+
${hoverStyles};
|
|
147
|
+
${disabledStyles};
|
|
148
|
+
|
|
149
|
+
${sizeStyles};
|
|
150
|
+
${transitionStyles('background-color')};
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The switch that can be enabled or disabled.
|
|
155
|
+
*/
|
|
156
|
+
const Switch = forwardRef<HTMLButtonElement, SwitchProps>(
|
|
157
|
+
(
|
|
158
|
+
{
|
|
159
|
+
disabled = false,
|
|
160
|
+
value,
|
|
161
|
+
defaultValue,
|
|
162
|
+
onChange = () => {},
|
|
163
|
+
size,
|
|
164
|
+
onMouseDown = () => {},
|
|
165
|
+
onKeyDown = () => {},
|
|
166
|
+
...rest
|
|
167
|
+
},
|
|
168
|
+
ref
|
|
169
|
+
) => {
|
|
170
|
+
const [forwardedValue, setForwardedValue] = useForwardedState({
|
|
171
|
+
value,
|
|
172
|
+
defaultValue,
|
|
173
|
+
onChange,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const clickHandler = useCallback(() => {
|
|
177
|
+
if (disabled) return;
|
|
178
|
+
setForwardedValue(!forwardedValue);
|
|
179
|
+
}, [disabled, forwardedValue, setForwardedValue]);
|
|
180
|
+
|
|
181
|
+
const mouseDownHandler = useCallback(
|
|
182
|
+
(e) => {
|
|
183
|
+
onMouseDown(e);
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
},
|
|
186
|
+
[onMouseDown]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const keyDownHandler = useCallback(
|
|
190
|
+
(e) => {
|
|
191
|
+
if (e.key === ' ') setForwardedValue(!forwardedValue);
|
|
192
|
+
onKeyDown(e);
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
},
|
|
195
|
+
[forwardedValue, onKeyDown, setForwardedValue]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<StyledSwitch
|
|
200
|
+
disabled={disabled}
|
|
201
|
+
checked={forwardedValue}
|
|
202
|
+
size={size}
|
|
203
|
+
onClick={clickHandler}
|
|
204
|
+
onMouseDown={mouseDownHandler}
|
|
205
|
+
onKeyDown={keyDownHandler}
|
|
206
|
+
role='switch'
|
|
207
|
+
aria-checked={forwardedValue}
|
|
208
|
+
{...rest}
|
|
209
|
+
ref={ref}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
Switch.displayName = 'Switch';
|
|
216
|
+
|
|
217
|
+
export default Switch;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
|
|
3
|
+
import { sizeStyles, WithSize } from '@os-design/styles';
|
|
4
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
5
|
+
import React, { forwardRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import Skeleton, { SkeletonProps } from '../Skeleton';
|
|
8
|
+
|
|
9
|
+
export type SwitchSkeletonProps = Omit<SkeletonProps, 'width'> & WithSize;
|
|
10
|
+
|
|
11
|
+
const StyledSwitchSkeleton = styled(
|
|
12
|
+
Skeleton,
|
|
13
|
+
omitEmotionProps('size')
|
|
14
|
+
)<WithSize>`
|
|
15
|
+
width: ${(p) => p.theme.switchWidth}em;
|
|
16
|
+
height: ${(p) => p.theme.switchHeight}em;
|
|
17
|
+
border-radius: ${(p) => p.theme.switchHeight / 2}em;
|
|
18
|
+
${sizeStyles};
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Provides a switch placeholder while a user waits for the content to load.
|
|
23
|
+
*/
|
|
24
|
+
const SwitchSkeleton = forwardRef<HTMLDivElement, SwitchSkeletonProps>(
|
|
25
|
+
(props, ref) => <StyledSwitchSkeleton width='100%' {...props} ref={ref} />
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
SwitchSkeleton.displayName = 'SwitchSkeleton';
|
|
29
|
+
|
|
30
|
+
export default SwitchSkeleton;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import {
|
|
3
|
+
ellipsisStyles,
|
|
4
|
+
sizeStyles,
|
|
5
|
+
transitionStyles,
|
|
6
|
+
WithSize,
|
|
7
|
+
} from '@os-design/styles';
|
|
8
|
+
import { clr } from '@os-design/theming';
|
|
9
|
+
|
|
10
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
11
|
+
import React, { forwardRef } from 'react';
|
|
12
|
+
|
|
13
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
14
|
+
|
|
15
|
+
export interface BaseTagProps extends WithSize {
|
|
16
|
+
/**
|
|
17
|
+
* The component located on the left side.
|
|
18
|
+
* @default undefined
|
|
19
|
+
*/
|
|
20
|
+
left?: React.ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* The component located on the right side.
|
|
23
|
+
* @default undefined
|
|
24
|
+
*/
|
|
25
|
+
right?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type TagProps = JsxDivProps & BaseTagProps;
|
|
29
|
+
|
|
30
|
+
export const TagContainer = styled('div', omitEmotionProps('size'))<WithSize>`
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
|
|
35
|
+
background-color: ${(p) => clr(p.theme.tagColorBg)};
|
|
36
|
+
color: ${(p) => clr(p.theme.tagColorText)};
|
|
37
|
+
|
|
38
|
+
border-radius: ${(p) => p.theme.borderRadius}em;
|
|
39
|
+
padding: ${(p) => p.theme.tagPaddingVertical}em
|
|
40
|
+
${(p) => p.theme.tagPaddingHorizontal}em;
|
|
41
|
+
|
|
42
|
+
${sizeStyles};
|
|
43
|
+
${transitionStyles('background-color')};
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const Content = styled.div`
|
|
47
|
+
${ellipsisStyles};
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
export const LeftAddon = styled.div`
|
|
51
|
+
display: inherit;
|
|
52
|
+
padding-right: ${(p) => p.theme.tagAddonPaddingHorizontal}em;
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export const RightAddon = styled.div`
|
|
56
|
+
display: inherit;
|
|
57
|
+
padding-left: ${(p) => p.theme.tagAddonPaddingHorizontal}em;
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The component to display a label, tag, or category.
|
|
62
|
+
*/
|
|
63
|
+
const Tag = forwardRef<HTMLDivElement, TagProps>(
|
|
64
|
+
({ left, right, children, ...rest }, ref) => (
|
|
65
|
+
<TagContainer {...rest} ref={ref}>
|
|
66
|
+
{left && <LeftAddon>{left}</LeftAddon>}
|
|
67
|
+
<Content>{children}</Content>
|
|
68
|
+
{right && <RightAddon>{right}</RightAddon>}
|
|
69
|
+
</TagContainer>
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
Tag.displayName = 'Tag';
|
|
74
|
+
|
|
75
|
+
export default Tag;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { clr } from '@os-design/theming';
|
|
3
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
4
|
+
import React, { forwardRef } from 'react';
|
|
5
|
+
|
|
6
|
+
import { LinkProps, ReactRouterLinkProps } from '../Link';
|
|
7
|
+
import { BaseTagProps, LeftAddon, RightAddon, TagContainer } from '../Tag';
|
|
8
|
+
|
|
9
|
+
type JsxAProps = Omit<JSX.IntrinsicElements['a'], 'ref'>;
|
|
10
|
+
export type TagLinkProps = JsxAProps &
|
|
11
|
+
ReactRouterLinkProps &
|
|
12
|
+
Pick<LinkProps, 'as'> &
|
|
13
|
+
BaseTagProps;
|
|
14
|
+
|
|
15
|
+
const StyledTagLink = styled(
|
|
16
|
+
TagContainer.withComponent('a'),
|
|
17
|
+
omitEmotionProps('as')
|
|
18
|
+
)`
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
text-decoration: none;
|
|
21
|
+
|
|
22
|
+
@media (hover: hover) {
|
|
23
|
+
&:hover,
|
|
24
|
+
&:focus {
|
|
25
|
+
background-color: ${(p) => clr(p.theme.tagColorBgHover)};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The tag component with a link.
|
|
32
|
+
*/
|
|
33
|
+
const TagLink = forwardRef<HTMLAnchorElement, TagLinkProps>(
|
|
34
|
+
({ left, right, as, onMouseDown = () => {}, children, ...rest }, ref) => (
|
|
35
|
+
<StyledTagLink
|
|
36
|
+
as={as}
|
|
37
|
+
onMouseDown={(e) => {
|
|
38
|
+
onMouseDown(e);
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
}}
|
|
41
|
+
{...rest}
|
|
42
|
+
ref={ref}
|
|
43
|
+
>
|
|
44
|
+
{left && <LeftAddon>{left}</LeftAddon>}
|
|
45
|
+
{children}
|
|
46
|
+
{right && <RightAddon>{right}</RightAddon>}
|
|
47
|
+
</StyledTagLink>
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
TagLink.displayName = 'TagLink';
|
|
52
|
+
|
|
53
|
+
export default TagLink;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { Down, Up } from '@os-design/icons';
|
|
4
|
+
import { WithSize, sizeStyles } from '@os-design/styles';
|
|
5
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
6
|
+
import React, { forwardRef, useState } from 'react';
|
|
7
|
+
import Button from '../Button';
|
|
8
|
+
|
|
9
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
10
|
+
export interface TagListProps extends JsxDivProps, WithSize {
|
|
11
|
+
/**
|
|
12
|
+
* Whether the tag list is collapsible.
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
collapsible?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const collapsibleStyles = (p) =>
|
|
19
|
+
p.collapsible &&
|
|
20
|
+
!p.opened &&
|
|
21
|
+
css`
|
|
22
|
+
// The height of the tag
|
|
23
|
+
height: ${p.theme.lineHeight + p.theme.tagPaddingVertical * 2}em;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
interface ContainerProps extends Pick<TagListProps, 'collapsible' | 'size'> {
|
|
27
|
+
opened: boolean;
|
|
28
|
+
}
|
|
29
|
+
const Container = styled(
|
|
30
|
+
'div',
|
|
31
|
+
omitEmotionProps('opened', 'collapsible', 'size')
|
|
32
|
+
)<ContainerProps>`
|
|
33
|
+
display: flex;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
${collapsibleStyles};
|
|
36
|
+
${sizeStyles};
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const Content = styled.div`
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-wrap: wrap;
|
|
42
|
+
flex-grow: 1;
|
|
43
|
+
|
|
44
|
+
margin: ${(p) => -p.theme.tagListGap}em 0 0 ${(p) => -p.theme.tagListGap}em;
|
|
45
|
+
& > * {
|
|
46
|
+
margin: ${(p) => p.theme.tagListGap}em 0 0 ${(p) => p.theme.tagListGap}em;
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const Control = styled.div`
|
|
51
|
+
margin-left: 0.2em;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const OpenButton = styled(Button)`
|
|
55
|
+
// The height of the tag
|
|
56
|
+
height: ${(p) => p.theme.lineHeight + p.theme.tagPaddingVertical * 2}em;
|
|
57
|
+
padding: 0 0.5em;
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The component to display a list of tags.
|
|
62
|
+
*/
|
|
63
|
+
const TagList = forwardRef<HTMLDivElement, TagListProps>(
|
|
64
|
+
({ collapsible = false, size, children, ...rest }, ref) => {
|
|
65
|
+
const [opened, setOpened] = useState(false);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Container
|
|
69
|
+
opened={opened}
|
|
70
|
+
collapsible={collapsible}
|
|
71
|
+
size={size}
|
|
72
|
+
{...rest}
|
|
73
|
+
ref={ref}
|
|
74
|
+
>
|
|
75
|
+
<Content>{children}</Content>
|
|
76
|
+
|
|
77
|
+
{collapsible && (
|
|
78
|
+
<Control>
|
|
79
|
+
<OpenButton
|
|
80
|
+
type='ghost'
|
|
81
|
+
wide='never'
|
|
82
|
+
onClick={() => setOpened(!opened)}
|
|
83
|
+
>
|
|
84
|
+
{opened ? <Up /> : <Down />}
|
|
85
|
+
</OpenButton>
|
|
86
|
+
</Control>
|
|
87
|
+
)}
|
|
88
|
+
</Container>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
TagList.displayName = 'TagList';
|
|
94
|
+
|
|
95
|
+
export default TagList;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import TagList, { TagListProps } from '../TagList';
|
|
3
|
+
import TagSkeleton from '../TagSkeleton';
|
|
4
|
+
|
|
5
|
+
export interface TagListSkeletonProps extends TagListProps {
|
|
6
|
+
/**
|
|
7
|
+
* The width of the tag skeleton.
|
|
8
|
+
* @default undefined
|
|
9
|
+
*/
|
|
10
|
+
width?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The number of tag skeletons.
|
|
13
|
+
* @default 3
|
|
14
|
+
*/
|
|
15
|
+
tagsCount?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let tagIndex = 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provides a tag list placeholder while a user waits for the content to load.
|
|
22
|
+
*/
|
|
23
|
+
const TagListSkeleton = forwardRef<HTMLDivElement, TagListSkeletonProps>(
|
|
24
|
+
({ width, tagsCount = 3, ...rest }, ref) => (
|
|
25
|
+
<TagList {...rest} ref={ref}>
|
|
26
|
+
{Array(tagsCount)
|
|
27
|
+
.fill({})
|
|
28
|
+
.map(() => {
|
|
29
|
+
tagIndex += 1;
|
|
30
|
+
return <TagSkeleton key={tagIndex} width={width} />;
|
|
31
|
+
})}
|
|
32
|
+
</TagList>
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
TagListSkeleton.displayName = 'TagListSkeleton';
|
|
37
|
+
|
|
38
|
+
export default TagListSkeleton;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
|
|
3
|
+
import { sizeStyles, WithSize } from '@os-design/styles';
|
|
4
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
5
|
+
import React, { forwardRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import Skeleton, { SkeletonProps } from '../Skeleton';
|
|
8
|
+
|
|
9
|
+
export interface TagSkeletonProps
|
|
10
|
+
extends Omit<SkeletonProps, 'width'>,
|
|
11
|
+
WithSize {
|
|
12
|
+
/**
|
|
13
|
+
* The width of the skeleton.
|
|
14
|
+
* @default 6em
|
|
15
|
+
*/
|
|
16
|
+
width?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type StyledTagSkeletonProps = Pick<TagSkeletonProps, 'width' | 'size'>;
|
|
20
|
+
const StyledTagSkeleton = styled(
|
|
21
|
+
Skeleton,
|
|
22
|
+
omitEmotionProps('width', 'size')
|
|
23
|
+
)<StyledTagSkeletonProps>`
|
|
24
|
+
width: ${(p) => p.width};
|
|
25
|
+
height: ${(p) => p.theme.lineHeight + p.theme.tagPaddingVertical * 2}em;
|
|
26
|
+
${sizeStyles};
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Provides a tag placeholder while a user waits for the content to load.
|
|
31
|
+
*/
|
|
32
|
+
const TagSkeleton = forwardRef<HTMLDivElement, TagSkeletonProps>(
|
|
33
|
+
({ width = '6em', ...rest }, ref) => (
|
|
34
|
+
<StyledTagSkeleton width={width} {...rest} ref={ref} />
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
TagSkeleton.displayName = 'TagSkeleton';
|
|
39
|
+
|
|
40
|
+
export default TagSkeleton;
|