@14ch/svelte-ui 0.0.23 → 0.0.25
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/dist/assets/styles/variables.scss +30 -0
- package/dist/components/Nav.svelte +218 -0
- package/dist/components/Nav.svelte.d.ts +23 -0
- package/dist/components/NavItem.svelte +289 -0
- package/dist/components/NavItem.svelte.d.ts +20 -0
- package/dist/components/PopupMenu.svelte +4 -1
- package/dist/components/Tab.svelte +4 -56
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/types/menuItem.d.ts +1 -0
- package/dist/types/propOptions.d.ts +5 -0
- package/dist/utils/navPath.d.ts +4 -0
- package/dist/utils/navPath.js +28 -0
- package/package.json +1 -1
|
@@ -373,6 +373,36 @@
|
|
|
373
373
|
--svelte-ui-tab-item-selected-bar-radius: 3px 3px 0 0;
|
|
374
374
|
--svelte-ui-tab-item-disabled-opacity: var(--svelte-ui-disabled-opacity);
|
|
375
375
|
|
|
376
|
+
/* Nav */
|
|
377
|
+
--svelte-ui-nav-item-text-color: var(--svelte-ui-text-color);
|
|
378
|
+
--svelte-ui-nav-item-selected-text-color: var(--svelte-ui-primary-color);
|
|
379
|
+
--svelte-ui-nav-item-selected-bg-color: var(--svelte-ui-hover-overlay);
|
|
380
|
+
--svelte-ui-nav-item-filled-text-color: #ffffff;
|
|
381
|
+
--svelte-ui-nav-item-tonal-bg-color: color-mix(
|
|
382
|
+
in srgb,
|
|
383
|
+
var(--svelte-ui-primary-color) 16%,
|
|
384
|
+
transparent
|
|
385
|
+
);
|
|
386
|
+
--svelte-ui-nav-item-selected-bar-color: var(--svelte-ui-primary-color);
|
|
387
|
+
--svelte-ui-nav-item-selected-bar-height: 3px;
|
|
388
|
+
--svelte-ui-nav-item-selected-bar-radius: 2px;
|
|
389
|
+
--svelte-ui-nav-item-min-height: 40px;
|
|
390
|
+
--svelte-ui-nav-item-icon-gap: 8px;
|
|
391
|
+
--svelte-ui-nav-item-disabled-opacity: var(--svelte-ui-disabled-opacity);
|
|
392
|
+
/* Nav: mobile-tab */
|
|
393
|
+
--svelte-ui-nav-mobile-min-height: 56px;
|
|
394
|
+
--svelte-ui-nav-mobile-item-padding: 8px;
|
|
395
|
+
--svelte-ui-nav-mobile-item-icon-gap: 8px;
|
|
396
|
+
--svelte-ui-nav-mobile-item-font-size: 0.75rem;
|
|
397
|
+
--svelte-ui-nav-mobile-item-bar-width: 40px;
|
|
398
|
+
--svelte-ui-nav-item-padding: 8px 12px;
|
|
399
|
+
--svelte-ui-nav-item-border-radius: var(--svelte-ui-border-radius);
|
|
400
|
+
/* Nav: vertical */
|
|
401
|
+
--svelte-ui-nav-vertical-padding: 8px;
|
|
402
|
+
--svelte-ui-nav-vertical-item-gap: 8px;
|
|
403
|
+
/* Nav: horizontal */
|
|
404
|
+
--svelte-ui-nav-horizontal-item-gap: 8px;
|
|
405
|
+
|
|
376
406
|
/* Input */
|
|
377
407
|
--svelte-ui-input-height: var(--svelte-ui-form-height);
|
|
378
408
|
--svelte-ui-input-padding: var(--svelte-ui-form-padding);
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<!-- Nav.svelte -->
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import NavItem from './NavItem.svelte';
|
|
5
|
+
import type { NavItemSelectedStyle } from './NavItem.svelte';
|
|
6
|
+
import type { MenuItem } from '../types/menuItem';
|
|
7
|
+
import type { NavVariant } from '../types/propOptions';
|
|
8
|
+
import { subscribeUrlChange } from '../utils/urlChange';
|
|
9
|
+
import { getCurrentPath, matchPath } from '../utils/navPath';
|
|
10
|
+
import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
|
|
11
|
+
|
|
12
|
+
// =========================================================================
|
|
13
|
+
// Props, States & Constants
|
|
14
|
+
// =========================================================================
|
|
15
|
+
export type NavProps = {
|
|
16
|
+
// 基本プロパティ
|
|
17
|
+
items?: MenuItem[];
|
|
18
|
+
variant?: NavVariant;
|
|
19
|
+
pathPrefix?: string;
|
|
20
|
+
customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
|
|
21
|
+
currentPath?: string;
|
|
22
|
+
|
|
23
|
+
// HTML属性
|
|
24
|
+
id?: string;
|
|
25
|
+
|
|
26
|
+
// アイコン関連
|
|
27
|
+
iconFilled?: boolean;
|
|
28
|
+
iconWeight?: IconWeight;
|
|
29
|
+
iconGrade?: IconGrade;
|
|
30
|
+
iconOpticalSize?: IconOpticalSize;
|
|
31
|
+
iconVariant?: IconVariant;
|
|
32
|
+
|
|
33
|
+
// スタイル/レイアウト
|
|
34
|
+
selectedStyle?: NavItemSelectedStyle;
|
|
35
|
+
|
|
36
|
+
// ARIA/アクセシビリティ
|
|
37
|
+
ariaLabel?: string;
|
|
38
|
+
ariaLabelledby?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
// 基本プロパティ
|
|
43
|
+
items = [],
|
|
44
|
+
variant = 'tab',
|
|
45
|
+
pathPrefix = '',
|
|
46
|
+
customPathMatcher,
|
|
47
|
+
currentPath,
|
|
48
|
+
|
|
49
|
+
// HTML属性
|
|
50
|
+
id,
|
|
51
|
+
|
|
52
|
+
// アイコン関連
|
|
53
|
+
iconFilled = false,
|
|
54
|
+
iconWeight = 300,
|
|
55
|
+
iconGrade = 0,
|
|
56
|
+
iconOpticalSize = 24,
|
|
57
|
+
iconVariant = 'outlined',
|
|
58
|
+
|
|
59
|
+
// スタイル/レイアウト
|
|
60
|
+
selectedStyle,
|
|
61
|
+
|
|
62
|
+
// ARIA/アクセシビリティ
|
|
63
|
+
ariaLabel,
|
|
64
|
+
ariaLabelledby
|
|
65
|
+
}: NavProps = $props();
|
|
66
|
+
|
|
67
|
+
let resolvedCurrentPath = $state('');
|
|
68
|
+
|
|
69
|
+
// =========================================================================
|
|
70
|
+
// Effects
|
|
71
|
+
// =========================================================================
|
|
72
|
+
$effect(() => {
|
|
73
|
+
resolvedCurrentPath = getCurrentPath(currentPath);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
$effect(() => {
|
|
77
|
+
return subscribeUrlChange(() => {
|
|
78
|
+
resolvedCurrentPath = getCurrentPath(currentPath);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// =========================================================================
|
|
83
|
+
// Methods
|
|
84
|
+
// =========================================================================
|
|
85
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
86
|
+
if (items.length === 0 || enabledIndices.length === 0) return;
|
|
87
|
+
|
|
88
|
+
const navEl = event.currentTarget as HTMLElement;
|
|
89
|
+
const navItems = Array.from(navEl.querySelectorAll('[data-nav-item]')) as HTMLElement[];
|
|
90
|
+
const currentItem = event.target as HTMLElement;
|
|
91
|
+
const currentIndex = navItems.indexOf(currentItem);
|
|
92
|
+
|
|
93
|
+
if (currentIndex === -1) return;
|
|
94
|
+
|
|
95
|
+
const currentEnabledPosition = enabledIndices.indexOf(currentIndex);
|
|
96
|
+
let nextEnabledPosition = currentEnabledPosition;
|
|
97
|
+
|
|
98
|
+
const isVertical = variant === 'vertical';
|
|
99
|
+
const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
|
|
100
|
+
const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
|
|
101
|
+
|
|
102
|
+
switch (event.key) {
|
|
103
|
+
case prevKey:
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
nextEnabledPosition =
|
|
106
|
+
currentEnabledPosition > 0 ? currentEnabledPosition - 1 : enabledIndices.length - 1;
|
|
107
|
+
break;
|
|
108
|
+
case nextKey:
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
nextEnabledPosition =
|
|
111
|
+
currentEnabledPosition < enabledIndices.length - 1 ? currentEnabledPosition + 1 : 0;
|
|
112
|
+
break;
|
|
113
|
+
case 'Home':
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
nextEnabledPosition = 0;
|
|
116
|
+
break;
|
|
117
|
+
case 'End':
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
nextEnabledPosition = enabledIndices.length - 1;
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nextIndex = enabledIndices[nextEnabledPosition];
|
|
126
|
+
navItems[nextIndex]?.focus();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// =========================================================================
|
|
130
|
+
// $derived
|
|
131
|
+
// =========================================================================
|
|
132
|
+
const selectedIndex = $derived.by(() => {
|
|
133
|
+
for (let i = 0; i < items.length; i++) {
|
|
134
|
+
const item = items[i];
|
|
135
|
+
if (!item.href) continue;
|
|
136
|
+
if (matchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
|
|
137
|
+
return i;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return -1;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const enabledIndices = $derived(
|
|
144
|
+
items.map((item, i) => (item.disabled ? -1 : i)).filter((i) => i >= 0)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const isTabVariant = $derived(variant === 'tab');
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<nav
|
|
151
|
+
class="nav nav--{variant}"
|
|
152
|
+
role={isTabVariant ? 'tablist' : undefined}
|
|
153
|
+
aria-label={ariaLabelledby ? undefined : ariaLabel}
|
|
154
|
+
aria-labelledby={ariaLabelledby}
|
|
155
|
+
aria-orientation={variant === 'vertical' ? 'vertical' : 'horizontal'}
|
|
156
|
+
tabindex="-1"
|
|
157
|
+
{id}
|
|
158
|
+
onkeydown={handleKeyDown}
|
|
159
|
+
data-testid="nav"
|
|
160
|
+
>
|
|
161
|
+
{#each items as item, index}
|
|
162
|
+
<NavItem
|
|
163
|
+
{item}
|
|
164
|
+
{variant}
|
|
165
|
+
{pathPrefix}
|
|
166
|
+
isSelected={index === selectedIndex}
|
|
167
|
+
isDisabled={item.disabled ?? false}
|
|
168
|
+
{iconFilled}
|
|
169
|
+
{iconWeight}
|
|
170
|
+
{iconGrade}
|
|
171
|
+
{iconOpticalSize}
|
|
172
|
+
{iconVariant}
|
|
173
|
+
{selectedStyle}
|
|
174
|
+
/>
|
|
175
|
+
{/each}
|
|
176
|
+
</nav>
|
|
177
|
+
|
|
178
|
+
<style>.nav {
|
|
179
|
+
display: flex;
|
|
180
|
+
box-sizing: border-box;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.nav--tab {
|
|
184
|
+
flex-direction: row;
|
|
185
|
+
justify-content: start;
|
|
186
|
+
position: relative;
|
|
187
|
+
width: 100%;
|
|
188
|
+
height: 100%;
|
|
189
|
+
min-height: var(--svelte-ui-tab-min-height);
|
|
190
|
+
overflow-x: auto;
|
|
191
|
+
overflow-y: visible;
|
|
192
|
+
-ms-overflow-style: none;
|
|
193
|
+
overscroll-behavior: contain;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.nav--tab::-webkit-scrollbar {
|
|
197
|
+
display: none;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.nav--mobile {
|
|
201
|
+
flex-direction: row;
|
|
202
|
+
width: 100%;
|
|
203
|
+
min-height: var(--svelte-ui-nav-mobile-min-height);
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.nav--vertical {
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
gap: var(--svelte-ui-nav-vertical-item-gap);
|
|
210
|
+
padding: var(--svelte-ui-nav-vertical-padding);
|
|
211
|
+
width: 100%;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.nav--horizontal {
|
|
215
|
+
flex-direction: row;
|
|
216
|
+
gap: var(--svelte-ui-nav-horizontal-item-gap);
|
|
217
|
+
align-items: center;
|
|
218
|
+
}</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { NavItemSelectedStyle } from './NavItem.svelte';
|
|
2
|
+
import type { MenuItem } from '../types/menuItem';
|
|
3
|
+
import type { NavVariant } from '../types/propOptions';
|
|
4
|
+
import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
|
|
5
|
+
export type NavProps = {
|
|
6
|
+
items?: MenuItem[];
|
|
7
|
+
variant?: NavVariant;
|
|
8
|
+
pathPrefix?: string;
|
|
9
|
+
customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
|
|
10
|
+
currentPath?: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
iconFilled?: boolean;
|
|
13
|
+
iconWeight?: IconWeight;
|
|
14
|
+
iconGrade?: IconGrade;
|
|
15
|
+
iconOpticalSize?: IconOpticalSize;
|
|
16
|
+
iconVariant?: IconVariant;
|
|
17
|
+
selectedStyle?: NavItemSelectedStyle;
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
ariaLabelledby?: string;
|
|
20
|
+
};
|
|
21
|
+
declare const Nav: import("svelte").Component<NavProps, {}, "">;
|
|
22
|
+
type Nav = ReturnType<typeof Nav>;
|
|
23
|
+
export default Nav;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
<!-- NavItem.svelte -->
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import Icon from './Icon.svelte';
|
|
5
|
+
import type { MenuItem } from '../types/menuItem';
|
|
6
|
+
import type { NavVariant } from '../types/propOptions';
|
|
7
|
+
import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
|
|
8
|
+
|
|
9
|
+
// =========================================================================
|
|
10
|
+
// Props, States & Constants
|
|
11
|
+
// =========================================================================
|
|
12
|
+
export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal';
|
|
13
|
+
|
|
14
|
+
export type NavItemProps = {
|
|
15
|
+
// 基本プロパティ
|
|
16
|
+
item: MenuItem;
|
|
17
|
+
variant?: NavVariant;
|
|
18
|
+
pathPrefix?: string;
|
|
19
|
+
|
|
20
|
+
// アイコン関連
|
|
21
|
+
iconFilled?: boolean;
|
|
22
|
+
iconWeight?: IconWeight;
|
|
23
|
+
iconGrade?: IconGrade;
|
|
24
|
+
iconOpticalSize?: IconOpticalSize;
|
|
25
|
+
iconVariant?: IconVariant;
|
|
26
|
+
|
|
27
|
+
// 状態/動作
|
|
28
|
+
isSelected?: boolean;
|
|
29
|
+
isDisabled?: boolean;
|
|
30
|
+
selectedStyle?: NavItemSelectedStyle;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
// 基本プロパティ
|
|
35
|
+
item,
|
|
36
|
+
variant = 'tab',
|
|
37
|
+
pathPrefix = '',
|
|
38
|
+
|
|
39
|
+
// アイコン関連
|
|
40
|
+
iconFilled = false,
|
|
41
|
+
iconWeight = 300,
|
|
42
|
+
iconGrade = 0,
|
|
43
|
+
iconOpticalSize = 24,
|
|
44
|
+
iconVariant = 'outlined',
|
|
45
|
+
|
|
46
|
+
// 状態/動作
|
|
47
|
+
isSelected = false,
|
|
48
|
+
isDisabled = false,
|
|
49
|
+
selectedStyle
|
|
50
|
+
}: NavItemProps = $props();
|
|
51
|
+
|
|
52
|
+
// =========================================================================
|
|
53
|
+
// $derived
|
|
54
|
+
// =========================================================================
|
|
55
|
+
const hrefWithPrefix = $derived.by(() => {
|
|
56
|
+
if (!item.href) return undefined;
|
|
57
|
+
if (!pathPrefix) return item.href;
|
|
58
|
+
if (item.href === pathPrefix || item.href.startsWith(`${pathPrefix}/`)) return item.href;
|
|
59
|
+
return `${pathPrefix}${item.href.startsWith('/') ? '' : '/'}${item.href}`;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const isTabVariant = $derived(variant === 'tab');
|
|
63
|
+
|
|
64
|
+
// selectedStyle 未指定時のバリアント別デフォルト
|
|
65
|
+
const resolvedSelectedStyle = $derived(
|
|
66
|
+
selectedStyle ?? (variant === 'vertical' || variant === 'horizontal' ? 'tonal' : 'color')
|
|
67
|
+
);
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
{#if isDisabled}
|
|
71
|
+
<span
|
|
72
|
+
class="nav-item nav-item--{variant} nav-item--disabled"
|
|
73
|
+
class:nav-item--selected={isSelected}
|
|
74
|
+
class:nav-item--style-color={isSelected && resolvedSelectedStyle === 'color'}
|
|
75
|
+
class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
|
|
76
|
+
class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
|
|
77
|
+
role={isTabVariant ? 'tab' : undefined}
|
|
78
|
+
aria-selected={isTabVariant ? isSelected : undefined}
|
|
79
|
+
aria-disabled="true"
|
|
80
|
+
tabindex="-1"
|
|
81
|
+
data-nav-item
|
|
82
|
+
data-testid="nav-item"
|
|
83
|
+
>
|
|
84
|
+
{#if item.icon}
|
|
85
|
+
<div class="nav-item__icon">
|
|
86
|
+
<Icon
|
|
87
|
+
filled={iconFilled || isSelected}
|
|
88
|
+
weight={iconWeight}
|
|
89
|
+
grade={iconGrade}
|
|
90
|
+
opticalSize={iconOpticalSize}
|
|
91
|
+
variant={iconVariant}>{item.icon}</Icon
|
|
92
|
+
>
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
{#if item.label}
|
|
96
|
+
<div class="nav-item__label">{item.label}</div>
|
|
97
|
+
{/if}
|
|
98
|
+
</span>
|
|
99
|
+
{:else}
|
|
100
|
+
<a
|
|
101
|
+
href={hrefWithPrefix}
|
|
102
|
+
class="nav-item nav-item--{variant}"
|
|
103
|
+
class:nav-item--selected={isSelected}
|
|
104
|
+
class:nav-item--style-color={isSelected && resolvedSelectedStyle === 'color'}
|
|
105
|
+
class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
|
|
106
|
+
class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
|
|
107
|
+
role={isTabVariant ? 'tab' : undefined}
|
|
108
|
+
aria-selected={isTabVariant ? isSelected : undefined}
|
|
109
|
+
aria-current={!isTabVariant && isSelected ? 'page' : undefined}
|
|
110
|
+
tabindex={0}
|
|
111
|
+
data-nav-item
|
|
112
|
+
data-testid="nav-item"
|
|
113
|
+
>
|
|
114
|
+
{#if item.icon}
|
|
115
|
+
<div class="nav-item__icon">
|
|
116
|
+
<Icon
|
|
117
|
+
filled={iconFilled || isSelected}
|
|
118
|
+
weight={iconWeight}
|
|
119
|
+
grade={iconGrade}
|
|
120
|
+
opticalSize={iconOpticalSize}
|
|
121
|
+
variant={iconVariant}>{item.icon}</Icon
|
|
122
|
+
>
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
125
|
+
{#if item.label}
|
|
126
|
+
<div class="nav-item__label">{item.label}</div>
|
|
127
|
+
{/if}
|
|
128
|
+
</a>
|
|
129
|
+
{/if}
|
|
130
|
+
|
|
131
|
+
<style>.nav-item {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: var(--svelte-ui-nav-item-icon-gap);
|
|
135
|
+
position: relative;
|
|
136
|
+
color: var(--svelte-ui-nav-item-text-color);
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
outline: none;
|
|
141
|
+
transition-property: background-color, color, outline;
|
|
142
|
+
transition-duration: var(--svelte-ui-transition-duration);
|
|
143
|
+
box-sizing: border-box;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.nav-item::after {
|
|
147
|
+
content: "";
|
|
148
|
+
position: absolute;
|
|
149
|
+
inset: 0;
|
|
150
|
+
background-color: var(--svelte-ui-hover-overlay);
|
|
151
|
+
opacity: 0;
|
|
152
|
+
pointer-events: none;
|
|
153
|
+
transition-property: opacity;
|
|
154
|
+
transition-duration: var(--svelte-ui-transition-duration);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.nav-item--disabled {
|
|
158
|
+
opacity: var(--svelte-ui-nav-item-disabled-opacity);
|
|
159
|
+
pointer-events: none;
|
|
160
|
+
cursor: default;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nav-item:focus-visible {
|
|
164
|
+
outline: var(--svelte-ui-focus-outline-inner);
|
|
165
|
+
outline-offset: var(--svelte-ui-focus-outline-offset-inner);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@supports not selector(:focus-visible) {
|
|
169
|
+
.nav-item:focus {
|
|
170
|
+
outline: var(--svelte-ui-focus-outline-inner);
|
|
171
|
+
outline-offset: var(--svelte-ui-focus-outline-offset-inner);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
.nav-item--tab {
|
|
175
|
+
justify-content: center;
|
|
176
|
+
padding: var(--svelte-ui-tab-item-padding);
|
|
177
|
+
min-height: var(--svelte-ui-nav-item-min-height);
|
|
178
|
+
color: var(--svelte-ui-tab-item-text-color);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@media (hover: hover) {
|
|
182
|
+
.nav-item--tab:hover {
|
|
183
|
+
color: var(--svelte-ui-tab-item-selected-text-color);
|
|
184
|
+
}
|
|
185
|
+
.nav-item--tab:hover::before {
|
|
186
|
+
opacity: 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
.nav-item--tab::before {
|
|
190
|
+
content: "";
|
|
191
|
+
display: block;
|
|
192
|
+
position: absolute;
|
|
193
|
+
left: calc(var(--svelte-ui-tab-item-padding-x) - var(--svelte-ui-tab-item-selected-bar-offset));
|
|
194
|
+
bottom: 0;
|
|
195
|
+
width: calc(100% - 2 * var(--svelte-ui-tab-item-padding-x) + 2 * var(--svelte-ui-tab-item-selected-bar-offset));
|
|
196
|
+
height: var(--svelte-ui-tab-item-selected-bar-height);
|
|
197
|
+
background-color: var(--svelte-ui-tab-item-selected-bar-color);
|
|
198
|
+
border-radius: var(--svelte-ui-tab-item-selected-bar-radius);
|
|
199
|
+
opacity: 0;
|
|
200
|
+
transition-property: opacity;
|
|
201
|
+
transition-duration: var(--svelte-ui-transition-duration);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.nav-item--tab.nav-item--selected::before {
|
|
205
|
+
opacity: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.nav-item--mobile {
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
justify-content: center;
|
|
211
|
+
flex: 1;
|
|
212
|
+
padding: var(--svelte-ui-nav-mobile-item-padding);
|
|
213
|
+
min-height: var(--svelte-ui-nav-mobile-min-height);
|
|
214
|
+
color: var(--svelte-ui-nav-item-text-color);
|
|
215
|
+
font-size: var(--svelte-ui-nav-mobile-item-font-size);
|
|
216
|
+
gap: var(--svelte-ui-nav-mobile-item-icon-gap);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (hover: hover) {
|
|
220
|
+
.nav-item--mobile:hover {
|
|
221
|
+
color: var(--svelte-ui-nav-item-selected-text-color);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
.nav-item--vertical {
|
|
225
|
+
width: 100%;
|
|
226
|
+
padding: var(--svelte-ui-nav-item-padding);
|
|
227
|
+
min-height: var(--svelte-ui-nav-item-min-height);
|
|
228
|
+
border-radius: var(--svelte-ui-nav-item-border-radius);
|
|
229
|
+
color: var(--svelte-ui-nav-item-text-color);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.nav-item--vertical::after {
|
|
233
|
+
border-radius: var(--svelte-ui-nav-item-border-radius);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@media (hover: hover) {
|
|
237
|
+
.nav-item--vertical:hover {
|
|
238
|
+
color: var(--svelte-ui-nav-item-selected-text-color);
|
|
239
|
+
}
|
|
240
|
+
.nav-item--vertical:hover::after {
|
|
241
|
+
opacity: 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
.nav-item--horizontal {
|
|
245
|
+
padding: var(--svelte-ui-nav-item-padding);
|
|
246
|
+
min-height: var(--svelte-ui-nav-item-min-height);
|
|
247
|
+
border-radius: var(--svelte-ui-nav-item-border-radius);
|
|
248
|
+
color: var(--svelte-ui-nav-item-text-color);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.nav-item--horizontal::after {
|
|
252
|
+
border-radius: var(--svelte-ui-nav-item-border-radius);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@media (hover: hover) {
|
|
256
|
+
.nav-item--horizontal:hover {
|
|
257
|
+
color: var(--svelte-ui-nav-item-selected-text-color);
|
|
258
|
+
}
|
|
259
|
+
.nav-item--horizontal:hover::after {
|
|
260
|
+
opacity: 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
.nav-item--style-color {
|
|
264
|
+
color: var(--svelte-ui-nav-item-selected-text-color);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.nav-item--tab.nav-item--style-color {
|
|
268
|
+
color: var(--svelte-ui-tab-item-selected-text-color);
|
|
269
|
+
background-color: transparent;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.nav-item--style-filled {
|
|
273
|
+
background-color: var(--svelte-ui-primary-color);
|
|
274
|
+
color: var(--svelte-ui-nav-item-filled-text-color);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.nav-item--style-tonal {
|
|
278
|
+
background-color: var(--svelte-ui-nav-item-tonal-bg-color);
|
|
279
|
+
color: var(--svelte-ui-nav-item-selected-text-color);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.nav-item__label {
|
|
283
|
+
text-box-trim: trim-both;
|
|
284
|
+
text-box-edge: cap alphabetic;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.nav-item--mobile .nav-item__label {
|
|
288
|
+
text-align: center;
|
|
289
|
+
}</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MenuItem } from '../types/menuItem';
|
|
2
|
+
import type { NavVariant } from '../types/propOptions';
|
|
3
|
+
import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
|
|
4
|
+
export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal';
|
|
5
|
+
export type NavItemProps = {
|
|
6
|
+
item: MenuItem;
|
|
7
|
+
variant?: NavVariant;
|
|
8
|
+
pathPrefix?: string;
|
|
9
|
+
iconFilled?: boolean;
|
|
10
|
+
iconWeight?: IconWeight;
|
|
11
|
+
iconGrade?: IconGrade;
|
|
12
|
+
iconOpticalSize?: IconOpticalSize;
|
|
13
|
+
iconVariant?: IconVariant;
|
|
14
|
+
isSelected?: boolean;
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
selectedStyle?: NavItemSelectedStyle;
|
|
17
|
+
};
|
|
18
|
+
declare const NavItem: import("svelte").Component<NavItemProps, {}, "">;
|
|
19
|
+
type NavItem = ReturnType<typeof NavItem>;
|
|
20
|
+
export default NavItem;
|
|
@@ -69,7 +69,9 @@
|
|
|
69
69
|
|
|
70
70
|
const executeMenuItem = (item: MenuItem, event?: MouseEvent) => {
|
|
71
71
|
event?.stopPropagation();
|
|
72
|
-
|
|
72
|
+
if (!item.href) {
|
|
73
|
+
event?.preventDefault();
|
|
74
|
+
}
|
|
73
75
|
if (item.callback) {
|
|
74
76
|
item.callback();
|
|
75
77
|
}
|
|
@@ -261,6 +263,7 @@
|
|
|
261
263
|
tabindex="-1"
|
|
262
264
|
aria-describedby={item.icon ? `${getMenuItemId(actionableIndex)}-icon` : undefined}
|
|
263
265
|
href={item.href}
|
|
266
|
+
target={item.target}
|
|
264
267
|
onclick={(e) => executeMenuItem(item, e)}
|
|
265
268
|
onmouseenter={() => handleMouseEnter(actionableIndex)}
|
|
266
269
|
onfocus={() => handleFocus(actionableIndex)}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import TabItem from './TabItem.svelte';
|
|
5
5
|
import type { MenuItem } from '../types/menuItem';
|
|
6
6
|
import { subscribeUrlChange } from '../utils/urlChange';
|
|
7
|
+
import { getCurrentPath as resolveCurrentPath, matchPath as doMatchPath } from '../utils/navPath';
|
|
7
8
|
|
|
8
9
|
// =========================================================================
|
|
9
10
|
// Props, States & Constants
|
|
@@ -49,72 +50,19 @@
|
|
|
49
50
|
// =========================================================================
|
|
50
51
|
$effect(() => {
|
|
51
52
|
// props の currentPath が変更されたとき
|
|
52
|
-
resolvedCurrentPath =
|
|
53
|
+
resolvedCurrentPath = resolveCurrentPath(currentPath);
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
$effect(() => {
|
|
56
57
|
// URL の変更を subscribe
|
|
57
58
|
return subscribeUrlChange(() => {
|
|
58
|
-
resolvedCurrentPath =
|
|
59
|
+
resolvedCurrentPath = resolveCurrentPath(currentPath);
|
|
59
60
|
});
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
// =========================================================================
|
|
63
64
|
// Methods
|
|
64
65
|
// =========================================================================
|
|
65
|
-
const getCurrentPath = () => {
|
|
66
|
-
// アプリ側から現在パスが明示的に渡されていればそれを優先(SSR対応)
|
|
67
|
-
if (currentPath && currentPath !== '') {
|
|
68
|
-
return currentPath;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// それ以外の場合はクライアント実行時のみ window.location から取得
|
|
72
|
-
if (typeof window !== 'undefined') {
|
|
73
|
-
return window.location.pathname;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return '';
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// パスの正規化処理
|
|
80
|
-
const normalizePath = (path: string): string => {
|
|
81
|
-
if (!pathPrefix) return path;
|
|
82
|
-
|
|
83
|
-
// pathPrefixが設定されている場合、それを除去
|
|
84
|
-
if (path.startsWith(pathPrefix)) {
|
|
85
|
-
const normalized = path.substring(pathPrefix.length);
|
|
86
|
-
return normalized.startsWith('/') ? normalized : '/' + normalized;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return path;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// パスマッチング関数
|
|
93
|
-
const matchPath = (currentPath: string, itemHref: string, item: MenuItem): boolean => {
|
|
94
|
-
if (customPathMatcher) {
|
|
95
|
-
return customPathMatcher(currentPath, itemHref, item);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const normalizedCurrentPath = normalizePath(currentPath);
|
|
99
|
-
|
|
100
|
-
// matchingPathのチェック
|
|
101
|
-
if (item.matchingPath?.some((href) => normalizedCurrentPath.startsWith(href))) {
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// strictMatchの場合
|
|
106
|
-
if (item.strictMatch) {
|
|
107
|
-
return normalizedCurrentPath === itemHref;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ルートパス (/) の特別な処理
|
|
111
|
-
if (itemHref === '/') {
|
|
112
|
-
return normalizedCurrentPath === '/';
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// その他のパス
|
|
116
|
-
return normalizedCurrentPath !== '' && normalizedCurrentPath.startsWith(itemHref);
|
|
117
|
-
};
|
|
118
66
|
|
|
119
67
|
// シンプルなキーボードナビゲーション(disabled タブはスキップ)
|
|
120
68
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
@@ -167,7 +115,7 @@
|
|
|
167
115
|
const item = tabItems[i];
|
|
168
116
|
if (!item.href) continue;
|
|
169
117
|
|
|
170
|
-
if (
|
|
118
|
+
if (doMatchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
|
|
171
119
|
return i;
|
|
172
120
|
}
|
|
173
121
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export { default as SkeletonText } from './components/skeleton/SkeletonText.svel
|
|
|
32
32
|
export { default as SkeletonBox } from './components/skeleton/SkeletonBox.svelte';
|
|
33
33
|
export { default as SkeletonAvatar } from './components/skeleton/SkeletonAvatar.svelte';
|
|
34
34
|
export { default as Switch } from './components/Switch.svelte';
|
|
35
|
+
export { default as Nav } from './components/Nav.svelte';
|
|
36
|
+
export { default as NavItem } from './components/NavItem.svelte';
|
|
35
37
|
export { default as Tab } from './components/Tab.svelte';
|
|
36
38
|
export { default as TabItem } from './components/TabItem.svelte';
|
|
37
39
|
export { default as Textarea } from './components/Textarea.svelte';
|
|
@@ -73,10 +75,12 @@ export type { SkeletonButtonProps } from './components/skeleton/SkeletonButton.s
|
|
|
73
75
|
export type { SkeletonHeadingProps } from './components/skeleton/SkeletonHeading.svelte';
|
|
74
76
|
export type { SkeletonMediaProps } from './components/skeleton/SkeletonMedia.svelte';
|
|
75
77
|
export type { SwitchProps } from './components/Switch.svelte';
|
|
78
|
+
export type { NavProps } from './components/Nav.svelte';
|
|
79
|
+
export type { NavItemProps, NavItemSelectedStyle } from './components/NavItem.svelte';
|
|
76
80
|
export type { TabProps } from './components/Tab.svelte';
|
|
77
81
|
export type { TabItemProps } from './components/TabItem.svelte';
|
|
78
82
|
export type { TextareaProps } from './components/Textarea.svelte';
|
|
79
|
-
export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle } from './types/propOptions';
|
|
83
|
+
export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle, NavVariant } from './types/propOptions';
|
|
80
84
|
export type { MenuItem } from './types/menuItem';
|
|
81
85
|
export type { SegmentedControlItem } from './types/segmentedControlItem';
|
|
82
86
|
export * from './utils/accessibility';
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,8 @@ export { default as SkeletonText } from './components/skeleton/SkeletonText.svel
|
|
|
33
33
|
export { default as SkeletonBox } from './components/skeleton/SkeletonBox.svelte';
|
|
34
34
|
export { default as SkeletonAvatar } from './components/skeleton/SkeletonAvatar.svelte';
|
|
35
35
|
export { default as Switch } from './components/Switch.svelte';
|
|
36
|
+
export { default as Nav } from './components/Nav.svelte';
|
|
37
|
+
export { default as NavItem } from './components/NavItem.svelte';
|
|
36
38
|
export { default as Tab } from './components/Tab.svelte';
|
|
37
39
|
export { default as TabItem } from './components/TabItem.svelte';
|
|
38
40
|
export { default as Textarea } from './components/Textarea.svelte';
|
package/dist/types/menuItem.d.ts
CHANGED
|
@@ -52,3 +52,8 @@ export type DatepickerMode = 'single' | 'range';
|
|
|
52
52
|
* Used by Input, Textarea, Datepicker components
|
|
53
53
|
*/
|
|
54
54
|
export type FocusStyle = 'background' | 'outline' | 'none';
|
|
55
|
+
/**
|
|
56
|
+
* Nav variant type
|
|
57
|
+
* Used by Nav component
|
|
58
|
+
*/
|
|
59
|
+
export type NavVariant = 'vertical' | 'horizontal' | 'mobile' | 'tab';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MenuItem } from '../types/menuItem';
|
|
2
|
+
export declare const getCurrentPath: (currentPath?: string) => string;
|
|
3
|
+
export declare const normalizePath: (path: string, pathPrefix: string) => string;
|
|
4
|
+
export declare const matchPath: (currentPath: string, itemHref: string, item: MenuItem, pathPrefix: string, customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean) => boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const getCurrentPath = (currentPath) => {
|
|
2
|
+
if (currentPath && currentPath !== '')
|
|
3
|
+
return currentPath;
|
|
4
|
+
if (typeof window !== 'undefined')
|
|
5
|
+
return window.location.pathname;
|
|
6
|
+
return '';
|
|
7
|
+
};
|
|
8
|
+
export const normalizePath = (path, pathPrefix) => {
|
|
9
|
+
if (!pathPrefix)
|
|
10
|
+
return path;
|
|
11
|
+
if (path.startsWith(pathPrefix)) {
|
|
12
|
+
const normalized = path.substring(pathPrefix.length);
|
|
13
|
+
return normalized.startsWith('/') ? normalized : '/' + normalized;
|
|
14
|
+
}
|
|
15
|
+
return path;
|
|
16
|
+
};
|
|
17
|
+
export const matchPath = (currentPath, itemHref, item, pathPrefix, customPathMatcher) => {
|
|
18
|
+
if (customPathMatcher)
|
|
19
|
+
return customPathMatcher(currentPath, itemHref, item);
|
|
20
|
+
const normalizedCurrentPath = normalizePath(currentPath, pathPrefix);
|
|
21
|
+
if (item.matchingPath?.some((href) => normalizedCurrentPath.startsWith(href)))
|
|
22
|
+
return true;
|
|
23
|
+
if (item.strictMatch)
|
|
24
|
+
return normalizedCurrentPath === itemHref;
|
|
25
|
+
if (itemHref === '/')
|
|
26
|
+
return normalizedCurrentPath === '/';
|
|
27
|
+
return normalizedCurrentPath !== '' && normalizedCurrentPath.startsWith(itemHref);
|
|
28
|
+
};
|