mtrl 0.0.2 → 0.1.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 (52) hide show
  1. package/package.json +2 -2
  2. package/src/components/button/styles.scss +198 -161
  3. package/src/components/checkbox/checkbox.js +4 -3
  4. package/src/components/checkbox/styles.scss +105 -55
  5. package/src/components/container/styles.scss +65 -58
  6. package/src/components/list/styles.scss +240 -11
  7. package/src/components/menu/features/items-manager.js +5 -1
  8. package/src/components/menu/styles.scss +37 -30
  9. package/src/components/navigation/constants.js +19 -54
  10. package/src/components/navigation/styles.scss +406 -6
  11. package/src/components/snackbar/styles.scss +46 -17
  12. package/src/components/switch/styles.scss +104 -40
  13. package/src/components/switch/switch.js +1 -1
  14. package/src/components/textfield/styles.scss +351 -5
  15. package/src/core/build/_ripple.scss +79 -0
  16. package/src/core/compose/features/disabled.js +27 -7
  17. package/src/core/compose/features/input.js +9 -1
  18. package/src/core/compose/features/textinput.js +16 -20
  19. package/src/core/dom/create.js +0 -1
  20. package/src/styles/abstract/_mixins.scss +9 -7
  21. package/src/styles/abstract/_theme.scss +157 -0
  22. package/src/styles/abstract/_variables.scss +72 -6
  23. package/src/styles/base/_reset.scss +86 -0
  24. package/src/styles/base/_typography.scss +155 -0
  25. package/src/styles/main.scss +104 -57
  26. package/src/styles/themes/_base-theme.scss +2 -27
  27. package/src/styles/themes/_baseline.scss +64 -39
  28. package/src/styles/utilities/_color.scss +154 -0
  29. package/src/styles/utilities/_flexbox.scss +194 -0
  30. package/src/styles/utilities/_spacing.scss +139 -0
  31. package/src/styles/utilities/_typography.scss +178 -0
  32. package/src/styles/utilities/_visibility.scss +142 -0
  33. package/test/components/button.test.js +46 -34
  34. package/test/components/checkbox.test.js +238 -0
  35. package/test/components/list.test.js +105 -0
  36. package/test/components/menu.test.js +385 -0
  37. package/test/components/navigation.test.js +227 -0
  38. package/test/components/snackbar.test.js +234 -0
  39. package/test/components/switch.test.js +186 -0
  40. package/test/components/textfield.test.js +314 -0
  41. package/test/core/ripple.test.js +21 -120
  42. package/test/setup.js +152 -239
  43. package/src/components/list/styles/_list-item.scss +0 -142
  44. package/src/components/list/styles/_list.scss +0 -89
  45. package/src/components/list/styles/_variables.scss +0 -13
  46. package/src/components/navigation/styles/_bar.scss +0 -51
  47. package/src/components/navigation/styles/_base.scss +0 -129
  48. package/src/components/navigation/styles/_drawer.scss +0 -169
  49. package/src/components/navigation/styles/_rail.scss +0 -65
  50. package/src/components/textfield/styles/base.scss +0 -107
  51. package/src/components/textfield/styles/filled.scss +0 -58
  52. package/src/components/textfield/styles/outlined.scss +0 -66
@@ -1,6 +1,352 @@
1
1
  // src/components/textfield/styles.scss
