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,154 @@
|
|
|
1
|
+
// src/components/segmented-button/segment.ts
|
|
2
|
+
import { createElement } from '../../core/dom/create';
|
|
3
|
+
import { createRipple } from '../../core/build/ripple';
|
|
4
|
+
import { SegmentConfig, Segment } from './types';
|
|
5
|
+
import { getSegmentConfig } from './config';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a segment for the segmented button
|
|
9
|
+
* @param {SegmentConfig} config - Segment configuration
|
|
10
|
+
* @param {HTMLElement} container - Container element
|
|
11
|
+
* @param {string} prefix - Component prefix
|
|
12
|
+
* @param {boolean} groupDisabled - Whether the entire group is disabled
|
|
13
|
+
* @param {Object} options - Additional options
|
|
14
|
+
* @returns {Segment} Segment object
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export const createSegment = (
|
|
18
|
+
config: SegmentConfig,
|
|
19
|
+
container: HTMLElement,
|
|
20
|
+
prefix: string,
|
|
21
|
+
groupDisabled = false,
|
|
22
|
+
options = { ripple: true, rippleConfig: {} }
|
|
23
|
+
): Segment => {
|
|
24
|
+
const segmentConfig = getSegmentConfig(config, prefix, groupDisabled);
|
|
25
|
+
const element = createElement(segmentConfig);
|
|
26
|
+
|
|
27
|
+
// Add to container
|
|
28
|
+
container.appendChild(element);
|
|
29
|
+
|
|
30
|
+
// Create ripple effect if enabled
|
|
31
|
+
let ripple;
|
|
32
|
+
if (options.ripple) {
|
|
33
|
+
ripple = createRipple(options.rippleConfig);
|
|
34
|
+
ripple.mount(element);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create text element if provided
|
|
38
|
+
let textElement;
|
|
39
|
+
if (config.text) {
|
|
40
|
+
textElement = createElement({
|
|
41
|
+
tag: 'span',
|
|
42
|
+
className: `${prefix}-segmentedbutton-segment-text`,
|
|
43
|
+
text: config.text,
|
|
44
|
+
container: element
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create icon and checkmark elements
|
|
49
|
+
let iconElement, checkmarkElement;
|
|
50
|
+
if (config.icon) {
|
|
51
|
+
// Create icon element
|
|
52
|
+
iconElement = createElement({
|
|
53
|
+
tag: 'span',
|
|
54
|
+
className: `${prefix}-segmentedbutton-segment-icon`,
|
|
55
|
+
html: config.icon,
|
|
56
|
+
container: element
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Create checkmark element (hidden initially)
|
|
60
|
+
checkmarkElement = createElement({
|
|
61
|
+
tag: 'span',
|
|
62
|
+
className: `${prefix}-segmentedbutton-segment-'checkmark'`,
|
|
63
|
+
html: 'icon',
|
|
64
|
+
container: element
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Hide checkmark if not selected
|
|
68
|
+
if (!config.selected) {
|
|
69
|
+
checkmarkElement.style.display = 'none';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Hide icon if selected and we have text (icon replaced by checkmark)
|
|
73
|
+
if (config.selected && config.text) {
|
|
74
|
+
iconElement.style.display = 'none';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Updates the visual state based on selection
|
|
80
|
+
* @param {boolean} selected - Whether the segment is selected
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
const updateSelectedState = (selected: boolean) => {
|
|
84
|
+
element.classList.toggle(`${prefix}-segmentedbutton-segment--selected`, selected);
|
|
85
|
+
element.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
|
86
|
+
|
|
87
|
+
// Handle icon/checkmark swap if we have both text and icon
|
|
88
|
+
if (iconElement && checkmarkElement && config.text) {
|
|
89
|
+
iconElement.style.display = selected ? 'none' : '';
|
|
90
|
+
checkmarkElement.style.display = selected ? '' : 'none';
|
|
91
|
+
} else if (checkmarkElement) {
|
|
92
|
+
// If we have only icons (no text), show checkmark based on selection
|
|
93
|
+
checkmarkElement.style.display = selected ? '' : 'none';
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Updates the disabled state
|
|
99
|
+
* @param {boolean} disabled - Whether the segment is disabled
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
const updateDisabledState = (disabled: boolean) => {
|
|
103
|
+
const isDisabled = disabled || groupDisabled;
|
|
104
|
+
element.classList.toggle(`${prefix}-segmentedbutton-segment--disabled`, isDisabled);
|
|
105
|
+
|
|
106
|
+
if (isDisabled) {
|
|
107
|
+
element.setAttribute('disabled', 'true');
|
|
108
|
+
} else {
|
|
109
|
+
element.removeAttribute('disabled');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Value to use for the segment
|
|
114
|
+
const value = config.value || config.text || '';
|
|
115
|
+
|
|
116
|
+
// Initialize state
|
|
117
|
+
let isSelected = config.selected || false;
|
|
118
|
+
let isDisabled = config.disabled || false;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
element,
|
|
122
|
+
value,
|
|
123
|
+
|
|
124
|
+
isSelected() {
|
|
125
|
+
return isSelected;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
setSelected(selected: boolean) {
|
|
129
|
+
isSelected = selected;
|
|
130
|
+
updateSelectedState(selected);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
isDisabled() {
|
|
134
|
+
return isDisabled || groupDisabled;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
setDisabled(disabled: boolean) {
|
|
138
|
+
isDisabled = disabled;
|
|
139
|
+
updateDisabledState(disabled);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
destroy() {
|
|
143
|
+
// Clean up ripple if it exists
|
|
144
|
+
if (ripple) {
|
|
145
|
+
ripple.unmount(element);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remove from DOM
|
|
149
|
+
if (element.parentNode) {
|
|
150
|
+
element.parentNode.removeChild(element);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// src/components/segmented-button/segmented-button.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 { createEmitter } from '../../core/state/emitter';
|
|
6
|
+
import { SegmentedButtonConfig, SegmentedButtonComponent, SelectionMode, Segment } from './types';
|
|
7
|
+
import { createBaseConfig, getContainerConfig } from './config';
|
|
8
|
+
import { createSegment } from './segment';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new Segmented Button component
|
|
12
|
+
* @param {SegmentedButtonConfig} config - Segmented Button configuration
|
|
13
|
+
* @returns {SegmentedButtonComponent} Segmented Button component instance
|
|
14
|
+
*/
|
|
15
|
+
const createSegmentedButton = (config: SegmentedButtonConfig = {}): SegmentedButtonComponent => {
|
|
16
|
+
// Process configuration
|
|
17
|
+
const baseConfig = createBaseConfig(config);
|
|
18
|
+
const mode = baseConfig.mode || SelectionMode.SINGLE;
|
|
19
|
+
const emitter = createEmitter();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Create the base component
|
|
23
|
+
const component = pipe(
|
|
24
|
+
createBase,
|
|
25
|
+
withEvents(),
|
|
26
|
+
withElement(getContainerConfig(baseConfig)),
|
|
27
|
+
withLifecycle()
|
|
28
|
+
)(baseConfig);
|
|
29
|
+
|
|
30
|
+
// Create segments
|
|
31
|
+
const segments: Segment[] = [];
|
|
32
|
+
if (baseConfig.segments && baseConfig.segments.length) {
|
|
33
|
+
baseConfig.segments.forEach(segmentConfig => {
|
|
34
|
+
const segment = createSegment(
|
|
35
|
+
segmentConfig,
|
|
36
|
+
component.element,
|
|
37
|
+
baseConfig.prefix,
|
|
38
|
+
baseConfig.disabled,
|
|
39
|
+
{
|
|
40
|
+
ripple: baseConfig.ripple,
|
|
41
|
+
rippleConfig: baseConfig.rippleConfig
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
segments.push(segment);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ensure at least one item is selected in single-select mode
|
|
50
|
+
if (mode === SelectionMode.SINGLE && !segments.some(s => s.isSelected())) {
|
|
51
|
+
// Select the first non-disabled segment by default
|
|
52
|
+
const firstSelectable = segments.find(s => !s.isDisabled());
|
|
53
|
+
if (firstSelectable) {
|
|
54
|
+
firstSelectable.setSelected(true);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handles click events on segments
|
|
60
|
+
* @param {Event} event - DOM click event
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
const handleSegmentClick = (event: Event) => {
|
|
64
|
+
const segmentElement = event.currentTarget as HTMLElement;
|
|
65
|
+
const clickedSegment = segments.find(s => s.element === segmentElement);
|
|
66
|
+
|
|
67
|
+
if (!clickedSegment || clickedSegment.isDisabled()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const oldValue = getSelectedValues();
|
|
72
|
+
|
|
73
|
+
// Handle selection based on mode
|
|
74
|
+
if (mode === SelectionMode.SINGLE) {
|
|
75
|
+
// In single-select, deselect all other segments
|
|
76
|
+
segments.forEach(segment => {
|
|
77
|
+
segment.setSelected(segment === clickedSegment);
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
// In multi-select, toggle the clicked segment
|
|
81
|
+
clickedSegment.setSelected(!clickedSegment.isSelected());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Emit change event
|
|
85
|
+
const newValue = getSelectedValues();
|
|
86
|
+
|
|
87
|
+
// Only emit if values actually changed
|
|
88
|
+
if (
|
|
89
|
+
oldValue.length !== newValue.length ||
|
|
90
|
+
oldValue.some(v => !newValue.includes(v)) ||
|
|
91
|
+
newValue.some(v => !oldValue.includes(v))
|
|
92
|
+
) {
|
|
93
|
+
emitter.emit('change', {
|
|
94
|
+
selected: getSelected(),
|
|
95
|
+
value: newValue,
|
|
96
|
+
oldValue
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Attach click handlers to segments
|
|
102
|
+
segments.forEach(segment => {
|
|
103
|
+
segment.element.addEventListener('click', handleSegmentClick);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets an array of selected segments
|
|
108
|
+
* @returns {Segment[]} Array of selected segments
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
const getSelected = () => segments.filter(segment => segment.isSelected());
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gets an array of selected segment values
|
|
115
|
+
* @returns {string[]} Array of selected segment values
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
const getSelectedValues = () => getSelected().map(segment => segment.value);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Finds a segment by its value
|
|
122
|
+
* @param {string} value - Segment value to find
|
|
123
|
+
* @returns {Segment|undefined} The found segment or undefined
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
const findSegmentByValue = (value: string) => segments.find(segment => segment.value === value);
|
|
127
|
+
|
|
128
|
+
// Create the component API
|
|
129
|
+
const segmentedButton: SegmentedButtonComponent = {
|
|
130
|
+
element: component.element,
|
|
131
|
+
segments,
|
|
132
|
+
|
|
133
|
+
getSelected,
|
|
134
|
+
|
|
135
|
+
getValue() {
|
|
136
|
+
return getSelectedValues();
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
select(value) {
|
|
140
|
+
const segment = findSegmentByValue(value);
|
|
141
|
+
if (segment && !segment.isDisabled()) {
|
|
142
|
+
const oldValue = getSelectedValues();
|
|
143
|
+
|
|
144
|
+
if (mode === SelectionMode.SINGLE) {
|
|
145
|
+
// Deselect all other segments
|
|
146
|
+
segments.forEach(s => s.setSelected(s === segment));
|
|
147
|
+
} else {
|
|
148
|
+
// Just select this segment
|
|
149
|
+
segment.setSelected(true);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Emit change event
|
|
153
|
+
const newValue = getSelectedValues();
|
|
154
|
+
if (oldValue.join(',') !== newValue.join(',')) {
|
|
155
|
+
emitter.emit('change', {
|
|
156
|
+
selected: getSelected(),
|
|
157
|
+
value: newValue,
|
|
158
|
+
oldValue
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return this;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
deselect(value) {
|
|
166
|
+
const segment = findSegmentByValue(value);
|
|
167
|
+
if (segment && !segment.isDisabled()) {
|
|
168
|
+
// In single select mode, only deselect if there's another selected segment
|
|
169
|
+
if (mode === SelectionMode.SINGLE) {
|
|
170
|
+
const selectedSegments = getSelected();
|
|
171
|
+
// Only allow deselection if there's more than one selected or we're selecting a different segment
|
|
172
|
+
if (selectedSegments.length > 1 || !segment.isSelected()) {
|
|
173
|
+
const oldValue = getSelectedValues();
|
|
174
|
+
segment.setSelected(false);
|
|
175
|
+
|
|
176
|
+
// Emit change event
|
|
177
|
+
const newValue = getSelectedValues();
|
|
178
|
+
if (oldValue.join(',') !== newValue.join(',')) {
|
|
179
|
+
emitter.emit('change', {
|
|
180
|
+
selected: getSelected(),
|
|
181
|
+
value: newValue,
|
|
182
|
+
oldValue
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// In multi-select, always allow deselection
|
|
188
|
+
const oldValue = getSelectedValues();
|
|
189
|
+
segment.setSelected(false);
|
|
190
|
+
|
|
191
|
+
// Emit change event
|
|
192
|
+
const newValue = getSelectedValues();
|
|
193
|
+
if (oldValue.join(',') !== newValue.join(',')) {
|
|
194
|
+
emitter.emit('change', {
|
|
195
|
+
selected: getSelected(),
|
|
196
|
+
value: newValue,
|
|
197
|
+
oldValue
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return this;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
enable() {
|
|
206
|
+
// Enable the entire component
|
|
207
|
+
component.element.classList.remove(`${baseConfig.prefix}-segmented-button--disabled`);
|
|
208
|
+
return this;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
disable() {
|
|
212
|
+
// Disable the entire component
|
|
213
|
+
component.element.classList.add(`${baseConfig.prefix}-segmented-button--disabled`);
|
|
214
|
+
return this;
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
on(event, handler) {
|
|
218
|
+
emitter.on(event, handler);
|
|
219
|
+
return this;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
off(event, handler) {
|
|
223
|
+
emitter.off(event, handler);
|
|
224
|
+
return this;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
destroy() {
|
|
228
|
+
// Remove event listeners
|
|
229
|
+
segments.forEach(segment => {
|
|
230
|
+
segment.element.removeEventListener('click', handleSegmentClick);
|
|
231
|
+
segment.destroy();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Clear emitter
|
|
235
|
+
emitter.clear();
|
|
236
|
+
|
|
237
|
+
// Destroy base component
|
|
238
|
+
component.lifecycle.destroy();
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return segmentedButton;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Segmented Button creation error:', error);
|
|
245
|
+
throw new Error(`Failed to create segmented button: ${(error as Error).message}`);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export default createSegmentedButton;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// src/components/segmented-button/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Segmented button selection mode
|
|
5
|
+
* @category Components
|
|
6
|
+
*/
|
|
7
|
+
export enum SelectionMode {
|
|
8
|
+
/** Only one segment can be selected at a time */
|
|
9
|
+
SINGLE = 'single',
|
|
10
|
+
/** Multiple segments can be selected */
|
|
11
|
+
MULTI = 'multi'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Event types for segmented button
|
|
16
|
+
*/
|
|
17
|
+
export type SegmentedButtonEventType = 'change';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Event data for segmented button events
|
|
21
|
+
*/
|
|
22
|
+
export interface SegmentedButtonEvent {
|
|
23
|
+
/** The segmented button component that triggered the event */
|
|
24
|
+
segmentedButton: SegmentedButtonComponent;
|
|
25
|
+
|
|
26
|
+
/** The selected segments */
|
|
27
|
+
selected: Segment[];
|
|
28
|
+
|
|
29
|
+
/** Values of the selected segments */
|
|
30
|
+
values: string[];
|
|
31
|
+
|
|
32
|
+
/** Original DOM event if available */
|
|
33
|
+
originalEvent: Event | null;
|
|
34
|
+
|
|
35
|
+
/** Function to prevent default behavior */
|
|
36
|
+
preventDefault: () => void;
|
|
37
|
+
|
|
38
|
+
/** Whether default behavior was prevented */
|
|
39
|
+
defaultPrevented: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configuration for a single segment within a segmented button
|
|
44
|
+
* @category Components
|
|
45
|
+
*/
|
|
46
|
+
export interface SegmentConfig {
|
|
47
|
+
/**
|
|
48
|
+
* Text content for the segment
|
|
49
|
+
* @example 'Day'
|
|
50
|
+
*/
|
|
51
|
+
text?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Icon HTML content
|
|
55
|
+
* @example '<svg>...</svg>'
|
|
56
|
+
*/
|
|
57
|
+
icon?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether this segment is initially selected
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
selected?: boolean;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Value associated with this segment
|
|
67
|
+
*/
|
|
68
|
+
value?: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether this segment is disabled
|
|
72
|
+
* @default false
|
|
73
|
+
*/
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Additional CSS class names for this segment
|
|
78
|
+
*/
|
|
79
|
+
class?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Configuration interface for the Segmented Button component
|
|
84
|
+
* @category Components
|
|
85
|
+
*/
|
|
86
|
+
export interface SegmentedButtonConfig {
|
|
87
|
+
/**
|
|
88
|
+
* Selection mode for the segmented button group
|
|
89
|
+
* @default SelectionMode.SINGLE
|
|
90
|
+
*/
|
|
91
|
+
mode?: SelectionMode;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Array of segment configurations
|
|
95
|
+
*/
|
|
96
|
+
segments?: SegmentConfig[];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Component prefix for class names
|
|
100
|
+
* @default 'mtrl'
|
|
101
|
+
*/
|
|
102
|
+
prefix?: string;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Component name used in class generation
|
|
106
|
+
*/
|
|
107
|
+
componentName?: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether the entire segmented button is initially disabled
|
|
111
|
+
* @default false
|
|
112
|
+
*/
|
|
113
|
+
disabled?: boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Additional CSS class for the segmented button container
|
|
117
|
+
*/
|
|
118
|
+
class?: string;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Whether to enable ripple effect
|
|
122
|
+
* @default true
|
|
123
|
+
*/
|
|
124
|
+
ripple?: boolean;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ripple effect configuration
|
|
128
|
+
*/
|
|
129
|
+
rippleConfig?: {
|
|
130
|
+
/** Duration of the ripple animation in milliseconds */
|
|
131
|
+
duration?: number;
|
|
132
|
+
/** Timing function for the ripple animation */
|
|
133
|
+
timing?: string;
|
|
134
|
+
/** Opacity values for ripple start and end [start, end] */
|
|
135
|
+
opacity?: [string, string];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Event handlers for segmented button events
|
|
140
|
+
*/
|
|
141
|
+
on?: {
|
|
142
|
+
[key in SegmentedButtonEventType]?: (event: SegmentedButtonEvent) => void;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Interface for a segment within a segmented button
|
|
148
|
+
* @category Components
|
|
149
|
+
*/
|
|
150
|
+
export interface Segment {
|
|
151
|
+
/** The segment's DOM element */
|
|
152
|
+
element: HTMLElement;
|
|
153
|
+
|
|
154
|
+
/** The segment's value */
|
|
155
|
+
value: string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gets whether the segment is selected
|
|
159
|
+
*/
|
|
160
|
+
isSelected: () => boolean;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Sets the segment's selected state
|
|
164
|
+
* @param selected - Whether the segment should be selected
|
|
165
|
+
*/
|
|
166
|
+
setSelected: (selected: boolean) => void;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gets whether the segment is disabled
|
|
170
|
+
*/
|
|
171
|
+
isDisabled: () => boolean;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Sets the segment's disabled state
|
|
175
|
+
* @param disabled - Whether the segment should be disabled
|
|
176
|
+
*/
|
|
177
|
+
setDisabled: (disabled: boolean) => void;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Destroys the segment and cleans up resources
|
|
181
|
+
*/
|
|
182
|
+
destroy: () => void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Segmented Button component interface
|
|
187
|
+
* @category Components
|
|
188
|
+
*/
|
|
189
|
+
export interface SegmentedButtonComponent {
|
|
190
|
+
/** The component's container DOM element */
|
|
191
|
+
element: HTMLElement;
|
|
192
|
+
|
|
193
|
+
/** Array of segment objects */
|
|
194
|
+
segments: Segment[];
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Gets the selected segment(s)
|
|
198
|
+
* @returns An array of selected segments
|
|
199
|
+
*/
|
|
200
|
+
getSelected: () => Segment[];
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gets the values of selected segment(s)
|
|
204
|
+
* @returns An array of selected segment values
|
|
205
|
+
*/
|
|
206
|
+
getValue: () => string[];
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Selects a segment by its value
|
|
210
|
+
* @param value - The value of the segment to select
|
|
211
|
+
* @returns The SegmentedButtonComponent for chaining
|
|
212
|
+
*/
|
|
213
|
+
select: (value: string) => SegmentedButtonComponent;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Deselects a segment by its value
|
|
217
|
+
* @param value - The value of the segment to deselect
|
|
218
|
+
* @returns The SegmentedButtonComponent for chaining
|
|
219
|
+
*/
|
|
220
|
+
deselect: (value: string) => SegmentedButtonComponent;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Enables the segmented button
|
|
224
|
+
* @returns The SegmentedButtonComponent for chaining
|
|
225
|
+
*/
|
|
226
|
+
enable: () => SegmentedButtonComponent;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Disables the segmented button
|
|
230
|
+
* @returns The SegmentedButtonComponent for chaining
|
|
231
|
+
*/
|
|
232
|
+
disable: () => SegmentedButtonComponent;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Adds an event listener to the segmented button
|
|
236
|
+
* @param event - Event name ('change', etc.)
|
|
237
|
+
* @param handler - Event handler function
|
|
238
|
+
* @returns The SegmentedButtonComponent for chaining
|
|
239
|
+
*/
|
|
240
|
+
on: (event: SegmentedButtonEventType, handler: (event: SegmentedButtonEvent) => void) => SegmentedButtonComponent;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Removes an event listener from the segmented button
|
|
244
|
+
* @param event - Event name
|
|
245
|
+
* @param handler - Event handler function
|
|
246
|
+
* @returns The SegmentedButtonComponent for chaining
|
|
247
|
+
*/
|
|
248
|
+
off: (event: SegmentedButtonEventType, handler: (event: SegmentedButtonEvent) => void) => SegmentedButtonComponent;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Destroys the component and cleans up resources
|
|
252
|
+
*/
|
|
253
|
+
destroy: () => void;
|
|
254
|
+
}
|
|
@@ -8,7 +8,7 @@ Based on the provided accessibility requirements, the slider component has been
|
|
|
8
8
|
|
|
9
9
|
### Focus and Keyboard Navigation
|
|
10
10
|
|
|
11
|
-
- **Direct
|
|
11
|
+
- **Direct Handle Focus**: The initial focus now lands directly on the handle (not the container)
|
|
12
12
|
- **Visual Feedback**: Added a clear outline on focus to provide visual cues for keyboard users
|
|
13
13
|
- **Arrow Key Navigation**:
|
|
14
14
|
- Left/Right arrows change the value by one step
|
|
@@ -19,11 +19,11 @@ Based on the provided accessibility requirements, the slider component has been
|
|
|
19
19
|
|
|
20
20
|
### Visual Feedback During Interaction
|
|
21
21
|
|
|
22
|
-
- **
|
|
22
|
+
- **Handle Shrinking**: The handle width shrinks slightly during interaction to provide feedback
|
|
23
23
|
- **Value Display**:
|
|
24
24
|
- Value appears during interaction (touch, drag, mouse click, keyboard navigation)
|
|
25
25
|
- Value remains visible briefly after interaction ends (1.5 seconds)
|
|
26
|
-
- Value position updates to follow the
|
|
26
|
+
- Value position updates to follow the handle
|
|
27
27
|
|
|
28
28
|
### Visual Anchors for Contrast
|
|
29
29
|
|
|
@@ -39,13 +39,13 @@ Based on the provided accessibility requirements, the slider component has been
|
|
|
39
39
|
- Set appropriate ARIA attributes for screen readers
|
|
40
40
|
|
|
41
41
|
2. **Interaction Feedback**:
|
|
42
|
-
- Modified CSS to shrink
|
|
42
|
+
- Modified CSS to shrink handle width during active states
|
|
43
43
|
- Enhanced value bubble display timing
|
|
44
44
|
- Improved touch and mouse event handling
|
|
45
45
|
|
|
46
46
|
3. **Focus Management**:
|
|
47
47
|
- Set clear focus styles that work cross-browser
|
|
48
|
-
- Applied focus directly to interactive
|
|
48
|
+
- Applied focus directly to interactive handle elements
|
|
49
49
|
- Ensured focus outline is visible against various backgrounds
|
|
50
50
|
|
|
51
51
|
## Keyboard Navigation Map
|