@cloud-ru/uikit-product-button-predefined 0.7.4
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/CHANGELOG.md +984 -0
- package/LICENSE +201 -0
- package/README.md +8 -0
- package/package.json +51 -0
- package/src/components/ButtonDropdown/ButtonDropdown.tsx +60 -0
- package/src/components/ButtonDropdown/index.ts +1 -0
- package/src/components/ButtonPromo/ButtonPromo.tsx +43 -0
- package/src/components/ButtonPromo/index.ts +1 -0
- package/src/components/ButtonPromo/styles.module.scss +81 -0
- package/src/components/ButtonPromoOutline/ButtonPromoOutline.tsx +43 -0
- package/src/components/ButtonPromoOutline/index.ts +1 -0
- package/src/components/ButtonPromoOutline/styles.module.scss +81 -0
- package/src/components/index.ts +3 -0
- package/src/constants.ts +29 -0
- package/src/helperComponents/ButtonPrivate/ButtonPrivate.tsx +99 -0
- package/src/helperComponents/ButtonPrivate/constants.ts +13 -0
- package/src/helperComponents/ButtonPrivate/index.ts +1 -0
- package/src/helperComponents/ButtonPrivate/styles.module.scss +46 -0
- package/src/helperComponents/ButtonPrivate/utils.tsx +92 -0
- package/src/helperComponents/index.ts +1 -0
- package/src/hooks.ts +15 -0
- package/src/index.ts +1 -0
- package/src/styles.module.scss +79 -0
- package/src/types.ts +63 -0
- package/src/utils.ts +15 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import { ForwardedRef, forwardRef, MouseEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
|
|
5
|
+
|
|
6
|
+
import { APPEARANCE, HTML_TYPE, ICON_POSITION, TARGET } from '../../constants';
|
|
7
|
+
import { CommonButtonProps } from '../../types';
|
|
8
|
+
import { APPEARANCE_TO_COLOR_MAP } from './constants';
|
|
9
|
+
import styles from './styles.module.scss';
|
|
10
|
+
import { getChildren, getVariant } from './utils';
|
|
11
|
+
|
|
12
|
+
export type ButtonPrivateProps = WithSupportProps<
|
|
13
|
+
CommonButtonProps & {
|
|
14
|
+
iconClassName: string;
|
|
15
|
+
labelClassName: string;
|
|
16
|
+
fullWidth?: boolean;
|
|
17
|
+
}
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export const ButtonPrivate = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPrivateProps>(
|
|
21
|
+
(
|
|
22
|
+
{
|
|
23
|
+
className,
|
|
24
|
+
disabled,
|
|
25
|
+
href,
|
|
26
|
+
icon,
|
|
27
|
+
iconClassName,
|
|
28
|
+
iconPosition = ICON_POSITION.After,
|
|
29
|
+
label,
|
|
30
|
+
labelClassName,
|
|
31
|
+
loading,
|
|
32
|
+
onClick,
|
|
33
|
+
onKeyDown,
|
|
34
|
+
onFocus,
|
|
35
|
+
onBlur,
|
|
36
|
+
size,
|
|
37
|
+
target = TARGET.Blank,
|
|
38
|
+
type = HTML_TYPE.Button,
|
|
39
|
+
appearance = APPEARANCE.Tertiary,
|
|
40
|
+
tabIndex: tabIndexProp = 0,
|
|
41
|
+
fullWidth,
|
|
42
|
+
...rest
|
|
43
|
+
},
|
|
44
|
+
ref,
|
|
45
|
+
) => {
|
|
46
|
+
const variant = getVariant({ label, iconPosition, icon });
|
|
47
|
+
const children = getChildren({
|
|
48
|
+
icon,
|
|
49
|
+
iconClassName,
|
|
50
|
+
iconPosition,
|
|
51
|
+
label,
|
|
52
|
+
labelClassName,
|
|
53
|
+
loading,
|
|
54
|
+
});
|
|
55
|
+
const tabIndex = loading || disabled ? -1 : tabIndexProp;
|
|
56
|
+
|
|
57
|
+
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
58
|
+
if (disabled || loading) {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (onClick) {
|
|
64
|
+
onClick(event);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const buttonProps = {
|
|
69
|
+
...extractSupportProps(rest),
|
|
70
|
+
className: cn(styles.button, className),
|
|
71
|
+
'data-disabled': disabled || undefined,
|
|
72
|
+
'aria-disabled': disabled || undefined,
|
|
73
|
+
'data-loading': loading || undefined,
|
|
74
|
+
'data-size': size,
|
|
75
|
+
'data-full-width': fullWidth || undefined,
|
|
76
|
+
'data-appearance': APPEARANCE_TO_COLOR_MAP[appearance],
|
|
77
|
+
'data-variant': variant,
|
|
78
|
+
onClick: handleClick,
|
|
79
|
+
onKeyDown,
|
|
80
|
+
onFocus,
|
|
81
|
+
onBlur,
|
|
82
|
+
tabIndex,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (href) {
|
|
86
|
+
return (
|
|
87
|
+
<a role='button' href={href} target={target} {...buttonProps} ref={ref as ForwardedRef<HTMLAnchorElement>}>
|
|
88
|
+
{children}
|
|
89
|
+
</a>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<button {...buttonProps} type={type} ref={ref as ForwardedRef<HTMLButtonElement>}>
|
|
95
|
+
{children}
|
|
96
|
+
</button>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { APPEARANCE } from '../../constants';
|
|
2
|
+
|
|
3
|
+
export enum Variant {
|
|
4
|
+
LabelOnly = 'label-only',
|
|
5
|
+
IconOnly = 'icon-only',
|
|
6
|
+
IconBefore = 'icon-before',
|
|
7
|
+
IconAfter = 'icon-after',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const APPEARANCE_TO_COLOR_MAP = {
|
|
11
|
+
[APPEARANCE.Tertiary]: 'fluorescent-yellow',
|
|
12
|
+
[APPEARANCE.Secondary]: 'graphite',
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ButtonPrivate';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-web/build/scss/components/styles-tokens-element';
|
|
2
|
+
|
|
3
|
+
.button {
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
|
|
6
|
+
position: relative;
|
|
7
|
+
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
flex-shrink: 0;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
width: max-content;
|
|
15
|
+
min-width: max-content;
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
|
|
19
|
+
color: transparent;
|
|
20
|
+
text-decoration: none;
|
|
21
|
+
text-transform: none;
|
|
22
|
+
|
|
23
|
+
background-color: transparent;
|
|
24
|
+
border: 0 solid transparent;
|
|
25
|
+
outline: 0;
|
|
26
|
+
outline-offset: 0;
|
|
27
|
+
|
|
28
|
+
&[data-full-width] {
|
|
29
|
+
flex-grow: 1;
|
|
30
|
+
flex-shrink: 1;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
> * {
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:disabled,
|
|
39
|
+
&[data-disabled],
|
|
40
|
+
&[data-loading] {
|
|
41
|
+
cursor: not-allowed;
|
|
42
|
+
> * {
|
|
43
|
+
cursor: not-allowed;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Sun } from '@snack-uikit/loaders';
|
|
2
|
+
|
|
3
|
+
import { ICON_POSITION } from '../../constants';
|
|
4
|
+
import { ButtonPrivateProps } from './ButtonPrivate';
|
|
5
|
+
import { Variant } from './constants';
|
|
6
|
+
|
|
7
|
+
type GetVariantProps = Pick<ButtonPrivateProps, 'label' | 'icon' | 'iconPosition'>;
|
|
8
|
+
|
|
9
|
+
export function getVariant({ label, icon, iconPosition }: GetVariantProps) {
|
|
10
|
+
if (label && icon && iconPosition === ICON_POSITION.After) {
|
|
11
|
+
return Variant.IconAfter;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (label && icon && iconPosition === ICON_POSITION.Before) {
|
|
15
|
+
return Variant.IconBefore;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (label) {
|
|
19
|
+
return Variant.LabelOnly;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Variant.IconOnly;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type GetWrappedIconProps = Pick<ButtonPrivateProps, 'icon' | 'iconClassName' | 'loading'>;
|
|
26
|
+
|
|
27
|
+
export function getWrappedIcon({ icon, iconClassName, loading }: GetWrappedIconProps) {
|
|
28
|
+
if (loading) {
|
|
29
|
+
return (
|
|
30
|
+
<span data-test-id={'loading-icon'} className={iconClassName}>
|
|
31
|
+
<Sun size='s' />
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (icon) {
|
|
37
|
+
return (
|
|
38
|
+
<span data-test-id={'icon'} className={iconClassName}>
|
|
39
|
+
{icon}
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type GetWrappedLabelProps = Pick<ButtonPrivateProps, 'label' | 'labelClassName'>;
|
|
48
|
+
|
|
49
|
+
export function getWrappedLabel({ label, labelClassName }: GetWrappedLabelProps) {
|
|
50
|
+
return label ? (
|
|
51
|
+
<span data-test-id={'label'} className={labelClassName}>
|
|
52
|
+
{label}
|
|
53
|
+
</span>
|
|
54
|
+
) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type GetChildrenProps = Pick<
|
|
58
|
+
ButtonPrivateProps,
|
|
59
|
+
'icon' | 'label' | 'iconPosition' | 'iconClassName' | 'labelClassName' | 'loading'
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
export function getChildren({ icon, label, iconPosition, iconClassName, labelClassName, loading }: GetChildrenProps) {
|
|
63
|
+
const wrappedIcon = getWrappedIcon({
|
|
64
|
+
icon,
|
|
65
|
+
iconClassName,
|
|
66
|
+
loading,
|
|
67
|
+
});
|
|
68
|
+
const wrappedLabel = getWrappedLabel({
|
|
69
|
+
label,
|
|
70
|
+
labelClassName,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
switch (iconPosition) {
|
|
74
|
+
case ICON_POSITION.Before: {
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
{wrappedIcon}
|
|
78
|
+
{wrappedLabel}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
case ICON_POSITION.After:
|
|
83
|
+
default: {
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
{wrappedLabel}
|
|
87
|
+
{wrappedIcon}
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ButtonPrivate';
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useUncontrolledProp } from 'uncontrollable';
|
|
2
|
+
|
|
3
|
+
type UseValueControl<TValue> = {
|
|
4
|
+
value?: TValue;
|
|
5
|
+
onChange?(value: TValue): void;
|
|
6
|
+
defaultValue?: TValue;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function useValueControl<TValue>({ value, onChange, defaultValue }: UseValueControl<TValue>) {
|
|
10
|
+
return useUncontrolledProp<TValue>(value, defaultValue, (newValue: TValue) => {
|
|
11
|
+
const newState = typeof newValue === 'function' ? newValue(value) : newValue;
|
|
12
|
+
|
|
13
|
+
onChange?.(newState);
|
|
14
|
+
});
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-web/build/scss/components/styles-tokens-element';
|
|
2
|
+
|
|
3
|
+
@mixin icon-fill($color) {
|
|
4
|
+
.icon {
|
|
5
|
+
color: $color;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@mixin label-color($color) {
|
|
10
|
+
.label {
|
|
11
|
+
color: $color;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@mixin icon-size($icon-sizes, $size) {
|
|
16
|
+
.icon {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
|
|
19
|
+
svg {
|
|
20
|
+
width: styles-tokens-element.$icon-s !important; /* stylelint-disable-line declaration-no-important */
|
|
21
|
+
height: styles-tokens-element.$icon-s !important; /* stylelint-disable-line declaration-no-important */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@mixin button-anatomy-styles($button-theme, $sizes, $variants, $typography, $hasLabel: true) {
|
|
27
|
+
@each $size in $sizes {
|
|
28
|
+
&[data-size='#{$size}'] {
|
|
29
|
+
@if $hasLabel == true {
|
|
30
|
+
.label {
|
|
31
|
+
@include styles-tokens-element.composite-var($typography, $size);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.icon {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
|
|
40
|
+
width: styles-tokens-element.$icon-s;
|
|
41
|
+
height: styles-tokens-element.$icon-s;
|
|
42
|
+
|
|
43
|
+
svg {
|
|
44
|
+
max-width: styles-tokens-element.$icon-s;
|
|
45
|
+
max-height: styles-tokens-element.$icon-s;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@each $variant in $variants {
|
|
50
|
+
&[data-variant='#{$variant}'] {
|
|
51
|
+
@include styles-tokens-element.composite-var($button-theme, 'container', $size, $variant);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@mixin loading-label-only($button-theme) {
|
|
59
|
+
&[data-loading][data-variant='label-only'] {
|
|
60
|
+
.icon {
|
|
61
|
+
position: absolute;
|
|
62
|
+
top: 50%;
|
|
63
|
+
left: 50%;
|
|
64
|
+
transform: translate(-50%, -50%);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.label {
|
|
68
|
+
@include styles-tokens-element.composite-var($button-theme, 'label', 'load-label-only');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@mixin button-common-state {
|
|
74
|
+
&:focus-visible {
|
|
75
|
+
@include styles-tokens-element.outline-var(styles-tokens-element.$container-focused-s);
|
|
76
|
+
|
|
77
|
+
outline-offset: styles-tokens-element.$spacing-state-focus-offset;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnchorHTMLAttributes,
|
|
3
|
+
ButtonHTMLAttributes,
|
|
4
|
+
FocusEventHandler,
|
|
5
|
+
KeyboardEventHandler,
|
|
6
|
+
MouseEventHandler,
|
|
7
|
+
ReactElement,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import { ValueOf } from '@snack-uikit/utils';
|
|
11
|
+
|
|
12
|
+
import { APPEARANCE, ICON_POSITION, SIZE } from './constants';
|
|
13
|
+
|
|
14
|
+
export type Appearance = ValueOf<typeof APPEARANCE>;
|
|
15
|
+
|
|
16
|
+
export type IconPosition = ValueOf<typeof ICON_POSITION>;
|
|
17
|
+
|
|
18
|
+
export type Size = ValueOf<typeof SIZE>;
|
|
19
|
+
|
|
20
|
+
export type BaseButtonProps = {
|
|
21
|
+
/** CSS-класс */
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Флаг неактивности компонента */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Иконка
|
|
27
|
+
* @type ReactElement
|
|
28
|
+
*/
|
|
29
|
+
icon?: ReactElement;
|
|
30
|
+
/** Позиция иконки */
|
|
31
|
+
iconPosition?: IconPosition;
|
|
32
|
+
/** Текст кнопки */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Флаг состояния загрузки */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
/** Колбек обработки клика */
|
|
37
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
38
|
+
/** Колбек обработки нажатия клавиши */
|
|
39
|
+
onKeyDown?: KeyboardEventHandler<HTMLElement>;
|
|
40
|
+
/** Колбек обработки фокуса */
|
|
41
|
+
onFocus?: FocusEventHandler<HTMLAnchorElement | HTMLButtonElement>;
|
|
42
|
+
/** Колбек обработки блюра */
|
|
43
|
+
onBlur?: FocusEventHandler<HTMLAnchorElement | HTMLButtonElement>;
|
|
44
|
+
/** Размер */
|
|
45
|
+
size?: Size;
|
|
46
|
+
/** Внешний вид кнопки */
|
|
47
|
+
appearance?: Appearance;
|
|
48
|
+
/** HTML-аттрибут type */
|
|
49
|
+
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
|
|
50
|
+
/** HTML-аттрибут tab-index */
|
|
51
|
+
tabIndex?: number;
|
|
52
|
+
/** Сделать кнопку во всю ширину */
|
|
53
|
+
fullWidth?: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type AnchorButtonProps = {
|
|
57
|
+
/** Ссылка */
|
|
58
|
+
href?: string;
|
|
59
|
+
/** HTML-аттрибут target */
|
|
60
|
+
target?: AnchorHTMLAttributes<HTMLAnchorElement>['target'];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type CommonButtonProps = AnchorButtonProps & BaseButtonProps;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CommonButtonProps } from './types';
|
|
2
|
+
|
|
3
|
+
export function extractCommonButtonProps({
|
|
4
|
+
disabled,
|
|
5
|
+
href,
|
|
6
|
+
icon,
|
|
7
|
+
label,
|
|
8
|
+
loading,
|
|
9
|
+
onClick,
|
|
10
|
+
onFocus,
|
|
11
|
+
onBlur,
|
|
12
|
+
onKeyDown,
|
|
13
|
+
}: CommonButtonProps) {
|
|
14
|
+
return { disabled, href, icon, label, loading, onClick, onKeyDown, onFocus, onBlur };
|
|
15
|
+
}
|