mtrl 0.2.6 → 0.2.8
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/demo/build.ts +349 -0
- package/demo/index.html +110 -0
- package/demo/main.js +448 -0
- package/demo/styles.css +239 -0
- package/index.ts +18 -0
- package/package.json +14 -3
- package/server.ts +86 -0
- package/src/components/badge/api.ts +70 -63
- package/src/components/badge/badge.ts +16 -2
- package/src/components/badge/config.ts +66 -13
- package/src/components/badge/features.ts +51 -42
- package/src/components/badge/index.ts +27 -2
- package/src/components/badge/types.ts +62 -30
- package/src/components/bottom-app-bar/bottom-app-bar.ts +154 -0
- package/src/components/bottom-app-bar/config.ts +29 -0
- package/src/components/bottom-app-bar/index.ts +17 -0
- package/src/components/bottom-app-bar/types.ts +114 -0
- package/src/components/button/api.ts +5 -0
- package/src/components/button/button.ts +0 -1
- package/src/components/button/config.ts +6 -2
- package/src/components/button/index.ts +10 -2
- package/src/components/button/types.ts +20 -2
- package/src/components/card/card.ts +13 -25
- package/src/components/card/config.ts +83 -30
- package/src/components/card/content.ts +8 -10
- package/src/components/card/features.ts +4 -3
- package/src/components/card/index.ts +29 -2
- package/src/components/card/types.ts +33 -22
- package/src/components/checkbox/config.ts +3 -4
- package/src/components/checkbox/index.ts +1 -2
- package/src/components/checkbox/types.ts +12 -3
- package/src/components/chip/api.ts +170 -221
- package/src/components/chip/chip.ts +34 -302
- package/src/components/chip/config.ts +1 -2
- package/src/components/chip/index.ts +10 -2
- package/src/components/chip/types.ts +224 -35
- package/src/components/datepicker/api.ts +265 -0
- package/src/components/datepicker/config.ts +141 -0
- package/src/components/datepicker/datepicker.ts +341 -0
- package/src/components/datepicker/index.ts +12 -0
- package/src/components/datepicker/render.ts +450 -0
- package/src/components/datepicker/types.ts +397 -0
- package/src/components/datepicker/utils.ts +289 -0
- package/src/components/dialog/api.ts +55 -21
- package/src/components/dialog/config.ts +12 -9
- package/src/components/dialog/dialog.ts +6 -3
- package/src/components/dialog/features.ts +345 -151
- package/src/components/dialog/index.ts +38 -8
- package/src/components/dialog/types.ts +40 -14
- 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 +9 -0
- package/src/components/divider/types.ts +55 -0
- package/src/components/extended-fab/api.ts +141 -0
- package/src/components/extended-fab/config.ts +112 -0
- package/src/components/extended-fab/extended-fab.ts +125 -0
- package/src/components/extended-fab/index.ts +9 -0
- package/src/components/extended-fab/types.ts +304 -0
- package/src/components/fab/api.ts +97 -0
- package/src/components/fab/config.ts +93 -0
- package/src/components/fab/fab.ts +67 -0
- package/src/components/fab/index.ts +9 -0
- package/src/components/fab/types.ts +251 -0
- package/src/components/list/config.ts +4 -5
- package/src/components/list/features.ts +6 -7
- package/src/components/list/index.ts +7 -9
- package/src/components/list/list-item.ts +12 -13
- package/src/components/list/types.ts +50 -5
- package/src/components/list/utils.ts +30 -3
- package/src/components/menu/features/items-manager.ts +9 -9
- package/src/components/menu/features/positioning.ts +7 -7
- package/src/components/menu/features/visibility.ts +7 -7
- package/src/components/menu/index.ts +7 -9
- package/src/components/menu/menu-item.ts +6 -6
- package/src/components/menu/menu.ts +22 -0
- package/src/components/menu/types.ts +29 -10
- package/src/components/menu/utils.ts +67 -0
- package/src/components/navigation/api.ts +78 -50
- package/src/components/navigation/config.ts +22 -10
- package/src/components/navigation/features/items.ts +284 -0
- package/src/components/navigation/index.ts +0 -6
- package/src/components/navigation/nav-item.ts +70 -33
- package/src/components/navigation/navigation.ts +53 -3
- package/src/components/navigation/types.ts +117 -70
- package/src/components/progress/api.ts +2 -3
- package/src/components/progress/config.ts +2 -3
- package/src/components/progress/index.ts +0 -1
- package/src/components/progress/progress.ts +1 -2
- package/src/components/progress/types.ts +186 -33
- package/src/components/radios/config.ts +1 -1
- package/src/components/radios/index.ts +0 -1
- package/src/components/radios/types.ts +0 -7
- package/src/components/search/api.ts +203 -0
- package/src/components/search/config.ts +86 -0
- package/src/components/search/features/index.ts +4 -0
- package/src/components/search/features/search.ts +717 -0
- package/src/components/search/features/states.ts +169 -0
- package/src/components/search/features/structure.ts +197 -0
- package/src/components/search/index.ts +7 -0
- package/src/components/search/search.ts +52 -0
- package/src/components/search/types.ts +175 -0
- package/src/components/segmented-button/config.ts +80 -0
- package/src/components/segmented-button/index.ts +4 -0
- package/src/components/segmented-button/segment.ts +154 -0
- package/src/components/segmented-button/segmented-button.ts +249 -0
- package/src/components/segmented-button/types.ts +254 -0
- package/src/components/slider/accessibility.md +5 -5
- package/src/components/slider/api.ts +41 -120
- package/src/components/slider/config.ts +51 -47
- 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 +136 -206
- package/src/components/slider/features/ui.ts +145 -206
- package/src/components/slider/index.ts +2 -11
- package/src/components/slider/slider.ts +9 -12
- package/src/components/slider/types.ts +67 -26
- package/src/components/snackbar/config.ts +2 -3
- package/src/components/snackbar/constants.ts +0 -32
- package/src/components/snackbar/index.ts +0 -1
- package/src/components/snackbar/position.ts +9 -1
- package/src/components/snackbar/types.ts +122 -46
- package/src/components/switch/config.ts +2 -3
- package/src/components/switch/index.ts +0 -1
- package/src/components/switch/types.ts +3 -2
- package/src/components/tabs/config.ts +3 -4
- package/src/components/tabs/features.ts +4 -2
- package/src/components/tabs/index.ts +0 -15
- package/src/components/tabs/indicator.ts +73 -13
- package/src/components/tabs/tab-api.ts +12 -4
- package/src/components/tabs/tab.ts +18 -6
- package/src/components/tabs/types.ts +23 -5
- package/src/components/textfield/config.ts +2 -3
- package/src/components/textfield/index.ts +0 -1
- package/src/components/textfield/types.ts +17 -3
- package/src/components/timepicker/README.md +277 -0
- package/src/components/timepicker/api.ts +632 -0
- package/src/components/timepicker/clockdial.ts +482 -0
- package/src/components/timepicker/config.ts +228 -0
- package/src/components/timepicker/index.ts +3 -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/tooltip/api.ts +1 -1
- package/src/components/tooltip/config.ts +27 -6
- package/src/components/tooltip/index.ts +0 -1
- package/src/components/tooltip/types.ts +13 -3
- 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/features/icon.ts +3 -1
- package/src/core/compose/features/ripple.ts +4 -1
- package/src/core/compose/features/textlabel.ts +23 -2
- package/src/core/dom/create.ts +5 -0
- package/src/index.ts +9 -0
- package/src/styles/abstract/_theme.scss +9 -1
- package/src/styles/components/_badge.scss +182 -0
- package/src/styles/components/_bottom-app-bar.scss +103 -0
- package/src/{components/button/_styles.scss → styles/components/_button.scss} +0 -10
- package/src/{components/checkbox/_styles.scss → styles/components/_checkbox.scss} +0 -2
- package/src/styles/components/_datepicker.scss +358 -0
- package/src/styles/components/_dialog.scss +259 -0
- package/src/styles/components/_divider.scss +57 -0
- package/src/styles/components/_extended-fab.scss +267 -0
- package/src/styles/components/_fab.scss +225 -0
- package/src/{components/navigation/_styles.scss → styles/components/_navigation.scss} +1 -0
- package/src/styles/components/_search.scss +306 -0
- package/src/styles/components/_segmented-button.scss +117 -0
- package/src/{components/slider/_styles.scss → styles/components/_slider.scss} +83 -24
- package/src/{components/switch/_styles.scss → styles/components/_switch.scss} +0 -2
- package/src/{components/tabs/_styles.scss → styles/components/_tabs.scss} +95 -33
- package/src/{components/textfield/_styles.scss → styles/components/_textfield.scss} +70 -67
- package/src/styles/components/_timepicker.scss +451 -0
- package/src/styles/components/_top-app-bar.scss +225 -0
- package/src/styles/main.scss +98 -49
- 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/badge/_styles.scss +0 -174
- package/src/components/badge/constants.ts +0 -30
- package/src/components/button/constants.ts +0 -11
- package/src/components/card/constants.ts +0 -84
- package/src/components/dialog/_styles.scss +0 -213
- package/src/components/dialog/constants.ts +0 -32
- package/src/components/menu/constants.ts +0 -154
- package/src/components/navigation/constants.ts +0 -200
- package/src/components/navigation/features/items.js +0 -192
- package/src/components/progress/constants.ts +0 -29
- package/src/components/slider/features/appearance.ts +0 -94
- package/src/components/slider/features/disabled.ts +0 -68
- package/src/components/slider/features/events.ts +0 -164
- package/src/components/slider/features/interactions.ts +0 -396
- package/src/components/slider/features/keyboard.ts +0 -233
- package/src/components/switch/constants.ts +0 -80
- package/src/components/tabs/constants.ts +0 -89
- package/src/core/collection/adapters/mongodb.js +0 -232
- /package/src/{components/card/_styles.scss → styles/components/_card.scss} +0 -0
- /package/src/{components/carousel/_styles.scss → styles/components/_carousel.scss} +0 -0
- /package/src/{components/chip/_styles.scss → styles/components/_chip.scss} +0 -0
- /package/src/{components/list/_styles.scss → styles/components/_list.scss} +0 -0
- /package/src/{components/menu/_styles.scss → styles/components/_menu.scss} +0 -0
- /package/src/{components/progress/_styles.scss → styles/components/_progress.scss} +0 -0
- /package/src/{components/radios/_styles.scss → styles/components/_radios.scss} +0 -0
- /package/src/{components/sheet/_styles.scss → styles/components/_sheet.scss} +0 -0
- /package/src/{components/snackbar/_styles.scss → styles/components/_snackbar.scss} +0 -0
- /package/src/{components/tooltip/_styles.scss → styles/components/_tooltip.scss} +0 -0
- /package/src/styles/utilities/{_color.scss → _colors.scss} +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/components/search/features/states.ts
|
|
2
|
+
import { SearchConfig } from '../types';
|
|
3
|
+
|
|
4
|
+
const SEARCH_VARIANTS = {
|
|
5
|
+
BAR: 'bar',
|
|
6
|
+
VIEW: 'view'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Add state management features to search component
|
|
11
|
+
*
|
|
12
|
+
* @param config Search configuration
|
|
13
|
+
* @returns Component enhancer with state management features
|
|
14
|
+
*/
|
|
15
|
+
export const withStates = (config: SearchConfig) => component => {
|
|
16
|
+
// Track initial disabled state
|
|
17
|
+
const isDisabled = config.disabled === true;
|
|
18
|
+
|
|
19
|
+
// Apply initial disabled state if needed
|
|
20
|
+
if (isDisabled) {
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
disableComponent();
|
|
23
|
+
}, 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Disables the component
|
|
28
|
+
*/
|
|
29
|
+
function disableComponent() {
|
|
30
|
+
component.element.classList.add(`${component.getClass('search')}--disabled`);
|
|
31
|
+
component.element.setAttribute('aria-disabled', 'true');
|
|
32
|
+
|
|
33
|
+
// Disable input - important to use the disabled property not the attribute
|
|
34
|
+
if (component.structure?.input) {
|
|
35
|
+
component.structure.input.disabled = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure buttons cannot receive focus when disabled
|
|
39
|
+
const buttons = [
|
|
40
|
+
component.structure?.leadingIcon,
|
|
41
|
+
component.structure?.clearButton,
|
|
42
|
+
component.structure?.trailingIcon,
|
|
43
|
+
component.structure?.trailingIcon2
|
|
44
|
+
].filter(Boolean);
|
|
45
|
+
|
|
46
|
+
buttons.forEach(button => {
|
|
47
|
+
button.tabIndex = -1;
|
|
48
|
+
button.setAttribute('aria-disabled', 'true');
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Enables the component
|
|
54
|
+
*/
|
|
55
|
+
function enableComponent() {
|
|
56
|
+
component.element.classList.remove(`${component.getClass('search')}--disabled`);
|
|
57
|
+
component.element.setAttribute('aria-disabled', 'false');
|
|
58
|
+
|
|
59
|
+
// Enable input - important to use the disabled property not the attribute
|
|
60
|
+
if (component.structure?.input) {
|
|
61
|
+
component.structure.input.disabled = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Re-enable buttons
|
|
65
|
+
const buttons = [
|
|
66
|
+
component.structure?.leadingIcon,
|
|
67
|
+
component.structure?.clearButton,
|
|
68
|
+
component.structure?.trailingIcon,
|
|
69
|
+
component.structure?.trailingIcon2
|
|
70
|
+
].filter(Boolean);
|
|
71
|
+
|
|
72
|
+
buttons.forEach(button => {
|
|
73
|
+
button.tabIndex = 0;
|
|
74
|
+
button.setAttribute('aria-disabled', 'false');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Clear button special case - only enable if there's text
|
|
78
|
+
if (component.structure?.clearButton &&
|
|
79
|
+
component.structure.input &&
|
|
80
|
+
!component.structure.input.value) {
|
|
81
|
+
component.structure.clearButton.tabIndex = -1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Gets the active variant
|
|
87
|
+
*/
|
|
88
|
+
function getActiveVariant() {
|
|
89
|
+
return Object.values(SEARCH_VARIANTS).find(variantName =>
|
|
90
|
+
component.element.classList.contains(`${component.getClass('search')}--${variantName}`)
|
|
91
|
+
) || SEARCH_VARIANTS.BAR;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Return enhanced component
|
|
95
|
+
return {
|
|
96
|
+
...component,
|
|
97
|
+
|
|
98
|
+
// Disabled state management
|
|
99
|
+
disabled: {
|
|
100
|
+
/**
|
|
101
|
+
* Enables the component
|
|
102
|
+
* @returns Disabled manager for chaining
|
|
103
|
+
*/
|
|
104
|
+
enable() {
|
|
105
|
+
enableComponent();
|
|
106
|
+
return this;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Disables the component
|
|
111
|
+
* @returns Disabled manager for chaining
|
|
112
|
+
*/
|
|
113
|
+
disable() {
|
|
114
|
+
disableComponent();
|
|
115
|
+
return this;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Checks if component is disabled
|
|
120
|
+
* @returns True if disabled
|
|
121
|
+
*/
|
|
122
|
+
isDisabled() {
|
|
123
|
+
return component.element.classList.contains(`${component.getClass('search')}--disabled`);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Appearance management
|
|
128
|
+
appearance: {
|
|
129
|
+
/**
|
|
130
|
+
* Sets search variant
|
|
131
|
+
* @param variant Variant name
|
|
132
|
+
* @returns Appearance manager for chaining
|
|
133
|
+
*/
|
|
134
|
+
setVariant(variant) {
|
|
135
|
+
const currentVariant = getActiveVariant();
|
|
136
|
+
|
|
137
|
+
// If already this variant, do nothing
|
|
138
|
+
if (currentVariant === variant) {
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Remove existing variant classes
|
|
143
|
+
Object.values(SEARCH_VARIANTS).forEach(variantName => {
|
|
144
|
+
component.element.classList.remove(`${component.getClass('search')}--${variantName}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Add new variant class
|
|
148
|
+
component.element.classList.add(`${component.getClass('search')}--${variant}`);
|
|
149
|
+
|
|
150
|
+
// Toggle expanded state class
|
|
151
|
+
if (variant === SEARCH_VARIANTS.VIEW) {
|
|
152
|
+
component.element.classList.add(`${component.getClass('search')}--expanded`);
|
|
153
|
+
} else {
|
|
154
|
+
component.element.classList.remove(`${component.getClass('search')}--expanded`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return this;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Gets search variant
|
|
162
|
+
* @returns Current variant name
|
|
163
|
+
*/
|
|
164
|
+
getVariant() {
|
|
165
|
+
return getActiveVariant();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// src/components/search/features/structure.ts
|
|
2
|
+
import { SearchConfig } from '../types';
|
|
3
|
+
import { createElement } from '../../../core/dom/create';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates the search component DOM structure
|
|
7
|
+
* @param config Search configuration
|
|
8
|
+
* @returns Component enhancer with DOM structure
|
|
9
|
+
*/
|
|
10
|
+
export const withStructure = (config: SearchConfig) => component => {
|
|
11
|
+
// Get initial config values
|
|
12
|
+
const isDisabled = config.disabled === true;
|
|
13
|
+
const variant = config.variant || 'bar';
|
|
14
|
+
const isViewMode = variant === 'view';
|
|
15
|
+
const placeholder = config.placeholder || 'Search';
|
|
16
|
+
const value = config.value || '';
|
|
17
|
+
|
|
18
|
+
// Get prefixed class names
|
|
19
|
+
const getClass = (className) => component.getClass(className);
|
|
20
|
+
|
|
21
|
+
// Build attributes object for the input
|
|
22
|
+
const inputAttrs = {
|
|
23
|
+
'type': 'text',
|
|
24
|
+
'placeholder': placeholder,
|
|
25
|
+
'value': value,
|
|
26
|
+
'aria-label': placeholder
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Only add disabled attribute if the input should be disabled
|
|
30
|
+
if (isDisabled) {
|
|
31
|
+
inputAttrs['disabled'] = 'disabled';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create container element
|
|
35
|
+
const container = createElement({
|
|
36
|
+
tag: 'div',
|
|
37
|
+
className: getClass('search-container'),
|
|
38
|
+
container: component.element,
|
|
39
|
+
attrs: {
|
|
40
|
+
style: `
|
|
41
|
+
${config.minWidth ? `min-width: ${config.minWidth}px;` : ''}
|
|
42
|
+
${config.maxWidth ? `max-width: ${config.maxWidth}px;` : ''}
|
|
43
|
+
${config.fullWidth ? 'width: 100%;' : ''}
|
|
44
|
+
`
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Create leading icon
|
|
49
|
+
const leadingIcon = createElement({
|
|
50
|
+
tag: 'div',
|
|
51
|
+
className: getClass('search-leading-icon'),
|
|
52
|
+
container: container,
|
|
53
|
+
html: config.leadingIcon || '',
|
|
54
|
+
attrs: {
|
|
55
|
+
'role': 'button',
|
|
56
|
+
'tabindex': isDisabled ? '-1' : '0',
|
|
57
|
+
'aria-label': 'Search'
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Create input wrapper
|
|
62
|
+
const inputWrapper = createElement({
|
|
63
|
+
tag: 'div',
|
|
64
|
+
className: getClass('search-input-wrapper'),
|
|
65
|
+
container: container
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Create input element with properly handled disabled state
|
|
69
|
+
const input = createElement({
|
|
70
|
+
tag: 'input',
|
|
71
|
+
className: getClass('search-input'),
|
|
72
|
+
container: inputWrapper,
|
|
73
|
+
attrs: inputAttrs
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Create clear button
|
|
77
|
+
const clearButton = createElement({
|
|
78
|
+
tag: 'div',
|
|
79
|
+
className: [
|
|
80
|
+
getClass('search-clear-button'),
|
|
81
|
+
value ? '' : getClass('search-clear-button--hidden')
|
|
82
|
+
],
|
|
83
|
+
container: container,
|
|
84
|
+
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>',
|
|
85
|
+
attrs: {
|
|
86
|
+
'role': 'button',
|
|
87
|
+
'tabindex': isDisabled || !value ? '-1' : '0',
|
|
88
|
+
'aria-label': 'Clear search'
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Create optional trailing elements
|
|
93
|
+
let trailingIcon, trailingIcon2, avatar;
|
|
94
|
+
|
|
95
|
+
if (config.trailingIcon) {
|
|
96
|
+
trailingIcon = createElement({
|
|
97
|
+
tag: 'div',
|
|
98
|
+
className: getClass('search-trailing-icon'),
|
|
99
|
+
container: container,
|
|
100
|
+
html: config.trailingIcon,
|
|
101
|
+
attrs: {
|
|
102
|
+
'role': 'button',
|
|
103
|
+
'tabindex': isDisabled ? '-1' : '0',
|
|
104
|
+
'aria-label': 'Search option'
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (config.trailingIcon2) {
|
|
110
|
+
trailingIcon2 = createElement({
|
|
111
|
+
tag: 'div',
|
|
112
|
+
className: getClass('search-trailing-icon'),
|
|
113
|
+
container: container,
|
|
114
|
+
html: config.trailingIcon2,
|
|
115
|
+
attrs: {
|
|
116
|
+
'role': 'button',
|
|
117
|
+
'tabindex': isDisabled ? '-1' : '0',
|
|
118
|
+
'aria-label': 'Search option'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (config.avatar) {
|
|
124
|
+
avatar = createElement({
|
|
125
|
+
tag: 'div',
|
|
126
|
+
className: getClass('search-avatar'),
|
|
127
|
+
container: container,
|
|
128
|
+
html: config.avatar
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create divider and suggestions container for view variant
|
|
133
|
+
let divider, suggestionsContainer;
|
|
134
|
+
|
|
135
|
+
if (isViewMode || config.suggestions) {
|
|
136
|
+
divider = createElement({
|
|
137
|
+
tag: 'div',
|
|
138
|
+
className: getClass('search-divider')
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
suggestionsContainer = createElement({
|
|
142
|
+
tag: 'div',
|
|
143
|
+
className: getClass('search-suggestions-container'),
|
|
144
|
+
container: component.element
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add component base class and accessibility attributes
|
|
149
|
+
component.element.classList.add(component.getClass('search'));
|
|
150
|
+
component.element.setAttribute('role', 'search');
|
|
151
|
+
component.element.setAttribute('aria-disabled', isDisabled ? 'true' : 'false');
|
|
152
|
+
|
|
153
|
+
// Apply style classes
|
|
154
|
+
applyStyleClasses(component, config, isViewMode, isDisabled);
|
|
155
|
+
|
|
156
|
+
// Return enhanced component with structure
|
|
157
|
+
return {
|
|
158
|
+
...component,
|
|
159
|
+
structure: {
|
|
160
|
+
container,
|
|
161
|
+
input,
|
|
162
|
+
inputWrapper,
|
|
163
|
+
leadingIcon,
|
|
164
|
+
clearButton,
|
|
165
|
+
trailingIcon,
|
|
166
|
+
trailingIcon2,
|
|
167
|
+
avatar,
|
|
168
|
+
divider,
|
|
169
|
+
suggestionsContainer
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Applies style classes based on configuration
|
|
176
|
+
*/
|
|
177
|
+
function applyStyleClasses(component, config, isViewMode, isDisabled) {
|
|
178
|
+
const baseClass = component.getClass('search');
|
|
179
|
+
|
|
180
|
+
// Apply variant class
|
|
181
|
+
component.element.classList.add(`${baseClass}--${config.variant || 'bar'}`);
|
|
182
|
+
|
|
183
|
+
// Apply disabled class if needed
|
|
184
|
+
if (isDisabled) {
|
|
185
|
+
component.element.classList.add(`${baseClass}--disabled`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Apply expanded class for view mode
|
|
189
|
+
if (isViewMode) {
|
|
190
|
+
component.element.classList.add(`${baseClass}--expanded`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Apply fullwidth class if needed
|
|
194
|
+
if (config.fullWidth) {
|
|
195
|
+
component.element.classList.add(`${baseClass}--fullwidth`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -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,175 @@
|
|
|
1
|
+
// src/components/search/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Navigation variants for the search component
|
|
5
|
+
*/
|
|
6
|
+
export type NavVariant = 'rail' | 'drawer' | 'bar' | 'modal' | 'standard';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Valid event types for search component
|
|
10
|
+
*/
|
|
11
|
+
export type SearchEventType = 'focus' | 'blur' | 'input' | 'submit' | 'clear' | 'iconClick';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration options for the search component
|
|
15
|
+
* @interface SearchConfig
|
|
16
|
+
*/
|
|
17
|
+
export interface SearchConfig {
|
|
18
|
+
/** The variant of the search component (rail, drawer, bar, modal, or standard) */
|
|
19
|
+
variant?: NavVariant | string;
|
|
20
|
+
|
|
21
|
+
/** Whether the search component is disabled */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
|
|
24
|
+
/** Placeholder text for the search input */
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
|
|
27
|
+
/** Initial value for the search input */
|
|
28
|
+
value?: string;
|
|
29
|
+
|
|
30
|
+
/** Leading icon HTML or 'none' to remove */
|
|
31
|
+
leadingIcon?: string;
|
|
32
|
+
|
|
33
|
+
/** Trailing icon HTML (optional) */
|
|
34
|
+
trailingIcon?: string;
|
|
35
|
+
|
|
36
|
+
/** Second trailing icon HTML (optional) */
|
|
37
|
+
trailingIcon2?: string;
|
|
38
|
+
|
|
39
|
+
/** Avatar HTML for trailing avatar (optional) */
|
|
40
|
+
avatar?: string;
|
|
41
|
+
|
|
42
|
+
/** If true, will show the clear button when the input has text */
|
|
43
|
+
showClearButton?: boolean;
|
|
44
|
+
|
|
45
|
+
/** Callback function when a search is submitted */
|
|
46
|
+
onSubmit?: (value: string) => void;
|
|
47
|
+
|
|
48
|
+
/** Callback function when the input value changes */
|
|
49
|
+
onInput?: (value: string) => void;
|
|
50
|
+
|
|
51
|
+
/** Callback function when the clear button is clicked */
|
|
52
|
+
onClear?: () => void;
|
|
53
|
+
|
|
54
|
+
/** Suggestions to show when the search is focused (array of strings or objects) */
|
|
55
|
+
suggestions?: string[] | Array<{text: string, value?: string, icon?: string}>;
|
|
56
|
+
|
|
57
|
+
/** Whether to show dividers between suggestion groups */
|
|
58
|
+
showDividers?: boolean;
|
|
59
|
+
|
|
60
|
+
/** Additional CSS classes */
|
|
61
|
+
class?: string;
|
|
62
|
+
|
|
63
|
+
/** Maximum width of the search component in pixels */
|
|
64
|
+
maxWidth?: number;
|
|
65
|
+
|
|
66
|
+
/** Minimum width of the search component in pixels */
|
|
67
|
+
minWidth?: number;
|
|
68
|
+
|
|
69
|
+
/** Whether the search component is full-width */
|
|
70
|
+
fullWidth?: boolean;
|
|
71
|
+
|
|
72
|
+
/** Event handlers for search events */
|
|
73
|
+
on?: {
|
|
74
|
+
[key in SearchEventType]?: (event: SearchEvent) => void;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Search event data
|
|
80
|
+
* @interface SearchEvent
|
|
81
|
+
*/
|
|
82
|
+
export interface SearchEvent {
|
|
83
|
+
/** The search component that triggered the event */
|
|
84
|
+
search: any;
|
|
85
|
+
|
|
86
|
+
/** Current search value */
|
|
87
|
+
value: string;
|
|
88
|
+
|
|
89
|
+
/** Original DOM event if available */
|
|
90
|
+
originalEvent: Event | null;
|
|
91
|
+
|
|
92
|
+
/** Function to prevent default behavior */
|
|
93
|
+
preventDefault: () => void;
|
|
94
|
+
|
|
95
|
+
/** Whether default behavior was prevented */
|
|
96
|
+
defaultPrevented: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Search component public API interface
|
|
101
|
+
* @interface SearchComponent
|
|
102
|
+
*/
|
|
103
|
+
export interface SearchComponent {
|
|
104
|
+
/** The root element of the search component */
|
|
105
|
+
element: HTMLElement;
|
|
106
|
+
|
|
107
|
+
/** Sets the search value */
|
|
108
|
+
setValue: (value: string, triggerEvent?: boolean) => SearchComponent;
|
|
109
|
+
|
|
110
|
+
/** Gets the search value */
|
|
111
|
+
getValue: () => string;
|
|
112
|
+
|
|
113
|
+
/** Sets placeholder text */
|
|
114
|
+
setPlaceholder: (text: string) => SearchComponent;
|
|
115
|
+
|
|
116
|
+
/** Gets placeholder text */
|
|
117
|
+
getPlaceholder: () => string;
|
|
118
|
+
|
|
119
|
+
/** Sets leading icon */
|
|
120
|
+
setLeadingIcon: (iconHtml: string) => SearchComponent;
|
|
121
|
+
|
|
122
|
+
/** Sets trailing icon */
|
|
123
|
+
setTrailingIcon: (iconHtml: string) => SearchComponent;
|
|
124
|
+
|
|
125
|
+
/** Sets second trailing icon */
|
|
126
|
+
setTrailingIcon2: (iconHtml: string) => SearchComponent;
|
|
127
|
+
|
|
128
|
+
/** Sets avatar */
|
|
129
|
+
setAvatar: (avatarHtml: string) => SearchComponent;
|
|
130
|
+
|
|
131
|
+
/** Shows or hides clear button */
|
|
132
|
+
showClearButton: (show: boolean) => SearchComponent;
|
|
133
|
+
|
|
134
|
+
/** Updates suggestions */
|
|
135
|
+
setSuggestions: (suggestions: string[] | Array<{text: string, value?: string, icon?: string}>) => SearchComponent;
|
|
136
|
+
|
|
137
|
+
/** Shows or hides suggestions */
|
|
138
|
+
showSuggestions: (show: boolean) => SearchComponent;
|
|
139
|
+
|
|
140
|
+
/** Focuses the search input */
|
|
141
|
+
focus: () => SearchComponent;
|
|
142
|
+
|
|
143
|
+
/** Blurs the search input */
|
|
144
|
+
blur: () => SearchComponent;
|
|
145
|
+
|
|
146
|
+
/** Expands the search bar into view mode (if in bar mode) */
|
|
147
|
+
expand: () => SearchComponent;
|
|
148
|
+
|
|
149
|
+
/** Collapses the search view back to bar mode */
|
|
150
|
+
collapse: () => SearchComponent;
|
|
151
|
+
|
|
152
|
+
/** Clears the search input */
|
|
153
|
+
clear: () => SearchComponent;
|
|
154
|
+
|
|
155
|
+
/** Submits the search */
|
|
156
|
+
submit: () => SearchComponent;
|
|
157
|
+
|
|
158
|
+
/** Enables the search component */
|
|
159
|
+
enable: () => SearchComponent;
|
|
160
|
+
|
|
161
|
+
/** Disables the search component */
|
|
162
|
+
disable: () => SearchComponent;
|
|
163
|
+
|
|
164
|
+
/** Checks if search is disabled */
|
|
165
|
+
isDisabled: () => boolean;
|
|
166
|
+
|
|
167
|
+
/** Adds event listener */
|
|
168
|
+
on: (event: SearchEventType, handler: (event: SearchEvent) => void) => SearchComponent;
|
|
169
|
+
|
|
170
|
+
/** Removes event listener */
|
|
171
|
+
off: (event: SearchEventType, handler: (event: SearchEvent) => void) => SearchComponent;
|
|
172
|
+
|
|
173
|
+
/** Destroys the search component and cleans up resources */
|
|
174
|
+
destroy: () => void;
|
|
175
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/components/segmented-button/config.ts
|
|
2
|
+
import { createComponentConfig } from '../../core/config/component-config';
|
|
3
|
+
import { SegmentedButtonConfig, SelectionMode } from './types';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CHECKMARK_ICON = `
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
7
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
8
|
+
</svg>`;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default configuration values for segmented buttons
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_CONFIG = {
|
|
15
|
+
mode: SelectionMode.SINGLE,
|
|
16
|
+
ripple: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates the base configuration for Segmented Button component
|
|
21
|
+
* @param {SegmentedButtonConfig} config - User provided configuration
|
|
22
|
+
* @returns {SegmentedButtonConfig} Complete configuration with defaults applied
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export const createBaseConfig = (config: SegmentedButtonConfig = {}): SegmentedButtonConfig =>
|
|
26
|
+
createComponentConfig(DEFAULT_CONFIG, config, 'segmented-button') as SegmentedButtonConfig;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generates element configuration for the Segmented Button container
|
|
30
|
+
* @param {SegmentedButtonConfig} config - Segmented Button configuration
|
|
31
|
+
* @returns {Object} Element configuration object for withElement
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export const getContainerConfig = (config: SegmentedButtonConfig) => ({
|
|
35
|
+
tag: 'div',
|
|
36
|
+
componentName: 'segmented-button',
|
|
37
|
+
attrs: {
|
|
38
|
+
role: 'group',
|
|
39
|
+
'aria-label': 'Segmented button',
|
|
40
|
+
'data-mode': config.mode || SelectionMode.SINGLE
|
|
41
|
+
},
|
|
42
|
+
className: [
|
|
43
|
+
config.class,
|
|
44
|
+
config.disabled ? `${config.prefix}-segmented-button--disabled` : null
|
|
45
|
+
],
|
|
46
|
+
interactive: true
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generates configuration for a segment element
|
|
51
|
+
* @param {Object} segment - Segment configuration
|
|
52
|
+
* @param {string} prefix - Component prefix
|
|
53
|
+
* @param {boolean} groupDisabled - Whether the entire group is disabled
|
|
54
|
+
* @returns {Object} Element configuration for the segment
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export const getSegmentConfig = (segment, prefix, groupDisabled = false) => {
|
|
58
|
+
const isDisabled = groupDisabled || segment.disabled;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
tag: 'button',
|
|
62
|
+
attrs: {
|
|
63
|
+
type: 'button',
|
|
64
|
+
role: 'button',
|
|
65
|
+
disabled: isDisabled ? true : undefined,
|
|
66
|
+
'aria-pressed': segment.selected ? 'true' : 'false',
|
|
67
|
+
value: segment.value
|
|
68
|
+
},
|
|
69
|
+
className: [
|
|
70
|
+
`${prefix}-segment`,
|
|
71
|
+
segment.selected ? `${prefix}-segment--selected` : null,
|
|
72
|
+
isDisabled ? `${prefix}-segment--disabled` : null,
|
|
73
|
+
segment.class
|
|
74
|
+
],
|
|
75
|
+
forwardEvents: {
|
|
76
|
+
click: (component) => !isDisabled
|
|
77
|
+
},
|
|
78
|
+
interactive: !isDisabled
|
|
79
|
+
};
|
|
80
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// src/components/segmented-button/index.ts
|
|
2
|
+
export { default, default as createSegmentedButton } from './segmented-button';
|
|
3
|
+
export { SelectionMode } from './types';
|
|
4
|
+
export type { SegmentedButtonConfig, SegmentedButtonComponent, SegmentConfig, Segment } from './types';
|