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,67 @@
|
|
|
1
|
+
// src/components/segmented-button/config.ts
|
|
2
|
+
import { createComponentConfig } from '../../core/config/component-config';
|
|
3
|
+
import { SegmentedButtonConfig, SelectionMode } from './types';
|
|
4
|
+
import { DEFAULT_CONFIG, CLASSES } from './constants';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates the base configuration for Segmented Button component
|
|
8
|
+
* @param {SegmentedButtonConfig} config - User provided configuration
|
|
9
|
+
* @returns {SegmentedButtonConfig} Complete configuration with defaults applied
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const createBaseConfig = (config: SegmentedButtonConfig = {}): SegmentedButtonConfig =>
|
|
13
|
+
createComponentConfig(DEFAULT_CONFIG, config, CLASSES.CONTAINER) as SegmentedButtonConfig;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generates element configuration for the Segmented Button container
|
|
17
|
+
* @param {SegmentedButtonConfig} config - Segmented Button configuration
|
|
18
|
+
* @returns {Object} Element configuration object for withElement
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export const getContainerConfig = (config: SegmentedButtonConfig) => ({
|
|
22
|
+
tag: 'div',
|
|
23
|
+
componentName: CLASSES.CONTAINER,
|
|
24
|
+
attrs: {
|
|
25
|
+
role: 'group',
|
|
26
|
+
'aria-label': 'Segmented button',
|
|
27
|
+
'data-mode': config.mode || SelectionMode.SINGLE
|
|
28
|
+
},
|
|
29
|
+
className: [
|
|
30
|
+
config.class,
|
|
31
|
+
config.disabled ? `${config.prefix}-${CLASSES.CONTAINER}--disabled` : null
|
|
32
|
+
],
|
|
33
|
+
interactive: true
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates configuration for a segment element
|
|
38
|
+
* @param {Object} segment - Segment configuration
|
|
39
|
+
* @param {string} prefix - Component prefix
|
|
40
|
+
* @param {boolean} groupDisabled - Whether the entire group is disabled
|
|
41
|
+
* @returns {Object} Element configuration for the segment
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export const getSegmentConfig = (segment, prefix, groupDisabled = false) => {
|
|
45
|
+
const isDisabled = groupDisabled || segment.disabled;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
tag: 'button',
|
|
49
|
+
attrs: {
|
|
50
|
+
type: 'button',
|
|
51
|
+
role: 'button',
|
|
52
|
+
disabled: isDisabled ? true : undefined,
|
|
53
|
+
'aria-pressed': segment.selected ? 'true' : 'false',
|
|
54
|
+
value: segment.value
|
|
55
|
+
},
|
|
56
|
+
className: [
|
|
57
|
+
`${prefix}-${CLASSES.SEGMENT}`,
|
|
58
|
+
segment.selected ? `${prefix}-${CLASSES.SEGMENT}--${CLASSES.SELECTED}` : null,
|
|
59
|
+
isDisabled ? `${prefix}-${CLASSES.SEGMENT}--${CLASSES.DISABLED}` : null,
|
|
60
|
+
segment.class
|
|
61
|
+
],
|
|
62
|
+
forwardEvents: {
|
|
63
|
+
click: (component) => !isDisabled
|
|
64
|
+
},
|
|
65
|
+
interactive: !isDisabled
|
|
66
|
+
};
|
|
67
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/components/segmented-button/constants.ts
|
|
2
|
+
import { SelectionMode } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default checkbox icon SVG used for selected state
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_CHECKMARK_ICON = `
|
|
9
|
+
<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">
|
|
10
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
11
|
+
</svg>`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default configuration values for segmented buttons
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_CONFIG = {
|
|
18
|
+
mode: SelectionMode.SINGLE,
|
|
19
|
+
ripple: true
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Event names used by the segmented button component
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export const EVENTS = {
|
|
27
|
+
CHANGE: 'change'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* CSS classes used by the segmented button component
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export const CLASSES = {
|
|
35
|
+
CONTAINER: 'segmented-button',
|
|
36
|
+
SEGMENT: 'segmented-button-segment',
|
|
37
|
+
SELECTED: 'selected',
|
|
38
|
+
DISABLED: 'disabled',
|
|
39
|
+
ICON: 'icon',
|
|
40
|
+
CHECKMARK: 'checkmark',
|
|
41
|
+
TEXT: 'text'
|
|
42
|
+
};
|
|
@@ -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';
|
|
@@ -0,0 +1,155 @@
|
|
|
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 { DEFAULT_CHECKMARK_ICON, CLASSES } from './constants';
|
|
6
|
+
import { getSegmentConfig } from './config';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a segment for the segmented button
|
|
10
|
+
* @param {SegmentConfig} config - Segment configuration
|
|
11
|
+
* @param {HTMLElement} container - Container element
|
|
12
|
+
* @param {string} prefix - Component prefix
|
|
13
|
+
* @param {boolean} groupDisabled - Whether the entire group is disabled
|
|
14
|
+
* @param {Object} options - Additional options
|
|
15
|
+
* @returns {Segment} Segment object
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export const createSegment = (
|
|
19
|
+
config: SegmentConfig,
|
|
20
|
+
container: HTMLElement,
|
|
21
|
+
prefix: string,
|
|
22
|
+
groupDisabled = false,
|
|
23
|
+
options = { ripple: true, rippleConfig: {} }
|
|
24
|
+
): Segment => {
|
|
25
|
+
const segmentConfig = getSegmentConfig(config, prefix, groupDisabled);
|
|
26
|
+
const element = createElement(segmentConfig);
|
|
27
|
+
|
|
28
|
+
// Add to container
|
|
29
|
+
container.appendChild(element);
|
|
30
|
+
|
|
31
|
+
// Create ripple effect if enabled
|
|
32
|
+
let ripple;
|
|
33
|
+
if (options.ripple) {
|
|
34
|
+
ripple = createRipple(options.rippleConfig);
|
|
35
|
+
ripple.mount(element);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create text element if provided
|
|
39
|
+
let textElement;
|
|
40
|
+
if (config.text) {
|
|
41
|
+
textElement = createElement({
|
|
42
|
+
tag: 'span',
|
|
43
|
+
className: `${prefix}-${CLASSES.SEGMENT}-${CLASSES.TEXT}`,
|
|
44
|
+
text: config.text,
|
|
45
|
+
container: element
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create icon and checkmark elements
|
|
50
|
+
let iconElement, checkmarkElement;
|
|
51
|
+
if (config.icon) {
|
|
52
|
+
// Create icon element
|
|
53
|
+
iconElement = createElement({
|
|
54
|
+
tag: 'span',
|
|
55
|
+
className: `${prefix}-${CLASSES.SEGMENT}-${CLASSES.ICON}`,
|
|
56
|
+
html: config.icon,
|
|
57
|
+
container: element
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Create checkmark element (hidden initially)
|
|
61
|
+
checkmarkElement = createElement({
|
|
62
|
+
tag: 'span',
|
|
63
|
+
className: `${prefix}-${CLASSES.SEGMENT}-${CLASSES.CHECKMARK}`,
|
|
64
|
+
html: DEFAULT_CHECKMARK_ICON,
|
|
65
|
+
container: element
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Hide checkmark if not selected
|
|
69
|
+
if (!config.selected) {
|
|
70
|
+
checkmarkElement.style.display = 'none';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Hide icon if selected and we have text (icon replaced by checkmark)
|
|
74
|
+
if (config.selected && config.text) {
|
|
75
|
+
iconElement.style.display = 'none';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Updates the visual state based on selection
|
|
81
|
+
* @param {boolean} selected - Whether the segment is selected
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
const updateSelectedState = (selected: boolean) => {
|
|
85
|
+
element.classList.toggle(`${prefix}-${CLASSES.SEGMENT}--${CLASSES.SELECTED}`, selected);
|
|
86
|
+
element.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
|
87
|
+
|
|
88
|
+
// Handle icon/checkmark swap if we have both text and icon
|
|
89
|
+
if (iconElement && checkmarkElement && config.text) {
|
|
90
|
+
iconElement.style.display = selected ? 'none' : '';
|
|
91
|
+
checkmarkElement.style.display = selected ? '' : 'none';
|
|
92
|
+
} else if (checkmarkElement) {
|
|
93
|
+
// If we have only icons (no text), show checkmark based on selection
|
|
94
|
+
checkmarkElement.style.display = selected ? '' : 'none';
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Updates the disabled state
|
|
100
|
+
* @param {boolean} disabled - Whether the segment is disabled
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
const updateDisabledState = (disabled: boolean) => {
|
|
104
|
+
const isDisabled = disabled || groupDisabled;
|
|
105
|
+
element.classList.toggle(`${prefix}-${CLASSES.SEGMENT}--${CLASSES.DISABLED}`, isDisabled);
|
|
106
|
+
|
|
107
|
+
if (isDisabled) {
|
|
108
|
+
element.setAttribute('disabled', 'true');
|
|
109
|
+
} else {
|
|
110
|
+
element.removeAttribute('disabled');
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Value to use for the segment
|
|
115
|
+
const value = config.value || config.text || '';
|
|
116
|
+
|
|
117
|
+
// Initialize state
|
|
118
|
+
let isSelected = config.selected || false;
|
|
119
|
+
let isDisabled = config.disabled || false;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
element,
|
|
123
|
+
value,
|
|
124
|
+
|
|
125
|
+
isSelected() {
|
|
126
|
+
return isSelected;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
setSelected(selected: boolean) {
|
|
130
|
+
isSelected = selected;
|
|
131
|
+
updateSelectedState(selected);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
isDisabled() {
|
|
135
|
+
return isDisabled || groupDisabled;
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
setDisabled(disabled: boolean) {
|
|
139
|
+
isDisabled = disabled;
|
|
140
|
+
updateDisabledState(disabled);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
destroy() {
|
|
144
|
+
// Clean up ripple if it exists
|
|
145
|
+
if (ripple) {
|
|
146
|
+
ripple.unmount(element);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Remove from DOM
|
|
150
|
+
if (element.parentNode) {
|
|
151
|
+
element.parentNode.removeChild(element);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
import { EVENTS } from './constants';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new Segmented Button component
|
|
13
|
+
* @param {SegmentedButtonConfig} config - Segmented Button configuration
|
|
14
|
+
* @returns {SegmentedButtonComponent} Segmented Button component instance
|
|
15
|
+
*/
|
|
16
|
+
const createSegmentedButton = (config: SegmentedButtonConfig = {}): SegmentedButtonComponent => {
|
|
17
|
+
// Process configuration
|
|
18
|
+
const baseConfig = createBaseConfig(config);
|
|
19
|
+
const mode = baseConfig.mode || SelectionMode.SINGLE;
|
|
20
|
+
const emitter = createEmitter();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Create the base component
|
|
24
|
+
const component = pipe(
|
|
25
|
+
createBase,
|
|
26
|
+
withEvents(),
|
|
27
|
+
withElement(getContainerConfig(baseConfig)),
|
|
28
|
+
withLifecycle()
|
|
29
|
+
)(baseConfig);
|
|
30
|
+
|
|
31
|
+
// Create segments
|
|
32
|
+
const segments: Segment[] = [];
|
|
33
|
+
if (baseConfig.segments && baseConfig.segments.length) {
|
|
34
|
+
baseConfig.segments.forEach(segmentConfig => {
|
|
35
|
+
const segment = createSegment(
|
|
36
|
+
segmentConfig,
|
|
37
|
+
component.element,
|
|
38
|
+
baseConfig.prefix,
|
|
39
|
+
baseConfig.disabled,
|
|
40
|
+
{
|
|
41
|
+
ripple: baseConfig.ripple,
|
|
42
|
+
rippleConfig: baseConfig.rippleConfig
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
segments.push(segment);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ensure at least one item is selected in single-select mode
|
|
51
|
+
if (mode === SelectionMode.SINGLE && !segments.some(s => s.isSelected())) {
|
|
52
|
+
// Select the first non-disabled segment by default
|
|
53
|
+
const firstSelectable = segments.find(s => !s.isDisabled());
|
|
54
|
+
if (firstSelectable) {
|
|
55
|
+
firstSelectable.setSelected(true);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Handles click events on segments
|
|
61
|
+
* @param {Event} event - DOM click event
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
const handleSegmentClick = (event: Event) => {
|
|
65
|
+
const segmentElement = event.currentTarget as HTMLElement;
|
|
66
|
+
const clickedSegment = segments.find(s => s.element === segmentElement);
|
|
67
|
+
|
|
68
|
+
if (!clickedSegment || clickedSegment.isDisabled()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const oldValue = getSelectedValues();
|
|
73
|
+
|
|
74
|
+
// Handle selection based on mode
|
|
75
|
+
if (mode === SelectionMode.SINGLE) {
|
|
76
|
+
// In single-select, deselect all other segments
|
|
77
|
+
segments.forEach(segment => {
|
|
78
|
+
segment.setSelected(segment === clickedSegment);
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
// In multi-select, toggle the clicked segment
|
|
82
|
+
clickedSegment.setSelected(!clickedSegment.isSelected());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Emit change event
|
|
86
|
+
const newValue = getSelectedValues();
|
|
87
|
+
|
|
88
|
+
// Only emit if values actually changed
|
|
89
|
+
if (
|
|
90
|
+
oldValue.length !== newValue.length ||
|
|
91
|
+
oldValue.some(v => !newValue.includes(v)) ||
|
|
92
|
+
newValue.some(v => !oldValue.includes(v))
|
|
93
|
+
) {
|
|
94
|
+
emitter.emit(EVENTS.CHANGE, {
|
|
95
|
+
selected: getSelected(),
|
|
96
|
+
value: newValue,
|
|
97
|
+
oldValue
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Attach click handlers to segments
|
|
103
|
+
segments.forEach(segment => {
|
|
104
|
+
segment.element.addEventListener('click', handleSegmentClick);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets an array of selected segments
|
|
109
|
+
* @returns {Segment[]} Array of selected segments
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
const getSelected = () => segments.filter(segment => segment.isSelected());
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Gets an array of selected segment values
|
|
116
|
+
* @returns {string[]} Array of selected segment values
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
const getSelectedValues = () => getSelected().map(segment => segment.value);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Finds a segment by its value
|
|
123
|
+
* @param {string} value - Segment value to find
|
|
124
|
+
* @returns {Segment|undefined} The found segment or undefined
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
const findSegmentByValue = (value: string) => segments.find(segment => segment.value === value);
|
|
128
|
+
|
|
129
|
+
// Create the component API
|
|
130
|
+
const segmentedButton: SegmentedButtonComponent = {
|
|
131
|
+
element: component.element,
|
|
132
|
+
segments,
|
|
133
|
+
|
|
134
|
+
getSelected,
|
|
135
|
+
|
|
136
|
+
getValue() {
|
|
137
|
+
return getSelectedValues();
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
select(value) {
|
|
141
|
+
const segment = findSegmentByValue(value);
|
|
142
|
+
if (segment && !segment.isDisabled()) {
|
|
143
|
+
const oldValue = getSelectedValues();
|
|
144
|
+
|
|
145
|
+
if (mode === SelectionMode.SINGLE) {
|
|
146
|
+
// Deselect all other segments
|
|
147
|
+
segments.forEach(s => s.setSelected(s === segment));
|
|
148
|
+
} else {
|
|
149
|
+
// Just select this segment
|
|
150
|
+
segment.setSelected(true);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Emit change event
|
|
154
|
+
const newValue = getSelectedValues();
|
|
155
|
+
if (oldValue.join(',') !== newValue.join(',')) {
|
|
156
|
+
emitter.emit(EVENTS.CHANGE, {
|
|
157
|
+
selected: getSelected(),
|
|
158
|
+
value: newValue,
|
|
159
|
+
oldValue
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return this;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
deselect(value) {
|
|
167
|
+
const segment = findSegmentByValue(value);
|
|
168
|
+
if (segment && !segment.isDisabled()) {
|
|
169
|
+
// In single select mode, only deselect if there's another selected segment
|
|
170
|
+
if (mode === SelectionMode.SINGLE) {
|
|
171
|
+
const selectedSegments = getSelected();
|
|
172
|
+
// Only allow deselection if there's more than one selected or we're selecting a different segment
|
|
173
|
+
if (selectedSegments.length > 1 || !segment.isSelected()) {
|
|
174
|
+
const oldValue = getSelectedValues();
|
|
175
|
+
segment.setSelected(false);
|
|
176
|
+
|
|
177
|
+
// Emit change event
|
|
178
|
+
const newValue = getSelectedValues();
|
|
179
|
+
if (oldValue.join(',') !== newValue.join(',')) {
|
|
180
|
+
emitter.emit(EVENTS.CHANGE, {
|
|
181
|
+
selected: getSelected(),
|
|
182
|
+
value: newValue,
|
|
183
|
+
oldValue
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// In multi-select, always allow deselection
|
|
189
|
+
const oldValue = getSelectedValues();
|
|
190
|
+
segment.setSelected(false);
|
|
191
|
+
|
|
192
|
+
// Emit change event
|
|
193
|
+
const newValue = getSelectedValues();
|
|
194
|
+
if (oldValue.join(',') !== newValue.join(',')) {
|
|
195
|
+
emitter.emit(EVENTS.CHANGE, {
|
|
196
|
+
selected: getSelected(),
|
|
197
|
+
value: newValue,
|
|
198
|
+
oldValue
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return this;
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
enable() {
|
|
207
|
+
// Enable the entire component
|
|
208
|
+
component.element.classList.remove(`${baseConfig.prefix}-segmented-button--disabled`);
|
|
209
|
+
return this;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
disable() {
|
|
213
|
+
// Disable the entire component
|
|
214
|
+
component.element.classList.add(`${baseConfig.prefix}-segmented-button--disabled`);
|
|
215
|
+
return this;
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
on(event, handler) {
|
|
219
|
+
emitter.on(event, handler);
|
|
220
|
+
return this;
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
off(event, handler) {
|
|
224
|
+
emitter.off(event, handler);
|
|
225
|
+
return this;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
destroy() {
|
|
229
|
+
// Remove event listeners
|
|
230
|
+
segments.forEach(segment => {
|
|
231
|
+
segment.element.removeEventListener('click', handleSegmentClick);
|
|
232
|
+
segment.destroy();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Clear emitter
|
|
236
|
+
emitter.clear();
|
|
237
|
+
|
|
238
|
+
// Destroy base component
|
|
239
|
+
component.lifecycle.destroy();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return segmentedButton;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('Segmented Button creation error:', error);
|
|
246
|
+
throw new Error(`Failed to create segmented button: ${(error as Error).message}`);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export default createSegmentedButton;
|