@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,165 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { m } from '@os-design/media';
|
|
4
|
+
import { WithSize } from '@os-design/styles';
|
|
5
|
+
import { clr } from '@os-design/theming';
|
|
6
|
+
import { omitEmotionProps, useForwardedState } from '@os-design/utils';
|
|
7
|
+
import React, { forwardRef } from 'react';
|
|
8
|
+
import Button, { ButtonProps } from '../Button';
|
|
9
|
+
|
|
10
|
+
export interface RadioGroupOption
|
|
11
|
+
extends Omit<ButtonProps, 'type' | 'wide' | 'size'> {
|
|
12
|
+
/**
|
|
13
|
+
* The title of the option.
|
|
14
|
+
*/
|
|
15
|
+
title: string;
|
|
16
|
+
/**
|
|
17
|
+
* The value of the option.
|
|
18
|
+
*/
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type JsxDivProps = Omit<
|
|
23
|
+
JSX.IntrinsicElements['div'],
|
|
24
|
+
'value' | 'defaultValue' | 'onChange' | 'ref'
|
|
25
|
+
>;
|
|
26
|
+
export interface RadioGroupProps extends JsxDivProps, WithSize {
|
|
27
|
+
/**
|
|
28
|
+
* Options of the radio group.
|
|
29
|
+
* @default undefined
|
|
30
|
+
*/
|
|
31
|
+
options?: RadioGroupOption[];
|
|
32
|
+
/**
|
|
33
|
+
* Whether the radio group has full width.
|
|
34
|
+
* Possible values:
|
|
35
|
+
* `default` – the radio group has full width if the screen width is less than xs;
|
|
36
|
+
* `always` – the radio group always has full width;
|
|
37
|
+
* `never` – the radio group never has full width.
|
|
38
|
+
* @default default
|
|
39
|
+
*/
|
|
40
|
+
wide?: 'default' | 'always' | 'never';
|
|
41
|
+
/**
|
|
42
|
+
* Whether the radio group is disabled.
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Selected option.
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
value?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The default value.
|
|
53
|
+
* @default undefined
|
|
54
|
+
*/
|
|
55
|
+
defaultValue?: string;
|
|
56
|
+
/**
|
|
57
|
+
* The change event handler.
|
|
58
|
+
* @default undefined
|
|
59
|
+
*/
|
|
60
|
+
onChange?: (value: string) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const wideDefaultStyles = (p) =>
|
|
64
|
+
p.wide === 'default' &&
|
|
65
|
+
css`
|
|
66
|
+
${m.max.xxs} {
|
|
67
|
+
width: 100%;
|
|
68
|
+
& > button {
|
|
69
|
+
flex: 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const wideAlwaysStyles = (p) =>
|
|
75
|
+
p.wide === 'always' &&
|
|
76
|
+
css`
|
|
77
|
+
width: 100%;
|
|
78
|
+
& > button {
|
|
79
|
+
flex: 1;
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
type ContainerProps = Required<Pick<RadioGroupProps, 'wide' | 'disabled'>>;
|
|
84
|
+
const Container = styled(
|
|
85
|
+
'div',
|
|
86
|
+
omitEmotionProps('wide', 'disabled')
|
|
87
|
+
)<ContainerProps>`
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
flex-wrap: wrap;
|
|
90
|
+
|
|
91
|
+
border-radius: ${(p) => p.theme.borderRadius}em;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
border: 1px solid
|
|
94
|
+
${(p) =>
|
|
95
|
+
p.disabled
|
|
96
|
+
? clr(p.theme.buttonDisabledGhostColorText)
|
|
97
|
+
: clr(p.theme.colorPrimary)};
|
|
98
|
+
|
|
99
|
+
${wideDefaultStyles};
|
|
100
|
+
${wideAlwaysStyles};
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const StyledButton = styled(Button)`
|
|
104
|
+
border-radius: 0;
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The radio buttons that allow a user to select only one of a limited number of options.
|
|
109
|
+
*/
|
|
110
|
+
const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
111
|
+
(
|
|
112
|
+
{
|
|
113
|
+
options = [],
|
|
114
|
+
wide = 'default',
|
|
115
|
+
disabled = false,
|
|
116
|
+
value,
|
|
117
|
+
defaultValue,
|
|
118
|
+
onChange = () => {},
|
|
119
|
+
size,
|
|
120
|
+
...rest
|
|
121
|
+
},
|
|
122
|
+
ref
|
|
123
|
+
) => {
|
|
124
|
+
const [forwardedValue, setForwardedValue] = useForwardedState({
|
|
125
|
+
value,
|
|
126
|
+
defaultValue,
|
|
127
|
+
onChange,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<Container wide={wide} disabled={disabled} {...rest} ref={ref}>
|
|
133
|
+
{options?.map(
|
|
134
|
+
({
|
|
135
|
+
title,
|
|
136
|
+
value: valueOption,
|
|
137
|
+
disabled: disabledOption,
|
|
138
|
+
onClick = () => {},
|
|
139
|
+
...buttonRest
|
|
140
|
+
}) => (
|
|
141
|
+
<StyledButton
|
|
142
|
+
key={valueOption}
|
|
143
|
+
type={forwardedValue === valueOption ? 'primary' : 'ghost'}
|
|
144
|
+
wide='never'
|
|
145
|
+
disabled={disabled || disabledOption}
|
|
146
|
+
size={size}
|
|
147
|
+
onClick={(e) => {
|
|
148
|
+
setForwardedValue(valueOption);
|
|
149
|
+
onClick(e);
|
|
150
|
+
}}
|
|
151
|
+
{...buttonRest}
|
|
152
|
+
>
|
|
153
|
+
{title}
|
|
154
|
+
</StyledButton>
|
|
155
|
+
)
|
|
156
|
+
)}
|
|
157
|
+
</Container>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
RadioGroup.displayName = 'RadioGroup';
|
|
164
|
+
|
|
165
|
+
export default RadioGroup;
|
|
@@ -0,0 +1,36 @@
|
|
|
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 RadioGroupSkeletonProps extends SkeletonProps, WithSize {
|
|
10
|
+
/**
|
|
11
|
+
* The width of the skeleton.
|
|
12
|
+
* @default 10em
|
|
13
|
+
*/
|
|
14
|
+
width?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const StyledRadioGroupSkeleton = styled(
|
|
18
|
+
Skeleton,
|
|
19
|
+
omitEmotionProps('size')
|
|
20
|
+
)<WithSize>`
|
|
21
|
+
height: calc(${(p) => p.theme.baseHeight}em + 2px);
|
|
22
|
+
${sizeStyles};
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provides a radio group placeholder while a user waits for the content to load.
|
|
27
|
+
*/
|
|
28
|
+
const RadioGroupSkeleton = forwardRef<HTMLDivElement, RadioGroupSkeletonProps>(
|
|
29
|
+
({ width = '10em', ...rest }, ref) => (
|
|
30
|
+
<StyledRadioGroupSkeleton width={width} {...rest} ref={ref} />
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
RadioGroupSkeleton.displayName = 'RadioGroupSkeleton';
|
|
35
|
+
|
|
36
|
+
export default RadioGroupSkeleton;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { m } from '@os-design/media';
|
|
3
|
+
import { sizeStyles, WithSize } from '@os-design/styles';
|
|
4
|
+
import { clr } from '@os-design/theming';
|
|
5
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
6
|
+
import React, { forwardRef } from 'react';
|
|
7
|
+
|
|
8
|
+
type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
|
|
9
|
+
export interface ResultProps extends JsxDivProps, WithSize {
|
|
10
|
+
/**
|
|
11
|
+
* The title of the result.
|
|
12
|
+
*/
|
|
13
|
+
title: string;
|
|
14
|
+
/**
|
|
15
|
+
* The description that is under the title.
|
|
16
|
+
* @default undefined
|
|
17
|
+
*/
|
|
18
|
+
description?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The icon that is located above the title.
|
|
21
|
+
* @default undefined
|
|
22
|
+
*/
|
|
23
|
+
icon?: React.ReactElement;
|
|
24
|
+
/**
|
|
25
|
+
* Active elements.
|
|
26
|
+
* @default undefined
|
|
27
|
+
*/
|
|
28
|
+
actions?: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Container = styled('div', omitEmotionProps('size'))<WithSize>`
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
align-items: center;
|
|
35
|
+
|
|
36
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
37
|
+
padding: 2.6em 0;
|
|
38
|
+
|
|
39
|
+
${sizeStyles};
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const IconContainer = styled.div`
|
|
43
|
+
font-size: 6em;
|
|
44
|
+
line-height: 1;
|
|
45
|
+
color: ${(p) => clr(p.theme.resultColorIcon)};
|
|
46
|
+
margin-bottom: 0.1em;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const Text = styled.div`
|
|
50
|
+
text-align: center;
|
|
51
|
+
max-width: 28em;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const Title = styled(Text)`
|
|
55
|
+
font-size: ${(p) => p.theme.sizes.large}em;
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const Description = styled(Text)`
|
|
60
|
+
font-size: ${(p) => p.theme.sizes.small}em;
|
|
61
|
+
color: ${(p) => clr(p.theme.resultColorDescription)};
|
|
62
|
+
margin-top: 0.5em;
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const ActiveElements = styled.div`
|
|
66
|
+
margin-top: 2em;
|
|
67
|
+
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
align-items: center;
|
|
72
|
+
|
|
73
|
+
& > *:not(:first-of-type) {
|
|
74
|
+
margin-top: 0.5em;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
${m.min.sm} {
|
|
78
|
+
flex-direction: row;
|
|
79
|
+
|
|
80
|
+
& > *:not(:first-of-type) {
|
|
81
|
+
margin-top: 0;
|
|
82
|
+
margin-left: 0.5em;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const ChildrenContainer = styled.div`
|
|
88
|
+
margin-top: 2em;
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Used to give the user feedback instead of results.
|
|
93
|
+
* For example, error happened, results not found, no results yet.
|
|
94
|
+
*/
|
|
95
|
+
const Result = forwardRef<HTMLDivElement, ResultProps>(
|
|
96
|
+
({ title, description, icon, actions, children, ...rest }, ref) => (
|
|
97
|
+
<Container {...rest} ref={ref}>
|
|
98
|
+
{icon && <IconContainer>{icon}</IconContainer>}
|
|
99
|
+
<Title>{title}</Title>
|
|
100
|
+
{description && <Description>{description}</Description>}
|
|
101
|
+
{actions && <ActiveElements>{actions}</ActiveElements>}
|
|
102
|
+
{children && <ChildrenContainer>{children}</ChildrenContainer>}
|
|
103
|
+
</Container>
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
Result.displayName = 'Result';
|
|
108
|
+
|
|
109
|
+
export default Result;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { css, keyframes } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { Down, Up } from '@os-design/icons';
|
|
4
|
+
import Portal from '@os-design/portal';
|
|
5
|
+
import { clr, ThemeOverrider, useTheme } from '@os-design/theming';
|
|
6
|
+
import { omitEmotionProps, useClosable } from '@os-design/utils';
|
|
7
|
+
|
|
8
|
+
import React, {
|
|
9
|
+
forwardRef,
|
|
10
|
+
MouseEventHandler,
|
|
11
|
+
RefObject,
|
|
12
|
+
useCallback,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import Button, { ButtonProps } from '../Button';
|
|
15
|
+
import useContainerPosition, {
|
|
16
|
+
ContainerPosition,
|
|
17
|
+
} from './utils/useContainerPosition';
|
|
18
|
+
import useVisibility from './utils/useVisibility';
|
|
19
|
+
|
|
20
|
+
export interface ScrollButtonProps extends Omit<ButtonProps, 'type' | 'wide'> {
|
|
21
|
+
/**
|
|
22
|
+
* The container that needs to be scrolled.
|
|
23
|
+
* @default undefined
|
|
24
|
+
*/
|
|
25
|
+
container?: Element | RefObject<Element>;
|
|
26
|
+
/**
|
|
27
|
+
* Where the container should be scrolled.
|
|
28
|
+
* @default top
|
|
29
|
+
*/
|
|
30
|
+
scrollTo?: 'top' | 'bottom';
|
|
31
|
+
/**
|
|
32
|
+
* The min scroll offset when the button is visible.
|
|
33
|
+
* @default 500
|
|
34
|
+
*/
|
|
35
|
+
minOffset?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fadeIn = keyframes`
|
|
39
|
+
from { opacity: 0; }
|
|
40
|
+
to { opacity: 1; }
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const fadeOut = keyframes`
|
|
44
|
+
from { opacity: 1; }
|
|
45
|
+
to { opacity: 0; }
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const visibleStyles = (p) =>
|
|
49
|
+
p.visible &&
|
|
50
|
+
css`
|
|
51
|
+
animation: ${fadeIn} ${p.theme.transitionDelay}ms forwards;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const invisibleStyles = (p) =>
|
|
55
|
+
!p.visible &&
|
|
56
|
+
css`
|
|
57
|
+
animation: ${fadeOut} ${p.theme.transitionDelay}ms forwards;
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
interface StyledButtonProps {
|
|
61
|
+
visible: boolean;
|
|
62
|
+
containerPosition: ContainerPosition;
|
|
63
|
+
}
|
|
64
|
+
const StyledButton = styled(
|
|
65
|
+
Button,
|
|
66
|
+
omitEmotionProps('visible', 'containerPosition')
|
|
67
|
+
)<StyledButtonProps>`
|
|
68
|
+
position: fixed;
|
|
69
|
+
right: ${(p) =>
|
|
70
|
+
p.containerPosition.right > 0
|
|
71
|
+
? `calc(${p.containerPosition.right}px + ${p.theme.scrollButtonMargin}em)`
|
|
72
|
+
: `${p.theme.scrollButtonMargin}em`};
|
|
73
|
+
bottom: ${(p) =>
|
|
74
|
+
`calc(${
|
|
75
|
+
p.containerPosition.bottom > 0 ? `${p.containerPosition.bottom}px + ` : ''
|
|
76
|
+
}${p.theme.scrollButtonMargin}em + env(safe-area-inset-bottom))`};
|
|
77
|
+
box-shadow: 0 0.15em 0.8em ${(p) => clr(p.theme.scrollButtonColorBoxShadow)};
|
|
78
|
+
|
|
79
|
+
${visibleStyles};
|
|
80
|
+
${invisibleStyles};
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The button to scroll to either the top or bottom of the container.
|
|
85
|
+
*/
|
|
86
|
+
const ScrollButton = forwardRef<HTMLButtonElement, ScrollButtonProps>(
|
|
87
|
+
(
|
|
88
|
+
{
|
|
89
|
+
container,
|
|
90
|
+
scrollTo = 'top',
|
|
91
|
+
minOffset = 500,
|
|
92
|
+
onClick = () => {},
|
|
93
|
+
...rest
|
|
94
|
+
},
|
|
95
|
+
ref
|
|
96
|
+
) => {
|
|
97
|
+
const visible = useVisibility({ container, scrollTo, minOffset });
|
|
98
|
+
const containerPosition = useContainerPosition(container);
|
|
99
|
+
const { theme } = useTheme();
|
|
100
|
+
const mounted = useClosable(visible, theme.transitionDelay);
|
|
101
|
+
|
|
102
|
+
// Scroll through the container when the user clicks the button
|
|
103
|
+
const clickHandler = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
|
104
|
+
(e) => {
|
|
105
|
+
// Scroll the window if the container is not defined
|
|
106
|
+
if (!container) {
|
|
107
|
+
window.scrollTo({
|
|
108
|
+
top: scrollTo === 'top' ? 0 : document.body.scrollHeight,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Otherwise scroll the container
|
|
114
|
+
const containerElement =
|
|
115
|
+
container instanceof Element ? container : container.current;
|
|
116
|
+
if (!containerElement) return;
|
|
117
|
+
|
|
118
|
+
containerElement.scrollTo({
|
|
119
|
+
top: scrollTo === 'top' ? 0 : containerElement.scrollHeight,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Call the passed onClick handler
|
|
123
|
+
onClick(e);
|
|
124
|
+
},
|
|
125
|
+
[container, scrollTo, onClick]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!mounted) return null;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Portal container={container}>
|
|
132
|
+
<ThemeOverrider
|
|
133
|
+
overrides={(t) => ({
|
|
134
|
+
buttonPrimaryColorBg: t.scrollButtonColorBg,
|
|
135
|
+
buttonPrimaryColorText: t.scrollButtonColorText,
|
|
136
|
+
buttonPrimaryColorBgHover: t.scrollButtonColorBgHover,
|
|
137
|
+
})}
|
|
138
|
+
>
|
|
139
|
+
<StyledButton
|
|
140
|
+
visible={visible}
|
|
141
|
+
containerPosition={containerPosition}
|
|
142
|
+
wide='never'
|
|
143
|
+
size='small'
|
|
144
|
+
onClick={clickHandler}
|
|
145
|
+
aria-label={`Scroll to ${scrollTo}`}
|
|
146
|
+
{...rest}
|
|
147
|
+
ref={ref}
|
|
148
|
+
>
|
|
149
|
+
{scrollTo === 'top' ? <Up /> : <Down />}
|
|
150
|
+
</StyledButton>
|
|
151
|
+
</ThemeOverrider>
|
|
152
|
+
</Portal>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
ScrollButton.displayName = 'ScrollButton';
|
|
158
|
+
|
|
159
|
+
export default ScrollButton;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useBrowserLayoutEffect, useResizeObserver } from '@os-design/utils';
|
|
2
|
+
import { RefObject, useCallback, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface ContainerPosition {
|
|
5
|
+
right: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Calculates the position of the container.
|
|
11
|
+
*/
|
|
12
|
+
const useContainerPosition = (
|
|
13
|
+
container?: Element | RefObject<Element>
|
|
14
|
+
): ContainerPosition => {
|
|
15
|
+
const [position, setPosition] = useState<ContainerPosition>({
|
|
16
|
+
right: 0,
|
|
17
|
+
bottom: 0,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const updatePosition = useCallback(() => {
|
|
21
|
+
if (!container) return;
|
|
22
|
+
const element =
|
|
23
|
+
container instanceof Element ? container : container.current;
|
|
24
|
+
if (!element) return;
|
|
25
|
+
|
|
26
|
+
const { right, bottom } = element.getBoundingClientRect();
|
|
27
|
+
const { innerWidth, innerHeight } = window;
|
|
28
|
+
|
|
29
|
+
setPosition({
|
|
30
|
+
right: innerWidth - right,
|
|
31
|
+
bottom: innerHeight - bottom,
|
|
32
|
+
});
|
|
33
|
+
}, [container]);
|
|
34
|
+
|
|
35
|
+
useBrowserLayoutEffect(() => updatePosition(), [updatePosition]);
|
|
36
|
+
useResizeObserver(container as never, updatePosition);
|
|
37
|
+
|
|
38
|
+
return position;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default useContainerPosition;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useBrowserLayoutEffect, useEvent } from '@os-design/utils';
|
|
2
|
+
import { RefObject, useCallback, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
interface UseVisibilityProps {
|
|
5
|
+
container?: Element | RefObject<Element>;
|
|
6
|
+
scrollTo: 'top' | 'bottom';
|
|
7
|
+
minOffset: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Controls the visibility of the button.
|
|
12
|
+
*/
|
|
13
|
+
const useVisibility = ({
|
|
14
|
+
container,
|
|
15
|
+
scrollTo,
|
|
16
|
+
minOffset,
|
|
17
|
+
}: UseVisibilityProps): boolean => {
|
|
18
|
+
const [visible, setVisible] = useState(false);
|
|
19
|
+
|
|
20
|
+
// Update the visibility of the button when the user scrolls the container
|
|
21
|
+
const updateVisibility = useCallback(() => {
|
|
22
|
+
let offset = 0;
|
|
23
|
+
|
|
24
|
+
if (!container) {
|
|
25
|
+
offset =
|
|
26
|
+
scrollTo === 'top'
|
|
27
|
+
? window.scrollY
|
|
28
|
+
: document.body.scrollHeight - window.scrollY - window.innerHeight;
|
|
29
|
+
} else {
|
|
30
|
+
const containerElement =
|
|
31
|
+
container instanceof Element ? container : container.current;
|
|
32
|
+
if (containerElement === null) return;
|
|
33
|
+
|
|
34
|
+
offset =
|
|
35
|
+
scrollTo === 'top'
|
|
36
|
+
? containerElement.scrollTop
|
|
37
|
+
: containerElement.scrollHeight -
|
|
38
|
+
containerElement.scrollTop -
|
|
39
|
+
containerElement.clientHeight;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setVisible(offset >= minOffset);
|
|
43
|
+
}, [container, scrollTo, minOffset]);
|
|
44
|
+
|
|
45
|
+
useBrowserLayoutEffect(() => updateVisibility(), [updateVisibility]);
|
|
46
|
+
useEvent(
|
|
47
|
+
container ||
|
|
48
|
+
((typeof window !== 'undefined' ? window : undefined) as EventTarget),
|
|
49
|
+
'scroll',
|
|
50
|
+
updateVisibility
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return visible;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default useVisibility;
|