@cloud-ru/ds-toggles 1.0.0

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 (106) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +492 -0
  4. package/dist/cjs/components/Checkbox/Checkbox.d.ts +8 -0
  5. package/dist/cjs/components/Checkbox/Checkbox.js +61 -0
  6. package/dist/cjs/components/Checkbox/index.d.ts +1 -0
  7. package/dist/cjs/components/Checkbox/index.js +17 -0
  8. package/dist/cjs/components/Checkbox/styles.module.css +223 -0
  9. package/dist/cjs/components/Favourite/Favourite.d.ts +9 -0
  10. package/dist/cjs/components/Favourite/Favourite.js +42 -0
  11. package/dist/cjs/components/Favourite/index.d.ts +1 -0
  12. package/dist/cjs/components/Favourite/index.js +17 -0
  13. package/dist/cjs/components/Favourite/styles.module.css +138 -0
  14. package/dist/cjs/components/Radio/Radio.d.ts +3 -0
  15. package/dist/cjs/components/Radio/Radio.js +34 -0
  16. package/dist/cjs/components/Radio/index.d.ts +1 -0
  17. package/dist/cjs/components/Radio/index.js +17 -0
  18. package/dist/cjs/components/Radio/styles.module.css +219 -0
  19. package/dist/cjs/components/Switch/Switch.d.ts +3 -0
  20. package/dist/cjs/components/Switch/Switch.js +42 -0
  21. package/dist/cjs/components/Switch/index.d.ts +1 -0
  22. package/dist/cjs/components/Switch/index.js +17 -0
  23. package/dist/cjs/components/Switch/styles.module.css +269 -0
  24. package/dist/cjs/components/ToggleGroup/ToggleGroup.d.ts +22 -0
  25. package/dist/cjs/components/ToggleGroup/ToggleGroup.js +33 -0
  26. package/dist/cjs/components/ToggleGroup/hooks.d.ts +9 -0
  27. package/dist/cjs/components/ToggleGroup/hooks.js +26 -0
  28. package/dist/cjs/components/ToggleGroup/index.d.ts +2 -0
  29. package/dist/cjs/components/ToggleGroup/index.js +18 -0
  30. package/dist/cjs/components/index.d.ts +5 -0
  31. package/dist/cjs/components/index.js +21 -0
  32. package/dist/cjs/constants.d.ts +23 -0
  33. package/dist/cjs/constants.js +26 -0
  34. package/dist/cjs/context.d.ts +9 -0
  35. package/dist/cjs/context.js +12 -0
  36. package/dist/cjs/index.d.ts +4 -0
  37. package/dist/cjs/index.js +20 -0
  38. package/dist/cjs/package.json +3 -0
  39. package/dist/cjs/style.css +849 -0
  40. package/dist/cjs/types.d.ts +57 -0
  41. package/dist/cjs/types.js +2 -0
  42. package/dist/cjs/utils.d.ts +3 -0
  43. package/dist/cjs/utils.js +22 -0
  44. package/dist/esm/components/Checkbox/Checkbox.d.ts +8 -0
  45. package/dist/esm/components/Checkbox/Checkbox.js +55 -0
  46. package/dist/esm/components/Checkbox/index.d.ts +1 -0
  47. package/dist/esm/components/Checkbox/index.js +1 -0
  48. package/dist/esm/components/Checkbox/styles.module.css +223 -0
  49. package/dist/esm/components/Favourite/Favourite.d.ts +9 -0
  50. package/dist/esm/components/Favourite/Favourite.js +36 -0
  51. package/dist/esm/components/Favourite/index.d.ts +1 -0
  52. package/dist/esm/components/Favourite/index.js +1 -0
  53. package/dist/esm/components/Favourite/styles.module.css +138 -0
  54. package/dist/esm/components/Radio/Radio.d.ts +3 -0
  55. package/dist/esm/components/Radio/Radio.js +28 -0
  56. package/dist/esm/components/Radio/index.d.ts +1 -0
  57. package/dist/esm/components/Radio/index.js +1 -0
  58. package/dist/esm/components/Radio/styles.module.css +219 -0
  59. package/dist/esm/components/Switch/Switch.d.ts +3 -0
  60. package/dist/esm/components/Switch/Switch.js +36 -0
  61. package/dist/esm/components/Switch/index.d.ts +1 -0
  62. package/dist/esm/components/Switch/index.js +1 -0
  63. package/dist/esm/components/Switch/styles.module.css +269 -0
  64. package/dist/esm/components/ToggleGroup/ToggleGroup.d.ts +22 -0
  65. package/dist/esm/components/ToggleGroup/ToggleGroup.js +30 -0
  66. package/dist/esm/components/ToggleGroup/hooks.d.ts +9 -0
  67. package/dist/esm/components/ToggleGroup/hooks.js +23 -0
  68. package/dist/esm/components/ToggleGroup/index.d.ts +2 -0
  69. package/dist/esm/components/ToggleGroup/index.js +2 -0
  70. package/dist/esm/components/index.d.ts +5 -0
  71. package/dist/esm/components/index.js +5 -0
  72. package/dist/esm/constants.d.ts +23 -0
  73. package/dist/esm/constants.js +23 -0
  74. package/dist/esm/context.d.ts +9 -0
  75. package/dist/esm/context.js +8 -0
  76. package/dist/esm/index.d.ts +4 -0
  77. package/dist/esm/index.js +4 -0
  78. package/dist/esm/style.css +849 -0
  79. package/dist/esm/types.d.ts +57 -0
  80. package/dist/esm/types.js +1 -0
  81. package/dist/esm/utils.d.ts +3 -0
  82. package/dist/esm/utils.js +18 -0
  83. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  84. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  85. package/package.json +46 -0
  86. package/src/components/Checkbox/Checkbox.tsx +140 -0
  87. package/src/components/Checkbox/index.ts +1 -0
  88. package/src/components/Checkbox/styles.module.scss +162 -0
  89. package/src/components/Favourite/Favourite.tsx +116 -0
  90. package/src/components/Favourite/index.ts +1 -0
  91. package/src/components/Favourite/styles.module.scss +138 -0
  92. package/src/components/Radio/Radio.tsx +84 -0
  93. package/src/components/Radio/index.ts +1 -0
  94. package/src/components/Radio/styles.module.scss +159 -0
  95. package/src/components/Switch/Switch.tsx +94 -0
  96. package/src/components/Switch/index.ts +1 -0
  97. package/src/components/Switch/styles.module.scss +250 -0
  98. package/src/components/ToggleGroup/ToggleGroup.tsx +71 -0
  99. package/src/components/ToggleGroup/hooks.ts +34 -0
  100. package/src/components/ToggleGroup/index.ts +2 -0
  101. package/src/components/index.ts +5 -0
  102. package/src/constants.ts +27 -0
  103. package/src/context.ts +18 -0
  104. package/src/index.ts +4 -0
  105. package/src/types.ts +80 -0
  106. package/src/utils.ts +21 -0
