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