2
- @use 'sass:map';
3
- @use '../../styles/abstract/config' as c;
4
- @use 'styles/base';
5
- @use 'styles/filled';
6
- @use 'styles/outlined';
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
+ // Define the component once
9
+ $component: '#{base.$prefix}-textfield';
10
+
11
+ // ===== BASE STYLES =====
12
+ .#{$component} {
13
+ position: relative;
14
+ display: inline-flex;
15
+ flex-direction: column;
16
+ min-width: 210px;
17
+
18
+ // Size variants
19
+ &--small {
20
+ .#{$component}-input {
21
+ height: 48px;
22
+ }
23
+ }
24
+
25
+ &--large {
26
+ .#{$component}-input {
27
+ height: 64px;
28
+ }
29
+ }
30
+
31
+ // Label
32
+ &-label {
33
+ @include m.typography('body-large');
34
+ user-select: none;
35
+ position: absolute;
36
+ left: 16px;
37
+ top: 50%;
38
+ transform: translateY(-50%);
39
+ transform-origin: left top;
40
+ pointer-events: none;
41
+ border-radius: 2px;
42
+ color: t.color('on-surface-variant');
43
+ transition: transform v.motion('duration-short4') v.motion('easing-emphasized'),
44
+ color v.motion('duration-short2') v.motion('easing-standard');
45
+ }
46
+
47
+ // Input element
48
+ &-input {
49
+ @include m.typography('body-large');
50
+ @include m.shape('extra-small');
51
+ padding: 13px 16px;
52
+ width: 100%;
53
+ color: t.color('on-surface');
54
+ border: 0;
55
+ appearance: none;
56
+ outline: none;
57
+
58
+ &::placeholder {
59
+ color: transparent;
60
+ }
61
+
62
+ // Autofill styles
63
+ &:-webkit-autofill {
64
+ -webkit-text-fill-color: t.color('on-surface');
65
+ transition: background-color 5000s ease-in-out 0s; // Long transition to keep the background
66
+
67
+ & ~ .#{$component}-label {
68
+ transform: translateY(-95%) scale(0.75);
69
+ background-color: t.color('surface');
70
+ }
71
+ }
72
+
73
+ // Firefox autofill
74
+ &:autofill {
75
+ color: t.color('on-surface');
76
+
77
+ & ~ .#{$component}-label {
78
+ transform: translateY(-95%) scale(0.75);
79
+ background-color: t.color('surface');
80
+ }
81
+ }
82
+ }
83
+
84
+ // Error state
85
+ &--error {
86
+ border-color: t.color('error');
87
+
88
+ .#{$component}-label {
89
+ color: t.color('error');
90
+ }
91
+ }
92
+
93
+ // Disabled state
94
+ &-input:disabled {
95
+ opacity: 0.38;
96
+ border-color: t.color('on-surface');
97
+ background-color: t.alpha('on-surface', 0.04);
98
+ pointer-events: none;
99
+
100
+ & ~ .#{$component}-label {
101
+ color: t.color('on-surface');
102
+ opacity: 0.38;
103
+ }
104
+ }
105
+
106
+ // Helper text
107
+ &-helper {
108
+ @include m.typography('body-small');
109
+ margin-top: 4px;
110
+ color: t.color('on-surface-variant');
111
+
112
+ &--error {
113
+ color: t.color('error');
114
+ }
115
+ }
116
+
117
+ // Required indicator
118
+ &-required {
119
+ color: t.color('error');
120
+ margin-left: 4px;
121
+ }
122
+
123
+ // Accessibility
124
+ @include m.reduced-motion {
125
+ &-label {
126
+ transition: none;
127
+ }
128
+ }
129
+
130
+ // RTL support
131
+ @include m.rtl {
132
+ &-label {
133
+ left: auto;
134
+ right: 16px;
135
+ transform-origin: right top;
136
+ }
137
+
138
+ &-required {
139
+ margin-left: 0;
140
+ margin-right: 4px;
141
+ }
142
+ }
143
+
144
+ // ===== FILLED VARIANT =====
145
+ &--filled {
146
+ border-bottom: 1px solid t.color('outline');
147
+
148
+ .#{$component}-input {
149
+ background-color: t.color('surface-container-highest');
150
+ padding: 20px 16px 7px;
151
+ border-radius: f.get-shape('extra-small') f.get-shape('extra-small') 0 0;
152
+ @include m.motion-transition(background-color, border-color);
153
+
154
+ &:focus {
155
+ padding-bottom: 6px;
156
+ }
157
+
158
+ // Autofill styles for filled variant
159
+ &:-webkit-autofill {
160
+ border-radius: f.get-shape('extra-small') f.get-shape('extra-small') 0 0;
161
+
162
+ & ~ .#{$component}-label {
163
+ transform: translateY(-95%) scale(0.75);
164
+ color: t.color('on-surface-variant');
165
+ }
166
+ }
167
+
168
+ &:autofill {
169
+ & ~ .#{$component}-label {
170
+ transform: translateY(-95%) scale(0.75);
171
+ color: t.color('on-surface-variant');
172
+ }
173
+ }
174
+ }
175
+
176
+ // Populated field (not empty) or focused field label position
177
+ &:not(.#{$component}--empty) .#{$component}-label,
178
+ &.#{$component}--focused .#{$component}-label {
179
+ transform: translateY(-95%) scale(0.75);
180
+ }
181
+
182
+ // Focus state
183
+ &.#{$component}--focused {
184
+ border-bottom: 2px solid t.color('primary');
185
+
186
+ .#{$component}-label {
187
+ color: t.color('primary');
188
+ }
189
+
190
+ &:hover {
191
+ border-bottom: 2px solid t.color('primary');
192
+ }
193
+ }
194
+
195
+ // Hover state
196
+ &:hover {
197
+ border-bottom: 1px solid t.color('primary');
198
+
199
+ .#{$component}-label {
200
+ color: t.color('primary');
201
+ }
202
+ }
203
+
204
+ // Error state
205
+ &.#{$component}--error {
206
+ border-bottom: 2px solid t.color('error');
207
+
208
+ .#{$component}-label {
209
+ color: t.color('error');
210
+ }
211
+
212
+ &:hover {
213
+ border-bottom: 2px solid t.color('error');
214
+ }
215
+
216
+ &.#{$component}--focused {
217
+ border-bottom: 2px solid t.color('error');
218
+ }
219
+ }
220
+
221
+ // Disabled state
222
+ &.#{$component}--disabled {
223
+ border-bottom-color: t.alpha('on-surface', 0.38);
224
+ pointer-events: none;
225
+
226
+ .#{$component}-input {
227
+ background-color: t.alpha('on-surface', 0.04);
228
+ }
229
+ }
230
+
231
+ // RTL support
232
+ @include m.rtl {
233
+ .#{$component}-label {
234
+ left: auto;
235
+ right: 16px;
236
+ }
237
+ }
238
+ }
239
+
240
+ // ===== OUTLINED VARIANT =====
241
+ &--outlined {
242
+ border: 1px solid t.color('outline');
243
+ border-radius: f.get-shape('extra-small');
244
+ @include m.motion-transition(border-color);
245
+
246
+ .#{$component}-input {
247
+ background-color: transparent;
248
+ padding: 13px 16px 14px;
249
+ @include m.motion-transition(padding);
250
+
251
+ // Autofill styles for outlined variant
252
+ &:-webkit-autofill {
253
+ border-radius: f.get-shape('extra-small');
254
+
255
+ & ~ .#{$component}-label {
256
+ background-color: t.color('surface');
257
+ transform: translateY(-145%) scale(0.75);
258
+ left: 13px;
259
+ padding: 0 4px;
260
+ }
261
+ }
262
+
263
+ &:autofill {
264
+ & ~ .#{$component}-label {
265
+ background-color: t.color('surface');
266
+ transform: translateY(-145%) scale(0.75);
267
+ left: 13px;
268
+ padding: 0 4px;
269
+ }
270
+ }
271
+ }
272
+
273
+ // Populated field (not empty) or focused field label position
274
+ &:not(.#{$component}--empty) .#{$component}-label,
275
+ &.#{$component}--focused .#{$component}-label {
276
+ background-color: t.color('surface');
277
+ transform: translateY(-145%) scale(0.75);
278
+ left: 13px;
279
+ padding: 0 4px;
280
+ }
281
+
282
+ // Focus state
283
+ &.#{$component}--focused {
284
+ border: 2px solid t.color('primary');
285
+
286
+ .#{$component}-label {
287
+ color: t.color('primary');
288
+ border-radius: 2px;
289
+ left: 12px;
290
+ }
291
+
292
+ .#{$component}-input {
293
+ padding: 12px 15px 13px;
294
+ }
295
+
296
+ &:hover {
297
+ border: 2px solid t.color('primary');
298
+ }
299
+ }
300
+
301
+ // Hover state
302
+ &:hover {
303
+ border: 1px solid t.color('primary');
304
+
305
+ .#{$component}-label {
306
+ color: t.color('primary');
307
+ }
308
+ }
309
+
310
+ // Error state
311
+ &.#{$component}--error {
312
+ border: 2px solid t.color('error');
313
+
314
+ .#{$component}-label {
315
+ color: t.color('error');
316
+ left: 12px;
317
+ }
318
+
319
+ .#{$component}-input {
320
+ padding: 12px 15px 13px;
321
+ }
322
+
323
+ &:hover,
324
+ &.#{$component}--focused {
325
+ border: 2px solid t.color('error');
326
+ }
327
+ }
328
+
329
+ // Disabled state
330
+ &.#{$component}--disabled {
331
+ border-color: t.alpha('on-surface', 0.38);
332
+ pointer-events: none;
333
+ }
334
+
335
+ // RTL support
336
+ @include m.rtl {
337
+ &:not(.#{$component}--empty) .#{$component}-label,
338
+ &.#{$component}--focused .#{$component}-label {
339
+ left: auto;
340
+ right: 13px;
341
+ }
342
+
343
+ &.#{$component}--focused .#{$component}-label {
344
+ right: 12px;
345
+ }
346
+
347
+ &.#{$component}--error .#{$component}-label {
348
+ right: 12px;
349
+ }
350
+ }
351
+ }
352
+ }
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ // src/core/compose/features/disabled.js
1
2
 
