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,146 @@
|
|
|
1
|
+
# TabView
|
|
2
|
+
|
|
3
|
+
TabView lays a set of tabs out as a strip, in the shape a browser gives its document tabs. Every tab can carry an icon and a close button, and a new tab button can sit at the end of the strip.
|
|
4
|
+
|
|
5
|
+
The strip owns the selection and nothing else. What a tab opens is rendered by you, wherever it belongs on the page, so a tab can open a panel underneath it, swap a region somewhere else, or drive a route.
|
|
6
|
+
|
|
7
|
+
To create a tab strip, you will need to use the following components:
|
|
8
|
+
|
|
9
|
+
- `TabView`: The root component that creates the context every tab reads: which tab is selected, whether the tabs close, and how they take their width. It renders the `tablist` and, optionally, the new tab button.
|
|
10
|
+
- `TabViewTab`: A single tab of the strip. It reports its own selection and can carry an icon and a close button.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
A tab strip expects a `value` on every tab and an `activeTab` to match it against. Bind `activeTab` to follow the selection, and render whatever the selected tab opens yourself.
|
|
15
|
+
|
|
16
|
+
```svelte
|
|
17
|
+
<script>
|
|
18
|
+
import { TabView, TabViewTab } from 'fluentui-svelte';
|
|
19
|
+
|
|
20
|
+
let activeTab = $state('document');
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<TabView bind:activeTab listProps={{ 'aria-label': 'Workspaces' }}>
|
|
24
|
+
<TabViewTab id="tab-document" value="document">Document</TabViewTab>
|
|
25
|
+
<TabViewTab id="tab-mail" value="mail">Mail</TabViewTab>
|
|
26
|
+
</TabView>
|
|
27
|
+
|
|
28
|
+
{#if activeTab === 'document'}
|
|
29
|
+
<div role="tabpanel" aria-labelledby="tab-document">The document you are working on.</div>
|
|
30
|
+
{:else}
|
|
31
|
+
<div role="tabpanel" aria-labelledby="tab-mail">Everything that landed in your inbox.</div>
|
|
32
|
+
{/if}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
### Closing Tabs
|
|
38
|
+
|
|
39
|
+
`closable` gives every tab a close button, and a single tab can opt in or out on its own. The strip never removes a tab: the tabs are your markup, so `onTabClose` hands the value back and leaves the removal — and which tab takes over — to you.
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<script>
|
|
43
|
+
import { TabView, TabViewTab } from 'fluentui-svelte';
|
|
44
|
+
|
|
45
|
+
let tabs = $state([
|
|
46
|
+
{ value: 'notes', label: 'Notes' },
|
|
47
|
+
{ value: 'draft', label: 'Draft' }
|
|
48
|
+
]);
|
|
49
|
+
let activeTab = $state('notes');
|
|
50
|
+
|
|
51
|
+
const closeTab = (event, value) => {
|
|
52
|
+
const index = tabs.findIndex((tab) => tab.value === value);
|
|
53
|
+
tabs = tabs.filter((tab) => tab.value !== value);
|
|
54
|
+
if (activeTab !== value) return;
|
|
55
|
+
activeTab = tabs[Math.min(index, tabs.length - 1)]?.value ?? '';
|
|
56
|
+
};
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<TabView bind:activeTab closable onTabClose={closeTab}>
|
|
60
|
+
{#each tabs as tab (tab.value)}
|
|
61
|
+
<TabViewTab value={tab.value}>{tab.label}</TabViewTab>
|
|
62
|
+
{/each}
|
|
63
|
+
</TabView>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### New Tab Button
|
|
67
|
+
|
|
68
|
+
`showNewTabButton` adds a button at the end of the strip, outside the `tablist` so it never reads as a tab. `onNewTab` fires when it is activated.
|
|
69
|
+
|
|
70
|
+
```svelte
|
|
71
|
+
<TabView bind:activeTab showNewTabButton onNewTab={addTab}>
|
|
72
|
+
{#each tabs as tab (tab.value)}
|
|
73
|
+
<TabViewTab value={tab.value}>{tab.label}</TabViewTab>
|
|
74
|
+
{/each}
|
|
75
|
+
</TabView>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Tab Width
|
|
79
|
+
|
|
80
|
+
`tabWidthMode` decides how the tabs take their width: `equal` shares the strip evenly, `size-to-content` sizes every tab to its own label, and `compact` shrinks every resting tab to its icon and spells out only the selected one.
|
|
81
|
+
|
|
82
|
+
```svelte
|
|
83
|
+
<TabView tabWidthMode="equal">...</TabView>
|
|
84
|
+
<TabView tabWidthMode="size-to-content">...</TabView>
|
|
85
|
+
<TabView tabWidthMode="compact">...</TabView>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Disabled
|
|
89
|
+
|
|
90
|
+
A disabled tab cannot be selected, and the arrow keys pass over it rather than landing on it.
|
|
91
|
+
|
|
92
|
+
```svelte
|
|
93
|
+
<TabView activeTab="mail">
|
|
94
|
+
<TabViewTab value="document">Document</TabViewTab>
|
|
95
|
+
<TabViewTab value="mail">Mail</TabViewTab>
|
|
96
|
+
<TabViewTab value="calendar" disabled>Calendar</TabViewTab>
|
|
97
|
+
</TabView>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Component Props (TabView)
|
|
101
|
+
|
|
102
|
+
<!-- DO NOT EDIT THIS SECTION (propsmith generated) -->
|
|
103
|
+
<!-- props:TabViewProps -->
|
|
104
|
+
|
|
105
|
+
| Name | Type | Default | Description |
|
|
106
|
+
| -------------------------- | ------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `ref` _bindable_ | `HTMLDivElement` | | The DOM reference of the element wrapping the strip. |
|
|
108
|
+
| `listRef` _bindable_ | `HTMLDivElement` | | The DOM reference of the tab list. |
|
|
109
|
+
| `listProps` | `HTMLAttributes` | | The props to spread on the tab list element, where the `tablist` role and its label belong. |
|
|
110
|
+
| `activeTab` _bindable_ | `string` | | The value of the selected tab. |
|
|
111
|
+
| `onTabChange` | `(event: Event, value: string) => void` | | Fired when the selected tab changes. |
|
|
112
|
+
| `closable` | `boolean` | `false` | Every tab shows a close button, unless the tab opts out of it. |
|
|
113
|
+
| `onTabClose` | `(event: Event, value: string) => void` | | Fired when a tab asks to be closed. Removing the tab is left to the consumer. |
|
|
114
|
+
| `showNewTabButton` | `boolean` | `false` | Shows the button that adds a new tab at the end of the strip. |
|
|
115
|
+
| `onNewTab` | `(event: MouseEvent) => void` | | Fired when the new tab button is activated. |
|
|
116
|
+
| `newTabButtonProps` | `HTMLButtonAttributes` | | The props to spread on the new tab button. |
|
|
117
|
+
| `tabWidthMode` | `'equal'` | `'size-to-content'` | `'compact'` | `'equal'` | How the tabs take their width: sharing the strip evenly, sizing to their own content, or shrinking to the icon of every tab but the selected one. |
|
|
118
|
+
| `disableTabspot` | `boolean` | `false` | Opts the strip out of the tabspot focus management, to wire your own. |
|
|
119
|
+
| Element Attributes (`div`) | | | |
|
|
120
|
+
|
|
121
|
+
<!-- /props:TabViewProps -->
|
|
122
|
+
|
|
123
|
+
## Component Props (TabViewTab)
|
|
124
|
+
|
|
125
|
+
<!-- DO NOT EDIT THIS SECTION (propsmith generated) -->
|
|
126
|
+
<!-- props:TabViewTabProps -->
|
|
127
|
+
|
|
128
|
+
| Name | Type | Default | Description |
|
|
129
|
+
| -------------------------- | --------------------------------------- | ------- | --------------------------------------------------------------------------------------- |
|
|
130
|
+
| `ref` _bindable_ | `HTMLDivElement` | | The DOM reference of the tab element. |
|
|
131
|
+
| `value` | `string` | | The value that identifies the tab. Defaults to a generated id. |
|
|
132
|
+
| `icon` | `Snippet` | `Component` | | The icon to display before the label. |
|
|
133
|
+
| `closable` | `boolean` | | Shows the close button of the tab. Falls back to the `closable` of the strip. |
|
|
134
|
+
| `disabled` | `boolean` | | Disables the user interaction. |
|
|
135
|
+
| `onClose` | `(event: Event, value: string) => void` | | Fired when the tab asks to be closed, either from its close button or the `Delete` key. |
|
|
136
|
+
| Element Attributes (`div`) | | | |
|
|
137
|
+
|
|
138
|
+
<!-- /props:TabViewTabProps -->
|
|
139
|
+
|
|
140
|
+
## Keyboard Navigation
|
|
141
|
+
|
|
142
|
+
The strip is a single tab stop. `ArrowLeft` and `ArrowRight` walk the tabs, `Home` and `End` jump to the ends, `Enter` and `Space` select the focused tab, and `Delete` closes it when it is closable. `disableTabspot` hands the focus management back to you.
|
|
143
|
+
|
|
144
|
+
## Accessibility
|
|
145
|
+
|
|
146
|
+
The strip renders a `tablist` and every tab a `tab`, so give the list a name through the `aria-label` you pass in `listProps`. Since you render what a tab opens, give that element a `tabpanel` role and point its `aria-labelledby` at the `id` of its tab.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { invokeHandlers, RenderSoC } from '../../internal/index.js';
|
|
3
|
+
import DismissRegular from 'fluentui-icons-svelte/DismissRegular.svelte';
|
|
4
|
+
import { COMPONENT_NAME, requireTabViewContext } from './tab-view.svelte.js';
|
|
5
|
+
import type { TabViewTabProps } from './types.js';
|
|
6
|
+
|
|
7
|
+
const ID = $props.id();
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
ref = $bindable(),
|
|
11
|
+
value = `${COMPONENT_NAME}-${ID}`,
|
|
12
|
+
icon,
|
|
13
|
+
closable,
|
|
14
|
+
disabled,
|
|
15
|
+
onClose,
|
|
16
|
+
onclick,
|
|
17
|
+
onkeydown,
|
|
18
|
+
class: classes,
|
|
19
|
+
children,
|
|
20
|
+
...attributes
|
|
21
|
+
}: TabViewTabProps = $props();
|
|
22
|
+
|
|
23
|
+
const context = requireTabViewContext();
|
|
24
|
+
|
|
25
|
+
const active = $derived(context.state.activeTab === value);
|
|
26
|
+
|
|
27
|
+
/** The tab decides for itself, and falls back to what the strip asked of every one of them. */
|
|
28
|
+
const _closable = $derived(closable ?? context.config.closable);
|
|
29
|
+
|
|
30
|
+
const close = (event: Event) => {
|
|
31
|
+
onClose?.(event, value);
|
|
32
|
+
context.methods.closeTab(event, value);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A `div` carries the tab, because a `button` cannot hold the close `button` a browser tab has.
|
|
37
|
+
* That costs the keys a button would have answered on its own, so they are handled here:
|
|
38
|
+
* `Enter`/`Space` select, and `Delete` closes — the tabs pattern reserves it for exactly that.
|
|
39
|
+
*
|
|
40
|
+
* The close button is held out of the tab order, so the strip stays a single tab stop. `Delete`
|
|
41
|
+
* is what reaches it from the keyboard, which is why the key is bound even while the pointer
|
|
42
|
+
* has a button of its own.
|
|
43
|
+
*/
|
|
44
|
+
const onTabKeydown = (event: KeyboardEvent) => {
|
|
45
|
+
// A key spent inside the close button is already answered; only the tab itself speaks here.
|
|
46
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
47
|
+
|
|
48
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
context.methods.setActiveTab(event, value);
|
|
51
|
+
} else if (event.key === 'Delete' && _closable) {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
close(event);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<!--
|
|
59
|
+
@component
|
|
60
|
+
A single tab of a `TabView`. It reports its own selection, and can carry an icon and a close
|
|
61
|
+
button.
|
|
62
|
+
|
|
63
|
+
- Usage:
|
|
64
|
+
```tsx
|
|
65
|
+
<script>
|
|
66
|
+
import { TabView, TabViewTab } from 'fluentui-svelte';
|
|
67
|
+
import { DocumentRegular } from 'fluentui-icons-svelte';
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<TabView activeTab="notes">
|
|
71
|
+
<TabViewTab value="notes" icon={DocumentRegular} closable>Notes</TabViewTab>
|
|
72
|
+
</TabView>
|
|
73
|
+
```
|
|
74
|
+
-->
|
|
75
|
+
<div
|
|
76
|
+
bind:this={ref}
|
|
77
|
+
role="tab"
|
|
78
|
+
tabindex={disabled ? -1 : 0}
|
|
79
|
+
data-value={value}
|
|
80
|
+
aria-selected={active}
|
|
81
|
+
aria-disabled={disabled || undefined}
|
|
82
|
+
class={['fs-tab-view-tab', context.config.tabWidthMode, { active, disabled }, classes]}
|
|
83
|
+
onclick={(e: MouseEvent) => invokeHandlers(e, disabled, [() => context.methods.setActiveTab(e, value), onclick])}
|
|
84
|
+
onkeydown={(e: KeyboardEvent) => invokeHandlers(e, disabled, [onTabKeydown, onkeydown])}
|
|
85
|
+
{...attributes}
|
|
86
|
+
>
|
|
87
|
+
{#if icon}
|
|
88
|
+
<span class="fs-tab-view-tab-icon">
|
|
89
|
+
<RenderSoC SoC={icon} />
|
|
90
|
+
</span>
|
|
91
|
+
{/if}
|
|
92
|
+
<span class="fs-tab-view-tab-label">{@render children?.()}</span>
|
|
93
|
+
{#if _closable}
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
class="fs-tab-view-tab-close"
|
|
97
|
+
aria-label="Close tab"
|
|
98
|
+
tabindex={-1}
|
|
99
|
+
{disabled}
|
|
100
|
+
onclick={(e: MouseEvent) => {
|
|
101
|
+
// The tab underneath would otherwise read the click as "select me".
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
close(e);
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<DismissRegular />
|
|
107
|
+
</button>
|
|
108
|
+
{/if}
|
|
109
|
+
{#if active}
|
|
110
|
+
<!--
|
|
111
|
+
The inverted corners of the base. They are elements rather than the two pseudo-elements of
|
|
112
|
+
the tab because each corner needs a box of its own to cast the fill and a second one to lay
|
|
113
|
+
the stroke over it: a box never paints its own border over its own shadow.
|
|
114
|
+
-->
|
|
115
|
+
<span class="fs-tab-view-tab-foot start"></span>
|
|
116
|
+
<span class="fs-tab-view-tab-foot end"></span>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<style>
|
|
121
|
+
.fs-tab-view-tab {
|
|
122
|
+
position: relative;
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 0.5rem;
|
|
126
|
+
box-sizing: border-box;
|
|
127
|
+
height: var(--fs-tab-view-tab-height, 2.25rem);
|
|
128
|
+
padding: 0 0.5rem 0 0.75rem;
|
|
129
|
+
border: 1px solid transparent;
|
|
130
|
+
/* Only the top corners round: the bottom edge is where the tab meets what it opens. */
|
|
131
|
+
border-radius: var(--fs-control-overlay-border-radius) var(--fs-control-overlay-border-radius) 0 0;
|
|
132
|
+
background: transparent;
|
|
133
|
+
color: var(--fs-text-secondary);
|
|
134
|
+
font-size: var(--fs-body-font-size);
|
|
135
|
+
line-height: var(--fs-body-line-height);
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
user-select: none;
|
|
138
|
+
outline: none;
|
|
139
|
+
/* Pull the tab down by 1px to overlap the strip below it. */
|
|
140
|
+
bottom: -1px;
|
|
141
|
+
&:hover {
|
|
142
|
+
background: var(--fs-subtle-fill-secondary);
|
|
143
|
+
color: var(--fs-text-primary);
|
|
144
|
+
}
|
|
145
|
+
&:focus-visible {
|
|
146
|
+
outline: 0.125rem var(--fs-focus-stroke-outer) solid;
|
|
147
|
+
outline-offset: -0.125rem;
|
|
148
|
+
}
|
|
149
|
+
&.active {
|
|
150
|
+
/* The selected tab is the page it opens, pulled up above the strip. */
|
|
151
|
+
background: var(--fs-solid-background-quaternary);
|
|
152
|
+
border-color: var(--fs-card-stroke-default);
|
|
153
|
+
/* The base is where the tab meets what it opens, so nothing is drawn across it. */
|
|
154
|
+
border-bottom: 1px solid var(--fs-solid-background-quaternary);
|
|
155
|
+
color: var(--fs-text-primary);
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
/* The feet reach over the neighbouring tabs, so the selected one paints above them. */
|
|
158
|
+
z-index: 1;
|
|
159
|
+
bottom: -2px;
|
|
160
|
+
}
|
|
161
|
+
/*
|
|
162
|
+
* The inverted corners a document tab has: instead of stopping square, each side curves out
|
|
163
|
+
* into the strip, which is what reads as one surface rather than a button sitting on top of
|
|
164
|
+
* one.
|
|
165
|
+
*
|
|
166
|
+
* The foot casts the fill. Its own shape — a box the size of the radius, rounded away from
|
|
167
|
+
* the tab — is the quarter disc the curve leaves behind, and a shadow is never painted under
|
|
168
|
+
* the shape casting it, so the spread lands exactly in the corner around the arc. The clip is
|
|
169
|
+
* what makes that usable: a shadow spreads in every direction, and without it the surface
|
|
170
|
+
* would bleed over the neighbouring tabs and below the base. A radius of spread reaches the
|
|
171
|
+
* far corner of the box, which is all it has to.
|
|
172
|
+
*/
|
|
173
|
+
& .fs-tab-view-tab-foot {
|
|
174
|
+
position: absolute;
|
|
175
|
+
bottom: 1px;
|
|
176
|
+
width: var(--fs-tab-view-fillet, 0.4rem);
|
|
177
|
+
height: var(--fs-tab-view-fillet, 0.4rem);
|
|
178
|
+
box-shadow: 0 0 0 var(--fs-tab-view-fillet, 0.4rem) var(--fs-solid-background-quaternary);
|
|
179
|
+
pointer-events: none;
|
|
180
|
+
/*
|
|
181
|
+
* The stroke that carries the border of the tab around the curve, laid over the fill
|
|
182
|
+
* rather than beside it. A border cannot do that from the foot itself: an outer shadow
|
|
183
|
+
* starts where the border box ends, so the two never overlap and the translucent stroke
|
|
184
|
+
* would read against the strip instead of against the surface. A child paints after the
|
|
185
|
+
* shadow of its parent, and sitting 1px outside the foot puts its border on the first
|
|
186
|
+
* pixel of the fill — which leaves its inner edge exactly on the arc.
|
|
187
|
+
*
|
|
188
|
+
* Only the two sides the corner is rounded between are drawn, which is the arc and nothing
|
|
189
|
+
* else. The other two run the length of the foot outside the clip, and a clipped edge
|
|
190
|
+
* still leaves the sliver its antialiasing painted.
|
|
191
|
+
*/
|
|
192
|
+
&::after {
|
|
193
|
+
content: '';
|
|
194
|
+
position: absolute;
|
|
195
|
+
inset: -1px;
|
|
196
|
+
border: 0 solid var(--fs-card-stroke-default);
|
|
197
|
+
}
|
|
198
|
+
&.start {
|
|
199
|
+
left: calc(-1 * var(--fs-tab-view-fillet, 0.4rem) - 1px);
|
|
200
|
+
border-bottom-right-radius: var(--fs-tab-view-fillet, 0.4rem);
|
|
201
|
+
clip-path: polygon(0% 20%, 120% 10%, 120% 132%, 10% 132%);
|
|
202
|
+
&::after {
|
|
203
|
+
border-width: 0 1px 1px 0;
|
|
204
|
+
border-bottom-right-radius: calc(var(--fs-tab-view-fillet, 0.4rem) + 1px);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
&.end {
|
|
208
|
+
right: calc(-1 * var(--fs-tab-view-fillet, 0.4rem) - 1px);
|
|
209
|
+
border-bottom-left-radius: var(--fs-tab-view-fillet, 0.4rem);
|
|
210
|
+
clip-path: polygon(-20% 10%, 90% 10%, 90% 132%, -20% 132%);
|
|
211
|
+
&::after {
|
|
212
|
+
border-width: 0 0 1px 1px;
|
|
213
|
+
border-bottom-left-radius: calc(var(--fs-tab-view-fillet, 0.4rem) + 1px);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
&.disabled {
|
|
218
|
+
color: var(--fs-text-disabled);
|
|
219
|
+
cursor: not-allowed;
|
|
220
|
+
&:hover {
|
|
221
|
+
background: transparent;
|
|
222
|
+
color: var(--fs-text-disabled);
|
|
223
|
+
}
|
|
224
|
+
& .fs-tab-view-tab-close {
|
|
225
|
+
cursor: not-allowed;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/* Share the strip evenly, within the width a document tab is worth. */
|
|
229
|
+
&.equal {
|
|
230
|
+
flex: 1 1 0;
|
|
231
|
+
min-width: 5rem;
|
|
232
|
+
max-width: 12.5rem;
|
|
233
|
+
}
|
|
234
|
+
&.size-to-content {
|
|
235
|
+
flex: 0 0 auto;
|
|
236
|
+
}
|
|
237
|
+
/* A resting tab shrinks to its icon, and only the selected one spells itself out. */
|
|
238
|
+
&.compact:not(.active) {
|
|
239
|
+
flex: 0 0 auto;
|
|
240
|
+
padding: 0 0.5rem;
|
|
241
|
+
& .fs-tab-view-tab-label,
|
|
242
|
+
& .fs-tab-view-tab-close {
|
|
243
|
+
display: none;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
& .fs-tab-view-tab-icon {
|
|
247
|
+
display: flex;
|
|
248
|
+
flex: 0 0 auto;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
& :global(svg) {
|
|
252
|
+
width: 1rem;
|
|
253
|
+
height: 1rem;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
& .fs-tab-view-tab-label {
|
|
257
|
+
flex: 1 1 auto;
|
|
258
|
+
min-width: 0;
|
|
259
|
+
overflow: hidden;
|
|
260
|
+
text-overflow: ellipsis;
|
|
261
|
+
white-space: nowrap;
|
|
262
|
+
text-align: start;
|
|
263
|
+
}
|
|
264
|
+
& .fs-tab-view-tab-close {
|
|
265
|
+
display: flex;
|
|
266
|
+
flex: 0 0 auto;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
width: 1.25rem;
|
|
270
|
+
height: 1.25rem;
|
|
271
|
+
padding: 0;
|
|
272
|
+
border: none;
|
|
273
|
+
border-radius: var(--fs-control-border-radius);
|
|
274
|
+
background: transparent;
|
|
275
|
+
color: inherit;
|
|
276
|
+
cursor: pointer;
|
|
277
|
+
&:hover:not(:disabled) {
|
|
278
|
+
background: var(--fs-subtle-fill-secondary);
|
|
279
|
+
}
|
|
280
|
+
&:active:not(:disabled) {
|
|
281
|
+
background: var(--fs-subtle-fill-tertiary);
|
|
282
|
+
}
|
|
283
|
+
& :global(svg) {
|
|
284
|
+
width: 0.75rem;
|
|
285
|
+
height: 0.75rem;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/* The hairline between two resting tabs is drawn by the strip: only it sees both of them. */
|
|
290
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TabViewTabProps } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A single tab of a `TabView`. It reports its own selection, and can carry an icon and a close
|
|
4
|
+
* button.
|
|
5
|
+
*
|
|
6
|
+
* - Usage:
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <script>
|
|
9
|
+
* import { TabView, TabViewTab } from 'fluentui-svelte';
|
|
10
|
+
* import { DocumentRegular } from 'fluentui-icons-svelte';
|
|
11
|
+
* </script>
|
|
12
|
+
*
|
|
13
|
+
* <TabView activeTab="notes">
|
|
14
|
+
* <TabViewTab value="notes" icon={DocumentRegular} closable>Notes</TabViewTab>
|
|
15
|
+
* </TabView>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare const TabViewTab: import("svelte").Component<TabViewTabProps, {}, "ref">;
|
|
19
|
+
type TabViewTab = ReturnType<typeof TabViewTab>;
|
|
20
|
+
export default TabViewTab;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTabspotAttributes } from 'tabspot';
|
|
3
|
+
import AddRegular from 'fluentui-icons-svelte/AddRegular.svelte';
|
|
4
|
+
import { setTabViewContext, TABSPOT_ITEMS, TABSPOT_SKIP } from './tab-view.svelte.js';
|
|
5
|
+
import type { TabViewContext, TabViewProps } from './types.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
ref = $bindable(),
|
|
9
|
+
listRef = $bindable(),
|
|
10
|
+
listProps,
|
|
11
|
+
activeTab = $bindable(''),
|
|
12
|
+
onTabChange,
|
|
13
|
+
closable = false,
|
|
14
|
+
onTabClose,
|
|
15
|
+
showNewTabButton = false,
|
|
16
|
+
onNewTab,
|
|
17
|
+
newTabButtonProps,
|
|
18
|
+
tabWidthMode = 'equal',
|
|
19
|
+
disableTabspot = false,
|
|
20
|
+
class: classes,
|
|
21
|
+
children,
|
|
22
|
+
...attributes
|
|
23
|
+
}: TabViewProps = $props();
|
|
24
|
+
|
|
25
|
+
const context: TabViewContext = $state({
|
|
26
|
+
config: {
|
|
27
|
+
get closable() {
|
|
28
|
+
return closable;
|
|
29
|
+
},
|
|
30
|
+
get tabWidthMode() {
|
|
31
|
+
return tabWidthMode;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
state: {
|
|
35
|
+
get activeTab() {
|
|
36
|
+
return activeTab;
|
|
37
|
+
},
|
|
38
|
+
set activeTab(value: string) {
|
|
39
|
+
activeTab = value;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
events: null,
|
|
43
|
+
methods: {
|
|
44
|
+
setActiveTab: (event: Event, value: string) => {
|
|
45
|
+
if (activeTab === value) return;
|
|
46
|
+
activeTab = value;
|
|
47
|
+
onTabChange?.(event, value);
|
|
48
|
+
},
|
|
49
|
+
// Which tab takes over — and whether the tab goes at all — is the consumer's call: the
|
|
50
|
+
// tabs are their markup, so only they can remove one.
|
|
51
|
+
closeTab: (event: Event, value: string) => onTabClose?.(event, value)
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
setTabViewContext(context);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A single tab stop for the whole strip: the arrows walk the tabs and `Home`/`End` jump to the
|
|
59
|
+
* ends. `Escape` is left alone, a tab strip has nothing to exit to.
|
|
60
|
+
*/
|
|
61
|
+
const tabspotAttributes = $derived(
|
|
62
|
+
!disableTabspot &&
|
|
63
|
+
getTabspotAttributes({
|
|
64
|
+
root: { manageSpecialKeys: { Home: true, End: true } },
|
|
65
|
+
mover: { axis: 'horizontal', items: TABSPOT_ITEMS, skip: TABSPOT_SKIP }
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<!--
|
|
71
|
+
@component
|
|
72
|
+
A tab view lays a set of tabs out as a strip, in the shape a browser gives its document tabs.
|
|
73
|
+
It owns the selection alone: what a tab shows is rendered by the consumer, wherever it belongs.
|
|
74
|
+
|
|
75
|
+
- Usage:
|
|
76
|
+
```tsx
|
|
77
|
+
<script>
|
|
78
|
+
import { TabView, TabViewTab } from 'fluentui-svelte';
|
|
79
|
+
|
|
80
|
+
let activeTab = $state('home');
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<TabView bind:activeTab closable showNewTabButton>
|
|
84
|
+
<TabViewTab value="home">Home</TabViewTab>
|
|
85
|
+
<TabViewTab value="about">About</TabViewTab>
|
|
86
|
+
</TabView>
|
|
87
|
+
|
|
88
|
+
{#if activeTab === 'home'}
|
|
89
|
+
<p>Home</p>
|
|
90
|
+
{/if}
|
|
91
|
+
```
|
|
92
|
+
-->
|
|
93
|
+
<div bind:this={ref} class={['fs-tab-view', classes]} {...attributes}>
|
|
94
|
+
<div bind:this={listRef} role="tablist" class="fs-tab-view-list" {...tabspotAttributes} {...listProps}>
|
|
95
|
+
{@render children?.()}
|
|
96
|
+
</div>
|
|
97
|
+
{#if showNewTabButton}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="fs-tab-view-new-tab"
|
|
101
|
+
aria-label="Add new tab"
|
|
102
|
+
onclick={(e: MouseEvent) => onNewTab?.(e)}
|
|
103
|
+
{...newTabButtonProps}
|
|
104
|
+
>
|
|
105
|
+
<AddRegular />
|
|
106
|
+
</button>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<style>
|
|
111
|
+
.fs-tab-view {
|
|
112
|
+
display: flex;
|
|
113
|
+
/* The tabs grow up from the baseline of the strip, the way a document tab meets its page. */
|
|
114
|
+
align-items: flex-end;
|
|
115
|
+
font-family: var(--fs-font-family-base);
|
|
116
|
+
color: var(--fs-text-primary);
|
|
117
|
+
/* Read by every tab, so a height change ripples out of the root alone. */
|
|
118
|
+
--fs-tab-view-tab-height: 2.25rem;
|
|
119
|
+
/* The radius of the feet the selected tab curves out into, on either side of its base. */
|
|
120
|
+
--fs-tab-view-fillet: 0.4rem;
|
|
121
|
+
& .fs-tab-view-list {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: stretch;
|
|
124
|
+
/* Room for the feet of the first and the last tab, which the scroll box would clip. */
|
|
125
|
+
padding-inline: calc(var(--fs-tab-view-fillet, 0.4rem) * 2);
|
|
126
|
+
/* `equal` divides this, and a strip that outgrows it scrolls rather than wrapping. */
|
|
127
|
+
flex: 1 1 auto;
|
|
128
|
+
min-width: 0;
|
|
129
|
+
scrollbar-width: none;
|
|
130
|
+
/*
|
|
131
|
+
* The hairline that tells one resting tab from the next. A selected or hovered tab draws
|
|
132
|
+
* an edge of its own, so the line steps aside on either side of it. It lives here rather
|
|
133
|
+
* than in the tab, which renders one element and so can never see its own sibling.
|
|
134
|
+
*/
|
|
135
|
+
& :global(.fs-tab-view-tab:not(.active):not(:hover) + .fs-tab-view-tab:not(.active):not(:hover)::before) {
|
|
136
|
+
content: '';
|
|
137
|
+
position: absolute;
|
|
138
|
+
inset-inline-start: 0;
|
|
139
|
+
top: 25%;
|
|
140
|
+
bottom: 25%;
|
|
141
|
+
width: 1px;
|
|
142
|
+
background: var(--fs-divider-stroke-default);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
& .fs-tab-view-new-tab {
|
|
146
|
+
position: relative;
|
|
147
|
+
display: flex;
|
|
148
|
+
flex: 0 0 auto;
|
|
149
|
+
align-items: center;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
width: 2rem;
|
|
152
|
+
height: var(--fs-tab-view-tab-height);
|
|
153
|
+
margin-inline-start: 0.5rem;
|
|
154
|
+
border: none;
|
|
155
|
+
border-radius: var(--fs-control-border-radius);
|
|
156
|
+
background: transparent;
|
|
157
|
+
color: var(--fs-text-secondary);
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
outline: none;
|
|
160
|
+
&:hover {
|
|
161
|
+
background: var(--fs-subtle-fill-secondary);
|
|
162
|
+
color: var(--fs-text-primary);
|
|
163
|
+
}
|
|
164
|
+
&:active {
|
|
165
|
+
background: var(--fs-subtle-fill-tertiary);
|
|
166
|
+
color: var(--fs-text-secondary);
|
|
167
|
+
}
|
|
168
|
+
&:focus-visible {
|
|
169
|
+
outline: 0.125rem var(--fs-focus-stroke-outer) solid;
|
|
170
|
+
outline-offset: 0.063rem;
|
|
171
|
+
}
|
|
172
|
+
/* The hairline that separates the button from the last tab. */
|
|
173
|
+
&::before {
|
|
174
|
+
content: '';
|
|
175
|
+
position: absolute;
|
|
176
|
+
inset-inline-start: -0.25rem;
|
|
177
|
+
top: 25%;
|
|
178
|
+
bottom: 25%;
|
|
179
|
+
width: 1px;
|
|
180
|
+
background: var(--fs-divider-stroke-default);
|
|
181
|
+
}
|
|
182
|
+
& :global(svg) {
|
|
183
|
+
width: 1rem;
|
|
184
|
+
height: 1rem;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TabViewProps } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A tab view lays a set of tabs out as a strip, in the shape a browser gives its document tabs.
|
|
4
|
+
* It owns the selection alone: what a tab shows is rendered by the consumer, wherever it belongs.
|
|
5
|
+
*
|
|
6
|
+
* - Usage:
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <script>
|
|
9
|
+
* import { TabView, TabViewTab } from 'fluentui-svelte';
|
|
10
|
+
*
|
|
11
|
+
* let activeTab = $state('home');
|
|
12
|
+
* </script>
|
|
13
|
+
*
|
|
14
|
+
* <TabView bind:activeTab closable showNewTabButton>
|
|
15
|
+
* <TabViewTab value="home">Home</TabViewTab>
|
|
16
|
+
* <TabViewTab value="about">About</TabViewTab>
|
|
17
|
+
* </TabView>
|
|
18
|
+
*
|
|
19
|
+
* {#if activeTab === 'home'}
|
|
20
|
+
* <p>Home</p>
|
|
21
|
+
* {/if}
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare const TabView: import("svelte").Component<TabViewProps, {}, "ref" | "listRef" | "activeTab">;
|
|
25
|
+
type TabView = ReturnType<typeof TabView>;
|
|
26
|
+
export default TabView;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PREFIX } from '../../internal/constants.js';
|
|
2
|
+
import { createFSContext } from '../../internal/index.js';
|
|
3
|
+
export const COMPONENT_NAME = `${PREFIX}-tab-view`;
|
|
4
|
+
export const [getTabViewContext, setTabViewContext] = createFSContext();
|
|
5
|
+
/** The tabs a mover lands on. The new tab button sits outside the list, so it is never one of them. */
|
|
6
|
+
export const TABSPOT_ITEMS = '.fs-tab-view-tab';
|
|
7
|
+
/** Tabs the cursor passes over instead of landing on: a disabled tab answers nothing once reached. */
|
|
8
|
+
export const TABSPOT_SKIP = '[aria-disabled="true"]';
|
|
9
|
+
/** Reads the tab view context, or fails loudly when a tab is rendered outside a `TabView`. */
|
|
10
|
+
export const requireTabViewContext = () => {
|
|
11
|
+
const context = getTabViewContext();
|
|
12
|
+
if (!context)
|
|
13
|
+
throw new Error(`No TabViewContext found for ${COMPONENT_NAME}.`);
|
|
14
|
+
return context;
|
|
15
|
+
};
|