@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.
- package/dist/cjs/InputSearch/index.js +2 -2
- package/dist/cjs/InputSearch/index.js.map +1 -1
- package/dist/esm/InputSearch/index.js +2 -2
- package/dist/esm/InputSearch/index.js.map +1 -1
- package/dist/types/InputSearch/index.d.ts.map +1 -1
- 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,201 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
|
|
3
|
+
import { Eye, EyeInvisible } from '@os-design/icons';
|
|
4
|
+
import { ThemeOverrider, useTheme } from '@os-design/theming';
|
|
5
|
+
import { useForwardedRef, useForwardedState } from '@os-design/utils';
|
|
6
|
+
import getPasswordScore from '@os-team/password-score';
|
|
7
|
+
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import Button from '../Button';
|
|
9
|
+
|
|
10
|
+
import Input, { InputProps } from '../Input';
|
|
11
|
+
|
|
12
|
+
import Progress from '../Progress';
|
|
13
|
+
import defaultLocale, { InputPasswordLocale } from './utils/defaultLocale';
|
|
14
|
+
|
|
15
|
+
export interface InputPasswordProps extends Omit<InputProps, 'type'> {
|
|
16
|
+
/**
|
|
17
|
+
* Whether the password strength meter is visible.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
showStrengthMeter?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The name of a weak, good, strong, and powerful password.
|
|
23
|
+
* Located to the right of the password strength meter.
|
|
24
|
+
* @default undefined
|
|
25
|
+
*/
|
|
26
|
+
strengthNames?: Record<'weak' | 'good' | 'strong' | 'powerful', string>;
|
|
27
|
+
/**
|
|
28
|
+
* From what number of score the password is considered
|
|
29
|
+
* good, strong, and powerful.
|
|
30
|
+
* @default [30, 60, 80]
|
|
31
|
+
*/
|
|
32
|
+
strengthThresholds?: [number, number, number];
|
|
33
|
+
/**
|
|
34
|
+
* The locale.
|
|
35
|
+
* @default undefined
|
|
36
|
+
*/
|
|
37
|
+
locale?: InputPasswordLocale;
|
|
38
|
+
/**
|
|
39
|
+
* The input value.
|
|
40
|
+
* @default undefined
|
|
41
|
+
*/
|
|
42
|
+
value?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The default value.
|
|
45
|
+
* @default undefined
|
|
46
|
+
*/
|
|
47
|
+
defaultValue?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The change event handler.
|
|
50
|
+
* @default undefined
|
|
51
|
+
*/
|
|
52
|
+
onChange?: (value: string) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Selection {
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const StrengthMeterProgress = styled(Progress)`
|
|
61
|
+
margin-top: 0.4em;
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The input for entering a password.
|
|
66
|
+
*/
|
|
67
|
+
const InputPassword = forwardRef<HTMLInputElement, InputPasswordProps>(
|
|
68
|
+
(
|
|
69
|
+
{
|
|
70
|
+
showStrengthMeter = false,
|
|
71
|
+
strengthNames,
|
|
72
|
+
strengthThresholds = [30, 60, 80],
|
|
73
|
+
locale = defaultLocale,
|
|
74
|
+
value,
|
|
75
|
+
defaultValue,
|
|
76
|
+
onChange = () => {},
|
|
77
|
+
onSelect = () => {},
|
|
78
|
+
disabled,
|
|
79
|
+
right,
|
|
80
|
+
...rest
|
|
81
|
+
},
|
|
82
|
+
ref
|
|
83
|
+
) => {
|
|
84
|
+
const [inputRef, mergedInputRef] = useForwardedRef(ref);
|
|
85
|
+
const [forwardedValue, setForwardedValue] = useForwardedState({
|
|
86
|
+
value,
|
|
87
|
+
defaultValue,
|
|
88
|
+
onChange,
|
|
89
|
+
});
|
|
90
|
+
const [selection, setSelection] = useState<Selection>({ start: 0, end: 0 });
|
|
91
|
+
const [invisible, setInvisible] = useState(true);
|
|
92
|
+
const { theme } = useTheme();
|
|
93
|
+
|
|
94
|
+
// Update the selection of the input when changing the invisible flag
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!inputRef.current) return;
|
|
97
|
+
inputRef.current.setSelectionRange(selection.start, selection.end);
|
|
98
|
+
}, [inputRef, selection, invisible]);
|
|
99
|
+
|
|
100
|
+
const score = useMemo(
|
|
101
|
+
() => getPasswordScore(forwardedValue || ''),
|
|
102
|
+
[forwardedValue]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const strength = useMemo(() => {
|
|
106
|
+
if (score >= strengthThresholds[2]) {
|
|
107
|
+
return {
|
|
108
|
+
name: 'powerful',
|
|
109
|
+
color: theme.inputPasswordColorPowerful,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (score >= strengthThresholds[1]) {
|
|
113
|
+
return {
|
|
114
|
+
name: 'strong',
|
|
115
|
+
color: theme.inputPasswordColorStrong,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (score >= strengthThresholds[0]) {
|
|
119
|
+
return {
|
|
120
|
+
name: 'good',
|
|
121
|
+
color: theme.inputPasswordColorGood,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
name: 'weak',
|
|
126
|
+
color: theme.inputPasswordColorWeak,
|
|
127
|
+
};
|
|
128
|
+
}, [
|
|
129
|
+
score,
|
|
130
|
+
strengthThresholds,
|
|
131
|
+
theme.inputPasswordColorPowerful,
|
|
132
|
+
theme.inputPasswordColorStrong,
|
|
133
|
+
theme.inputPasswordColorGood,
|
|
134
|
+
theme.inputPasswordColorWeak,
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<>
|
|
139
|
+
<Input
|
|
140
|
+
type={invisible ? 'password' : 'text'}
|
|
141
|
+
right={
|
|
142
|
+
<>
|
|
143
|
+
<Button
|
|
144
|
+
key='invisible-button'
|
|
145
|
+
type='ghost'
|
|
146
|
+
wide='never'
|
|
147
|
+
size='small'
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
onClick={() => {
|
|
150
|
+
setInvisible(!invisible);
|
|
151
|
+
if (!inputRef.current) return;
|
|
152
|
+
inputRef.current.focus();
|
|
153
|
+
}}
|
|
154
|
+
aria-label={invisible ? locale.showLabel : locale.hideLabel}
|
|
155
|
+
>
|
|
156
|
+
{invisible ? <EyeInvisible /> : <Eye />}
|
|
157
|
+
</Button>
|
|
158
|
+
{right}
|
|
159
|
+
</>
|
|
160
|
+
}
|
|
161
|
+
value={forwardedValue}
|
|
162
|
+
onChange={setForwardedValue}
|
|
163
|
+
onSelect={(e) => {
|
|
164
|
+
// Update the selection state
|
|
165
|
+
const { selectionStart, selectionEnd } = e.currentTarget;
|
|
166
|
+
setSelection({
|
|
167
|
+
start: selectionStart || 0,
|
|
168
|
+
end: selectionEnd || 0,
|
|
169
|
+
});
|
|
170
|
+
onSelect(e);
|
|
171
|
+
}}
|
|
172
|
+
disabled={disabled}
|
|
173
|
+
{...rest}
|
|
174
|
+
ref={mergedInputRef}
|
|
175
|
+
/>
|
|
176
|
+
{showStrengthMeter && (
|
|
177
|
+
<ThemeOverrider
|
|
178
|
+
overrides={{
|
|
179
|
+
progressColorStroke: strength.color,
|
|
180
|
+
get progressColorStrokeSuccess() {
|
|
181
|
+
// eslint-disable-next-line react/no-this-in-sfc
|
|
182
|
+
return this.progressColorStroke;
|
|
183
|
+
},
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<StrengthMeterProgress
|
|
187
|
+
size='small'
|
|
188
|
+
height='0.25em'
|
|
189
|
+
percent={score}
|
|
190
|
+
text={strengthNames ? strengthNames[strength.name] : undefined}
|
|
191
|
+
/>
|
|
192
|
+
</ThemeOverrider>
|
|
193
|
+
)}
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
InputPassword.displayName = 'InputPassword';
|
|
200
|
+
|
|
201
|
+
export default InputPassword;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { keyframes } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { CloseCircle, Search } from '@os-design/icons';
|
|
4
|
+
import { ThemeOverrider } from '@os-design/theming';
|
|
5
|
+
import { useForwardedRef, useForwardedState } from '@os-design/utils';
|
|
6
|
+
import React, { forwardRef } from 'react';
|
|
7
|
+
import Button from '../Button';
|
|
8
|
+
import Input, { InputProps } from '../Input';
|
|
9
|
+
import defaultLocale, { InputSearchLocale } from './utils/defaultLocale';
|
|
10
|
+
|
|
11
|
+
export interface InputSearchProps
|
|
12
|
+
extends Omit<InputProps, 'type' | 'onChange'> {
|
|
13
|
+
/**
|
|
14
|
+
* The locale.
|
|
15
|
+
* @default undefined
|
|
16
|
+
*/
|
|
17
|
+
locale?: InputSearchLocale;
|
|
18
|
+
/**
|
|
19
|
+
* The input value.
|
|
20
|
+
* @default undefined
|
|
21
|
+
*/
|
|
22
|
+
value?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The default value.
|
|
25
|
+
* @default undefined
|
|
26
|
+
*/
|
|
27
|
+
defaultValue?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The change event handler.
|
|
30
|
+
* @default undefined
|
|
31
|
+
*/
|
|
32
|
+
onChange?: (value: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fadeIn = keyframes`
|
|
36
|
+
from { opacity: 0; }
|
|
37
|
+
to { opacity: 1; }
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const ClearButton = styled(Button)`
|
|
41
|
+
animation: ${fadeIn} ${(p) => p.theme.transitionDelay}ms;
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The search input.
|
|
46
|
+
*/
|
|
47
|
+
const InputSearch = forwardRef<HTMLInputElement, InputSearchProps>(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
locale = defaultLocale,
|
|
51
|
+
value,
|
|
52
|
+
defaultValue,
|
|
53
|
+
onChange = () => {},
|
|
54
|
+
disabled,
|
|
55
|
+
left,
|
|
56
|
+
leftHasPadding = true,
|
|
57
|
+
right,
|
|
58
|
+
...rest
|
|
59
|
+
},
|
|
60
|
+
ref
|
|
61
|
+
) => {
|
|
62
|
+
const [inputRef, mergedInputRef] = useForwardedRef(ref);
|
|
63
|
+
const [forwardedValue, setForwardedValue] = useForwardedState({
|
|
64
|
+
value,
|
|
65
|
+
defaultValue,
|
|
66
|
+
onChange,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Input
|
|
71
|
+
type='text'
|
|
72
|
+
left={left || <Search key='search-icon' />}
|
|
73
|
+
leftHasPadding={leftHasPadding}
|
|
74
|
+
right={
|
|
75
|
+
<>
|
|
76
|
+
{!!forwardedValue && (
|
|
77
|
+
<ThemeOverrider overrides={{ buttonIconScaleFactor: 1.2 }}>
|
|
78
|
+
<ClearButton
|
|
79
|
+
key='clear-button'
|
|
80
|
+
type='ghost'
|
|
81
|
+
wide='never'
|
|
82
|
+
size='small'
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
onClick={() => {
|
|
85
|
+
setForwardedValue('');
|
|
86
|
+
if (!inputRef.current) return;
|
|
87
|
+
inputRef.current.focus();
|
|
88
|
+
}}
|
|
89
|
+
aria-label={locale.clearLabel}
|
|
90
|
+
>
|
|
91
|
+
<CloseCircle />
|
|
92
|
+
</ClearButton>
|
|
93
|
+
</ThemeOverrider>
|
|
94
|
+
)}
|
|
95
|
+
{right}
|
|
96
|
+
</>
|
|
97
|
+
}
|
|
98
|
+
value={forwardedValue}
|
|
99
|
+
onChange={setForwardedValue}
|
|
100
|
+
role='searchbox'
|
|
101
|
+
disabled={disabled}
|
|
102
|
+
{...rest}
|
|
103
|
+
ref={mergedInputRef}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
InputSearch.displayName = 'InputSearch';
|
|
110
|
+
|
|
111
|
+
export default InputSearch;
|
|
@@ -0,0 +1,28 @@
|
|
|
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 InputSkeletonProps = Omit<SkeletonProps, 'width'> & WithSize;
|
|
10
|
+
|
|
11
|
+
const StyledInputSkeleton = styled(
|
|
12
|
+
Skeleton,
|
|
13
|
+
omitEmotionProps('size')
|
|
14
|
+
)<WithSize>`
|
|
15
|
+
height: ${(p) => p.theme.baseHeight}em;
|
|
16
|
+
${sizeStyles};
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provides an input placeholder while a user waits for the content to load.
|
|
21
|
+
*/
|
|
22
|
+
const InputSkeleton = forwardRef<HTMLDivElement, InputSkeletonProps>(
|
|
23
|
+
(props, ref) => <StyledInputSkeleton width='100%' {...props} ref={ref} />
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
InputSkeleton.displayName = 'InputSkeleton';
|
|
27
|
+
|
|
28
|
+
export default InputSkeleton;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface LayoutContextProps {
|
|
4
|
+
/**
|
|
5
|
+
* Whether there is the navigation in the layout.
|
|
6
|
+
*/
|
|
7
|
+
hasNavigation: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Whether there is the page header in the layout.
|
|
10
|
+
*/
|
|
11
|
+
hasPageHeader: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LayoutContext = React.createContext<LayoutContextProps>({
|
|
15
|
+
hasNavigation: false,
|
|
16
|
+
hasPageHeader: false,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
LayoutContext.displayName = 'LayoutContext';
|
|
20
|
+
|
|
21
|
+
export default LayoutContext;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import LayoutContext from './LayoutContext';
|
|
3
|
+
|
|
4
|
+
export interface LayoutProps {
|
|
5
|
+
/**
|
|
6
|
+
* Whether there is the navigation in the layout.
|
|
7
|
+
* @default false
|
|
8
|
+
*/
|
|
9
|
+
hasNavigation?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether there is the page header in the layout.
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
hasPageHeader?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The children.
|
|
17
|
+
* @default undefined
|
|
18
|
+
*/
|
|
19
|
+
children?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The layout of the page.
|
|
24
|
+
*/
|
|
25
|
+
const Layout: React.FC<LayoutProps> = ({
|
|
26
|
+
hasNavigation = false,
|
|
27
|
+
hasPageHeader = false,
|
|
28
|
+
children,
|
|
29
|
+
}) => {
|
|
30
|
+
const contextValue = useMemo(
|
|
31
|
+
() => ({ hasNavigation, hasPageHeader }),
|
|
32
|
+
[hasNavigation, hasPageHeader]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<LayoutContext.Provider value={contextValue}>
|
|
37
|
+
{children}
|
|
38
|
+
</LayoutContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
Layout.displayName = 'Layout';
|
|
43
|
+
|
|
44
|
+
export default Layout;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import {
|
|
4
|
+
resetFocusStyles,
|
|
5
|
+
sizeStyles,
|
|
6
|
+
transitionStyles,
|
|
7
|
+
WithSize,
|
|
8
|
+
} from '@os-design/styles';
|
|
9
|
+
import { clr } from '@os-design/theming';
|
|
10
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
11
|
+
import React, { forwardRef } from 'react';
|
|
12
|
+
|
|
13
|
+
export interface ReactRouterLinkProps {
|
|
14
|
+
to?: string;
|
|
15
|
+
replace?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type JsxAProps = Omit<JSX.IntrinsicElements['a'], 'ref'>;
|
|
19
|
+
export interface LinkProps extends JsxAProps, ReactRouterLinkProps, WithSize {
|
|
20
|
+
/**
|
|
21
|
+
* Type of the underline styles.
|
|
22
|
+
* @default hover
|
|
23
|
+
*/
|
|
24
|
+
underline?: 'hover' | 'always' | 'never';
|
|
25
|
+
/**
|
|
26
|
+
* The custom link component.
|
|
27
|
+
* For example, the Link from react-router-dom.
|
|
28
|
+
* @default undefined
|
|
29
|
+
*/
|
|
30
|
+
as?: React.ElementType;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sets base underline styles.
|
|
35
|
+
*/
|
|
36
|
+
const underlineBaseStyles = (p) => css`
|
|
37
|
+
position: relative;
|
|
38
|
+
display: inline-block;
|
|
39
|
+
padding-bottom: 0.1em;
|
|
40
|
+
|
|
41
|
+
&::after {
|
|
42
|
+
position: absolute;
|
|
43
|
+
bottom: 0;
|
|
44
|
+
left: 0;
|
|
45
|
+
content: '';
|
|
46
|
+
height: 0.125em;
|
|
47
|
+
background-color: ${clr(p.theme.linkColor)};
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sets underline styles on hover.
|
|
53
|
+
*/
|
|
54
|
+
const underlineHoverStyles = (p) =>
|
|
55
|
+
p.underline === 'hover' &&
|
|
56
|
+
css`
|
|
57
|
+
@media (hover: hover) {
|
|
58
|
+
${underlineBaseStyles(p)};
|
|
59
|
+
|
|
60
|
+
&::after {
|
|
61
|
+
width: 0;
|
|
62
|
+
opacity: 0;
|
|
63
|
+
${transitionStyles('width', 'opacity')(p)};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:hover::after,
|
|
67
|
+
&:focus::after {
|
|
68
|
+
width: 100%;
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sets underline styles always.
|
|
76
|
+
*/
|
|
77
|
+
const underlineAlwaysStyles = (p) =>
|
|
78
|
+
p.underline === 'always' &&
|
|
79
|
+
css`
|
|
80
|
+
${underlineBaseStyles(p)};
|
|
81
|
+
|
|
82
|
+
&::after {
|
|
83
|
+
width: 100%;
|
|
84
|
+
opacity: 1;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const StyledLink = styled(
|
|
89
|
+
'a',
|
|
90
|
+
omitEmotionProps('size', 'underline', 'as')
|
|
91
|
+
)<LinkProps>`
|
|
92
|
+
${resetFocusStyles};
|
|
93
|
+
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
line-height: 1.2;
|
|
97
|
+
|
|
98
|
+
&,
|
|
99
|
+
&:active,
|
|
100
|
+
&:focus {
|
|
101
|
+
color: ${(p) => clr(p.theme.linkColor)};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
${underlineHoverStyles};
|
|
105
|
+
${underlineAlwaysStyles};
|
|
106
|
+
${sizeStyles};
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The link component to navigate between pages.
|
|
111
|
+
*/
|
|
112
|
+
const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
|
113
|
+
({ underline = 'hover', as, onMouseDown = () => {}, ...rest }, ref) => (
|
|
114
|
+
<StyledLink
|
|
115
|
+
underline={underline}
|
|
116
|
+
as={as}
|
|
117
|
+
onMouseDown={(e) => {
|
|
118
|
+
onMouseDown(e);
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
}}
|
|
121
|
+
{...rest}
|
|
122
|
+
ref={ref}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
Link.displayName = 'Link';
|
|
128
|
+
|
|
129
|
+
export default Link;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
|
|
3
|
+
import styled from '@emotion/styled';
|
|
4
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
5
|
+
import React, { forwardRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { ButtonProps, StyledButton } from '../Button';
|
|
8
|
+
import ButtonContent from '../Button/ButtonContent';
|
|
9
|
+
|
|
10
|
+
import useButtonColors from '../Button/utils/useButtonColors';
|
|
11
|
+
|
|
12
|
+
import { LinkProps, ReactRouterLinkProps } from '../Link';
|
|
13
|
+
|
|
14
|
+
type JsxAProps = Omit<JSX.IntrinsicElements['a'], 'type' | 'ref'>;
|
|
15
|
+
export type LinkButtonProps = JsxAProps &
|
|
16
|
+
ReactRouterLinkProps &
|
|
17
|
+
Pick<LinkProps, 'as'> &
|
|
18
|
+
ButtonProps;
|
|
19
|
+
|
|
20
|
+
const disabledStyles = (p) =>
|
|
21
|
+
p.disabled &&
|
|
22
|
+
css`
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const StyledLinkButton = styled(
|
|
27
|
+
StyledButton.withComponent('a'),
|
|
28
|
+
omitEmotionProps('as', 'disabled')
|
|
29
|
+
)`
|
|
30
|
+
text-decoration: none;
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
${disabledStyles};
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The button that is rendered as the a tag.
|
|
37
|
+
*/
|
|
38
|
+
const LinkButton = forwardRef<HTMLAnchorElement, LinkButtonProps>(
|
|
39
|
+
(
|
|
40
|
+
{
|
|
41
|
+
type = 'primary',
|
|
42
|
+
danger = false,
|
|
43
|
+
left,
|
|
44
|
+
right,
|
|
45
|
+
wide = 'default',
|
|
46
|
+
loading = false,
|
|
47
|
+
disabled = false,
|
|
48
|
+
size,
|
|
49
|
+
as,
|
|
50
|
+
onMouseDown = () => {},
|
|
51
|
+
onKeyDown = () => {},
|
|
52
|
+
children,
|
|
53
|
+
...rest
|
|
54
|
+
},
|
|
55
|
+
ref
|
|
56
|
+
) => {
|
|
57
|
+
const { buttonColors, loadingColors } = useButtonColors({
|
|
58
|
+
type,
|
|
59
|
+
danger,
|
|
60
|
+
disabled,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<StyledLinkButton
|
|
65
|
+
btnType={type}
|
|
66
|
+
colors={buttonColors}
|
|
67
|
+
wide={wide}
|
|
68
|
+
loading={loading}
|
|
69
|
+
disabled={disabled || loading}
|
|
70
|
+
size={size}
|
|
71
|
+
as={as}
|
|
72
|
+
onMouseDown={(e) => {
|
|
73
|
+
onMouseDown(e);
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
}}
|
|
76
|
+
onKeyDown={(e) => {
|
|
77
|
+
onKeyDown(e);
|
|
78
|
+
if (disabled || loading) e.preventDefault();
|
|
79
|
+
}}
|
|
80
|
+
aria-disabled={disabled || loading}
|
|
81
|
+
aria-busy={loading}
|
|
82
|
+
{...rest}
|
|
83
|
+
ref={ref}
|
|
84
|
+
>
|
|
85
|
+
<ButtonContent
|
|
86
|
+
left={left}
|
|
87
|
+
right={right}
|
|
88
|
+
loading={loading}
|
|
89
|
+
loadingColors={loadingColors}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</ButtonContent>
|
|
93
|
+
</StyledLinkButton>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
LinkButton.displayName = 'LinkButton';
|
|
99
|
+
|
|
100
|
+
export default LinkButton;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEvent } from '@os-design/utils';
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
disableBodyPointerEvents,
|
|
7
|
+
enableBodyPointerEventsAfterDelay,
|
|
8
|
+
} from './utils/bodyPointerEvents';
|
|
9
|
+
|
|
10
|
+
export interface ScrollPosition {
|
|
11
|
+
top: number;
|
|
12
|
+
left: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface WindowScrollerProps {
|
|
16
|
+
onScroll?: (props: ScrollPosition) => void;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Specifies the number of milliseconds during which to disable pointer events while
|
|
22
|
+
* a scroll is in progress. This improves performance and makes scrolling smoother.
|
|
23
|
+
*/
|
|
24
|
+
export const DISABLE_BODY_POINTER_EVENTS_TIMEOUT = 150;
|
|
25
|
+
|
|
26
|
+
const WindowScroller: React.FC<WindowScrollerProps> = ({
|
|
27
|
+
onScroll = () => {},
|
|
28
|
+
children,
|
|
29
|
+
}) => {
|
|
30
|
+
const onScrollRef = useRef<WindowScrollerProps['onScroll']>();
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
onScrollRef.current = onScroll;
|
|
34
|
+
}, [onScroll]);
|
|
35
|
+
|
|
36
|
+
const scrollListener = useCallback(() => {
|
|
37
|
+
disableBodyPointerEvents();
|
|
38
|
+
enableBodyPointerEventsAfterDelay(DISABLE_BODY_POINTER_EVENTS_TIMEOUT);
|
|
39
|
+
if (!onScrollRef.current) return;
|
|
40
|
+
onScrollRef.current({
|
|
41
|
+
top: window.pageYOffset,
|
|
42
|
+
left: window.pageXOffset,
|
|
43
|
+
});
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
useEffect(() => () => enableBodyPointerEventsAfterDelay(0), []);
|
|
47
|
+
useEvent(document, 'scroll', scrollListener);
|
|
48
|
+
|
|
49
|
+
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
50
|
+
return <>{children}</>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default WindowScroller;
|