@@ -0,0 +1,116 @@
1
+ import { HeartFilledSVG, HeartSVG, StarFilledSVG, StarSVG } from '@cloud-ru/ds-icons/interface/system';
2
+ import { LOADER_SIZE, Spinner } from '@cloud-ru/ds-loader';
3
+ import { useUncontrolledProp } from '@cloud-ru/ds-utils';
4
+ import cn from 'classnames';
5
+ import { KeyboardEventHandler, useState } from 'react';
6
+
7
+ import { FAVOURITE_ICON, MODE, NATIVE_INPUT_SUFFIX, SIZE } from '../../constants';
8
+ import { FavouriteIcon, ToggleProps } from '../../types';
9
+ import { getIconSize } from '../../utils';
10
+ import styles from './styles.module.scss';
11
+
12
+ export type FavouriteProps = ToggleProps & {
13
+ /** Вариант иконки: звезда или сердце */
14
+ icon?: FavouriteIcon;
15
+ /** Обработчик keyup */
16
+ onKeyUp?: KeyboardEventHandler<HTMLSpanElement>;
17
+ };
18
+
19
+ const SPINNER_SIZE_MAP = {
20
+ [SIZE.XS]: LOADER_SIZE.XS,
21
+ [SIZE.S]: LOADER_SIZE.S,
22
+ };
23
+
24
+ const ICONS = {
25
+ [FAVOURITE_ICON.Star]: { checked: StarFilledSVG, unchecked: StarSVG },
26
+ [FAVOURITE_ICON.Heart]: { checked: HeartFilledSVG, unchecked: HeartSVG },
27
+ } as const;
28
+
29
+ function Icon({
30
+ size,
31
+ checked,
32
+ loading,
33
+ variant,
34
+ disabled,
35
+ }: Required<Pick<FavouriteProps, 'checked' | 'loading' | 'size' | 'disabled'>> & { variant: FavouriteIcon }) {
36
+ if (loading) {
37
+ return <Spinner size={SPINNER_SIZE_MAP[size]} />;
38
+ }
39
+
40
+ const iconSize = getIconSize(size);
41
+ const IconComponent = checked ? ICONS[variant].checked : ICONS[variant].unchecked;
42
+
43
+ return (
44
+ <IconComponent
45
+ className={styles.icon}
46
+ size={iconSize}
47
+ data-checked={checked}
48
+ data-variant={variant}
49
+ data-disabled={disabled || undefined}
50
+ />
51
+ );
52
+ }
53
+
54
+ export function Favourite({
55
+ inputRef,
56
+ checked: checkedProp,
57
+ defaultChecked,
58
+ onChange,
59
+ onBlur,
60
+ onFocus,
61
+ disabled = false,
62
+ loading = false,
63
+ size = SIZE.S,
64
+ className,
65
+ 'data-test-id': dataTestId,
66
+ icon = FAVOURITE_ICON.Heart,
67
+ ...otherProps
68
+ }: FavouriteProps) {
69
+ const [checked, setChecked] = useUncontrolledProp(checkedProp, Boolean(defaultChecked), onChange);
70
+ const [focusVisible, setFocusVisible] = useState(false);
71
+
72
+ return (
73
+ <span
74
+ className={cn(styles.root, className)}
75
+ role={MODE.Checkbox}
76
+ data-size={size}
77
+ data-icon={icon}
78
+ data-loading={loading}
79
+ data-disabled={disabled}
80
+ data-checked={checked}
81
+ data-focusvisible={focusVisible || undefined}
82
+ data-test-id={dataTestId}
83
+ >
84
+ <div className={styles.container}>
85
+ <div className={styles.framing}>
86
+ <div className={styles.flag}>
87
+ <div className={styles.surface} data-text-opacity={(!loading && !disabled) || undefined}>
88
+ <Icon size={size} checked={checked} variant={icon} loading={loading} disabled={disabled} />
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ {!loading && (
94
+ <input
95
+ {...otherProps}
96
+ data-test-id={dataTestId ? `${dataTestId}${NATIVE_INPUT_SUFFIX}` : undefined}
97
+ ref={inputRef}
98
+ type={MODE.Checkbox}
99
+ className={styles.inputPrivate}
100
+ checked={checked}
101
+ disabled={disabled}
102
+ onChange={e => setChecked(e.target.checked)}
103
+ onFocus={event => {
104
+ setFocusVisible(event.target.matches(':focus-visible'));
105
+ onFocus?.(event);
106
+ }}
107
+ onBlur={event => {
108
+ setFocusVisible(false);
109
+ onBlur?.(event);
110
+ }}
111
+ />
112
+ )}
113
+ </div>
114
+ </span>
115
+ );
116
+ }
@@ -0,0 +1 @@
1
+ export * from './Favourite';
@@ -0,0 +1,138 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/toggles.module' as toggles;
3
+ @use '@cloud-ru/ds-materials' as m;
4
+
5
+ $sizes: 'xs', 's';
6
+
7
+ .inputPrivate {
8
+ cursor: pointer;
9
+
10
+ position: absolute;
11
+ inset: 0;
12
+
13
+ width: 100%;
14
+ height: 100%;
15
+ margin: 0;
16
+ padding: 0;
17
+
18
+ opacity: 0;
19
+
20
+ &:disabled {
21
+ cursor: not-allowed;
22
+ }
23
+ }
24
+
25
+ .backgroundStateLayer {
26
+ border-radius: 0 !important;
27
+ visibility: initial;
28
+ }
29
+
30
+ .flag {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ }
35
+
36
+ .framing {
37
+ position: relative;
38
+
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ .surface {
47
+ position: relative;
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ }
52
+
53
+ .icon {
54
+ position: absolute;
55
+
56
+ // Невыбранный контур: без этого цвет наследуется от окружения потребителя.
57
+ color: base.$sn-theme-color-neutral-accent;
58
+
59
+ &[data-checked='true'] {
60
+ &[data-variant='star'] {
61
+ color: base.$sn-theme-color-yellow-accent;
62
+ }
63
+
64
+ &[data-variant='heart'] {
65
+ color: base.$sn-theme-color-red-accent;
66
+ }
67
+
68
+ &[data-disabled='true'] {
69
+ color: base.$sn-theme-color-neutral-accent;
70
+ }
71
+ }
72
+ }
73
+
74
+ .container {
75
+ position: relative;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ }
80
+
81
+ .root {
82
+ position: relative;
83
+
84
+ display: inline-flex;
85
+ flex-direction: row;
86
+ flex-grow: 0;
87
+ flex-shrink: 0;
88
+ align-items: center;
89
+
90
+ vertical-align: middle;
91
+
92
+ @each $size in $sizes {
93
+ &[data-size='#{$size}'] {
94
+ .container {
95
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'checkbox', 'container');
96
+ }
97
+
98
+ .framing {
99
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'checkbox', 'framing');
100
+
101
+ width: base.simple-var(toggles.$toggles, 'anatomy', 'size', $size, 'checkbox', 'framing', 'min-width');
102
+ height: base.simple-var(toggles.$toggles, 'anatomy', 'size', $size, 'checkbox', 'framing', 'min-height');
103
+ }
104
+
105
+ .flag {
106
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'checkbox', 'framing', 'flag');
107
+ }
108
+
109
+ .surface {
110
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'checkbox', 'framing', 'flag', 'surface');
111
+ }
112
+ }
113
+ }
114
+
115
+ &[data-loading='false'][data-disabled='false'] {
116
+ .container {
117
+ @include m.has-content-with-text-opacity(#{framing});
118
+ }
119
+ }
120
+
121
+ // Спиннер в макете красится `neutral/accent`; без явного цвета он наследует текст потребителя.
122
+ &[data-loading='true'] .flag {
123
+ color: base.$sn-theme-color-neutral-accent;
124
+ }
125
+
126
+ &[data-disabled='true'] {
127
+ .framing {
128
+ opacity: base.$sn-theme-effect-opacity-disabled;
129
+ }
130
+ }
131
+
132
+ &[data-focusvisible='true'] {
133
+ .framing {
134
+ outline: base.$sn-primitive-strokeWeight-strokeSemiBold solid base.$sn-theme-color-available-complementary;
135
+ outline-offset: base.$sn-primitive-strokeWeight-strokeSemiBold;
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,84 @@
1
+ import { LOADER_SIZE, Spinner } from '@cloud-ru/ds-loader';
2
+ import { useUncontrolledProp } from '@cloud-ru/ds-utils';
3
+ import cn from 'classnames';
4
+ import { useState } from 'react';
5
+
6
+ import { MODE, NATIVE_INPUT_SUFFIX, SIZE } from '../../constants';
7
+ import { ToggleProps } from '../../types';
8
+ import { getVisualStateAttributes } from '../../utils';
9
+ import styles from './styles.module.scss';
10
+
11
+ export type RadioProps = ToggleProps;
12
+
13
+ const SPINNER_SIZE_MAP = {
14
+ [SIZE.XS]: LOADER_SIZE.XS,
15
+ [SIZE.S]: LOADER_SIZE.S,
16
+ };
17
+
18
+ export function Radio({
19
+ inputRef,
20
+ 'data-test-id': dataTestId,
21
+ checked: checkedProp,
22
+ defaultChecked,
23
+ onChange,
24
+ onBlur,
25
+ onFocus,
26
+ disabled = false,
27
+ loading = false,
28
+ className,
29
+ size = SIZE.S,
30
+ ...otherProps
31
+ }: RadioProps) {
32
+ const [checked, setChecked] = useUncontrolledProp(checkedProp, Boolean(defaultChecked), onChange);
33
+ const [focusVisible, setFocusVisible] = useState(false);
34
+
35
+ const stateDataAttributes = getVisualStateAttributes({
36
+ loading,
37
+ disabled,
38
+ checked,
39
+ });
40
+
41
+ return (
42
+ <span
43
+ className={cn(styles.root, className)}
44
+ role={MODE.Radio}
45
+ data-size={size}
46
+ {...stateDataAttributes}
47
+ data-test-id={dataTestId || undefined}
48
+ >
49
+ <div className={styles.container}>
50
+ <div className={styles.framing} {...stateDataAttributes} data-focusvisible={focusVisible || undefined}>
51
+ <div className={styles.backgroundStateLayer} data-state='emptyNeutralOnBackground' />
52
+ <div className={styles.flag}>
53
+ {loading ? (
54
+ <Spinner size={SPINNER_SIZE_MAP[size]} />
55
+ ) : (
56
+ <div className={styles.surface} {...stateDataAttributes} data-text-opacity={checked || undefined} />
57
+ )}
58
+ </div>
59
+ </div>
60
+
61
+ {!loading && (
62
+ <input
63
+ {...otherProps}
64
+ data-test-id={dataTestId ? `${dataTestId}${NATIVE_INPUT_SUFFIX}` : undefined}
65
+ ref={inputRef}
66
+ type={MODE.Radio}
67
+ className={styles.inputPrivate}
68
+ checked={checked}
69
+ disabled={disabled}
70
+ onChange={e => setChecked(e.target.checked)}
71
+ onFocus={event => {
72
+ setFocusVisible(event.target.matches(':focus-visible'));
73
+ onFocus?.(event);
74
+ }}
75
+ onBlur={event => {
76
+ setFocusVisible(false);
77
+ onBlur?.(event);
78
+ }}
79
+ />
80
+ )}
81
+ </div>
82
+ </span>
83
+ );
84
+ }
@@ -0,0 +1 @@
1
+ export * from './Radio';
@@ -0,0 +1,159 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/toggles.module' as toggles;
3
+ @use '@cloud-ru/ds-materials' as m;
4
+
5
+ $sizes: 'xs', 's';
6
+
7
+ .inputPrivate {
8
+ cursor: pointer;
9
+
10
+ position: absolute;
11
+ inset: 0;
12
+
13
+ width: 100%;
14
+ height: 100%;
15
+ margin: 0;
16
+ padding: 0;
17
+
18
+ opacity: 0;
19
+
20
+ &:disabled {
21
+ cursor: not-allowed;
22
+ }
23
+ }
24
+
25
+ .backgroundStateLayer {
26
+ visibility: initial;
27
+ }
28
+
29
+ .framing {
30
+ position: relative;
31
+
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+
36
+ box-sizing: border-box;
37
+
38
+ &[data-loading="false"] {
39
+ border-color: base.$sn-theme-color-neutral-accent;
40
+ border-style: solid;
41
+ background-color: base.$sn-theme-color-neutral-background1Level;
42
+
43
+ &[data-disabled="true"] {
44
+ opacity: base.$sn-theme-effect-opacity-disabled;
45
+ }
46
+
47
+ &[data-disabled="false"] {
48
+ &[data-checked="false"] {
49
+ border-color: base.$sn-theme-color-neutral-accent;
50
+ }
51
+
52
+ &[data-checked="true"] {
53
+ border-color: base.$sn-theme-color-primary-accent;
54
+ }
55
+
56
+ &[data-focusvisible="true"] {
57
+ $outlineWidth: base.$sn-primitive-strokeWeight-strokeSemiBold;
58
+
59
+ outline: $outlineWidth solid base.$sn-theme-color-available-complementary;
60
+ outline-offset: $outlineWidth;
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ .flag {
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ }
71
+
72
+ .surface {
73
+ position: relative;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+
78
+ &[data-loading="false"][data-checked="true"] {
79
+ &[data-disabled="false"] {
80
+ background: base.$sn-theme-color-primary-accent;
81
+ }
82
+
83
+ &[data-disabled="true"] {
84
+ background: base.$sn-theme-color-neutral-accent;
85
+ }
86
+ }
87
+ }
88
+
89
+ .container {
90
+ position: relative;
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ }
95
+
96
+ .root {
97
+ position: relative;
98
+
99
+ display: inline-flex;
100
+ flex-direction: row;
101
+ flex-grow: 0;
102
+ flex-shrink: 0;
103
+ align-items: center;
104
+
105
+ vertical-align: middle;
106
+
107
+ // Sizes
108
+ @each $size in $sizes {
109
+ &[data-size='#{$size}'] {
110
+ .container {
111
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'radio', 'container');
112
+ }
113
+
114
+ .framing {
115
+ width: base.simple-var(toggles.$toggles, 'anatomy', 'size', $size, 'radio', 'framing', 'min-width');
116
+ height: base.simple-var(toggles.$toggles, 'anatomy', 'size', $size, 'radio', 'framing', 'min-height');
117
+
118
+
119
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'radio', 'framing');
120
+ }
121
+
122
+ .flag {
123
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'radio', 'framing', 'flag')
124
+ }
125
+
126
+ .surface {
127
+ @include base.composite-var(toggles.$toggles, 'anatomy', 'size', $size, 'radio', 'framing', 'flag', 'surface');
128
+ }
129
+ }
130
+ }
131
+
132
+ // States management
133
+ &[data-loading="false"][data-disabled="false"] {
134
+ .container {
135
+ @include m.has-state-layer-as-child(#{backgroundStateLayer});
136
+ }
137
+
138
+ // Слой тонирует и рамку: `inset: 0` из миксина накрыл бы только padding-box.
139
+ @each $size in $sizes {
140
+ &[data-size='#{$size}'] .backgroundStateLayer {
141
+ inset: calc(
142
+ -1 * #{base.simple-var(toggles.$toggles, 'anatomy', 'size', $size, 'radio', 'framing', 'border-width')}
143
+ );
144
+ }
145
+ }
146
+ }
147
+
148
+ // Спиннер в макете красится `neutral/accent`; без явного цвета он наследует текст потребителя.
149
+ &[data-loading="true"] .flag {
150
+ color: base.$sn-theme-color-neutral-accent;
151
+ }
152
+
153
+ &[data-loading="true"],
154
+ &[data-disabled="true"] {
155
+ .backgroundStateLayer {
156
+ visibility: hidden;
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,94 @@
1
+ import { CheckSVG } from '@cloud-ru/ds-icons/interface/system';
2
+ import { LOADER_SIZE, Spinner } from '@cloud-ru/ds-loader';
3
+ import { useUncontrolledProp } from '@cloud-ru/ds-utils';
4
+ import cn from 'classnames';
5
+ import { useState } from 'react';
6
+
7
+ import { MODE, NATIVE_INPUT_SUFFIX, SIZE } from '../../constants';
8
+ import { ToggleProps } from '../../types';
9
+ import { getIconSize, getVisualStateAttributes } from '../../utils';
10
+ import styles from './styles.module.scss';
11
+
12
+ export type SwitchProps = ToggleProps;
13
+
14
+ const SPINNER_SIZE_MAP = {
15
+ [SIZE.XS]: LOADER_SIZE.XS,
16
+ [SIZE.S]: LOADER_SIZE.S,
17
+ };
18
+
19
+ function Icon({ size, loading }: Required<Pick<SwitchProps, 'loading' | 'size'>>) {
20
+ if (loading) {
21
+ return <Spinner size={SPINNER_SIZE_MAP[size]} className={styles.spinner} />;
22
+ }
23
+
24
+ const iconSize = getIconSize(size);
25
+
26
+ return <CheckSVG aria-hidden className={styles.icon} size={iconSize} />;
27
+ }
28
+
29
+ export function Switch({
30
+ inputRef,
31
+ checked: checkedProp,
32
+ defaultChecked,
33
+ onChange,
34
+ onBlur,
35
+ onFocus,
36
+ disabled = false,
37
+ loading = false,
38
+ size = SIZE.S,
39
+ className,
40
+ 'data-test-id': dataTestId,
41
+ ...otherProps
42
+ }: SwitchProps) {
43
+ const [checked, setChecked] = useUncontrolledProp(checkedProp, Boolean(defaultChecked), onChange);
44
+ const [focusVisible, setFocusVisible] = useState(false);
45
+
46
+ const stateDataAttributes = getVisualStateAttributes({
47
+ loading,
48
+ disabled,
49
+ checked,
50
+ });
51
+
52
+ return (
53
+ <span
54
+ className={cn(styles.root, className)}
55
+ role={MODE.Checkbox}
56
+ data-size={size}
57
+ {...stateDataAttributes}
58
+ data-focusvisible={focusVisible || undefined}
59
+ data-test-id={dataTestId || undefined}
60
+ >
61
+ <div className={styles.container}>
62
+ <div className={styles.framing} {...stateDataAttributes}>
63
+ <div className={styles.backgroundStateLayer} data-state='emptyDarkOnAccent' />
64
+ <div className={styles.flag}>
65
+ <div className={styles.surface} {...stateDataAttributes}>
66
+ <Icon size={size} loading={Boolean(loading)} />
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ {!loading && (
72
+ <input
73
+ {...otherProps}
74
+ data-test-id={dataTestId ? `${dataTestId}${NATIVE_INPUT_SUFFIX}` : undefined}
75
+ ref={inputRef}
76
+ type={MODE.Checkbox}
77
+ className={styles.inputPrivate}
78
+ checked={checked}
79
+ disabled={disabled}
80
+ onChange={e => setChecked(e.target.checked)}
81
+ onFocus={event => {
82
+ setFocusVisible(event.target.matches(':focus-visible'));
83
+ onFocus?.(event);
84
+ }}
85
+ onBlur={event => {
86
+ setFocusVisible(false);
87
+ onBlur?.(event);
88
+ }}
89
+ />
90
+ )}
91
+ </div>
92
+ </span>
93
+ );
94
+ }
@@ -0,0 +1 @@
1
+ export * from './Switch';