2
3
  /**
3
4
  * Adds disabled state management to a component
@@ -5,22 +6,37 @@
5
6
  * @returns {Function} Component enhancer
6
7
  */
7
8
  export const withDisabled = (config = {}) => (component) => {
9
+ // Get the disabled class based on component name
10
+ const disabledClass = `${component.getClass(component.componentName)}--disabled`
11
+
8
12
  // Directly implement disabled functionality
9
13
  const disabled = {
10
14
  enable () {
11
- component.element.disabled = false
12
- component.element.removeAttribute('disabled')
15
+ component.element.classList.remove(disabledClass)
16
+ if (component.input) {
17
+ component.input.disabled = false
18
+ component.input.removeAttribute('disabled')
19
+ } else {
20
+ component.element.disabled = false
21
+ component.element.removeAttribute('disabled')
22
+ }
13
23
  return this
14
24
  },
15
25
 
16
26
  disable () {
17
- component.element.disabled = true
18
- component.element.setAttribute('disabled', 'true')
27
+ component.element.classList.add(disabledClass)
28
+ if (component.input) {
29
+ component.input.disabled = true
30
+ component.input.setAttribute('disabled', 'true')
31
+ } else {
32
+ component.element.disabled = true
33
+ component.element.setAttribute('disabled', 'true')
34
+ }
19
35
  return this
20
36
  },
21
37
 
22
38
  toggle () {
23
- if (component.element.disabled) {
39
+ if (this.isDisabled()) {
24
40
  this.enable()
25
41
  } else {
26
42
  this.disable()
@@ -29,12 +45,16 @@ export const withDisabled = (config = {}) => (component) => {
29
45
  },
30
46
 
31
47
  isDisabled () {
32
- return component.element.disabled === true
48
+ return component.input ? component.input.disabled : component.element.disabled
33
49
  }
34
50
  }
35
51
 
52
+ // Initialize disabled state if configured
36
53
  if (config.disabled) {
37
- disabled.disable()
54
+ // Use requestAnimationFrame to ensure DOM is ready
55
+ requestAnimationFrame(() => {
56
+ disabled.disable()
57
+ })
38
58
  }
39
59
 
40
60
  return {
@@ -42,7 +42,15 @@ export const withInput = (config = {}) => (component) => {
42
42
 
43
43
  Object.entries(attributes).forEach(([key, value]) => {
44
44
  if (value !== null && value !== undefined) {
45
- input.setAttribute(key, value)
45
+ if (key === 'disabled' && value === true) {
46
+ input.disabled = true
47
+ input.setAttribute('disabled', 'true')
48
+ // Note: We don't add the class here because that's handled by withDisabled
49
+ } else if (value === true) {
50
+ input.setAttribute(key, key)
51
+ } else {
52
+ input.setAttribute(key, value)
53
+ }
46
54
  }
47
55
  })
48
56
 
@@ -35,30 +35,28 @@ export const withTextInput = (config = {}) => (component) => {
35
35
  return isEmpty
36
36
  }
37
37
 
38
- // Detect autocomplete
39
- const handleAutocomplete = (event) => {
40
- // Chrome/Safari trigger animationstart
41
- if (event.animationName === 'onAutoFillStart') {
38
+ // Detect autofill using input events instead of animation
39
+ // This is more compatible with our testing environment
40
+ const handleAutofill = () => {
41
+ // Check for webkit autofill background
42
+ const isAutofilled =
43
+ input.matches(':-webkit-autofill') ||
44
+ // For Firefox and other browsers
45
+ (window.getComputedStyle(input).backgroundColor === 'rgb(250, 255, 189)' ||
46
+ window.getComputedStyle(input).backgroundColor === 'rgb(232, 240, 254)')
47
+
48
+ if (isAutofilled) {
42
49
  component.element.classList.remove(`${component.getClass('textfield')}--empty`)
43
50
  component.emit('input', { value: input.value, isEmpty: false, isAutofilled: true })
44
51
  }
45
52
  }
46
53
 
47
- // Add required animation for autocomplete detection
48
- const style = document.createElement('style')
49
- style.textContent = `
50
- @keyframes onAutoFillStart { from {} to {} }
51
- .${component.getClass('textfield')}-input:-webkit-autofill {
52
- animation-name: onAutoFillStart;
53
- animation-duration: 1ms;
54
- }
55
- `
56
- document.head.appendChild(style)
57
-
58
54
  // Event listeners
59
55
  input.addEventListener('focus', () => {
60
56
  component.element.classList.add(`${component.getClass('textfield')}--focused`)
61
57
  component.emit('focus', { isEmpty: updateInputState() })
58
+ // Also check for autofill on focus
59
+ setTimeout(handleAutofill, 100)
62
60
  })
63
61
 
64
62
  input.addEventListener('blur', () => {
@@ -74,8 +72,6 @@ export const withTextInput = (config = {}) => (component) => {
74
72
  })
75
73
  })
76
74
 
77
- input.addEventListener('animationstart', handleAutocomplete)
78
-
79
75
  // Initial state
80
76
  updateInputState()
81
77
 
@@ -85,10 +81,10 @@ export const withTextInput = (config = {}) => (component) => {
85
81
  if (component.lifecycle) {
86
82
  const originalDestroy = component.lifecycle.destroy
87
83
  component.lifecycle.destroy = () => {
88
- input.removeEventListener('animationstart', handleAutocomplete)
89
- style.remove()
90
84
  input.remove()
91
- originalDestroy.call(component.lifecycle)
85
+ if (originalDestroy) {
86
+ originalDestroy.call(component.lifecycle)
87
+ }
92
88
  }
93
89
  }
94
90
 
@@ -86,7 +86,6 @@ export const createElement = (options = {}) => {
86
86
  }
87
87
 
88
88
  if (typeof onCreate === 'function') {
89
- log.info('onCreate', element, context)
90
89
  onCreate(element, context)
91
90
  }
92
91
 
@@ -232,8 +232,8 @@ $icons: (
232
232
  }
233
233
  }
234
234
 
235
- // Touch Target
236
235
  @mixin touch-target($size: 48px) {
236
+ // Position first to avoid the deprecation warning
237
237
  position: relative;
238
238
 
239
239
  &::after {
@@ -244,6 +244,14 @@ $icons: (
244
244
  height: $size;
245
245
  transform: translate(-50%, -50%);
246
246
  }
247
+
248
+ @media (prefers-reduced-motion: reduce) {
249
+ @content;
250
+ }
251
+
252
+ @media (forced-colors: active) {
253
+ @content;
254
+ }
247
255
  }
248
256
 
249
257
  // Scrollbars
@@ -322,12 +330,6 @@ $icons: (
322
330
  }
323
331
  }
324
332
 
325
- @mixin flex-center {
326
- display: flex;
327
- align-items: center;
328
- justify-content: center;
329
- }
330
-
331
333
  // Print
332
334
  @mixin print {
333
335
  @media print {