mtrl 0.2.2 → 0.2.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/.typedocignore +11 -0
- package/DOCS.md +153 -0
- package/index.ts +18 -3
- package/package.json +7 -2
- package/src/components/badge/_styles.scss +174 -0
- package/src/components/badge/api.ts +292 -0
- package/src/components/badge/badge.ts +52 -0
- package/src/components/badge/config.ts +68 -0
- package/src/components/badge/constants.ts +30 -0
- package/src/components/badge/features.ts +185 -0
- package/src/components/badge/index.ts +4 -0
- package/src/components/badge/types.ts +105 -0
- package/src/components/button/types.ts +174 -29
- package/src/components/carousel/_styles.scss +645 -0
- package/src/components/carousel/api.ts +147 -0
- package/src/components/carousel/carousel.ts +178 -0
- package/src/components/carousel/config.ts +91 -0
- package/src/components/carousel/constants.ts +95 -0
- package/src/components/carousel/features/drag.ts +388 -0
- package/src/components/carousel/features/index.ts +8 -0
- package/src/components/carousel/features/slides.ts +682 -0
- package/src/components/carousel/index.ts +38 -0
- package/src/components/carousel/types.ts +327 -0
- package/src/components/dialog/_styles.scss +213 -0
- package/src/components/dialog/api.ts +283 -0
- package/src/components/dialog/config.ts +113 -0
- package/src/components/dialog/constants.ts +32 -0
- package/src/components/dialog/dialog.ts +56 -0
- package/src/components/dialog/features.ts +713 -0
- package/src/components/dialog/index.ts +15 -0
- package/src/components/dialog/types.ts +221 -0
- package/src/components/progress/_styles.scss +13 -1
- package/src/components/progress/api.ts +2 -2
- package/src/components/progress/progress.ts +2 -2
- package/src/components/progress/types.ts +3 -0
- package/src/components/radios/_styles.scss +232 -0
- package/src/components/radios/api.ts +100 -0
- package/src/components/radios/config.ts +60 -0
- package/src/components/radios/constants.ts +28 -0
- package/src/components/radios/index.ts +4 -0
- package/src/components/radios/radio.ts +269 -0
- package/src/components/radios/radios.ts +42 -0
- package/src/components/radios/types.ts +232 -0
- package/src/components/sheet/_styles.scss +236 -0
- package/src/components/sheet/api.ts +96 -0
- package/src/components/sheet/config.ts +66 -0
- package/src/components/sheet/constants.ts +20 -0
- package/src/components/sheet/features/content.ts +51 -0
- package/src/components/sheet/features/gestures.ts +177 -0
- package/src/components/sheet/features/index.ts +6 -0
- package/src/components/sheet/features/position.ts +42 -0
- package/src/components/sheet/features/state.ts +116 -0
- package/src/components/sheet/features/title.ts +86 -0
- package/src/components/sheet/index.ts +4 -0
- package/src/components/sheet/sheet.ts +57 -0
- package/src/components/sheet/types.ts +266 -0
- package/src/components/slider/_styles.scss +518 -0
- package/src/components/slider/api.ts +336 -0
- package/src/components/slider/config.ts +145 -0
- package/src/components/slider/constants.ts +28 -0
- package/src/components/slider/features/appearance.ts +140 -0
- package/src/components/slider/features/disabled.ts +43 -0
- package/src/components/slider/features/events.ts +164 -0
- package/src/components/slider/features/index.ts +5 -0
- package/src/components/slider/features/interactions.ts +256 -0
- package/src/components/slider/features/keyboard.ts +114 -0
- package/src/components/slider/features/slider.ts +336 -0
- package/src/components/slider/features/structure.ts +264 -0
- package/src/components/slider/features/ui.ts +518 -0
- package/src/components/slider/index.ts +9 -0
- package/src/components/slider/slider.ts +58 -0
- package/src/components/slider/types.ts +166 -0
- package/src/components/tabs/_styles.scss +224 -0
- package/src/components/tabs/api.ts +443 -0
- package/src/components/tabs/config.ts +80 -0
- package/src/components/tabs/constants.ts +12 -0
- package/src/components/tabs/index.ts +4 -0
- package/src/components/tabs/tabs.ts +52 -0
- package/src/components/tabs/types.ts +247 -0
- package/src/components/textfield/_styles.scss +97 -4
- package/src/components/tooltip/_styles.scss +241 -0
- package/src/components/tooltip/api.ts +411 -0
- package/src/components/tooltip/config.ts +78 -0
- package/src/components/tooltip/constants.ts +27 -0
- package/src/components/tooltip/index.ts +4 -0
- package/src/components/tooltip/tooltip.ts +60 -0
- package/src/components/tooltip/types.ts +178 -0
- package/src/core/build/_ripple.scss +79 -0
- package/src/core/build/constants.ts +48 -0
- package/src/core/build/icon.ts +137 -0
- package/src/core/build/ripple.ts +216 -0
- package/src/core/build/text.ts +91 -0
- package/src/index.ts +9 -1
- package/src/styles/abstract/_variables.scss +24 -12
- package/tsconfig.json +22 -0
- package/typedoc.json +28 -0
- package/typedoc.simple.json +14 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/components/tooltip/types.ts
|
|
2
|
+
import { TOOLTIP_POSITIONS, TOOLTIP_VARIANTS } from './constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration interface for the Tooltip component
|
|
6
|
+
* @category Components
|
|
7
|
+
*/
|
|
8
|
+
export interface TooltipConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Tooltip content text
|
|
11
|
+
* @example 'Delete item'
|
|
12
|
+
*/
|
|
13
|
+
text?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* HTML element that triggers the tooltip
|
|
17
|
+
* @example document.querySelector('#my-button')
|
|
18
|
+
*/
|
|
19
|
+
target?: HTMLElement;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Tooltip position relative to the target
|
|
23
|
+
* @default 'bottom'
|
|
24
|
+
*/
|
|
25
|
+
position?: keyof typeof TOOLTIP_POSITIONS | string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tooltip variant that determines visual styling
|
|
29
|
+
* @default 'default'
|
|
30
|
+
*/
|
|
31
|
+
variant?: keyof typeof TOOLTIP_VARIANTS | string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the tooltip is initially visible
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
visible?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Show delay in milliseconds
|
|
41
|
+
* @default 300
|
|
42
|
+
*/
|
|
43
|
+
showDelay?: number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hide delay in milliseconds
|
|
47
|
+
* @default 100
|
|
48
|
+
*/
|
|
49
|
+
hideDelay?: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether to show the tooltip on focus
|
|
53
|
+
* @default true
|
|
54
|
+
*/
|
|
55
|
+
showOnFocus?: boolean;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether to show the tooltip on hover
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
showOnHover?: boolean;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Additional CSS classes to add to the tooltip
|
|
65
|
+
*/
|
|
66
|
+
class?: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Component prefix for class names
|
|
70
|
+
* @default 'mtrl'
|
|
71
|
+
*/
|
|
72
|
+
prefix?: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Component name used in class generation
|
|
76
|
+
*/
|
|
77
|
+
componentName?: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Optional z-index for the tooltip
|
|
81
|
+
*/
|
|
82
|
+
zIndex?: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether to enable rich (HTML) content
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
rich?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Tooltip component interface
|
|
93
|
+
* @category Components
|
|
94
|
+
*/
|
|
95
|
+
export interface TooltipComponent {
|
|
96
|
+
/** The tooltip's DOM element */
|
|
97
|
+
element: HTMLElement;
|
|
98
|
+
|
|
99
|
+
/** The tooltip's target element */
|
|
100
|
+
target: HTMLElement | null;
|
|
101
|
+
|
|
102
|
+
/** API for managing component lifecycle */
|
|
103
|
+
lifecycle: {
|
|
104
|
+
/** Destroys the component and cleans up resources */
|
|
105
|
+
destroy: () => void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Gets a class name with the component's prefix
|
|
110
|
+
* @param name - Base class name
|
|
111
|
+
* @returns Prefixed class name
|
|
112
|
+
*/
|
|
113
|
+
getClass: (name: string) => string;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sets the tooltip text content
|
|
117
|
+
* @param text - New text content
|
|
118
|
+
* @returns The tooltip component for chaining
|
|
119
|
+
*/
|
|
120
|
+
setText: (text: string) => TooltipComponent;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Gets the tooltip text content
|
|
124
|
+
* @returns Tooltip text content
|
|
125
|
+
*/
|
|
126
|
+
getText: () => string;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sets the tooltip position
|
|
130
|
+
* @param position - Position value
|
|
131
|
+
* @returns The tooltip component for chaining
|
|
132
|
+
*/
|
|
133
|
+
setPosition: (position: keyof typeof TOOLTIP_POSITIONS | string) => TooltipComponent;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Gets the current tooltip position
|
|
137
|
+
* @returns Current position
|
|
138
|
+
*/
|
|
139
|
+
getPosition: () => string;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sets the tooltip target element
|
|
143
|
+
* @param target - Element to attach tooltip to
|
|
144
|
+
* @returns The tooltip component for chaining
|
|
145
|
+
*/
|
|
146
|
+
setTarget: (target: HTMLElement) => TooltipComponent;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Shows the tooltip
|
|
150
|
+
* @param immediate - Whether to show immediately (bypassing delay)
|
|
151
|
+
* @returns The tooltip component for chaining
|
|
152
|
+
*/
|
|
153
|
+
show: (immediate?: boolean) => TooltipComponent;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Hides the tooltip
|
|
157
|
+
* @param immediate - Whether to hide immediately (bypassing delay)
|
|
158
|
+
* @returns The tooltip component for chaining
|
|
159
|
+
*/
|
|
160
|
+
hide: (immediate?: boolean) => TooltipComponent;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Checks if the tooltip is currently visible
|
|
164
|
+
* @returns Whether the tooltip is visible
|
|
165
|
+
*/
|
|
166
|
+
isVisible: () => boolean;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Updates the tooltip's position relative to its target
|
|
170
|
+
* @returns The tooltip component for chaining
|
|
171
|
+
*/
|
|
172
|
+
updatePosition: () => TooltipComponent;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Destroys the tooltip component and cleans up resources
|
|
176
|
+
*/
|
|
177
|
+
destroy: () => void;
|
|
178
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/components/ripple/_ripple.scss
|
|
2
|
+
@use '../../styles/abstract/base' as base;
|
|
3
|
+
@use '../../styles/abstract/variables' as v;
|
|
4
|
+
@use '../../styles/abstract/functions' as f;
|
|
5
|
+
@use '../../styles/abstract/mixins' as m;
|
|
6
|
+
@use '../../styles/abstract/theme' as t;
|
|
7
|
+
|
|
8
|
+
$component: '#{base.$prefix}-ripple';
|
|
9
|
+
|
|
10
|
+
.#{$component} {
|
|
11
|
+
// Ripple container
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 0;
|
|
14
|
+
left: 0;
|
|
15
|
+
right: 0;
|
|
16
|
+
bottom: 0;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
border-radius: inherit;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
z-index: 0;
|
|
21
|
+
|
|
22
|
+
// Ripple element
|
|
23
|
+
&-wave {
|
|
24
|
+
position: absolute;
|
|
25
|
+
border-radius: 50%;
|
|
26
|
+
background-color: currentColor;
|
|
27
|
+
transform: scale(0);
|
|
28
|
+
opacity: 0;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
will-change: transform, opacity;
|
|
31
|
+
|
|
32
|
+
// Animation
|
|
33
|
+
transition-property: transform, opacity;
|
|
34
|
+
transition-duration: v.motion('duration-short4');
|
|
35
|
+
transition-timing-function: v.motion('easing-standard');
|
|
36
|
+
|
|
37
|
+
// Active ripple
|
|
38
|
+
&.active {
|
|
39
|
+
transform: scale(1);
|
|
40
|
+
opacity: v.state('hover-state-layer-opacity');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.fade-out {
|
|
44
|
+
opacity: 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Standalone utility for adding ripple to any element
|
|
50
|
+
[data-ripple] {
|
|
51
|
+
position: relative;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
|
|
54
|
+
&::after {
|
|
55
|
+
content: '';
|
|
56
|
+
position: absolute;
|
|
57
|
+
top: 0;
|
|
58
|
+
left: 0;
|
|
59
|
+
right: 0;
|
|
60
|
+
bottom: 0;
|
|
61
|
+
z-index: 0;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle ripple color based on data attribute
|
|
66
|
+
&[data-ripple="light"]::after {
|
|
67
|
+
background-color: rgba(255, 255, 255, 0.3);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&[data-ripple="dark"]::after {
|
|
71
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Make content appear above ripple
|
|
75
|
+
> * {
|
|
76
|
+
position: relative;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/core/build/constants.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Animation timing functions for ripple effect
|
|
5
|
+
*/
|
|
6
|
+
export enum RIPPLE_TIMING {
|
|
7
|
+
LINEAR = 'linear',
|
|
8
|
+
EASE = 'ease',
|
|
9
|
+
EASE_IN = 'ease-in',
|
|
10
|
+
EASE_OUT = 'ease-out',
|
|
11
|
+
EASE_IN_OUT = 'ease-in-out',
|
|
12
|
+
MATERIAL = 'cubic-bezier(0.4, 0.0, 0.2, 1)'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default configuration for ripple effect
|
|
17
|
+
*/
|
|
18
|
+
export const RIPPLE_CONFIG = {
|
|
19
|
+
duration: 375,
|
|
20
|
+
timing: RIPPLE_TIMING.LINEAR,
|
|
21
|
+
opacity: ['1', '0.3'] as [string, string]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validation schema for ripple configuration
|
|
26
|
+
*/
|
|
27
|
+
export const RIPPLE_SCHEMA = {
|
|
28
|
+
duration: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
minimum: 0,
|
|
31
|
+
default: RIPPLE_CONFIG.duration
|
|
32
|
+
},
|
|
33
|
+
timing: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: Object.values(RIPPLE_TIMING),
|
|
36
|
+
default: RIPPLE_CONFIG.timing
|
|
37
|
+
},
|
|
38
|
+
opacity: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
pattern: '^[0-1](\\.\\d+)?$'
|
|
43
|
+
},
|
|
44
|
+
minItems: 2,
|
|
45
|
+
maxItems: 2,
|
|
46
|
+
default: RIPPLE_CONFIG.opacity
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/core/build/icon.ts
|
|
2
|
+
/**
|
|
3
|
+
* @module core/build
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for creating an icon element
|
|
8
|
+
*/
|
|
9
|
+
export interface IconElementOptions {
|
|
10
|
+
/**
|
|
11
|
+
* CSS class prefix
|
|
12
|
+
*/
|
|
13
|
+
prefix?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Additional CSS class
|
|
17
|
+
*/
|
|
18
|
+
class?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Icon size variant
|
|
22
|
+
*/
|
|
23
|
+
size?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for icon manager
|
|
28
|
+
*/
|
|
29
|
+
export interface IconConfig {
|
|
30
|
+
/**
|
|
31
|
+
* CSS class prefix
|
|
32
|
+
*/
|
|
33
|
+
prefix?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Component type
|
|
37
|
+
*/
|
|
38
|
+
type?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Icon position ('start' or 'end')
|
|
42
|
+
*/
|
|
43
|
+
position?: 'start' | 'end';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Icon size
|
|
47
|
+
*/
|
|
48
|
+
iconSize?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Icon manager interface
|
|
53
|
+
*/
|
|
54
|
+
export interface IconManager {
|
|
55
|
+
/**
|
|
56
|
+
* Sets icon HTML content
|
|
57
|
+
* @param html - Icon HTML content
|
|
58
|
+
* @returns IconManager instance for chaining
|
|
59
|
+
*/
|
|
60
|
+
setIcon: (html: string) => IconManager;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets current icon HTML content
|
|
64
|
+
* @returns Current icon HTML
|
|
65
|
+
*/
|
|
66
|
+
getIcon: () => string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Gets icon element
|
|
70
|
+
* @returns Icon element or null if not created
|
|
71
|
+
*/
|
|
72
|
+
getElement: () => HTMLElement | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates an icon DOM element
|
|
77
|
+
*
|
|
78
|
+
* @param html - Icon HTML content
|
|
79
|
+
* @param options - Icon options
|
|
80
|
+
* @returns Icon element
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
const createIconElement = (html: string, options: IconElementOptions = {}): HTMLElement => {
|
|
84
|
+
const PREFIX = options.prefix || 'mtrl';
|
|
85
|
+
const element = document.createElement('span');
|
|
86
|
+
element.className = `${PREFIX}-icon`;
|
|
87
|
+
|
|
88
|
+
if (options.class) {
|
|
89
|
+
element.classList.add(options.class);
|
|
90
|
+
}
|
|
91
|
+
if (options.size) {
|
|
92
|
+
element.classList.add(`${PREFIX}-icon--${options.size}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
element.innerHTML = html;
|
|
96
|
+
return element;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates an icon manager for a component
|
|
101
|
+
*
|
|
102
|
+
* @param element - Parent element
|
|
103
|
+
* @param config - Icon configuration
|
|
104
|
+
* @returns Icon manager interface
|
|
105
|
+
*/
|
|
106
|
+
export const createIcon = (element: HTMLElement, config: IconConfig = {}): IconManager => {
|
|
107
|
+
let iconElement: HTMLElement | null = null;
|
|
108
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
setIcon(html: string): IconManager {
|
|
112
|
+
if (!iconElement && html) {
|
|
113
|
+
iconElement = createIconElement(html, {
|
|
114
|
+
prefix: PREFIX,
|
|
115
|
+
class: `${PREFIX}-${config.type || 'component'}-icon`,
|
|
116
|
+
size: config.iconSize
|
|
117
|
+
});
|
|
118
|
+
if (config.position === 'end') {
|
|
119
|
+
element.appendChild(iconElement);
|
|
120
|
+
} else {
|
|
121
|
+
element.insertBefore(iconElement, element.firstChild);
|
|
122
|
+
}
|
|
123
|
+
} else if (iconElement && html) {
|
|
124
|
+
iconElement.innerHTML = html;
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
getIcon(): string {
|
|
130
|
+
return iconElement ? iconElement.innerHTML : '';
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
getElement(): HTMLElement | null {
|
|
134
|
+
return iconElement;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/core/build/ripple.ts
|
|
2
|
+
|
|
3
|
+
import { RIPPLE_CONFIG, RIPPLE_TIMING } from './constants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ripple animation configuration
|
|
7
|
+
*/
|
|
8
|
+
export interface RippleConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Animation duration in milliseconds
|
|
11
|
+
*/
|
|
12
|
+
duration?: number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Animation timing function
|
|
16
|
+
*/
|
|
17
|
+
timing?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Opacity start and end values
|
|
21
|
+
*/
|
|
22
|
+
opacity?: [string, string];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* End coordinates for ripple animation
|
|
27
|
+
*/
|
|
28
|
+
interface EndCoordinates {
|
|
29
|
+
size: string;
|
|
30
|
+
top: string;
|
|
31
|
+
left: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Document event listener
|
|
36
|
+
*/
|
|
37
|
+
interface DocumentListener {
|
|
38
|
+
event: string;
|
|
39
|
+
handler: EventListener;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ripple controller interface
|
|
44
|
+
*/
|
|
45
|
+
export interface RippleController {
|
|
46
|
+
/**
|
|
47
|
+
* Attaches ripple effect to an element
|
|
48
|
+
* @param element - Target element
|
|
49
|
+
*/
|
|
50
|
+
mount: (element: HTMLElement) => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Removes ripple effect from an element
|
|
54
|
+
* @param element - Target element
|
|
55
|
+
*/
|
|
56
|
+
unmount: (element: HTMLElement) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a ripple effect instance
|
|
61
|
+
*
|
|
62
|
+
* @param config - Ripple configuration
|
|
63
|
+
* @returns Ripple controller instance
|
|
64
|
+
*/
|
|
65
|
+
export const createRipple = (config: RippleConfig = {}): RippleController => {
|
|
66
|
+
// Make sure we fully merge the config options
|
|
67
|
+
const options = {
|
|
68
|
+
...RIPPLE_CONFIG,
|
|
69
|
+
...config,
|
|
70
|
+
// Handle nested objects like opacity array
|
|
71
|
+
opacity: config.opacity || RIPPLE_CONFIG.opacity
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getEndCoordinates = (bounds: DOMRect): EndCoordinates => {
|
|
75
|
+
const size = Math.max(bounds.width, bounds.height);
|
|
76
|
+
const top = bounds.height > bounds.width
|
|
77
|
+
? -bounds.height / 2
|
|
78
|
+
: -(bounds.width - bounds.height / 2);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
size: `${size * 2}px`,
|
|
82
|
+
top: `${top}px`,
|
|
83
|
+
left: `${size / -2}px`
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const createRippleElement = (): HTMLDivElement => {
|
|
88
|
+
const ripple = document.createElement('div');
|
|
89
|
+
ripple.className = 'ripple';
|
|
90
|
+
// Initial styles already set in CSS
|
|
91
|
+
ripple.style.transition = `all ${options.duration}ms ${options.timing}`;
|
|
92
|
+
return ripple;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Store document event listeners for cleanup
|
|
96
|
+
let documentListeners: DocumentListener[] = [];
|
|
97
|
+
|
|
98
|
+
// Safe document event handling
|
|
99
|
+
const addDocumentListener = (event: string, handler: EventListener): void => {
|
|
100
|
+
if (typeof document.addEventListener === 'function') {
|
|
101
|
+
document.addEventListener(event, handler);
|
|
102
|
+
documentListeners.push({ event, handler });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const removeDocumentListener = (event: string, handler: EventListener): void => {
|
|
107
|
+
if (typeof document.removeEventListener === 'function') {
|
|
108
|
+
document.removeEventListener(event, handler);
|
|
109
|
+
documentListeners = documentListeners.filter(
|
|
110
|
+
listener => !(listener.event === event && listener.handler === handler)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const animate = (event: MouseEvent, container: HTMLElement): void => {
|
|
116
|
+
if (!container) return;
|
|
117
|
+
|
|
118
|
+
const bounds = container.getBoundingClientRect();
|
|
119
|
+
const ripple = createRippleElement();
|
|
120
|
+
|
|
121
|
+
// Set initial position and state
|
|
122
|
+
Object.assign(ripple.style, {
|
|
123
|
+
left: `${event.offsetX || bounds.width / 2}px`,
|
|
124
|
+
top: `${event.offsetY || bounds.height / 2}px`,
|
|
125
|
+
transform: 'scale(0)',
|
|
126
|
+
opacity: options.opacity[0]
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
container.appendChild(ripple);
|
|
130
|
+
|
|
131
|
+
// Force reflow
|
|
132
|
+
// eslint-disable-next-line no-unused-expressions
|
|
133
|
+
ripple.offsetHeight;
|
|
134
|
+
|
|
135
|
+
// Animate to end position
|
|
136
|
+
const end = getEndCoordinates(bounds);
|
|
137
|
+
Object.assign(ripple.style, {
|
|
138
|
+
...end,
|
|
139
|
+
transform: 'scale(1)',
|
|
140
|
+
opacity: options.opacity[1]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const cleanup = () => {
|
|
144
|
+
ripple.style.opacity = '0';
|
|
145
|
+
|
|
146
|
+
// Use setTimeout to remove element after animation
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
if (ripple.parentNode) {
|
|
149
|
+
ripple.parentNode.removeChild(ripple);
|
|
150
|
+
}
|
|
151
|
+
}, options.duration);
|
|
152
|
+
|
|
153
|
+
removeDocumentListener('mouseup', cleanup);
|
|
154
|
+
removeDocumentListener('mouseleave', cleanup);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
addDocumentListener('mouseup', cleanup);
|
|
158
|
+
addDocumentListener('mouseleave', cleanup);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
mount: (element: HTMLElement): void => {
|
|
163
|
+
if (!element) return;
|
|
164
|
+
|
|
165
|
+
// Ensure proper positioning context
|
|
166
|
+
const currentPosition = window.getComputedStyle(element).position;
|
|
167
|
+
if (currentPosition === 'static') {
|
|
168
|
+
element.style.position = 'relative';
|
|
169
|
+
}
|
|
170
|
+
element.style.overflow = 'hidden';
|
|
171
|
+
|
|
172
|
+
// Store the mousedown handler to be able to remove it later
|
|
173
|
+
const mousedownHandler = (e: MouseEvent) => animate(e, element);
|
|
174
|
+
|
|
175
|
+
// Store handler reference on the element
|
|
176
|
+
if (!element.__rippleHandlers) {
|
|
177
|
+
element.__rippleHandlers = [];
|
|
178
|
+
}
|
|
179
|
+
element.__rippleHandlers.push(mousedownHandler);
|
|
180
|
+
|
|
181
|
+
element.addEventListener('mousedown', mousedownHandler);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
unmount: (element: HTMLElement): void => {
|
|
185
|
+
if (!element) return;
|
|
186
|
+
|
|
187
|
+
// Clear document event listeners
|
|
188
|
+
documentListeners.forEach(({ event, handler }) => {
|
|
189
|
+
removeDocumentListener(event, handler);
|
|
190
|
+
});
|
|
191
|
+
documentListeners = [];
|
|
192
|
+
|
|
193
|
+
// Remove event listeners
|
|
194
|
+
if (element.__rippleHandlers) {
|
|
195
|
+
element.__rippleHandlers.forEach(handler => {
|
|
196
|
+
element.removeEventListener('mousedown', handler);
|
|
197
|
+
});
|
|
198
|
+
element.__rippleHandlers = [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Remove all ripple elements
|
|
202
|
+
const ripples = element.querySelectorAll('.ripple');
|
|
203
|
+
ripples.forEach(ripple => {
|
|
204
|
+
// Call remove directly to match the test expectation
|
|
205
|
+
ripple.remove();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Extend the HTMLElement interface to add rippleHandlers property
|
|
212
|
+
declare global {
|
|
213
|
+
interface HTMLElement {
|
|
214
|
+
__rippleHandlers?: Array<(e: MouseEvent) => void>;
|
|
215
|
+
}
|
|
216
|
+
}
|