fluentui-svelte 0.2.1 → 0.4.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/dist/components/breadcrumb/breadcrumb-button.svelte +130 -0
- package/dist/components/breadcrumb/breadcrumb-button.svelte.d.ts +35 -0
- package/dist/components/breadcrumb/breadcrumb-divider.svelte +39 -0
- package/dist/components/breadcrumb/breadcrumb-divider.svelte.d.ts +14 -0
- package/dist/components/breadcrumb/breadcrumb-item.svelte +30 -0
- package/dist/components/breadcrumb/breadcrumb-item.svelte.d.ts +16 -0
- package/dist/components/breadcrumb/breadcrumb.svelte +101 -0
- package/dist/components/breadcrumb/breadcrumb.svelte.d.ts +25 -0
- package/dist/components/breadcrumb/docs/llms.md +193 -0
- package/dist/components/breadcrumb/types.d.ts +80 -0
- package/dist/components/breadcrumb/types.js +1 -0
- package/dist/components/tab-view/docs/llms.md +146 -0
- package/dist/components/tab-view/tab-view-tab.svelte +290 -0
- package/dist/components/tab-view/tab-view-tab.svelte.d.ts +20 -0
- package/dist/components/tab-view/tab-view.svelte +188 -0
- package/dist/components/tab-view/tab-view.svelte.d.ts +26 -0
- package/dist/components/tab-view/tab-view.svelte.js +15 -0
- package/dist/components/tab-view/types.d.ts +79 -0
- package/dist/components/tab-view/types.js +1 -0
- package/dist/components/table/docs/llms.md +531 -0
- package/dist/components/table/table-body.svelte +31 -0
- package/dist/components/table/table-body.svelte.d.ts +25 -0
- package/dist/components/table/table-cell-actions.svelte +48 -0
- package/dist/components/table/table-cell-actions.svelte.d.ts +17 -0
- package/dist/components/table/table-cell-layout.svelte +88 -0
- package/dist/components/table/table-cell-layout.svelte.d.ts +14 -0
- package/dist/components/table/table-cell.svelte +74 -0
- package/dist/components/table/table-cell.svelte.d.ts +25 -0
- package/dist/components/table/table-header-cell.svelte +257 -0
- package/dist/components/table/table-header-cell.svelte.d.ts +25 -0
- package/dist/components/table/table-header.svelte +28 -0
- package/dist/components/table/table-header.svelte.d.ts +25 -0
- package/dist/components/table/table-row.svelte +57 -0
- package/dist/components/table/table-row.svelte.d.ts +25 -0
- package/dist/components/table/table-selection-cell.svelte +89 -0
- package/dist/components/table/table-selection-cell.svelte.d.ts +25 -0
- package/dist/components/table/table.svelte +189 -0
- package/dist/components/table/table.svelte.d.ts +49 -0
- package/dist/components/table/table.svelte.js +138 -0
- package/dist/components/table/types.d.ts +287 -0
- package/dist/components/table/types.js +1 -0
- package/dist/components/top-nav/docs/llms.md +176 -0
- package/dist/components/top-nav/top-nav-item.svelte +234 -0
- package/dist/components/top-nav/top-nav-item.svelte.d.ts +41 -0
- package/dist/components/top-nav/top-nav.svelte +259 -0
- package/dist/components/top-nav/top-nav.svelte.d.ts +26 -0
- package/dist/components/top-nav/top-nav.svelte.js +18 -0
- package/dist/components/top-nav/types.d.ts +89 -0
- package/dist/components/top-nav/types.js +1 -0
- package/dist/css/class-darkmode.css +1 -1
- package/dist/css/media-darkmode.css +1 -1
- package/dist/css/theme.css +1 -1
- package/dist/index.d.ts +17 -0
- package/dist/index.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends TopNavItemGenerics = 'button'">
|
|
2
|
+
import { invokeHandlers, RenderSoC } from '../../internal/index.js';
|
|
3
|
+
import { COMPONENT_NAME, requireTopNavContext } from './top-nav.svelte.js';
|
|
4
|
+
import type { TopNavItemElementDOMType, TopNavItemGenerics, TopNavItemProps } from './types.js';
|
|
5
|
+
|
|
6
|
+
const ID = $props.id();
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
as = 'button' as T,
|
|
10
|
+
ref = $bindable(),
|
|
11
|
+
value = `${COMPONENT_NAME}-${ID}`,
|
|
12
|
+
icon,
|
|
13
|
+
disabled,
|
|
14
|
+
onclick,
|
|
15
|
+
onfocus,
|
|
16
|
+
class: classes,
|
|
17
|
+
children,
|
|
18
|
+
...attributes
|
|
19
|
+
}: TopNavItemProps<T> = $props();
|
|
20
|
+
|
|
21
|
+
const context = requireTopNavContext();
|
|
22
|
+
|
|
23
|
+
const selected = $derived(context.state.selectedValue === value);
|
|
24
|
+
|
|
25
|
+
/** The item can be disabled on its own, and the strip can disable every one of them at once. */
|
|
26
|
+
const _disabled = $derived(disabled || context.config.disabled);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The tag the invariant rules over. The markup renders it through here, so it is checked during
|
|
30
|
+
* SSR and on every prop update rather than only once on mount.
|
|
31
|
+
*/
|
|
32
|
+
const _as = $derived.by(() => {
|
|
33
|
+
if (as !== 'button' && as !== 'a') throw new Error(`Invalid tag: ${as}. Must be one of button, a`);
|
|
34
|
+
return as;
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!--
|
|
39
|
+
@component
|
|
40
|
+
A single item of a `TopNav`. It reports its own selection and can carry an icon.
|
|
41
|
+
|
|
42
|
+
- Usage:
|
|
43
|
+
```tsx
|
|
44
|
+
<script>
|
|
45
|
+
import { TopNav, TopNavItem } from 'fluentui-svelte';
|
|
46
|
+
import { AirplaneRegular } from 'fluentui-icons-svelte';
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<TopNav selectedValue="arrivals" aria-label="Flights">
|
|
50
|
+
<TopNavItem value="arrivals" icon={AirplaneRegular}>Arrivals</TopNavItem>
|
|
51
|
+
<TopNavItem as="a" href="/departures" value="departures">Departures</TopNavItem>
|
|
52
|
+
</TopNav>
|
|
53
|
+
```
|
|
54
|
+
-->
|
|
55
|
+
<svelte:element
|
|
56
|
+
this={_as}
|
|
57
|
+
bind:this={ref as TopNavItemElementDOMType[T]}
|
|
58
|
+
type={_as === 'button' ? 'button' : undefined}
|
|
59
|
+
role="tab"
|
|
60
|
+
data-value={value}
|
|
61
|
+
aria-selected={selected}
|
|
62
|
+
disabled={_as === 'button' ? _disabled : undefined}
|
|
63
|
+
aria-disabled={_as === 'a' && _disabled ? true : undefined}
|
|
64
|
+
class={[
|
|
65
|
+
'fs-top-nav-item',
|
|
66
|
+
context.config.appearance,
|
|
67
|
+
{ selected, vertical: context.config.vertical, disabled: _disabled },
|
|
68
|
+
classes
|
|
69
|
+
]}
|
|
70
|
+
onclick={(e: MouseEvent) => invokeHandlers(e, _disabled, [() => context.methods.selectTab(e, value), onclick])}
|
|
71
|
+
onfocus={(e: FocusEvent) =>
|
|
72
|
+
invokeHandlers(e, _disabled, [
|
|
73
|
+
() => context.config.selectTabOnFocus && context.methods.selectTab(e, value),
|
|
74
|
+
onfocus
|
|
75
|
+
])}
|
|
76
|
+
{...attributes}
|
|
77
|
+
>
|
|
78
|
+
{#if icon}
|
|
79
|
+
<span class="fs-top-nav-item-icon">
|
|
80
|
+
<RenderSoC SoC={icon} />
|
|
81
|
+
</span>
|
|
82
|
+
{/if}
|
|
83
|
+
<span class="fs-top-nav-item-content">
|
|
84
|
+
{@render children?.()}
|
|
85
|
+
{#if context.config.reserveSelectedTabSpace}
|
|
86
|
+
<!--
|
|
87
|
+
The label again, in the weight it takes once selected, laid out but never shown. The item
|
|
88
|
+
is then already as wide as its widest state, so selecting it cannot shove its neighbours
|
|
89
|
+
along. It is held out of the accessibility tree, where it would read as a second label.
|
|
90
|
+
-->
|
|
91
|
+
<span class="fs-top-nav-item-reserve" aria-hidden="true">{@render children?.()}</span>
|
|
92
|
+
{/if}
|
|
93
|
+
</span>
|
|
94
|
+
</svelte:element>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
.fs-top-nav-item {
|
|
98
|
+
position: relative;
|
|
99
|
+
display: flex;
|
|
100
|
+
flex: 0 0 auto;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
gap: var(--fs-top-nav-item-gap, 0.5rem);
|
|
104
|
+
padding: var(--fs-top-nav-item-padding-block, 0.375rem) var(--fs-top-nav-item-padding-inline, 0.375rem);
|
|
105
|
+
border: none;
|
|
106
|
+
border-radius: var(--fs-control-border-radius);
|
|
107
|
+
background: transparent;
|
|
108
|
+
color: var(--fs-text-secondary);
|
|
109
|
+
font-family: inherit;
|
|
110
|
+
font-size: var(--fs-top-nav-item-font-size, var(--fs-body2-font-size));
|
|
111
|
+
line-height: var(--fs-top-nav-item-line-height, var(--fs-body2-line-height));
|
|
112
|
+
white-space: nowrap;
|
|
113
|
+
/* An anchor arrives underlined and in the colour of a link; an item is neither. */
|
|
114
|
+
text-decoration: none;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
user-select: none;
|
|
117
|
+
outline: none;
|
|
118
|
+
transition:
|
|
119
|
+
color var(--fs-fast-duration) var(--fs-point-to-point),
|
|
120
|
+
background var(--fs-fast-duration) var(--fs-point-to-point);
|
|
121
|
+
&:hover {
|
|
122
|
+
color: var(--fs-text-primary);
|
|
123
|
+
}
|
|
124
|
+
&:active {
|
|
125
|
+
color: var(--fs-text-secondary);
|
|
126
|
+
}
|
|
127
|
+
&:focus-visible {
|
|
128
|
+
outline: 0.125rem var(--fs-focus-stroke-outer) solid;
|
|
129
|
+
outline-offset: 0.125rem;
|
|
130
|
+
}
|
|
131
|
+
&.selected {
|
|
132
|
+
color: var(--fs-text-primary);
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
}
|
|
135
|
+
/* An anchor has no `:disabled` of its own, so the class is what both tags answer to. */
|
|
136
|
+
&.disabled {
|
|
137
|
+
color: var(--fs-text-disabled);
|
|
138
|
+
cursor: not-allowed;
|
|
139
|
+
}
|
|
140
|
+
/*
|
|
141
|
+
* The line an item shows under itself while the pointer is on it. It is the item's own, not
|
|
142
|
+
* the bar of the strip: the bar belongs to whatever is selected and never leaves it.
|
|
143
|
+
*/
|
|
144
|
+
&::after {
|
|
145
|
+
content: '';
|
|
146
|
+
position: absolute;
|
|
147
|
+
left: var(--fs-top-nav-item-padding-inline, 0.375rem);
|
|
148
|
+
right: var(--fs-top-nav-item-padding-inline, 0.375rem);
|
|
149
|
+
bottom: 0;
|
|
150
|
+
height: var(--fs-top-nav-bar-thickness, 0.1875rem);
|
|
151
|
+
border-radius: var(--fs-top-nav-bar-thickness, 0.1875rem);
|
|
152
|
+
background: var(--fs-control-strong-stroke-disabled);
|
|
153
|
+
opacity: 0;
|
|
154
|
+
transition: opacity var(--fs-fast-duration) var(--fs-point-to-point);
|
|
155
|
+
pointer-events: none;
|
|
156
|
+
}
|
|
157
|
+
&:hover:not(.selected):not(.disabled)::after {
|
|
158
|
+
opacity: 1;
|
|
159
|
+
}
|
|
160
|
+
&.vertical::after {
|
|
161
|
+
left: auto;
|
|
162
|
+
right: auto;
|
|
163
|
+
inset-inline-start: 0;
|
|
164
|
+
top: var(--fs-top-nav-item-padding-block, 0.375rem);
|
|
165
|
+
bottom: var(--fs-top-nav-item-padding-block, 0.375rem);
|
|
166
|
+
width: var(--fs-top-nav-bar-thickness, 0.1875rem);
|
|
167
|
+
height: auto;
|
|
168
|
+
}
|
|
169
|
+
&.subtle {
|
|
170
|
+
&:hover:not(.disabled) {
|
|
171
|
+
background: var(--fs-subtle-fill-secondary);
|
|
172
|
+
}
|
|
173
|
+
&:active:not(.disabled) {
|
|
174
|
+
background: var(--fs-subtle-fill-tertiary);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/* A pill takes the place of the bar, so the hover line steps aside for it. */
|
|
178
|
+
&.subtle-circular,
|
|
179
|
+
&.filled-circular {
|
|
180
|
+
border-radius: 999rem;
|
|
181
|
+
&::after {
|
|
182
|
+
content: none;
|
|
183
|
+
}
|
|
184
|
+
&:hover:not(.disabled) {
|
|
185
|
+
background: var(--fs-subtle-fill-secondary);
|
|
186
|
+
}
|
|
187
|
+
&:active:not(.disabled) {
|
|
188
|
+
background: var(--fs-subtle-fill-tertiary);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
&.filled-circular {
|
|
192
|
+
background: var(--fs-subtle-fill-secondary);
|
|
193
|
+
}
|
|
194
|
+
&.subtle-circular.selected {
|
|
195
|
+
background: var(--fs-system-attention-bg);
|
|
196
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fs-system-attention-text), transparent 50%);
|
|
197
|
+
color: var(--fs-system-attention-text);
|
|
198
|
+
}
|
|
199
|
+
&.filled-circular.selected {
|
|
200
|
+
background: var(--fs-accent-fill-default);
|
|
201
|
+
color: var(--fs-text-on-accent-primary);
|
|
202
|
+
&:hover:not(.disabled) {
|
|
203
|
+
background: var(--fs-accent-fill-secondary);
|
|
204
|
+
}
|
|
205
|
+
&:active:not(.disabled) {
|
|
206
|
+
background: var(--fs-accent-fill-tertiary);
|
|
207
|
+
}
|
|
208
|
+
&.disabled {
|
|
209
|
+
background: var(--fs-accent-fill-disabled);
|
|
210
|
+
color: var(--fs-text-on-accent-disabled);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
& .fs-top-nav-item-icon {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex: 0 0 auto;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
& :global(svg) {
|
|
219
|
+
width: var(--fs-top-nav-item-icon-size, 1.25rem);
|
|
220
|
+
height: var(--fs-top-nav-item-icon-size, 1.25rem);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
& .fs-top-nav-item-content {
|
|
224
|
+
display: block;
|
|
225
|
+
}
|
|
226
|
+
& .fs-top-nav-item-reserve {
|
|
227
|
+
display: block;
|
|
228
|
+
height: 0;
|
|
229
|
+
overflow: hidden;
|
|
230
|
+
visibility: hidden;
|
|
231
|
+
font-weight: 600;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TopNavItemGenerics, TopNavItemProps } from './types.ts';
|
|
2
|
+
declare function $$render<T extends TopNavItemGenerics = 'button'>(): {
|
|
3
|
+
props: TopNavItemProps<T>;
|
|
4
|
+
exports: {};
|
|
5
|
+
bindings: "ref";
|
|
6
|
+
slots: {};
|
|
7
|
+
events: {};
|
|
8
|
+
};
|
|
9
|
+
declare class __sveltets_Render<T extends TopNavItemGenerics = 'button'> {
|
|
10
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
11
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
12
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
13
|
+
bindings(): "ref";
|
|
14
|
+
exports(): {};
|
|
15
|
+
}
|
|
16
|
+
interface $$IsomorphicComponent {
|
|
17
|
+
new <T extends TopNavItemGenerics = 'button'>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
18
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
19
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
20
|
+
<T extends TopNavItemGenerics = 'button'>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
21
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A single item of a `TopNav`. It reports its own selection and can carry an icon.
|
|
25
|
+
*
|
|
26
|
+
* - Usage:
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <script>
|
|
29
|
+
* import { TopNav, TopNavItem } from 'fluentui-svelte';
|
|
30
|
+
* import { AirplaneRegular } from 'fluentui-icons-svelte';
|
|
31
|
+
* </script>
|
|
32
|
+
*
|
|
33
|
+
* <TopNav selectedValue="arrivals" aria-label="Flights">
|
|
34
|
+
* <TopNavItem value="arrivals" icon={AirplaneRegular}>Arrivals</TopNavItem>
|
|
35
|
+
* <TopNavItem as="a" href="/departures" value="departures">Departures</TopNavItem>
|
|
36
|
+
* </TopNav>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare const TopNavItem: $$IsomorphicComponent;
|
|
40
|
+
type TopNavItem<T extends TopNavItemGenerics = 'button'> = InstanceType<typeof TopNavItem<T>>;
|
|
41
|
+
export default TopNavItem;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTabspotAttributes } from 'tabspot';
|
|
3
|
+
import { setTopNavContext, TABSPOT_ITEMS, TABSPOT_SKIP } from './top-nav.svelte.js';
|
|
4
|
+
import type { TopNavContext, TopNavProps } from './types.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
ref = $bindable(),
|
|
8
|
+
selectedValue = $bindable(''),
|
|
9
|
+
onTabSelect,
|
|
10
|
+
appearance = 'transparent',
|
|
11
|
+
size = 'medium',
|
|
12
|
+
vertical = false,
|
|
13
|
+
disabled = false,
|
|
14
|
+
selectTabOnFocus = false,
|
|
15
|
+
reserveSelectedTabSpace = true,
|
|
16
|
+
disableTabspot = false,
|
|
17
|
+
class: classes,
|
|
18
|
+
children,
|
|
19
|
+
...attributes
|
|
20
|
+
}: TopNavProps = $props();
|
|
21
|
+
|
|
22
|
+
const context: TopNavContext = $state({
|
|
23
|
+
config: {
|
|
24
|
+
get appearance() {
|
|
25
|
+
return appearance;
|
|
26
|
+
},
|
|
27
|
+
get size() {
|
|
28
|
+
return size;
|
|
29
|
+
},
|
|
30
|
+
get vertical() {
|
|
31
|
+
return vertical;
|
|
32
|
+
},
|
|
33
|
+
get disabled() {
|
|
34
|
+
return disabled;
|
|
35
|
+
},
|
|
36
|
+
get selectTabOnFocus() {
|
|
37
|
+
return selectTabOnFocus;
|
|
38
|
+
},
|
|
39
|
+
get reserveSelectedTabSpace() {
|
|
40
|
+
return reserveSelectedTabSpace;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
state: {
|
|
44
|
+
get selectedValue() {
|
|
45
|
+
return selectedValue;
|
|
46
|
+
},
|
|
47
|
+
set selectedValue(value: string) {
|
|
48
|
+
selectedValue = value;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
events: null,
|
|
52
|
+
methods: {
|
|
53
|
+
selectTab: (event: Event, value: string) => {
|
|
54
|
+
if (selectedValue === value) return;
|
|
55
|
+
selectedValue = value;
|
|
56
|
+
onTabSelect?.(event, value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
setTopNavContext(context);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A single tab stop for the whole strip: the arrows walk the items along the axis they are laid
|
|
65
|
+
* out on, and `Home`/`End` jump to the ends.
|
|
66
|
+
*/
|
|
67
|
+
const tabspotAttributes = $derived(
|
|
68
|
+
!disableTabspot &&
|
|
69
|
+
getTabspotAttributes({
|
|
70
|
+
root: { manageSpecialKeys: { Home: true, End: true } },
|
|
71
|
+
mover: {
|
|
72
|
+
axis: vertical ? 'vertical' : 'horizontal',
|
|
73
|
+
items: TABSPOT_ITEMS,
|
|
74
|
+
skip: TABSPOT_SKIP
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/** The circular appearances fill the selected item instead of underlining it. */
|
|
80
|
+
const hasIndicator = $derived(appearance === 'transparent' || appearance === 'subtle');
|
|
81
|
+
|
|
82
|
+
/** Where the bar sits along the axis, and how far it reaches. Both are read off the DOM. */
|
|
83
|
+
let offset = $state(0);
|
|
84
|
+
let extent = $state(0);
|
|
85
|
+
let measured = $state(false);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The bar belongs to the strip rather than to the selected item, which is what lets it travel:
|
|
89
|
+
* one element that stays mounted and moves, instead of one per item appearing and disappearing.
|
|
90
|
+
* That costs a measurement, since only the DOM knows where an item ended up.
|
|
91
|
+
*
|
|
92
|
+
* It spans the content of the item rather than its whole box, so the padding is taken off both
|
|
93
|
+
* ends — the same padding the item lays its own hover bar within.
|
|
94
|
+
*/
|
|
95
|
+
const measure = () => {
|
|
96
|
+
const root = ref;
|
|
97
|
+
if (!root) return;
|
|
98
|
+
|
|
99
|
+
const item = [...root.querySelectorAll<HTMLElement>(TABSPOT_ITEMS)].find(
|
|
100
|
+
(el) => el.dataset.value === selectedValue
|
|
101
|
+
);
|
|
102
|
+
if (!item) {
|
|
103
|
+
measured = false;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const style = getComputedStyle(item);
|
|
108
|
+
const before = parseFloat(vertical ? style.paddingTop : style.paddingLeft) || 0;
|
|
109
|
+
const after = parseFloat(vertical ? style.paddingBottom : style.paddingRight) || 0;
|
|
110
|
+
|
|
111
|
+
offset = (vertical ? item.offsetTop : item.offsetLeft) + before;
|
|
112
|
+
extent = Math.max(0, (vertical ? item.offsetHeight : item.offsetWidth) - before - after);
|
|
113
|
+
measured = true;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// The selection and the axis are what move the bar; both are read through `measure`.
|
|
117
|
+
$effect(() => {
|
|
118
|
+
measure();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Everything else that moves it does so through the layout: a label that rewrapped, a font that
|
|
123
|
+
* finished loading, an item that was added or removed. The observers are what catch those, since
|
|
124
|
+
* none of them is a change to a prop.
|
|
125
|
+
*/
|
|
126
|
+
$effect(() => {
|
|
127
|
+
const root = ref;
|
|
128
|
+
if (!root || !hasIndicator) return;
|
|
129
|
+
|
|
130
|
+
const resize = new ResizeObserver(measure);
|
|
131
|
+
const watch = () => {
|
|
132
|
+
resize.disconnect();
|
|
133
|
+
resize.observe(root);
|
|
134
|
+
for (const item of root.querySelectorAll(TABSPOT_ITEMS)) resize.observe(item);
|
|
135
|
+
};
|
|
136
|
+
const mutations = new MutationObserver(() => {
|
|
137
|
+
watch();
|
|
138
|
+
measure();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
watch();
|
|
142
|
+
mutations.observe(root, { childList: true, subtree: true });
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
resize.disconnect();
|
|
146
|
+
mutations.disconnect();
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<!--
|
|
152
|
+
@component
|
|
153
|
+
A top nav gives a single selection from a row of items, with a bar that travels to whichever one
|
|
154
|
+
is selected. It owns the selection alone: what an item opens is rendered by the consumer.
|
|
155
|
+
|
|
156
|
+
- Usage:
|
|
157
|
+
```tsx
|
|
158
|
+
<script>
|
|
159
|
+
import { TopNav, TopNavItem } from 'fluentui-svelte';
|
|
160
|
+
|
|
161
|
+
let selectedValue = $state('arrivals');
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<TopNav bind:selectedValue aria-label="Flights">
|
|
165
|
+
<TopNavItem value="arrivals">Arrivals</TopNavItem>
|
|
166
|
+
<TopNavItem value="departures">Departures</TopNavItem>
|
|
167
|
+
</TopNav>
|
|
168
|
+
|
|
169
|
+
{#if selectedValue === 'arrivals'}
|
|
170
|
+
<p>Arrivals</p>
|
|
171
|
+
{/if}
|
|
172
|
+
```
|
|
173
|
+
-->
|
|
174
|
+
<div
|
|
175
|
+
bind:this={ref}
|
|
176
|
+
role="tablist"
|
|
177
|
+
aria-orientation={vertical ? 'vertical' : undefined}
|
|
178
|
+
class={['fs-top-nav', appearance, size, { vertical }, classes]}
|
|
179
|
+
{...tabspotAttributes}
|
|
180
|
+
{...attributes}
|
|
181
|
+
>
|
|
182
|
+
{@render children?.()}
|
|
183
|
+
{#if hasIndicator && measured}
|
|
184
|
+
<span
|
|
185
|
+
class="fs-top-nav-indicator"
|
|
186
|
+
aria-hidden="true"
|
|
187
|
+
style:--fs-top-nav-indicator-offset="{offset}px"
|
|
188
|
+
style:--fs-top-nav-indicator-extent="{extent}px"
|
|
189
|
+
></span>
|
|
190
|
+
{/if}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<style>
|
|
194
|
+
.fs-top-nav {
|
|
195
|
+
position: relative;
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: stretch;
|
|
198
|
+
font-family: var(--fs-font-family-base);
|
|
199
|
+
color: var(--fs-text-primary);
|
|
200
|
+
/* Read by every item, so a size change ripples out of the strip alone. */
|
|
201
|
+
--fs-top-nav-item-padding-inline: 0.625rem;
|
|
202
|
+
--fs-top-nav-item-padding-block: 0.375rem;
|
|
203
|
+
--fs-top-nav-item-font-size: var(--fs-body2-font-size);
|
|
204
|
+
--fs-top-nav-item-line-height: var(--fs-body2-line-height);
|
|
205
|
+
--fs-top-nav-item-gap: 0.5rem;
|
|
206
|
+
--fs-top-nav-item-icon-size: 1.25rem;
|
|
207
|
+
--fs-top-nav-bar-thickness: 0.1875rem;
|
|
208
|
+
&.small {
|
|
209
|
+
--fs-top-nav-item-padding-inline: 0.5rem;
|
|
210
|
+
--fs-top-nav-item-padding-block: 0.375rem;
|
|
211
|
+
--fs-top-nav-item-font-size: var(--fs-caption-font-size);
|
|
212
|
+
--fs-top-nav-item-line-height: var(--fs-caption-line-height);
|
|
213
|
+
--fs-top-nav-item-gap: 0.375rem;
|
|
214
|
+
--fs-top-nav-item-icon-size: 1rem;
|
|
215
|
+
--fs-top-nav-bar-thickness: 0.125rem;
|
|
216
|
+
}
|
|
217
|
+
&.large {
|
|
218
|
+
--fs-top-nav-item-padding-block: 0.875rem;
|
|
219
|
+
--fs-top-nav-item-font-size: var(--fs-body-font-size);
|
|
220
|
+
--fs-top-nav-item-line-height: var(--fs-body-line-height);
|
|
221
|
+
}
|
|
222
|
+
&.vertical {
|
|
223
|
+
flex-direction: column;
|
|
224
|
+
align-items: stretch;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* The bar. It is placed from the measurement rather than laid out, so moving it is a transform
|
|
230
|
+
* the compositor can carry — and a transform is what keeps it on screen the whole way across,
|
|
231
|
+
* instead of leaving one item and arriving at the next.
|
|
232
|
+
*/
|
|
233
|
+
.fs-top-nav-indicator {
|
|
234
|
+
position: absolute;
|
|
235
|
+
left: 0;
|
|
236
|
+
bottom: 0;
|
|
237
|
+
width: var(--fs-top-nav-indicator-extent, 0);
|
|
238
|
+
height: var(--fs-top-nav-bar-thickness);
|
|
239
|
+
border-radius: var(--fs-top-nav-bar-thickness);
|
|
240
|
+
background: var(--fs-accent-fill-default);
|
|
241
|
+
/* `offsetLeft` is measured from the left whichever way the strip is written. */
|
|
242
|
+
transform: translateX(var(--fs-top-nav-indicator-offset, 0));
|
|
243
|
+
transition:
|
|
244
|
+
transform var(--fs-normal-duration) var(--fs-point-to-point),
|
|
245
|
+
width var(--fs-normal-duration) var(--fs-point-to-point),
|
|
246
|
+
height var(--fs-normal-duration) var(--fs-point-to-point);
|
|
247
|
+
pointer-events: none;
|
|
248
|
+
}
|
|
249
|
+
.fs-top-nav.vertical .fs-top-nav-indicator {
|
|
250
|
+
top: 0;
|
|
251
|
+
bottom: auto;
|
|
252
|
+
/* The leading edge, so a strip written right to left grows its bar on the right. */
|
|
253
|
+
left: auto;
|
|
254
|
+
inset-inline-start: 0;
|
|
255
|
+
width: var(--fs-top-nav-bar-thickness);
|
|
256
|
+
height: var(--fs-top-nav-indicator-extent, 0);
|
|
257
|
+
transform: translateY(var(--fs-top-nav-indicator-offset, 0));
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TopNavProps } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A top nav gives a single selection from a row of items, with a bar that travels to whichever one
|
|
4
|
+
* is selected. It owns the selection alone: what an item opens is rendered by the consumer.
|
|
5
|
+
*
|
|
6
|
+
* - Usage:
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <script>
|
|
9
|
+
* import { TopNav, TopNavItem } from 'fluentui-svelte';
|
|
10
|
+
*
|
|
11
|
+
* let selectedValue = $state('arrivals');
|
|
12
|
+
* </script>
|
|
13
|
+
*
|
|
14
|
+
* <TopNav bind:selectedValue aria-label="Flights">
|
|
15
|
+
* <TopNavItem value="arrivals">Arrivals</TopNavItem>
|
|
16
|
+
* <TopNavItem value="departures">Departures</TopNavItem>
|
|
17
|
+
* </TopNav>
|
|
18
|
+
*
|
|
19
|
+
* {#if selectedValue === 'arrivals'}
|
|
20
|
+
* <p>Arrivals</p>
|
|
21
|
+
* {/if}
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare const TopNav: import("svelte").Component<TopNavProps, {}, "ref" | "selectedValue">;
|
|
25
|
+
type TopNav = ReturnType<typeof TopNav>;
|
|
26
|
+
export default TopNav;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PREFIX } from '../../internal/constants.js';
|
|
2
|
+
import { createFSContext } from '../../internal/index.js';
|
|
3
|
+
export const COMPONENT_NAME = `${PREFIX}-top-nav`;
|
|
4
|
+
export const [getTopNavContext, setTopNavContext] = createFSContext();
|
|
5
|
+
/** The items a mover lands on, and what the bar is measured against. */
|
|
6
|
+
export const TABSPOT_ITEMS = '.fs-top-nav-item';
|
|
7
|
+
/**
|
|
8
|
+
* Items the cursor passes over instead of landing on: a disabled item answers nothing once reached.
|
|
9
|
+
* A button says so with the attribute of its own; an anchor, which has none, says it to ARIA.
|
|
10
|
+
*/
|
|
11
|
+
export const TABSPOT_SKIP = '[disabled], [aria-disabled="true"]';
|
|
12
|
+
/** Reads the top nav context, or fails loudly when an item is rendered outside a `TopNav`. */
|
|
13
|
+
export const requireTopNavContext = () => {
|
|
14
|
+
const context = getTopNavContext();
|
|
15
|
+
if (!context)
|
|
16
|
+
throw new Error(`No TopNavContext found for ${COMPONENT_NAME}.`);
|
|
17
|
+
return context;
|
|
18
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { FSContext } from '../../internal/index.js';
|
|
2
|
+
import type { PolymorphicProps, Sizes } from '../../types/index.js';
|
|
3
|
+
import type { Snippet, Component } from 'svelte';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
/** How the strip and every item in it is painted. */
|
|
6
|
+
export type TopNavAppearance = 'transparent' | 'subtle' | 'subtle-circular' | 'filled-circular';
|
|
7
|
+
/** An item is a button by default, and an anchor when it goes somewhere. */
|
|
8
|
+
export type TopNavItemGenerics = 'button' | 'a';
|
|
9
|
+
export type TopNavItemElementDOMType = {
|
|
10
|
+
button: HTMLButtonElement;
|
|
11
|
+
a: HTMLAnchorElement;
|
|
12
|
+
};
|
|
13
|
+
export type TopNavContext = FSContext<{
|
|
14
|
+
appearance: TopNavAppearance;
|
|
15
|
+
size: Sizes;
|
|
16
|
+
vertical: boolean;
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
selectTabOnFocus: boolean;
|
|
19
|
+
reserveSelectedTabSpace: boolean;
|
|
20
|
+
}, {
|
|
21
|
+
selectedValue: string;
|
|
22
|
+
}, null, {
|
|
23
|
+
selectTab: (event: Event, value: string) => void;
|
|
24
|
+
}>;
|
|
25
|
+
/** @propsmith TopNavProps */
|
|
26
|
+
export type TopNavProps = {
|
|
27
|
+
/** The DOM reference of the strip.
|
|
28
|
+
* @bindable
|
|
29
|
+
*/
|
|
30
|
+
ref?: HTMLDivElement;
|
|
31
|
+
/** The value of the selected item.
|
|
32
|
+
* @bindable
|
|
33
|
+
*/
|
|
34
|
+
selectedValue?: string;
|
|
35
|
+
/** Fired when an item is selected. */
|
|
36
|
+
onTabSelect?: (event: Event, value: string) => void;
|
|
37
|
+
/** How the strip is painted: `transparent` has no background of its own, `subtle` fills an item
|
|
38
|
+
* while it is engaged, and the two circular appearances trade the moving bar for a pill.
|
|
39
|
+
* @default 'transparent'
|
|
40
|
+
*/
|
|
41
|
+
appearance?: TopNavAppearance;
|
|
42
|
+
/** The size of every item of the strip.
|
|
43
|
+
* @default 'medium'
|
|
44
|
+
*/
|
|
45
|
+
size?: Sizes;
|
|
46
|
+
/** Arranges the items in a column, with the bar down their leading edge.
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
vertical?: boolean;
|
|
50
|
+
/** Disables the user interaction on every item.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
/** Selects an item as soon as the focus reaches it, rather than waiting to be asked.
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
selectTabOnFocus?: boolean;
|
|
58
|
+
/** A selected item sets its label in semibold, which is wider than the same label at rest. This
|
|
59
|
+
* holds that width from the start, so selecting an item never shifts the ones beside it.
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
reserveSelectedTabSpace?: boolean;
|
|
63
|
+
/** Opts the strip out of the tabspot focus management, to wire your own.
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
disableTabspot?: boolean;
|
|
67
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
68
|
+
/** @propsmith TopNavItemProps */
|
|
69
|
+
export type TopNavItemProps<T extends TopNavItemGenerics = 'button'> = {
|
|
70
|
+
/** The DOM reference of the item.
|
|
71
|
+
* @type TopNavItemElementDOMType[T]
|
|
72
|
+
* @bindable
|
|
73
|
+
*/
|
|
74
|
+
ref?: TopNavItemElementDOMType[T];
|
|
75
|
+
/** The DOM element to render. An anchor takes the attributes of one, `href` among them, and is
|
|
76
|
+
* what an item that navigates should be.
|
|
77
|
+
* @type 'button' | 'a'
|
|
78
|
+
* @default 'button'
|
|
79
|
+
*/
|
|
80
|
+
as?: T;
|
|
81
|
+
/** The value that identifies the item. Defaults to a generated id. */
|
|
82
|
+
value?: string;
|
|
83
|
+
/** The icon to display before the label.
|
|
84
|
+
* @type Snippet | Component
|
|
85
|
+
*/
|
|
86
|
+
icon?: Snippet | Component;
|
|
87
|
+
/** Disables the user interaction. */
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
} & PolymorphicProps<T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|