@lumen-design/cascader 0.0.2

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.
@@ -0,0 +1,191 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import { createDropdownTransition } from '../../utils'
4
+ import Icon from '@lumen-design/icon'
5
+ import { ChevronRight, ChevronDown, X, Check } from 'lucide'
6
+ import type { CascaderOption, CascaderProps as Props } from './types'
7
+
8
+ let {
9
+ modelValue = $bindable([]),
10
+ options = [],
11
+ placeholder = '请选择',
12
+ disabled = false,
13
+ clearable = false,
14
+ separator = ' / ',
15
+ expandTrigger = 'click',
16
+ size = 'default',
17
+ showMenuArrow = true,
18
+ class: cls = '',
19
+ ...attrs
20
+ }: Props = $props()
21
+
22
+ let visible = $state(false)
23
+ let reduceMotion = $state(false)
24
+ let inputRef: HTMLElement | null = $state(null)
25
+ let menus = $state<CascaderOption[][]>([])
26
+ let activeOptions = $state<CascaderOption[]>([])
27
+
28
+ $effect(() => {
29
+ menus = [options]
30
+ })
31
+
32
+ const getLabelsFromValue = (values: (string | number)[], opts: CascaderOption[]): string[] => {
33
+ const labels: string[] = []
34
+ let current = opts
35
+ for (const val of values) {
36
+ const opt = current.find((o) => o.value === val)
37
+ if (opt) {
38
+ labels.push(opt.label)
39
+ current = opt.children || []
40
+ }
41
+ }
42
+ return labels
43
+ }
44
+
45
+ const displayValue = $derived(activeOptions.length > 0 ? activeOptions.map((o) => o.label).join(separator) : modelValue.length > 0 ? getLabelsFromValue(modelValue, options).join(separator) : '')
46
+
47
+ const initMenus = (): void => {
48
+ menus = [options]
49
+ activeOptions = []
50
+ if (modelValue.length > 0) {
51
+ let current = options
52
+ for (const val of modelValue) {
53
+ const opt = current.find((o) => o.value === val)
54
+ if (opt) {
55
+ activeOptions = [...activeOptions, opt]
56
+ if (opt.children?.length) {
57
+ menus = [...menus, opt.children]
58
+ current = opt.children
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ const handleToggle = (): void => {
66
+ if (disabled) return
67
+ visible = !visible
68
+ if (visible) initMenus()
69
+ }
70
+
71
+ const handleOptionClick = (option: CascaderOption, level: number): void => {
72
+ if (option.disabled) return
73
+ activeOptions = [...activeOptions.slice(0, level), option]
74
+ menus = [...menus.slice(0, level + 1)]
75
+ if (option.children?.length) {
76
+ menus = [...menus, option.children]
77
+ } else {
78
+ modelValue = activeOptions.map((o) => o.value)
79
+ visible = false
80
+ }
81
+ }
82
+
83
+ const handleOptionHover = (option: CascaderOption, level: number): void => {
84
+ if (expandTrigger !== 'hover' || option.disabled) return
85
+ activeOptions = [...activeOptions.slice(0, level), option]
86
+ menus = [...menus.slice(0, level + 1)]
87
+ if (option.children?.length) menus = [...menus, option.children]
88
+ }
89
+
90
+ const handleClear = (e: MouseEvent): void => {
91
+ e.stopPropagation()
92
+ modelValue = []
93
+ activeOptions = []
94
+ menus = [options]
95
+ }
96
+
97
+ const handleClickOutside = (e: MouseEvent): void => {
98
+ if (inputRef && !inputRef.contains(e.target as Node)) visible = false
99
+ }
100
+
101
+ const handleKeydown = (e: KeyboardEvent): void => {
102
+ if (e.key === 'Escape') visible = false
103
+ }
104
+
105
+ const dropdownTransition = $derived(createDropdownTransition(reduceMotion))
106
+
107
+ $effect(() => {
108
+ if (visible) document.addEventListener('click', handleClickOutside)
109
+ return () => document.removeEventListener('click', handleClickOutside)
110
+ })
111
+
112
+ $effect(() => {
113
+ if (typeof window !== 'undefined') {
114
+ reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
115
+ }
116
+ })
117
+
118
+ onMount(() => {
119
+ if (typeof window !== 'undefined') {
120
+ reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
121
+ }
122
+ })
123
+
124
+ const isActive = (option: CascaderOption, level: number): boolean => activeOptions[level]?.value === option.value
125
+
126
+ const classes = $derived(['lm-cascader', disabled && 'is-disabled', size !== 'default' && `lm-cascader--${size}`, cls].filter(Boolean).join(' '))
127
+ </script>
128
+
129
+ <div bind:this={inputRef} class={classes} {...attrs}>
130
+ <div
131
+ class="lm-cascader__input"
132
+ class:is-focus={visible}
133
+ role="combobox"
134
+ tabindex={disabled ? -1 : 0}
135
+ aria-expanded={visible}
136
+ aria-controls="cascader-dropdown"
137
+ aria-haspopup="listbox"
138
+ onclick={handleToggle}
139
+ onkeydown={handleKeydown}
140
+ >
141
+ <span class="lm-cascader__value" class:is-placeholder={!displayValue}>
142
+ {displayValue || placeholder}
143
+ </span>
144
+ {#if clearable && modelValue.length > 0 && !disabled}
145
+ <button type="button" class="lm-cascader__clear" onclick={handleClear} aria-label="清除">
146
+ <Icon icon={X} size={14} />
147
+ </button>
148
+ {:else}
149
+ <span class="lm-cascader__arrow" class:is-open={visible}>
150
+ <Icon icon={ChevronDown} size={14} />
151
+ </span>
152
+ {/if}
153
+ </div>
154
+
155
+ {#if visible}
156
+ <div
157
+ class="lm-cascader__dropdown"
158
+ role="listbox"
159
+ id="cascader-dropdown"
160
+ transition:dropdownTransition
161
+ >
162
+ {#if showMenuArrow}
163
+ <div class="lm-cascader__menu-arrow" aria-hidden="true"></div>
164
+ {/if}
165
+ {#each menus as menu, level}
166
+ <ul class="lm-cascader__menu" role="group">
167
+ {#each menu as option (option.value)}
168
+ <li
169
+ class="lm-cascader__option"
170
+ class:is-active={isActive(option, level)}
171
+ class:is-disabled={option.disabled}
172
+ role="option"
173
+ aria-selected={isActive(option, level)}
174
+ onmousedown={() => handleOptionClick(option, level)}
175
+ onmouseenter={() => handleOptionHover(option, level)}
176
+ >
177
+ <span class="lm-cascader__option-label">{option.label}</span>
178
+ <span class="lm-cascader__option-suffix" aria-hidden="true">
179
+ {#if option.children?.length}
180
+ <Icon icon={ChevronRight} size={14} />
181
+ {:else if isActive(option, level) && level === menus.length - 1}
182
+ <Icon icon={Check} size={14} />
183
+ {/if}
184
+ </span>
185
+ </li>
186
+ {/each}
187
+ </ul>
188
+ {/each}
189
+ </div>
190
+ {/if}
191
+ </div>
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import Cascader from './Cascader.svelte';
2
+ export default Cascader;
3
+ export { Cascader };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lumen-design/cascader",
3
+ "version": "0.0.2",
4
+ "description": "Cascader component for Lumen UI",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "svelte": "./dist/index.js",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "svelte-package -i src -o dist --types",
21
+ "build:watch": "svelte-package -i src -o dist --types -w"
22
+ },
23
+ "dependencies": {
24
+ "@lumen-design/icon": "0.0.2",
25
+ "lucide": "^0.563.0",
26
+ "@lumen-design/utils": "0.0.2"
27
+ },
28
+ "devDependencies": {
29
+ "@sveltejs/package": "^2.5.7",
30
+ "svelte": "5.48.2"
31
+ },
32
+ "peerDependencies": {
33
+ "svelte": "^5.0.0"
34
+ }
35
+ }