mtrl 0.2.5 → 0.2.7
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/index.ts +18 -0
- package/package.json +1 -1
- package/src/components/badge/_styles.scss +123 -115
- package/src/components/badge/api.ts +57 -59
- package/src/components/badge/badge.ts +16 -2
- package/src/components/badge/config.ts +65 -11
- package/src/components/badge/constants.ts +22 -12
- package/src/components/badge/features.ts +44 -40
- package/src/components/badge/types.ts +42 -30
- package/src/components/bottom-app-bar/_styles.scss +103 -0
- package/src/components/bottom-app-bar/bottom-app-bar.ts +196 -0
- package/src/components/bottom-app-bar/config.ts +73 -0
- package/src/components/bottom-app-bar/index.ts +11 -0
- package/src/components/bottom-app-bar/types.ts +108 -0
- package/src/components/button/_styles.scss +0 -66
- package/src/components/button/api.ts +5 -0
- package/src/components/button/button.ts +0 -2
- package/src/components/button/config.ts +5 -0
- package/src/components/button/constants.ts +0 -6
- package/src/components/button/index.ts +2 -2
- package/src/components/button/types.ts +7 -7
- package/src/components/card/_styles.scss +67 -25
- package/src/components/card/api.ts +54 -3
- package/src/components/card/card.ts +25 -6
- package/src/components/card/config.ts +189 -22
- package/src/components/card/constants.ts +20 -19
- package/src/components/card/content.ts +299 -2
- package/src/components/card/features.ts +158 -4
- package/src/components/card/index.ts +31 -9
- package/src/components/card/types.ts +166 -15
- package/src/components/checkbox/_styles.scss +0 -2
- package/src/components/chip/chip.ts +1 -9
- package/src/components/chip/constants.ts +0 -10
- package/src/components/chip/index.ts +1 -1
- package/src/components/chip/types.ts +1 -4
- package/src/components/datepicker/_styles.scss +358 -0
- package/src/components/datepicker/api.ts +272 -0
- package/src/components/datepicker/config.ts +144 -0
- package/src/components/datepicker/constants.ts +98 -0
- package/src/components/datepicker/datepicker.ts +346 -0
- package/src/components/datepicker/index.ts +9 -0
- package/src/components/datepicker/render.ts +452 -0
- package/src/components/datepicker/types.ts +268 -0
- package/src/components/datepicker/utils.ts +290 -0
- package/src/components/dialog/_styles.scss +174 -128
- package/src/components/dialog/api.ts +48 -13
- package/src/components/dialog/config.ts +9 -5
- package/src/components/dialog/dialog.ts +6 -3
- package/src/components/dialog/features.ts +290 -130
- package/src/components/dialog/types.ts +7 -4
- package/src/components/divider/_styles.scss +57 -0
- package/src/components/divider/config.ts +81 -0
- package/src/components/divider/divider.ts +37 -0
- package/src/components/divider/features.ts +207 -0
- package/src/components/divider/index.ts +5 -0
- package/src/components/divider/types.ts +55 -0
- package/src/components/extended-fab/_styles.scss +267 -0
- package/src/components/extended-fab/api.ts +141 -0
- package/src/components/extended-fab/config.ts +108 -0
- package/src/components/extended-fab/constants.ts +36 -0
- package/src/components/extended-fab/extended-fab.ts +125 -0
- package/src/components/extended-fab/index.ts +4 -0
- package/src/components/extended-fab/types.ts +287 -0
- package/src/components/fab/_styles.scss +225 -0
- package/src/components/fab/api.ts +97 -0
- package/src/components/fab/config.ts +94 -0
- package/src/components/fab/constants.ts +41 -0
- package/src/components/fab/fab.ts +67 -0
- package/src/components/fab/index.ts +4 -0
- package/src/components/fab/types.ts +234 -0
- package/src/components/navigation/_styles.scss +1 -0
- package/src/components/navigation/api.ts +78 -50
- package/src/components/navigation/features/items.ts +280 -0
- package/src/components/navigation/nav-item.ts +72 -23
- package/src/components/navigation/navigation.ts +54 -2
- package/src/components/navigation/types.ts +210 -188
- package/src/components/progress/_styles.scss +0 -65
- package/src/components/progress/config.ts +1 -2
- package/src/components/progress/constants.ts +0 -14
- package/src/components/progress/index.ts +1 -1
- package/src/components/progress/progress.ts +1 -4
- package/src/components/progress/types.ts +1 -4
- package/src/components/radios/_styles.scss +0 -45
- package/src/components/radios/api.ts +85 -60
- package/src/components/radios/config.ts +1 -2
- package/src/components/radios/constants.ts +0 -9
- package/src/components/radios/index.ts +1 -1
- package/src/components/radios/radio.ts +34 -11
- package/src/components/radios/radios.ts +2 -1
- package/src/components/radios/types.ts +1 -7
- package/src/components/search/_styles.scss +306 -0
- package/src/components/search/api.ts +203 -0
- package/src/components/search/config.ts +87 -0
- package/src/components/search/constants.ts +21 -0
- package/src/components/search/features/index.ts +4 -0
- package/src/components/search/features/search.ts +718 -0
- package/src/components/search/features/states.ts +165 -0
- package/src/components/search/features/structure.ts +198 -0
- package/src/components/search/index.ts +10 -0
- package/src/components/search/search.ts +52 -0
- package/src/components/search/types.ts +163 -0
- package/src/components/segmented-button/_styles.scss +117 -0
- package/src/components/segmented-button/config.ts +67 -0
- package/src/components/segmented-button/constants.ts +42 -0
- package/src/components/segmented-button/index.ts +4 -0
- package/src/components/segmented-button/segment.ts +155 -0
- package/src/components/segmented-button/segmented-button.ts +250 -0
- package/src/components/segmented-button/types.ts +219 -0
- package/src/components/slider/_styles.scss +221 -168
- package/src/components/slider/accessibility.md +59 -0
- package/src/components/slider/api.ts +41 -120
- package/src/components/slider/config.ts +51 -49
- package/src/components/slider/features/handlers.ts +495 -0
- package/src/components/slider/features/index.ts +1 -2
- package/src/components/slider/features/slider.ts +66 -84
- package/src/components/slider/features/states.ts +195 -0
- package/src/components/slider/features/structure.ts +141 -184
- package/src/components/slider/features/ui.ts +150 -201
- package/src/components/slider/index.ts +2 -11
- package/src/components/slider/slider.ts +9 -12
- package/src/components/slider/types.ts +39 -24
- package/src/components/switch/_styles.scss +0 -2
- package/src/components/tabs/_styles.scss +346 -154
- package/src/components/tabs/api.ts +178 -400
- package/src/components/tabs/config.ts +46 -52
- package/src/components/tabs/constants.ts +85 -8
- package/src/components/tabs/features.ts +403 -0
- package/src/components/tabs/index.ts +60 -3
- package/src/components/tabs/indicator.ts +285 -0
- package/src/components/tabs/responsive.ts +144 -0
- package/src/components/tabs/scroll-indicators.ts +149 -0
- package/src/components/tabs/state.ts +186 -0
- package/src/components/tabs/tab-api.ts +258 -0
- package/src/components/tabs/tab.ts +255 -0
- package/src/components/tabs/tabs.ts +50 -31
- package/src/components/tabs/types.ts +332 -128
- package/src/components/tabs/utils.ts +107 -0
- package/src/components/textfield/_styles.scss +0 -98
- package/src/components/textfield/config.ts +2 -3
- package/src/components/textfield/constants.ts +0 -14
- package/src/components/textfield/index.ts +2 -2
- package/src/components/textfield/textfield.ts +0 -2
- package/src/components/textfield/types.ts +1 -4
- package/src/components/timepicker/README.md +277 -0
- package/src/components/timepicker/_styles.scss +451 -0
- package/src/components/timepicker/api.ts +632 -0
- package/src/components/timepicker/clockdial.ts +482 -0
- package/src/components/timepicker/config.ts +130 -0
- package/src/components/timepicker/constants.ts +138 -0
- package/src/components/timepicker/index.ts +8 -0
- package/src/components/timepicker/render.ts +613 -0
- package/src/components/timepicker/timepicker.ts +117 -0
- package/src/components/timepicker/types.ts +336 -0
- package/src/components/timepicker/utils.ts +241 -0
- package/src/components/top-app-bar/_styles.scss +225 -0
- package/src/components/top-app-bar/config.ts +83 -0
- package/src/components/top-app-bar/index.ts +11 -0
- package/src/components/top-app-bar/top-app-bar.ts +316 -0
- package/src/components/top-app-bar/types.ts +140 -0
- package/src/core/build/_ripple.scss +6 -6
- package/src/core/build/ripple.ts +72 -95
- package/src/core/compose/component.ts +1 -1
- package/src/core/compose/features/badge.ts +79 -0
- package/src/core/compose/features/icon.ts +3 -1
- package/src/core/compose/features/index.ts +3 -1
- package/src/core/compose/features/ripple.ts +4 -1
- package/src/core/compose/features/textlabel.ts +26 -2
- package/src/core/dom/create.ts +5 -0
- package/src/index.ts +9 -0
- package/src/styles/abstract/_theme.scss +115 -3
- package/src/styles/themes/_autumn.scss +21 -0
- package/src/styles/themes/_base-theme.scss +61 -0
- package/src/styles/themes/_baseline.scss +58 -0
- package/src/styles/themes/_bluekhaki.scss +125 -0
- package/src/styles/themes/_brownbeige.scss +125 -0
- package/src/styles/themes/_browngreen.scss +125 -0
- package/src/styles/themes/_forest.scss +6 -0
- package/src/styles/themes/_greenbeige.scss +125 -0
- package/src/styles/themes/_material.scss +125 -0
- package/src/styles/themes/_ocean.scss +6 -0
- package/src/styles/themes/_sageivory.scss +125 -0
- package/src/styles/themes/_spring.scss +6 -0
- package/src/styles/themes/_summer.scss +5 -0
- package/src/styles/themes/_sunset.scss +5 -0
- package/src/styles/themes/_tealcaramel.scss +125 -0
- package/src/styles/themes/_winter.scss +6 -0
- package/src/components/card/actions.ts +0 -48
- package/src/components/card/header.ts +0 -88
- package/src/components/card/media.ts +0 -52
- package/src/components/navigation/features/items.js +0 -192
- package/src/components/slider/features/appearance.ts +0 -94
- package/src/components/slider/features/disabled.ts +0 -43
- package/src/components/slider/features/events.ts +0 -164
- package/src/components/slider/features/interactions.ts +0 -261
- package/src/components/slider/features/keyboard.ts +0 -112
- package/src/core/collection/adapters/mongodb.js +0 -232
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// src/components/search/features/states.ts
|
|
2
|
+
import { SEARCH_VARIANTS } from '../constants';
|
|
3
|
+
import { SearchConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add state management features to search component
|
|
7
|
+
*
|
|
8
|
+
* @param config Search configuration
|
|
9
|
+
* @returns Component enhancer with state management features
|
|
10
|
+
*/
|
|
11
|
+
export const withStates = (config: SearchConfig) => component => {
|
|
12
|
+
// Track initial disabled state
|
|
13
|
+
const isDisabled = config.disabled === true;
|
|
14
|
+
|
|
15
|
+
// Apply initial disabled state if needed
|
|
16
|
+
if (isDisabled) {
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
disableComponent();
|
|
19
|
+
}, 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Disables the component
|
|
24
|
+
*/
|
|
25
|
+
function disableComponent() {
|
|
26
|
+
component.element.classList.add(`${component.getClass('search')}--disabled`);
|
|
27
|
+
component.element.setAttribute('aria-disabled', 'true');
|
|
28
|
+
|
|
29
|
+
// Disable input - important to use the disabled property not the attribute
|
|
30
|
+
if (component.structure?.input) {
|
|
31
|
+
component.structure.input.disabled = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Ensure buttons cannot receive focus when disabled
|
|
35
|
+
const buttons = [
|
|
36
|
+
component.structure?.leadingIcon,
|
|
37
|
+
component.structure?.clearButton,
|
|
38
|
+
component.structure?.trailingIcon,
|
|
39
|
+
component.structure?.trailingIcon2
|
|
40
|
+
].filter(Boolean);
|
|
41
|
+
|
|
42
|
+
buttons.forEach(button => {
|
|
43
|
+
button.tabIndex = -1;
|
|
44
|
+
button.setAttribute('aria-disabled', 'true');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Enables the component
|
|
50
|
+
*/
|
|
51
|
+
function enableComponent() {
|
|
52
|
+
component.element.classList.remove(`${component.getClass('search')}--disabled`);
|
|
53
|
+
component.element.setAttribute('aria-disabled', 'false');
|
|
54
|
+
|
|
55
|
+
// Enable input - important to use the disabled property not the attribute
|
|
56
|
+
if (component.structure?.input) {
|
|
57
|
+
component.structure.input.disabled = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Re-enable buttons
|
|
61
|
+
const buttons = [
|
|
62
|
+
component.structure?.leadingIcon,
|
|
63
|
+
component.structure?.clearButton,
|
|
64
|
+
component.structure?.trailingIcon,
|
|
65
|
+
component.structure?.trailingIcon2
|
|
66
|
+
].filter(Boolean);
|
|
67
|
+
|
|
68
|
+
buttons.forEach(button => {
|
|
69
|
+
button.tabIndex = 0;
|
|
70
|
+
button.setAttribute('aria-disabled', 'false');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Clear button special case - only enable if there's text
|
|
74
|
+
if (component.structure?.clearButton &&
|
|
75
|
+
component.structure.input &&
|
|
76
|
+
!component.structure.input.value) {
|
|
77
|
+
component.structure.clearButton.tabIndex = -1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets the active variant
|
|
83
|
+
*/
|
|
84
|
+
function getActiveVariant() {
|
|
85
|
+
return Object.values(SEARCH_VARIANTS).find(variantName =>
|
|
86
|
+
component.element.classList.contains(`${component.getClass('search')}--${variantName}`)
|
|
87
|
+
) || SEARCH_VARIANTS.BAR;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Return enhanced component
|
|
91
|
+
return {
|
|
92
|
+
...component,
|
|
93
|
+
|
|
94
|
+
// Disabled state management
|
|
95
|
+
disabled: {
|
|
96
|
+
/**
|
|
97
|
+
* Enables the component
|
|
98
|
+
* @returns Disabled manager for chaining
|
|
99
|
+
*/
|
|
100
|
+
enable() {
|
|
101
|
+
enableComponent();
|
|
102
|
+
return this;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Disables the component
|
|
107
|
+
* @returns Disabled manager for chaining
|
|
108
|
+
*/
|
|
109
|
+
disable() {
|
|
110
|
+
disableComponent();
|
|
111
|
+
return this;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Checks if component is disabled
|
|
116
|
+
* @returns True if disabled
|
|
117
|
+
*/
|
|
118
|
+
isDisabled() {
|
|
119
|
+
return component.element.classList.contains(`${component.getClass('search')}--disabled`);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Appearance management
|
|
124
|
+
appearance: {
|
|
125
|
+
/**
|
|
126
|
+
* Sets search variant
|
|
127
|
+
* @param variant Variant name
|
|
128
|
+
* @returns Appearance manager for chaining
|
|
129
|
+
*/
|
|
130
|
+
setVariant(variant) {
|
|
131
|
+
const currentVariant = getActiveVariant();
|
|
132
|
+
|
|
133
|
+
// If already this variant, do nothing
|
|
134
|
+
if (currentVariant === variant) {
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Remove existing variant classes
|
|
139
|
+
Object.values(SEARCH_VARIANTS).forEach(variantName => {
|
|
140
|
+
component.element.classList.remove(`${component.getClass('search')}--${variantName}`);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Add new variant class
|
|
144
|
+
component.element.classList.add(`${component.getClass('search')}--${variant}`);
|
|
145
|
+
|
|
146
|
+
// Toggle expanded state class
|
|
147
|
+
if (variant === SEARCH_VARIANTS.VIEW) {
|
|
148
|
+
component.element.classList.add(`${component.getClass('search')}--expanded`);
|
|
149
|
+
} else {
|
|
150
|
+
component.element.classList.remove(`${component.getClass('search')}--expanded`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Gets search variant
|
|
158
|
+
* @returns Current variant name
|
|
159
|
+
*/
|
|
160
|
+
getVariant() {
|
|
161
|
+
return getActiveVariant();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/components/search/features/structure.ts
|
|
2
|
+
import { SEARCH_VARIANTS } from '../constants';
|
|
3
|
+
import { SearchConfig } from '../types';
|
|
4
|
+
import { createElement } from '../../../core/dom/create';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates the search component DOM structure
|
|
8
|
+
* @param config Search configuration
|
|
9
|
+
* @returns Component enhancer with DOM structure
|
|
10
|
+
*/
|
|
11
|
+
export const withStructure = (config: SearchConfig) => component => {
|
|
12
|
+
// Get initial config values
|
|
13
|
+
const isDisabled = config.disabled === true;
|
|
14
|
+
const variant = config.variant || SEARCH_VARIANTS.BAR;
|
|
15
|
+
const isViewMode = variant === SEARCH_VARIANTS.VIEW;
|
|
16
|
+
const placeholder = config.placeholder || 'Search';
|
|
17
|
+
const value = config.value || '';
|
|
18
|
+
|
|
19
|
+
// Get prefixed class names
|
|
20
|
+
const getClass = (className) => component.getClass(className);
|
|
21
|
+
|
|
22
|
+
// Build attributes object for the input
|
|
23
|
+
const inputAttrs = {
|
|
24
|
+
'type': 'text',
|
|
25
|
+
'placeholder': placeholder,
|
|
26
|
+
'value': value,
|
|
27
|
+
'aria-label': placeholder
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Only add disabled attribute if the input should be disabled
|
|
31
|
+
if (isDisabled) {
|
|
32
|
+
inputAttrs['disabled'] = 'disabled';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create container element
|
|
36
|
+
const container = createElement({
|
|
37
|
+
tag: 'div',
|
|
38
|
+
className: getClass('search-container'),
|
|
39
|
+
container: component.element,
|
|
40
|
+
attrs: {
|
|
41
|
+
style: `
|
|
42
|
+
${config.minWidth ? `min-width: ${config.minWidth}px;` : ''}
|
|
43
|
+
${config.maxWidth ? `max-width: ${config.maxWidth}px;` : ''}
|
|
44
|
+
${config.fullWidth ? 'width: 100%;' : ''}
|
|
45
|
+
`
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Create leading icon
|
|
50
|
+
const leadingIcon = createElement({
|
|
51
|
+
tag: 'div',
|
|
52
|
+
className: getClass('search-leading-icon'),
|
|
53
|
+
container: container,
|
|
54
|
+
html: config.leadingIcon || '',
|
|
55
|
+
attrs: {
|
|
56
|
+
'role': 'button',
|
|
57
|
+
'tabindex': isDisabled ? '-1' : '0',
|
|
58
|
+
'aria-label': 'Search'
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Create input wrapper
|
|
63
|
+
const inputWrapper = createElement({
|
|
64
|
+
tag: 'div',
|
|
65
|
+
className: getClass('search-input-wrapper'),
|
|
66
|
+
container: container
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Create input element with properly handled disabled state
|
|
70
|
+
const input = createElement({
|
|
71
|
+
tag: 'input',
|
|
72
|
+
className: getClass('search-input'),
|
|
73
|
+
container: inputWrapper,
|
|
74
|
+
attrs: inputAttrs
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Create clear button
|
|
78
|
+
const clearButton = createElement({
|
|
79
|
+
tag: 'div',
|
|
80
|
+
className: [
|
|
81
|
+
getClass('search-clear-button'),
|
|
82
|
+
value ? '' : getClass('search-clear-button--hidden')
|
|
83
|
+
],
|
|
84
|
+
container: container,
|
|
85
|
+
html: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
|
|
86
|
+
attrs: {
|
|
87
|
+
'role': 'button',
|
|
88
|
+
'tabindex': isDisabled || !value ? '-1' : '0',
|
|
89
|
+
'aria-label': 'Clear search'
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Create optional trailing elements
|
|
94
|
+
let trailingIcon, trailingIcon2, avatar;
|
|
95
|
+
|
|
96
|
+
if (config.trailingIcon) {
|
|
97
|
+
trailingIcon = createElement({
|
|
98
|
+
tag: 'div',
|
|
99
|
+
className: getClass('search-trailing-icon'),
|
|
100
|
+
container: container,
|
|
101
|
+
html: config.trailingIcon,
|
|
102
|
+
attrs: {
|
|
103
|
+
'role': 'button',
|
|
104
|
+
'tabindex': isDisabled ? '-1' : '0',
|
|
105
|
+
'aria-label': 'Search option'
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (config.trailingIcon2) {
|
|
111
|
+
trailingIcon2 = createElement({
|
|
112
|
+
tag: 'div',
|
|
113
|
+
className: getClass('search-trailing-icon'),
|
|
114
|
+
container: container,
|
|
115
|
+
html: config.trailingIcon2,
|
|
116
|
+
attrs: {
|
|
117
|
+
'role': 'button',
|
|
118
|
+
'tabindex': isDisabled ? '-1' : '0',
|
|
119
|
+
'aria-label': 'Search option'
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (config.avatar) {
|
|
125
|
+
avatar = createElement({
|
|
126
|
+
tag: 'div',
|
|
127
|
+
className: getClass('search-avatar'),
|
|
128
|
+
container: container,
|
|
129
|
+
html: config.avatar
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create divider and suggestions container for view variant
|
|
134
|
+
let divider, suggestionsContainer;
|
|
135
|
+
|
|
136
|
+
if (isViewMode || config.suggestions) {
|
|
137
|
+
divider = createElement({
|
|
138
|
+
tag: 'div',
|
|
139
|
+
className: getClass('search-divider')
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
suggestionsContainer = createElement({
|
|
143
|
+
tag: 'div',
|
|
144
|
+
className: getClass('search-suggestions-container'),
|
|
145
|
+
container: component.element
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add component base class and accessibility attributes
|
|
150
|
+
component.element.classList.add(component.getClass('search'));
|
|
151
|
+
component.element.setAttribute('role', 'search');
|
|
152
|
+
component.element.setAttribute('aria-disabled', isDisabled ? 'true' : 'false');
|
|
153
|
+
|
|
154
|
+
// Apply style classes
|
|
155
|
+
applyStyleClasses(component, config, isViewMode, isDisabled);
|
|
156
|
+
|
|
157
|
+
// Return enhanced component with structure
|
|
158
|
+
return {
|
|
159
|
+
...component,
|
|
160
|
+
structure: {
|
|
161
|
+
container,
|
|
162
|
+
input,
|
|
163
|
+
inputWrapper,
|
|
164
|
+
leadingIcon,
|
|
165
|
+
clearButton,
|
|
166
|
+
trailingIcon,
|
|
167
|
+
trailingIcon2,
|
|
168
|
+
avatar,
|
|
169
|
+
divider,
|
|
170
|
+
suggestionsContainer
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Applies style classes based on configuration
|
|
177
|
+
*/
|
|
178
|
+
function applyStyleClasses(component, config, isViewMode, isDisabled) {
|
|
179
|
+
const baseClass = component.getClass('search');
|
|
180
|
+
|
|
181
|
+
// Apply variant class
|
|
182
|
+
component.element.classList.add(`${baseClass}--${config.variant || SEARCH_VARIANTS.BAR}`);
|
|
183
|
+
|
|
184
|
+
// Apply disabled class if needed
|
|
185
|
+
if (isDisabled) {
|
|
186
|
+
component.element.classList.add(`${baseClass}--disabled`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Apply expanded class for view mode
|
|
190
|
+
if (isViewMode) {
|
|
191
|
+
component.element.classList.add(`${baseClass}--expanded`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Apply fullwidth class if needed
|
|
195
|
+
if (config.fullWidth) {
|
|
196
|
+
component.element.classList.add(`${baseClass}--fullwidth`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/components/search/index.ts
|
|
2
|
+
|
|
3
|
+
// Export main component creator
|
|
4
|
+
export { default } from './search';
|
|
5
|
+
|
|
6
|
+
// Export constants
|
|
7
|
+
export { SEARCH_VARIANTS, SEARCH_EVENTS } from './constants';
|
|
8
|
+
|
|
9
|
+
// Export types for TypeScript users
|
|
10
|
+
export type { SearchConfig, SearchComponent, SearchEvent } from './types';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/components/search/search.ts
|
|
2
|
+
import { pipe } from '../../core/compose/pipe';
|
|
3
|
+
import { createBase, withElement } from '../../core/compose/component';
|
|
4
|
+
import { withEvents, withLifecycle } from '../../core/compose/features';
|
|
5
|
+
import { withStructure, withSearch, withStates } from './features';
|
|
6
|
+
import { withAPI } from './api';
|
|
7
|
+
import { SearchConfig, SearchComponent } from './types';
|
|
8
|
+
import { createBaseConfig, getElementConfig, getApiConfig } from './config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new Search component
|
|
12
|
+
* @param {SearchConfig} config - Search configuration object
|
|
13
|
+
* @returns {SearchComponent} Search component instance
|
|
14
|
+
*/
|
|
15
|
+
const createSearch = (config: SearchConfig = {}): SearchComponent => {
|
|
16
|
+
const baseConfig = createBaseConfig(config);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Create the component with all required features
|
|
20
|
+
const component = pipe(
|
|
21
|
+
createBase,
|
|
22
|
+
withEvents(),
|
|
23
|
+
withElement(getElementConfig(baseConfig)),
|
|
24
|
+
withStructure(baseConfig),
|
|
25
|
+
withStates(baseConfig),
|
|
26
|
+
withSearch(baseConfig),
|
|
27
|
+
withLifecycle()
|
|
28
|
+
)(baseConfig);
|
|
29
|
+
|
|
30
|
+
// Generate the API configuration
|
|
31
|
+
const apiOptions = getApiConfig(component);
|
|
32
|
+
|
|
33
|
+
// Apply the API layer
|
|
34
|
+
const search = withAPI(apiOptions)(component);
|
|
35
|
+
|
|
36
|
+
// Register event handlers from config
|
|
37
|
+
if (baseConfig.on && typeof search.on === 'function') {
|
|
38
|
+
Object.entries(baseConfig.on).forEach(([event, handler]) => {
|
|
39
|
+
if (typeof handler === 'function') {
|
|
40
|
+
search.on(event, handler);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return search;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Search creation error:', error);
|
|
48
|
+
throw new Error(`Failed to create search: ${(error as Error).message}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default createSearch;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/components/search/types.ts
|
|
2
|
+
import { SEARCH_VARIANTS, SEARCH_EVENTS } from './constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the search component
|
|
6
|
+
*/
|
|
7
|
+
export interface SearchConfig {
|
|
8
|
+
/** The variant of the search component (bar or view) */
|
|
9
|
+
variant?: keyof typeof SEARCH_VARIANTS | typeof SEARCH_VARIANTS[keyof typeof SEARCH_VARIANTS];
|
|
10
|
+
|
|
11
|
+
/** Whether the search component is disabled */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
|
|
14
|
+
/** Placeholder text for the search input */
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
|
|
17
|
+
/** Initial value for the search input */
|
|
18
|
+
value?: string;
|
|
19
|
+
|
|
20
|
+
/** Leading icon HTML or 'none' to remove */
|
|
21
|
+
leadingIcon?: string;
|
|
22
|
+
|
|
23
|
+
/** Trailing icon HTML (optional) */
|
|
24
|
+
trailingIcon?: string;
|
|
25
|
+
|
|
26
|
+
/** Second trailing icon HTML (optional) */
|
|
27
|
+
trailingIcon2?: string;
|
|
28
|
+
|
|
29
|
+
/** Avatar HTML for trailing avatar (optional) */
|
|
30
|
+
avatar?: string;
|
|
31
|
+
|
|
32
|
+
/** If true, will show the clear button when the input has text */
|
|
33
|
+
showClearButton?: boolean;
|
|
34
|
+
|
|
35
|
+
/** Callback function when a search is submitted */
|
|
36
|
+
onSubmit?: (value: string) => void;
|
|
37
|
+
|
|
38
|
+
/** Callback function when the input value changes */
|
|
39
|
+
onInput?: (value: string) => void;
|
|
40
|
+
|
|
41
|
+
/** Callback function when the clear button is clicked */
|
|
42
|
+
onClear?: () => void;
|
|
43
|
+
|
|
44
|
+
/** Suggestions to show when the search is focused (array of strings or objects) */
|
|
45
|
+
suggestions?: string[] | Array<{text: string, value?: string, icon?: string}>;
|
|
46
|
+
|
|
47
|
+
/** Whether to show dividers between suggestion groups */
|
|
48
|
+
showDividers?: boolean;
|
|
49
|
+
|
|
50
|
+
/** Additional CSS classes */
|
|
51
|
+
class?: string;
|
|
52
|
+
|
|
53
|
+
/** Maximum width of the search component in pixels */
|
|
54
|
+
maxWidth?: number;
|
|
55
|
+
|
|
56
|
+
/** Minimum width of the search component in pixels */
|
|
57
|
+
minWidth?: number;
|
|
58
|
+
|
|
59
|
+
/** Whether the search component is full-width */
|
|
60
|
+
fullWidth?: boolean;
|
|
61
|
+
|
|
62
|
+
/** Event handlers for search events */
|
|
63
|
+
on?: {
|
|
64
|
+
[key in keyof typeof SEARCH_EVENTS | typeof SEARCH_EVENTS[keyof typeof SEARCH_EVENTS]]?: (event: SearchEvent) => void;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Search event data
|
|
70
|
+
*/
|
|
71
|
+
export interface SearchEvent {
|
|
72
|
+
/** The search component that triggered the event */
|
|
73
|
+
search: any;
|
|
74
|
+
|
|
75
|
+
/** Current search value */
|
|
76
|
+
value: string;
|
|
77
|
+
|
|
78
|
+
/** Original DOM event if available */
|
|
79
|
+
originalEvent: Event | null;
|
|
80
|
+
|
|
81
|
+
/** Function to prevent default behavior */
|
|
82
|
+
preventDefault: () => void;
|
|
83
|
+
|
|
84
|
+
/** Whether default behavior was prevented */
|
|
85
|
+
defaultPrevented: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Search component public API interface
|
|
90
|
+
*/
|
|
91
|
+
export interface SearchComponent {
|
|
92
|
+
/** The root element of the search component */
|
|
93
|
+
element: HTMLElement;
|
|
94
|
+
|
|
95
|
+
/** Sets the search value */
|
|
96
|
+
setValue: (value: string, triggerEvent?: boolean) => SearchComponent;
|
|
97
|
+
|
|
98
|
+
/** Gets the search value */
|
|
99
|
+
getValue: () => string;
|
|
100
|
+
|
|
101
|
+
/** Sets placeholder text */
|
|
102
|
+
setPlaceholder: (text: string) => SearchComponent;
|
|
103
|
+
|
|
104
|
+
/** Gets placeholder text */
|
|
105
|
+
getPlaceholder: () => string;
|
|
106
|
+
|
|
107
|
+
/** Sets leading icon */
|
|
108
|
+
setLeadingIcon: (iconHtml: string) => SearchComponent;
|
|
109
|
+
|
|
110
|
+
/** Sets trailing icon */
|
|
111
|
+
setTrailingIcon: (iconHtml: string) => SearchComponent;
|
|
112
|
+
|
|
113
|
+
/** Sets second trailing icon */
|
|
114
|
+
setTrailingIcon2: (iconHtml: string) => SearchComponent;
|
|
115
|
+
|
|
116
|
+
/** Sets avatar */
|
|
117
|
+
setAvatar: (avatarHtml: string) => SearchComponent;
|
|
118
|
+
|
|
119
|
+
/** Shows or hides clear button */
|
|
120
|
+
showClearButton: (show: boolean) => SearchComponent;
|
|
121
|
+
|
|
122
|
+
/** Updates suggestions */
|
|
123
|
+
setSuggestions: (suggestions: string[] | Array<{text: string, value?: string, icon?: string}>) => SearchComponent;
|
|
124
|
+
|
|
125
|
+
/** Shows or hides suggestions */
|
|
126
|
+
showSuggestions: (show: boolean) => SearchComponent;
|
|
127
|
+
|
|
128
|
+
/** Focuses the search input */
|
|
129
|
+
focus: () => SearchComponent;
|
|
130
|
+
|
|
131
|
+
/** Blurs the search input */
|
|
132
|
+
blur: () => SearchComponent;
|
|
133
|
+
|
|
134
|
+
/** Expands the search bar into view mode (if in bar mode) */
|
|
135
|
+
expand: () => SearchComponent;
|
|
136
|
+
|
|
137
|
+
/** Collapses the search view back to bar mode */
|
|
138
|
+
collapse: () => SearchComponent;
|
|
139
|
+
|
|
140
|
+
/** Clears the search input */
|
|
141
|
+
clear: () => SearchComponent;
|
|
142
|
+
|
|
143
|
+
/** Submits the search */
|
|
144
|
+
submit: () => SearchComponent;
|
|
145
|
+
|
|
146
|
+
/** Enables the search component */
|
|
147
|
+
enable: () => SearchComponent;
|
|
148
|
+
|
|
149
|
+
/** Disables the search component */
|
|
150
|
+
disable: () => SearchComponent;
|
|
151
|
+
|
|
152
|
+
/** Checks if search is disabled */
|
|
153
|
+
isDisabled: () => boolean;
|
|
154
|
+
|
|
155
|
+
/** Adds event listener */
|
|
156
|
+
on: (event: keyof typeof SEARCH_EVENTS | typeof SEARCH_EVENTS[keyof typeof SEARCH_EVENTS], handler: (event: SearchEvent) => void) => SearchComponent;
|
|
157
|
+
|
|
158
|
+
/** Removes event listener */
|
|
159
|
+
off: (event: keyof typeof SEARCH_EVENTS | typeof SEARCH_EVENTS[keyof typeof SEARCH_EVENTS], handler: (event: SearchEvent) => void) => SearchComponent;
|
|
160
|
+
|
|
161
|
+
/** Destroys the search component and cleans up resources */
|
|
162
|
+
destroy: () => void;
|
|
163
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// src/components/segmented-button/_styles.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}-segmented-button';
|
|
9
|
+
|
|
10
|
+
.#{$component} {
|
|
11
|
+
// Base styles
|
|
12
|
+
position: relative;
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
height: 40px;
|
|
17
|
+
border-radius: v.shape('full');
|
|
18
|
+
border: 1px solid t.color('outline');
|
|
19
|
+
background-color: transparent;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
|
|
22
|
+
// Disabled state
|
|
23
|
+
&--disabled {
|
|
24
|
+
opacity: 0.38;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Segment container
|
|
29
|
+
&-segment {
|
|
30
|
+
// Base styles
|
|
31
|
+
position: relative;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
height: 100%;
|
|
36
|
+
min-width: 48px;
|
|
37
|
+
padding: 0 12px;
|
|
38
|
+
border: none;
|
|
39
|
+
background-color: transparent;
|
|
40
|
+
color: t.color('on-surface');
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
user-select: none;
|
|
43
|
+
|
|
44
|
+
// Fix segmented borders
|
|
45
|
+
&:not(:first-child) {
|
|
46
|
+
border-left: 1px solid t.color('outline');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Typography
|
|
50
|
+
@include m.typography('label-large');
|
|
51
|
+
|
|
52
|
+
// Transition
|
|
53
|
+
@include m.motion-transition(
|
|
54
|
+
background-color,
|
|
55
|
+
color
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// States
|
|
59
|
+
&:focus {
|
|
60
|
+
outline: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&:focus-visible {
|
|
64
|
+
outline: 2px solid t.color('outline');
|
|
65
|
+
outline-offset: -2px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&:hover:not(.#{$component}-segment--disabled) {
|
|
69
|
+
background-color: t.alpha('on-surface', 0.08);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Selected state
|
|
73
|
+
&--selected {
|
|
74
|
+
background-color: t.color('secondary-container');
|
|
75
|
+
color: t.color('on-secondary-container');
|
|
76
|
+
|
|
77
|
+
&:hover:not(.#{$component}-segment--disabled) {
|
|
78
|
+
background-color: t.alpha('secondary-container', 0.8);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Disabled state
|
|
83
|
+
&--disabled {
|
|
84
|
+
opacity: 0.38;
|
|
85
|
+
cursor: not-allowed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Text element
|
|
89
|
+
&-text {
|
|
90
|
+
// For when both icon and text are present
|
|
91
|
+
margin: 0 auto;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Icon styles
|
|
95
|
+
&-icon, &-checkmark {
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
width: 18px;
|
|
100
|
+
height: 18px;
|
|
101
|
+
|
|
102
|
+
svg {
|
|
103
|
+
width: 18px;
|
|
104
|
+
height: 18px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
+ .#{$component}-segment-text {
|
|
108
|
+
margin-left: 8px;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Space the checkmark icon
|
|
113
|
+
&-checkmark + .#{$component}-segment-text {
|
|
114
|
+
margin-left: 8px;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|