mtrl 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/index.js +10 -0
- package/package.json +17 -0
- package/src/components/button/api.js +54 -0
- package/src/components/button/button.js +81 -0
- package/src/components/button/config.js +8 -0
- package/src/components/button/constants.js +63 -0
- package/src/components/button/index.js +2 -0
- package/src/components/button/styles.scss +231 -0
- package/src/components/checkbox/api.js +45 -0
- package/src/components/checkbox/checkbox.js +95 -0
- package/src/components/checkbox/constants.js +88 -0
- package/src/components/checkbox/index.js +2 -0
- package/src/components/checkbox/styles.scss +183 -0
- package/src/components/container/api.js +42 -0
- package/src/components/container/container.js +45 -0
- package/src/components/container/index.js +2 -0
- package/src/components/container/styles.scss +59 -0
- package/src/components/list/constants.js +89 -0
- package/src/components/list/index.js +2 -0
- package/src/components/list/list-item.js +147 -0
- package/src/components/list/list.js +267 -0
- package/src/components/list/styles/_list-item.scss +142 -0
- package/src/components/list/styles/_list.scss +89 -0
- package/src/components/list/styles/_variables.scss +13 -0
- package/src/components/list/styles.scss +19 -0
- package/src/components/navigation/api.js +43 -0
- package/src/components/navigation/constants.js +235 -0
- package/src/components/navigation/features/items.js +192 -0
- package/src/components/navigation/index.js +2 -0
- package/src/components/navigation/nav-item.js +137 -0
- package/src/components/navigation/navigation.js +55 -0
- package/src/components/navigation/styles/_bar.scss +51 -0
- package/src/components/navigation/styles/_base.scss +129 -0
- package/src/components/navigation/styles/_drawer.scss +169 -0
- package/src/components/navigation/styles/_rail.scss +65 -0
- package/src/components/navigation/styles.scss +6 -0
- package/src/components/snackbar/api.js +125 -0
- package/src/components/snackbar/constants.js +41 -0
- package/src/components/snackbar/features.js +69 -0
- package/src/components/snackbar/index.js +2 -0
- package/src/components/snackbar/position.js +63 -0
- package/src/components/snackbar/queue.js +74 -0
- package/src/components/snackbar/snackbar.js +70 -0
- package/src/components/snackbar/styles.scss +182 -0
- package/src/components/switch/api.js +44 -0
- package/src/components/switch/constants.js +80 -0
- package/src/components/switch/index.js +2 -0
- package/src/components/switch/styles.scss +172 -0
- package/src/components/switch/switch.js +71 -0
- package/src/components/textfield/api.js +49 -0
- package/src/components/textfield/constants.js +81 -0
- package/src/components/textfield/index.js +2 -0
- package/src/components/textfield/styles/base.scss +107 -0
- package/src/components/textfield/styles/filled.scss +58 -0
- package/src/components/textfield/styles/outlined.scss +66 -0
- package/src/components/textfield/styles.scss +6 -0
- package/src/components/textfield/textfield.js +68 -0
- package/src/core/build/constants.js +51 -0
- package/src/core/build/icon.js +78 -0
- package/src/core/build/ripple.js +92 -0
- package/src/core/build/text.js +54 -0
- package/src/core/collection/adapters/base.js +26 -0
- package/src/core/collection/adapters/mongodb.js +232 -0
- package/src/core/collection/adapters/route.js +201 -0
- package/src/core/collection/collection.js +259 -0
- package/src/core/collection/list-manager.js +157 -0
- package/src/core/compose/base.js +8 -0
- package/src/core/compose/component.js +225 -0
- package/src/core/compose/features/checkable.js +114 -0
- package/src/core/compose/features/disabled.js +25 -0
- package/src/core/compose/features/events.js +48 -0
- package/src/core/compose/features/icon.js +33 -0
- package/src/core/compose/features/index.js +20 -0
- package/src/core/compose/features/input.js +92 -0
- package/src/core/compose/features/lifecycle.js +69 -0
- package/src/core/compose/features/position.js +60 -0
- package/src/core/compose/features/ripple.js +32 -0
- package/src/core/compose/features/size.js +9 -0
- package/src/core/compose/features/style.js +12 -0
- package/src/core/compose/features/text.js +17 -0
- package/src/core/compose/features/textinput.js +118 -0
- package/src/core/compose/features/textlabel.js +28 -0
- package/src/core/compose/features/track.js +49 -0
- package/src/core/compose/features/variant.js +9 -0
- package/src/core/compose/features/withEvents.js +67 -0
- package/src/core/compose/index.js +16 -0
- package/src/core/compose/pipe.js +69 -0
- package/src/core/config.js +140 -0
- package/src/core/dom/attributes.js +33 -0
- package/src/core/dom/classes.js +70 -0
- package/src/core/dom/create.js +133 -0
- package/src/core/dom/events.js +175 -0
- package/src/core/dom/index.js +5 -0
- package/src/core/dom/utils.js +22 -0
- package/src/core/index.js +23 -0
- package/src/core/layout/index.js +93 -0
- package/src/core/state/disabled.js +14 -0
- package/src/core/state/emitter.js +63 -0
- package/src/core/state/events.js +29 -0
- package/src/core/state/index.js +6 -0
- package/src/core/state/lifecycle.js +64 -0
- package/src/core/state/store.js +112 -0
- package/src/core/utils/index.js +39 -0
- package/src/core/utils/mobile.js +74 -0
- package/src/core/utils/object.js +22 -0
- package/src/core/utils/validate.js +37 -0
- package/src/index.js +11 -0
- package/src/styles/abstract/_base.scss +2 -0
- package/src/styles/abstract/_config.scss +28 -0
- package/src/styles/abstract/_functions.scss +124 -0
- package/src/styles/abstract/_mixins.scss +261 -0
- package/src/styles/abstract/_variables.scss +158 -0
- package/src/styles/main.scss +78 -0
- package/src/styles/themes/_base-theme.scss +49 -0
- package/src/styles/themes/_baseline.scss +90 -0
- package/src/styles/themes/_forest.scss +71 -0
- package/src/styles/themes/_index.scss +6 -0
- package/src/styles/themes/_ocean.scss +71 -0
- package/src/styles/themes/_sunset.scss +55 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/components/container/container.js
|
|
2
|
+
import { PREFIX } from '../../core/config'
|
|
3
|
+
import { pipe } from '../../core/compose'
|
|
4
|
+
import { createBase, withElement } from '../../core/compose/component'
|
|
5
|
+
import { withEvents, withLifecycle } from '../../core/compose/features'
|
|
6
|
+
import { withAPI } from './api'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new Container component
|
|
10
|
+
* @param {Object} config - Container configuration
|
|
11
|
+
* @param {string} [config.variant] - Visual variant
|
|
12
|
+
* @param {number} [config.elevation] - Elevation level
|
|
13
|
+
* @param {string} [config.class] - Additional CSS classes
|
|
14
|
+
*/
|
|
15
|
+
const createContainer = (config = {}) => {
|
|
16
|
+
const baseConfig = {
|
|
17
|
+
...config,
|
|
18
|
+
componentName: 'container',
|
|
19
|
+
prefix: PREFIX
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return pipe(
|
|
24
|
+
createBase,
|
|
25
|
+
withElement({
|
|
26
|
+
tag: 'div',
|
|
27
|
+
componentName: 'container',
|
|
28
|
+
className: [
|
|
29
|
+
config.variant && `${PREFIX}-container--${config.variant}`,
|
|
30
|
+
config.elevation && `${PREFIX}-container--elevation-${config.elevation}`,
|
|
31
|
+
config.class
|
|
32
|
+
]
|
|
33
|
+
}),
|
|
34
|
+
withEvents(),
|
|
35
|
+
withLifecycle(),
|
|
36
|
+
comp => withAPI({
|
|
37
|
+
lifecycle: comp.lifecycle
|
|
38
|
+
})(comp)
|
|
39
|
+
)(baseConfig)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Failed to create container: ${error.message}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default createContainer
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/components/container/styles.scss
|
|
2
|
+
@use 'sass:map';
|
|
3
|
+
@use '../../styles/abstract/config' as c;
|
|
4
|
+
|
|
5
|
+
.#{c.$prefix}-container {
|
|
6
|
+
@include c.shape('medium');
|
|
7
|
+
@include c.motion-transition(
|
|
8
|
+
background-color,
|
|
9
|
+
box-shadow
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
padding: 16px;
|
|
13
|
+
background-color: var(--mtrl-sys-color-surface-container);
|
|
14
|
+
|
|
15
|
+
&--low {
|
|
16
|
+
background-color: var(--mtrl-sys-color-surface-container-low);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&--lowest {
|
|
20
|
+
background-color: var(--mtrl-sys-color-surface-container-lowest);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&--high {
|
|
24
|
+
background-color: var(--mtrl-sys-color-surface-container-high);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&--highest {
|
|
28
|
+
background-color: var(--mtrl-sys-color-surface-container-highest);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Elevation variants
|
|
32
|
+
&--elevation-0 {
|
|
33
|
+
@include c.elevation(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&--elevation-1 {
|
|
37
|
+
@include c.elevation(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&--elevation-2 {
|
|
41
|
+
@include c.elevation(2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&--elevation-3 {
|
|
45
|
+
@include c.elevation(3);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&--elevation-4 {
|
|
49
|
+
@include c.elevation(4);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@include c.reduced-motion {
|
|
53
|
+
transition: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@include c.high-contrast {
|
|
57
|
+
border: 1px solid currentColor;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/components/list/constants.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List types/variants
|
|
5
|
+
*/
|
|
6
|
+
export const LIST_TYPES = {
|
|
7
|
+
DEFAULT: 'default', // Standard list
|
|
8
|
+
SINGLE_SELECT: 'single', // Single selection list
|
|
9
|
+
MULTI_SELECT: 'multi', // Multiple selection list
|
|
10
|
+
RADIO: 'radio' // Radio button list
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* List layout variants
|
|
15
|
+
*/
|
|
16
|
+
export const LIST_LAYOUTS = {
|
|
17
|
+
HORIZONTAL: 'horizontal', // Default horizontal layout
|
|
18
|
+
VERTICAL: 'vertical' // Items with more content stacked vertically
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* List element class names
|
|
23
|
+
*/
|
|
24
|
+
export const LIST_CLASSES = {
|
|
25
|
+
ROOT: 'list',
|
|
26
|
+
GROUP: 'list-group',
|
|
27
|
+
GROUP_TITLE: 'list-group-title',
|
|
28
|
+
DIVIDER: 'list-divider',
|
|
29
|
+
SECTION: 'list-section',
|
|
30
|
+
SECTION_TITLE: 'list-section-title'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* List configuration schema
|
|
35
|
+
*/
|
|
36
|
+
export const LIST_SCHEMA = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
type: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: Object.values(LIST_TYPES),
|
|
42
|
+
default: LIST_TYPES.DEFAULT
|
|
43
|
+
},
|
|
44
|
+
layout: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: Object.values(LIST_LAYOUTS),
|
|
47
|
+
default: LIST_LAYOUTS.HORIZONTAL
|
|
48
|
+
},
|
|
49
|
+
items: {
|
|
50
|
+
type: 'array',
|
|
51
|
+
items: {
|
|
52
|
+
type: 'object'
|
|
53
|
+
},
|
|
54
|
+
default: []
|
|
55
|
+
},
|
|
56
|
+
groups: {
|
|
57
|
+
type: 'array',
|
|
58
|
+
items: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
id: { type: 'string', required: true },
|
|
62
|
+
title: { type: 'string' },
|
|
63
|
+
items: { type: 'array' }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
optional: true
|
|
67
|
+
},
|
|
68
|
+
sections: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
items: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
id: { type: 'string', required: true },
|
|
74
|
+
title: { type: 'string', required: true },
|
|
75
|
+
items: { type: 'array', required: true }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
optional: true
|
|
79
|
+
},
|
|
80
|
+
disabled: {
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
default: false
|
|
83
|
+
},
|
|
84
|
+
class: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
optional: true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/components/list/list-item.js
|
|
2
|
+
|
|
3
|
+
import { PREFIX } from '../../core/config'
|
|
4
|
+
import { pipe } from '../../core/compose'
|
|
5
|
+
import { createBase, withElement } from '../../core/compose/component'
|
|
6
|
+
import { withEvents, withDisabled } from '../../core/compose/features'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Supported list item layouts
|
|
10
|
+
*/
|
|
11
|
+
export const LIST_ITEM_LAYOUTS = {
|
|
12
|
+
HORIZONTAL: 'horizontal', // Default horizontal layout
|
|
13
|
+
VERTICAL: 'vertical' // Stacked layout with vertical alignment
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a DOM element with optional class and content
|
|
18
|
+
* @param {string} tag - HTML tag name
|
|
19
|
+
* @param {string} className - CSS class name
|
|
20
|
+
* @param {string|HTMLElement} [content] - Element content or child element
|
|
21
|
+
* @returns {HTMLElement} Created element
|
|
22
|
+
*/
|
|
23
|
+
const createElement = (tag, className, content) => {
|
|
24
|
+
const element = document.createElement(tag)
|
|
25
|
+
element.className = className
|
|
26
|
+
if (content) {
|
|
27
|
+
if (typeof content === 'string') {
|
|
28
|
+
element.textContent = content
|
|
29
|
+
} else {
|
|
30
|
+
element.appendChild(content)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return element
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a list item component
|
|
38
|
+
* @param {Object} config - List item configuration
|
|
39
|
+
* @param {string} [config.layout='horizontal'] - Item layout (horizontal/vertical)
|
|
40
|
+
* @param {string|HTMLElement} [config.leading] - Leading content (icon/avatar)
|
|
41
|
+
* @param {string} [config.headline] - Primary text
|
|
42
|
+
* @param {string} [config.supportingText] - Secondary text
|
|
43
|
+
* @param {string|HTMLElement} [config.trailing] - Trailing content (icon/meta)
|
|
44
|
+
* @param {string} [config.overline] - Text above headline (vertical only)
|
|
45
|
+
* @param {string|HTMLElement} [config.meta] - Meta information (vertical only)
|
|
46
|
+
* @param {boolean} [config.disabled] - Disabled state
|
|
47
|
+
* @param {boolean} [config.selected] - Selected state
|
|
48
|
+
* @param {string} [config.class] - Additional CSS classes
|
|
49
|
+
* @param {string} [config.role='listitem'] - ARIA role
|
|
50
|
+
*/
|
|
51
|
+
const createListItem = (config = {}) => {
|
|
52
|
+
const baseConfig = {
|
|
53
|
+
...config,
|
|
54
|
+
componentName: 'list-item',
|
|
55
|
+
prefix: PREFIX
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const createContent = (component) => {
|
|
59
|
+
const { element } = component
|
|
60
|
+
const { prefix } = baseConfig
|
|
61
|
+
const isVertical = config.layout === LIST_ITEM_LAYOUTS.VERTICAL
|
|
62
|
+
|
|
63
|
+
// Create content container
|
|
64
|
+
const content = createElement('div', `${prefix}-list-item-content`)
|
|
65
|
+
|
|
66
|
+
// Add leading content (icon/avatar)
|
|
67
|
+
if (config.leading) {
|
|
68
|
+
const leading = createElement('div', `${prefix}-list-item-leading`)
|
|
69
|
+
if (typeof config.leading === 'string') {
|
|
70
|
+
leading.innerHTML = config.leading
|
|
71
|
+
} else {
|
|
72
|
+
leading.appendChild(config.leading)
|
|
73
|
+
}
|
|
74
|
+
element.appendChild(leading)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Text wrapper for proper alignment
|
|
78
|
+
const textWrapper = createElement('div', `${prefix}-list-item-text`)
|
|
79
|
+
|
|
80
|
+
// Add overline text (vertical only)
|
|
81
|
+
if (isVertical && config.overline) {
|
|
82
|
+
const overline = createElement('div', `${prefix}-list-item-overline`, config.overline)
|
|
83
|
+
textWrapper.appendChild(overline)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Add headline (primary text)
|
|
87
|
+
if (config.headline) {
|
|
88
|
+
const headline = createElement('div', `${prefix}-list-item-headline`, config.headline)
|
|
89
|
+
textWrapper.appendChild(headline)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add supporting text (secondary text)
|
|
93
|
+
if (config.supportingText) {
|
|
94
|
+
const supporting = createElement('div', `${prefix}-list-item-supporting`, config.supportingText)
|
|
95
|
+
textWrapper.appendChild(supporting)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
content.appendChild(textWrapper)
|
|
99
|
+
|
|
100
|
+
// Add meta information (vertical only)
|
|
101
|
+
if (isVertical && config.meta) {
|
|
102
|
+
const meta = createElement('div', `${prefix}-list-item-meta`)
|
|
103
|
+
if (typeof config.meta === 'string') {
|
|
104
|
+
meta.textContent = config.meta
|
|
105
|
+
} else {
|
|
106
|
+
meta.appendChild(config.meta)
|
|
107
|
+
}
|
|
108
|
+
content.appendChild(meta)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
element.appendChild(content)
|
|
112
|
+
|
|
113
|
+
// Add trailing content (icon/meta)
|
|
114
|
+
if (config.trailing) {
|
|
115
|
+
const trailing = createElement('div', `${prefix}-list-item-trailing`)
|
|
116
|
+
if (typeof config.trailing === 'string') {
|
|
117
|
+
trailing.innerHTML = config.trailing
|
|
118
|
+
} else {
|
|
119
|
+
trailing.appendChild(config.trailing)
|
|
120
|
+
}
|
|
121
|
+
element.appendChild(trailing)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle selected state
|
|
125
|
+
if (config.selected) {
|
|
126
|
+
element.setAttribute('aria-selected', 'true')
|
|
127
|
+
element.classList.add(`${prefix}-list-item--selected`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return component
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return pipe(
|
|
134
|
+
createBase,
|
|
135
|
+
withEvents(),
|
|
136
|
+
withElement({
|
|
137
|
+
tag: 'div',
|
|
138
|
+
role: config.role || 'listitem',
|
|
139
|
+
componentName: 'list-item',
|
|
140
|
+
className: `${config.layout === LIST_ITEM_LAYOUTS.VERTICAL ? 'vertical' : ''} ${config.class || ''}`
|
|
141
|
+
}),
|
|
142
|
+
withDisabled(),
|
|
143
|
+
createContent
|
|
144
|
+
)(baseConfig)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default createListItem
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/components/list/list.js
|
|
2
|
+
|
|
3
|
+
import { PREFIX } from '../../core/config'
|
|
4
|
+
import { pipe } from '../../core/compose'
|
|
5
|
+
import { createBase, withElement } from '../../core/compose/component'
|
|
6
|
+
import { withEvents, withDisabled, withLifecycle } from '../../core/compose/features'
|
|
7
|
+
import createListItem from './list-item'
|
|
8
|
+
import { LIST_TYPES, LIST_LAYOUTS, LIST_CLASSES } from './constants'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a divider element
|
|
12
|
+
* @param {string} prefix - CSS class prefix
|
|
13
|
+
* @returns {HTMLElement} Divider element
|
|
14
|
+
*/
|
|
15
|
+
const createDivider = (prefix) => {
|
|
16
|
+
const divider = document.createElement('div')
|
|
17
|
+
divider.className = `${prefix}-${LIST_CLASSES.DIVIDER}`
|
|
18
|
+
divider.setAttribute('role', 'separator')
|
|
19
|
+
return divider
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a section title element
|
|
24
|
+
* @param {string} title - Section title text
|
|
25
|
+
* @param {string} prefix - CSS class prefix
|
|
26
|
+
* @returns {HTMLElement} Section title element
|
|
27
|
+
*/
|
|
28
|
+
const createSectionTitle = (title, prefix) => {
|
|
29
|
+
const titleEl = document.createElement('div')
|
|
30
|
+
titleEl.className = `${prefix}-${LIST_CLASSES.SECTION_TITLE}`
|
|
31
|
+
titleEl.textContent = title
|
|
32
|
+
return titleEl
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a list component
|
|
37
|
+
* @param {Object} config - List configuration
|
|
38
|
+
*/
|
|
39
|
+
const createList = (config = {}) => {
|
|
40
|
+
const baseConfig = {
|
|
41
|
+
...config,
|
|
42
|
+
componentName: 'list',
|
|
43
|
+
prefix: PREFIX
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const createContent = (component) => {
|
|
47
|
+
const { element, prefix } = component
|
|
48
|
+
const items = new Map()
|
|
49
|
+
const selectedItems = new Set()
|
|
50
|
+
|
|
51
|
+
// Set list type
|
|
52
|
+
element.setAttribute('data-type', config.type || LIST_TYPES.DEFAULT)
|
|
53
|
+
|
|
54
|
+
// Handle keyboard navigation
|
|
55
|
+
const handleKeyDown = (event) => {
|
|
56
|
+
const focusedItem = document.activeElement
|
|
57
|
+
if (!focusedItem?.classList.contains(`${prefix}-list-item`)) return
|
|
58
|
+
|
|
59
|
+
const items = Array.from(element.querySelectorAll(`.${prefix}-list-item`))
|
|
60
|
+
const currentIndex = items.indexOf(focusedItem)
|
|
61
|
+
|
|
62
|
+
switch (event.key) {
|
|
63
|
+
case 'ArrowDown':
|
|
64
|
+
case 'ArrowRight':
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
const nextItem = items[currentIndex + 1]
|
|
67
|
+
if (nextItem) nextItem.focus()
|
|
68
|
+
break
|
|
69
|
+
case 'ArrowUp':
|
|
70
|
+
case 'ArrowLeft':
|
|
71
|
+
event.preventDefault()
|
|
72
|
+
const prevItem = items[currentIndex - 1]
|
|
73
|
+
if (prevItem) prevItem.focus()
|
|
74
|
+
break
|
|
75
|
+
case 'Home':
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
items[0]?.focus()
|
|
78
|
+
break
|
|
79
|
+
case 'End':
|
|
80
|
+
event.preventDefault()
|
|
81
|
+
items[items.length - 1]?.focus()
|
|
82
|
+
break
|
|
83
|
+
case ' ':
|
|
84
|
+
case 'Enter':
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
handleItemClick(focusedItem)
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Handle item selection
|
|
92
|
+
const handleItemClick = (itemElement) => {
|
|
93
|
+
const id = itemElement.dataset.id
|
|
94
|
+
if (!id) return
|
|
95
|
+
|
|
96
|
+
const itemData = items.get(id)
|
|
97
|
+
if (!itemData || itemData.disabled) return
|
|
98
|
+
|
|
99
|
+
switch (config.type) {
|
|
100
|
+
case LIST_TYPES.SINGLE_SELECT:
|
|
101
|
+
// Deselect previously selected item
|
|
102
|
+
selectedItems.forEach(selectedId => {
|
|
103
|
+
const selected = items.get(selectedId)
|
|
104
|
+
if (selected) {
|
|
105
|
+
selected.element.classList.remove(`${prefix}-list-item--selected`)
|
|
106
|
+
selected.element.setAttribute('aria-selected', 'false')
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
selectedItems.clear()
|
|
110
|
+
|
|
111
|
+
// Select new item
|
|
112
|
+
itemElement.classList.add(`${prefix}-list-item--selected`)
|
|
113
|
+
itemElement.setAttribute('aria-selected', 'true')
|
|
114
|
+
selectedItems.add(id)
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
case LIST_TYPES.MULTI_SELECT:
|
|
118
|
+
const isSelected = selectedItems.has(id)
|
|
119
|
+
if (isSelected) {
|
|
120
|
+
itemElement.classList.remove(`${prefix}-list-item--selected`)
|
|
121
|
+
itemElement.setAttribute('aria-selected', 'false')
|
|
122
|
+
selectedItems.delete(id)
|
|
123
|
+
} else {
|
|
124
|
+
itemElement.classList.add(`${prefix}-list-item--selected`)
|
|
125
|
+
itemElement.setAttribute('aria-selected', 'true')
|
|
126
|
+
selectedItems.add(id)
|
|
127
|
+
}
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
component.emit('selectionChange', {
|
|
132
|
+
selected: Array.from(selectedItems),
|
|
133
|
+
item: itemData,
|
|
134
|
+
type: config.type
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Create items from configuration
|
|
139
|
+
const createItems = (itemsConfig = [], container = element) => {
|
|
140
|
+
itemsConfig.forEach((itemConfig, index) => {
|
|
141
|
+
if (itemConfig.divider) {
|
|
142
|
+
container.appendChild(createDivider(prefix))
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const item = createListItem({
|
|
147
|
+
...itemConfig,
|
|
148
|
+
layout: config.layout || LIST_LAYOUTS.HORIZONTAL,
|
|
149
|
+
role: config.type === LIST_TYPES.RADIO ? 'radio' : 'option'
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
item.element.dataset.id = itemConfig.id
|
|
153
|
+
item.element.tabIndex = index === 0 ? 0 : -1
|
|
154
|
+
items.set(itemConfig.id, item)
|
|
155
|
+
|
|
156
|
+
if (itemConfig.selected) {
|
|
157
|
+
selectedItems.add(itemConfig.id)
|
|
158
|
+
item.element.classList.add(`${prefix}-list-item--selected`)
|
|
159
|
+
item.element.setAttribute('aria-selected', 'true')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
container.appendChild(item.element)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create sections if configured
|
|
167
|
+
if (config.sections?.length) {
|
|
168
|
+
config.sections.forEach(section => {
|
|
169
|
+
const sectionEl = document.createElement('div')
|
|
170
|
+
sectionEl.className = `${prefix}-${LIST_CLASSES.SECTION}`
|
|
171
|
+
sectionEl.setAttribute('role', 'group')
|
|
172
|
+
if (section.title) {
|
|
173
|
+
sectionEl.appendChild(createSectionTitle(section.title, prefix))
|
|
174
|
+
}
|
|
175
|
+
createItems(section.items, sectionEl)
|
|
176
|
+
element.appendChild(sectionEl)
|
|
177
|
+
})
|
|
178
|
+
} else {
|
|
179
|
+
createItems(config.items)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add event listeners
|
|
183
|
+
element.addEventListener('click', (event) => {
|
|
184
|
+
const item = event.target.closest(`.${prefix}-list-item`)
|
|
185
|
+
if (item) handleItemClick(item)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
element.addEventListener('keydown', handleKeyDown)
|
|
189
|
+
|
|
190
|
+
// Clean up
|
|
191
|
+
if (component.lifecycle) {
|
|
192
|
+
const originalDestroy = component.lifecycle.destroy
|
|
193
|
+
component.lifecycle.destroy = () => {
|
|
194
|
+
items.clear()
|
|
195
|
+
selectedItems.clear()
|
|
196
|
+
element.removeEventListener('keydown', handleKeyDown)
|
|
197
|
+
originalDestroy?.()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
...component,
|
|
203
|
+
items,
|
|
204
|
+
selectedItems,
|
|
205
|
+
|
|
206
|
+
// Public methods
|
|
207
|
+
getSelected: () => Array.from(selectedItems),
|
|
208
|
+
|
|
209
|
+
setSelected: (ids) => {
|
|
210
|
+
selectedItems.clear()
|
|
211
|
+
items.forEach((item, id) => {
|
|
212
|
+
const isSelected = ids.includes(id)
|
|
213
|
+
item.element.classList.toggle(`${prefix}-list-item--selected`, isSelected)
|
|
214
|
+
item.element.setAttribute('aria-selected', isSelected.toString())
|
|
215
|
+
if (isSelected) selectedItems.add(id)
|
|
216
|
+
})
|
|
217
|
+
component.emit('selectionChange', {
|
|
218
|
+
selected: Array.from(selectedItems),
|
|
219
|
+
type: config.type
|
|
220
|
+
})
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
addItem: (itemConfig) => {
|
|
224
|
+
if (items.has(itemConfig.id)) return
|
|
225
|
+
|
|
226
|
+
const item = createListItem({
|
|
227
|
+
...itemConfig,
|
|
228
|
+
layout: config.layout || LIST_LAYOUTS.HORIZONTAL
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
item.element.dataset.id = itemConfig.id
|
|
232
|
+
items.set(itemConfig.id, item)
|
|
233
|
+
element.appendChild(item.element)
|
|
234
|
+
|
|
235
|
+
component.emit('itemAdded', { id: itemConfig.id, item })
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
removeItem: (id) => {
|
|
239
|
+
const item = items.get(id)
|
|
240
|
+
if (!item) return
|
|
241
|
+
|
|
242
|
+
item.element.remove()
|
|
243
|
+
items.delete(id)
|
|
244
|
+
selectedItems.delete(id)
|
|
245
|
+
|
|
246
|
+
component.emit('itemRemoved', { id, item })
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return pipe(
|
|
252
|
+
createBase,
|
|
253
|
+
withEvents(),
|
|
254
|
+
withElement({
|
|
255
|
+
tag: 'div',
|
|
256
|
+
role: config.type === LIST_TYPES.DEFAULT ? 'list' : 'listbox',
|
|
257
|
+
'aria-multiselectable': config.type === LIST_TYPES.MULTI_SELECT ? 'true' : undefined,
|
|
258
|
+
componentName: LIST_CLASSES.ROOT,
|
|
259
|
+
className: config.class
|
|
260
|
+
}),
|
|
261
|
+
withDisabled(),
|
|
262
|
+
withLifecycle(),
|
|
263
|
+
createContent
|
|
264
|
+
)(baseConfig)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export default createList
|