fluentui-svelte 0.5.0 → 0.6.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/dialog/dialog-actions.svelte +17 -2
- package/dist/components/dialog/dialog-content.svelte +2 -2
- package/dist/components/dialog/dialog-content.svelte.d.ts +1 -1
- package/dist/components/dialog/dialog-surface.svelte +32 -3
- package/dist/components/dialog/dialog-title.svelte +23 -6
- package/dist/components/dialog/dialog-trigger.svelte +8 -2
- package/dist/components/dialog/dialog-trigger.svelte.d.ts +1 -1
- package/dist/components/dialog/docs/llms.md +2 -2
- package/dist/components/dialog/types.d.ts +6 -2
- package/dist/components/dialog/utils.d.ts +2 -0
- package/dist/components/dialog/utils.js +1 -0
- package/dist/components/rating/docs/llms.md +110 -0
- package/dist/components/rating/rating.svelte +210 -0
- package/dist/components/rating/rating.svelte.d.ts +19 -0
- package/dist/components/rating/types.d.ts +44 -0
- package/dist/components/rating/types.js +1 -0
- package/dist/components/tag/docs/llms.md +383 -0
- package/dist/components/tag/tag-group.d.ts +2 -0
- package/dist/components/tag/tag-group.js +2 -0
- package/dist/components/tag/tag-group.svelte +117 -0
- package/dist/components/tag/tag-group.svelte.d.ts +20 -0
- package/dist/components/tag/tag.svelte +458 -0
- package/dist/components/tag/tag.svelte.d.ts +19 -0
- package/dist/components/tag/types.d.ts +135 -0
- package/dist/components/tag/types.js +1 -0
- package/dist/components/tag-picker/docs/llms.md +292 -0
- package/dist/components/tag-picker/option-group.svelte +38 -0
- package/dist/components/tag-picker/option-group.svelte.d.ts +4 -0
- package/dist/components/tag-picker/option.svelte +71 -0
- package/dist/components/tag-picker/option.svelte.d.ts +4 -0
- package/dist/components/tag-picker/tag-picker.d.ts +2 -0
- package/dist/components/tag-picker/tag-picker.js +2 -0
- package/dist/components/tag-picker/tag-picker.svelte +644 -0
- package/dist/components/tag-picker/tag-picker.svelte.d.ts +21 -0
- package/dist/components/tag-picker/types.d.ts +122 -0
- package/dist/components/tag-picker/types.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/internal/constants.d.ts +0 -4
- package/dist/internal/constants.js +0 -4002
- package/package.json +2 -2
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
import { getTabspotAttributes } from 'tabspot';
|
|
3
3
|
import type { DialogActionsProps } from './types.js';
|
|
4
4
|
|
|
5
|
-
let {
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(),
|
|
7
|
+
position = 'end',
|
|
8
|
+
fluid,
|
|
9
|
+
class: classes,
|
|
10
|
+
children,
|
|
11
|
+
...attributes
|
|
12
|
+
}: DialogActionsProps = $props();
|
|
6
13
|
|
|
7
14
|
// The bar is one tab stop holding a row of buttons: without the mover the root would
|
|
8
15
|
// take the tab stop and then offer no way to reach the rest of the buttons.
|
|
@@ -12,7 +19,12 @@
|
|
|
12
19
|
});
|
|
13
20
|
</script>
|
|
14
21
|
|
|
15
|
-
<div
|
|
22
|
+
<div
|
|
23
|
+
class={['dialog-actions', `justify-${position}`, { fluid }, classes]}
|
|
24
|
+
bind:this={ref}
|
|
25
|
+
{...tabspotAttributes}
|
|
26
|
+
{...attributes}
|
|
27
|
+
>
|
|
16
28
|
{@render children?.()}
|
|
17
29
|
</div>
|
|
18
30
|
|
|
@@ -33,5 +45,8 @@
|
|
|
33
45
|
&.justify-end {
|
|
34
46
|
justify-content: flex-end;
|
|
35
47
|
}
|
|
48
|
+
&.fluid > :global(*) {
|
|
49
|
+
flex: 1 1 0;
|
|
50
|
+
}
|
|
36
51
|
}
|
|
37
52
|
</style>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { DialogContentProps } from './types.js';
|
|
3
3
|
|
|
4
|
-
let { children }: DialogContentProps = $props();
|
|
4
|
+
let { ref = $bindable(), class: classes, children, ...attributes }: DialogContentProps = $props();
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
|
-
<div class=
|
|
7
|
+
<div class={['dialog-content', classes]} bind:this={ref} {...attributes}>
|
|
8
8
|
{@render children?.()}
|
|
9
9
|
</div>
|
|
10
10
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { DialogContentProps } from './types.ts';
|
|
2
|
-
declare const DialogContent: import("svelte").Component<DialogContentProps, {}, "">;
|
|
2
|
+
declare const DialogContent: import("svelte").Component<DialogContentProps, {}, "ref">;
|
|
3
3
|
type DialogContent = ReturnType<typeof DialogContent>;
|
|
4
4
|
export default DialogContent;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onClickOutside } from 'runed';
|
|
3
|
+
import { on } from 'svelte/events';
|
|
3
4
|
import type { DialogSurfaceProps } from './types.js';
|
|
4
5
|
import { getDialogContext } from './dialog.svelte.js';
|
|
5
6
|
import { Button } from '../../index.js';
|
|
6
7
|
import DismissFilled from 'fluentui-icons-svelte/DismissFilled.svelte';
|
|
7
8
|
|
|
8
|
-
let { ref = $bindable(), children, ...attributes }: DialogSurfaceProps = $props();
|
|
9
|
+
let { ref = $bindable(), class: classes, oncancel, children, ...attributes }: DialogSurfaceProps = $props();
|
|
9
10
|
|
|
10
11
|
const CONTEXT = getDialogContext();
|
|
11
12
|
|
|
@@ -17,22 +18,50 @@
|
|
|
17
18
|
|
|
18
19
|
const { closeDialog } = methods;
|
|
19
20
|
|
|
21
|
+
// runed reports an outside press 10ms after the pointer goes down. By then the press that opens the
|
|
22
|
+
// dialog has already opened it, and a press while it is closed has nothing to dismiss, so remember
|
|
23
|
+
// whether the dialog was open at the moment the pointer went down.
|
|
24
|
+
let openOnPointerDown = false;
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
const doc = ref?.ownerDocument;
|
|
28
|
+
if (!doc) return;
|
|
29
|
+
return on(doc, 'pointerdown', () => (openOnPointerDown = !!ref?.open), { capture: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
20
32
|
// Registered unconditionally and guarded from inside, so switching `type` after mount is honored.
|
|
21
33
|
onClickOutside(
|
|
22
34
|
() => ref,
|
|
23
35
|
() => {
|
|
24
|
-
if (config.type === 'alert') return;
|
|
36
|
+
if (config.type === 'alert' || !openOnPointerDown) return;
|
|
25
37
|
closeDialog();
|
|
26
38
|
}
|
|
27
39
|
);
|
|
28
40
|
|
|
41
|
+
// Escape cancels a modal dialog natively. An alert has to be answered with one of its actions, and any
|
|
42
|
+
// other modal closes through `closeDialog` so `onOpenChange` hears about it. A consumer's `oncancel`
|
|
43
|
+
// can still veto it with `preventDefault`.
|
|
44
|
+
function handleCancel(e: Event & { currentTarget: EventTarget & HTMLDialogElement }) {
|
|
45
|
+
oncancel?.(e);
|
|
46
|
+
if (e.defaultPrevented) return;
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
if (config.type !== 'alert') closeDialog();
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
$effect(() => {
|
|
30
52
|
if (!ref) return;
|
|
31
53
|
_state.dialogRef = ref;
|
|
32
54
|
});
|
|
33
55
|
</script>
|
|
34
56
|
|
|
35
|
-
<dialog
|
|
57
|
+
<dialog
|
|
58
|
+
open={_state.open}
|
|
59
|
+
class={['fs-dialog', classes]}
|
|
60
|
+
aria-labelledby={_state.titleId}
|
|
61
|
+
bind:this={ref}
|
|
62
|
+
oncancel={handleCancel}
|
|
63
|
+
{...attributes}
|
|
64
|
+
>
|
|
36
65
|
<div class="dialog-wrapper">
|
|
37
66
|
{#if config.type === 'non-modal'}
|
|
38
67
|
<Button class="close-icon" appearance="subtle" onclick={() => methods.closeDialog()} aria-label="Close dialog">
|
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends DialogTitleTag">
|
|
2
2
|
import { PREFIX } from '../../internal/constants.js';
|
|
3
3
|
import { getDialogContext } from './dialog.svelte.js';
|
|
4
|
+
import { TITLE_TAG } from './utils.js';
|
|
4
5
|
import type { DialogTitleProps, DialogTitleTag } from './types.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const _ID = $props.id();
|
|
8
|
+
const FALLBACK_ID = `${PREFIX}dialog-title-${_ID}`;
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
as = 'h3' as T,
|
|
12
|
+
ref = $bindable(),
|
|
13
|
+
id = FALLBACK_ID,
|
|
14
|
+
class: classes,
|
|
15
|
+
children,
|
|
16
|
+
...attributes
|
|
17
|
+
}: DialogTitleProps<T> = $props();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The tag the invariant rules over. The markup renders it through here, so it is checked
|
|
21
|
+
* during SSR and on every prop update rather than only once on mount.
|
|
22
|
+
*/
|
|
23
|
+
const _as = $derived.by(() => {
|
|
24
|
+
if (!TITLE_TAG.includes(as)) throw new Error(`Invalid tag: ${as}. Must be one of ${TITLE_TAG.join(', ')}`);
|
|
25
|
+
return as;
|
|
26
|
+
});
|
|
10
27
|
|
|
11
28
|
const context = getDialogContext();
|
|
12
29
|
|
|
13
30
|
$effect.pre(() => {
|
|
14
31
|
if (!context) return;
|
|
15
32
|
|
|
16
|
-
context.state.titleId =
|
|
33
|
+
context.state.titleId = id ?? undefined;
|
|
17
34
|
|
|
18
35
|
return () => {
|
|
19
36
|
context.state.titleId = undefined;
|
|
@@ -21,7 +38,7 @@
|
|
|
21
38
|
});
|
|
22
39
|
</script>
|
|
23
40
|
|
|
24
|
-
<svelte:element this={
|
|
41
|
+
<svelte:element this={_as} {id} class={['dialog-title', classes]} bind:this={ref} {...attributes}>
|
|
25
42
|
{@render children?.()}
|
|
26
43
|
</svelte:element>
|
|
27
44
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Button } from '../../index.js';
|
|
3
|
+
import { invokeHandlers } from '../../internal/index.js';
|
|
3
4
|
import { getDialogContext } from './dialog.svelte.js';
|
|
4
5
|
import type { DialogTriggerProps } from './types.js';
|
|
5
6
|
|
|
6
|
-
let { children }: DialogTriggerProps = $props();
|
|
7
|
+
let { ref = $bindable(), class: classes, onclick, children, ...attributes }: DialogTriggerProps = $props();
|
|
7
8
|
|
|
8
9
|
const CONTEXT = getDialogContext();
|
|
9
10
|
|
|
@@ -14,7 +15,12 @@
|
|
|
14
15
|
const { methods } = CONTEXT;
|
|
15
16
|
</script>
|
|
16
17
|
|
|
17
|
-
<Button
|
|
18
|
+
<Button
|
|
19
|
+
bind:ref
|
|
20
|
+
class={['dialog-trigger', classes]}
|
|
21
|
+
onclick={(e: MouseEvent) => invokeHandlers(e, [], [onclick, methods.openDialog])}
|
|
22
|
+
{...attributes}
|
|
23
|
+
>
|
|
18
24
|
{#if children}
|
|
19
25
|
{@render children()}
|
|
20
26
|
{:else}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { DialogTriggerProps } from './types.ts';
|
|
2
|
-
declare const DialogTrigger: import("svelte").Component<DialogTriggerProps, {}, "">;
|
|
2
|
+
declare const DialogTrigger: import("svelte").Component<DialogTriggerProps, {}, "ref">;
|
|
3
3
|
type DialogTrigger = ReturnType<typeof DialogTrigger>;
|
|
4
4
|
export default DialogTrigger;
|
|
@@ -99,7 +99,7 @@ Add a `DialogActions` region to render action buttons in the dialog footer. Use
|
|
|
99
99
|
|
|
100
100
|
| Name | Type | Default | Description |
|
|
101
101
|
| ----------------------- | ------------------- | ------- | ----------------------------------------- |
|
|
102
|
-
| `ref`
|
|
102
|
+
| `ref` _bindable_ | `HTMLButtonElement` | | The DOM reference of the trigger element. |
|
|
103
103
|
| `ButtonProps<'button'>` | | | |
|
|
104
104
|
|
|
105
105
|
<!-- /props:DialogTriggerProps -->
|
|
@@ -136,7 +136,7 @@ Add a `DialogActions` region to render action buttons in the dialog footer. Use
|
|
|
136
136
|
|
|
137
137
|
| Name | Type | Default | Description |
|
|
138
138
|
| -------------------------- | ---------------- | ------- | ----------------------------------------- |
|
|
139
|
-
| `ref`
|
|
139
|
+
| `ref` _bindable_ | `HTMLDivElement` | | The DOM reference of the content element. |
|
|
140
140
|
| Element Attributes (`div`) | | | |
|
|
141
141
|
|
|
142
142
|
<!-- /props:DialogContentProps -->
|
|
@@ -35,12 +35,16 @@ export type DialogSurfaceProps = {
|
|
|
35
35
|
} & HTMLAttributes<HTMLDialogElement>;
|
|
36
36
|
/** @propsmith DialogTriggerProps */
|
|
37
37
|
export type DialogTriggerProps = {
|
|
38
|
-
/** The DOM reference of the trigger element.
|
|
38
|
+
/** The DOM reference of the trigger element.
|
|
39
|
+
* @bindable
|
|
40
|
+
*/
|
|
39
41
|
ref?: HTMLButtonElement;
|
|
40
42
|
} & ButtonProps<'button'>;
|
|
41
43
|
/** @propsmith DialogContentProps */
|
|
42
44
|
export type DialogContentProps = {
|
|
43
|
-
/** The DOM reference of the content element.
|
|
45
|
+
/** The DOM reference of the content element.
|
|
46
|
+
* @bindable
|
|
47
|
+
*/
|
|
44
48
|
ref?: HTMLDivElement;
|
|
45
49
|
} & HTMLAttributes<HTMLDivElement>;
|
|
46
50
|
/** @propsmith DialogActionsProps */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TITLE_TAG = ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Rating
|
|
2
|
+
|
|
3
|
+
A rating lets users give a value to an item by selecting one of a row of stars. It supports half-filled stars, four sizes and a custom number of items.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```svelte
|
|
8
|
+
<script>
|
|
9
|
+
import { Rating } from 'fluentui-svelte';
|
|
10
|
+
|
|
11
|
+
let value = $state(3);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<Rating bind:value />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Component API
|
|
18
|
+
|
|
19
|
+
A rating lets users give a value to an item, using a row of symbols such as stars. It supports half-filled items and four sizes.
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
### Bind the value
|
|
24
|
+
|
|
25
|
+
Use `bind:value` to read the rating the user picks.
|
|
26
|
+
|
|
27
|
+
```svelte
|
|
28
|
+
<script>
|
|
29
|
+
let value = $state(3);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<Rating bind:value />
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Half stars
|
|
36
|
+
|
|
37
|
+
Set `step={0.5}` to let users pick half-filled stars.
|
|
38
|
+
|
|
39
|
+
```svelte
|
|
40
|
+
<Rating value={2.5} step={0.5} />
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Quarter stars
|
|
44
|
+
|
|
45
|
+
Until the user rates, a fractional `value` is drawn in quarters, which is handy to show an average. Once the user picks a star, the rating snaps to the `step`.
|
|
46
|
+
|
|
47
|
+
```svelte
|
|
48
|
+
<Rating value={3.75} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Size
|
|
52
|
+
|
|
53
|
+
Use `size` to choose between `small`, `medium`, `large` and `extra-large`.
|
|
54
|
+
|
|
55
|
+
```svelte
|
|
56
|
+
<Rating value={3} size="small" />
|
|
57
|
+
<Rating value={3} size="medium" />
|
|
58
|
+
<Rating value={3} size="large" />
|
|
59
|
+
<Rating value={3} size="extra-large" />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Shape
|
|
63
|
+
|
|
64
|
+
Use `iconFilled` and `iconOutline` to replace the stars with any other icon component. Both fall back to the star when omitted, and partially filled items are clipped, so custom shapes also work with `step={0.5}`.
|
|
65
|
+
|
|
66
|
+
```svelte
|
|
67
|
+
<script>
|
|
68
|
+
import { HeartFilled, HeartRegular } from 'fluentui-icons-svelte';
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<Rating value={3} iconFilled={HeartFilled} iconOutline={HeartRegular} />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Max
|
|
75
|
+
|
|
76
|
+
Use `max` to change the number of stars.
|
|
77
|
+
|
|
78
|
+
```svelte
|
|
79
|
+
<Rating value={4} max={10} />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Item label
|
|
83
|
+
|
|
84
|
+
Use `itemLabel` to customize the accessible label of each radio input.
|
|
85
|
+
|
|
86
|
+
```svelte
|
|
87
|
+
<Rating value={3} itemLabel={(rating) => `${rating} out of 5 stars`} />
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Component Props
|
|
91
|
+
|
|
92
|
+
<!-- DO NOT EDIT THIS SECTION (propsmith generated) -->
|
|
93
|
+
<!-- props:RatingProps -->
|
|
94
|
+
|
|
95
|
+
| Name | Type | Default | Description |
|
|
96
|
+
| --------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `value` _bindable_ | `number` | `0` | The current rating. Fractions are drawn in quarters until the user rates. |
|
|
98
|
+
| `max` | `number` | `5` | The number of items displayed. Must be a whole number greater than 1. |
|
|
99
|
+
| `step` | `1` | `0.5` | `1` | The precision of the rating. `0.5` allows half-filled items. |
|
|
100
|
+
| `size` | `'small'` | `'medium'` | `'large'` | `'extra-large'` | `'extra-large'` | The size of the rating items. |
|
|
101
|
+
| `iconFilled` | `Component` | | The icon of a filled item. Falls back to a filled star. Partially filled items are clipped, so it also works with half steps. |
|
|
102
|
+
| `iconOutline` | `Component` | | The icon of an empty item. Falls back to an outlined star. |
|
|
103
|
+
| `name` | `string` | | The name of the radio inputs. Generated when not provided. |
|
|
104
|
+
| `itemLabel` | `(rating: number) => string` | `` (rating) => `${rating}` `` | Generates the accessible label of the radio input for a rating value. |
|
|
105
|
+
| `onChange` | `(value: number) => void` | | Called when the user changes the rating. |
|
|
106
|
+
| `class` | `string` | | The class to apply to the rating. |
|
|
107
|
+
| `ref` _bindable_ | `HTMLElement` | | The DOM reference of the rating element. |
|
|
108
|
+
| `HTMLAttributes<HTMLDivElement>` without `onchange` | | | |
|
|
109
|
+
|
|
110
|
+
<!-- /props:RatingProps -->
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { PREFIX } from '../../internal/constants.js';
|
|
3
|
+
import StarFilled from 'fluentui-icons-svelte/StarFilled.svelte';
|
|
4
|
+
import StarHalfFilled from 'fluentui-icons-svelte/StarHalfFilled.svelte';
|
|
5
|
+
import StarOneQuarterFilled from 'fluentui-icons-svelte/StarOneQuarterFilled.svelte';
|
|
6
|
+
import StarRegular from 'fluentui-icons-svelte/StarRegular.svelte';
|
|
7
|
+
import StarThreeQuarterFilled from 'fluentui-icons-svelte/StarThreeQuarterFilled.svelte';
|
|
8
|
+
import type { RatingProps } from './types.js';
|
|
9
|
+
|
|
10
|
+
const FALLBACK_ID = $props.id();
|
|
11
|
+
const COMPONENT_NAME = 'rating';
|
|
12
|
+
const ID = `${PREFIX}${COMPONENT_NAME}-${FALLBACK_ID}`;
|
|
13
|
+
const VALUE_ID = `${ID}-value`;
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
value = $bindable(0),
|
|
17
|
+
max = 5,
|
|
18
|
+
step = 1,
|
|
19
|
+
size = 'extra-large',
|
|
20
|
+
iconFilled,
|
|
21
|
+
iconOutline,
|
|
22
|
+
name = ID,
|
|
23
|
+
itemLabel = (rating) => `${rating}`,
|
|
24
|
+
onChange,
|
|
25
|
+
class: classes,
|
|
26
|
+
ref = $bindable(),
|
|
27
|
+
role = 'radiogroup',
|
|
28
|
+
...attributes
|
|
29
|
+
}: RatingProps = $props();
|
|
30
|
+
|
|
31
|
+
/** Until the user rates, the value is only displayed, so it is drawn with the precision of a quarter. */
|
|
32
|
+
let rated = $state(false);
|
|
33
|
+
let hovered = $state<number>();
|
|
34
|
+
|
|
35
|
+
const _max = $derived.by(() => {
|
|
36
|
+
if (!Number.isInteger(max) || max <= 1) throw new Error('Max must be a whole number greater than 1');
|
|
37
|
+
return max;
|
|
38
|
+
});
|
|
39
|
+
const items = $derived(Array.from({ length: _max }, (_, index) => index + 1));
|
|
40
|
+
const shown = $derived(hovered ?? value);
|
|
41
|
+
const checkedValue = $derived(value === 0 ? 0 : Math.min(_max, Math.max(step, Math.round(value / step) * step)));
|
|
42
|
+
|
|
43
|
+
const custom = $derived(!!(iconFilled || iconOutline));
|
|
44
|
+
|
|
45
|
+
/** How much of the item is filled, snapped to the precision it is shown with. */
|
|
46
|
+
function fillFor(item: number) {
|
|
47
|
+
const fill = Math.min(Math.max(shown - (item - 1), 0), 1);
|
|
48
|
+
const precision = hovered !== undefined || rated ? step / 2 : 0.25;
|
|
49
|
+
return Math.round(fill / precision) * precision;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function iconFor(snapped: number) {
|
|
53
|
+
if (snapped >= 1) return StarFilled;
|
|
54
|
+
if (snapped >= 0.75) return StarThreeQuarterFilled;
|
|
55
|
+
if (snapped >= 0.5) return StarHalfFilled;
|
|
56
|
+
if (snapped >= 0.25) return StarOneQuarterFilled;
|
|
57
|
+
return StarRegular;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function select(next: number) {
|
|
61
|
+
rated = true;
|
|
62
|
+
value = next;
|
|
63
|
+
onChange?.(next);
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<!--
|
|
68
|
+
@component
|
|
69
|
+
A rating lets users give a value to an item, using a row of symbols such as stars. It supports half-filled items and four sizes.
|
|
70
|
+
|
|
71
|
+
- Usage:
|
|
72
|
+
```tsx
|
|
73
|
+
<script>
|
|
74
|
+
import { Rating } from 'fluentui-svelte';
|
|
75
|
+
|
|
76
|
+
let value = $state(3);
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<Rating bind:value />
|
|
80
|
+
<Rating bind:value step={0.5} size="medium" />
|
|
81
|
+
```
|
|
82
|
+
-->
|
|
83
|
+
<div
|
|
84
|
+
class={['fs-rating', `size-${size}`, classes]}
|
|
85
|
+
bind:this={ref}
|
|
86
|
+
{role}
|
|
87
|
+
onpointerleave={() => (hovered = undefined)}
|
|
88
|
+
{...attributes}
|
|
89
|
+
aria-label={attributes['aria-label'] ?? (attributes['aria-labelledby'] ? undefined : 'Rating')}
|
|
90
|
+
aria-describedby={[attributes['aria-describedby'], VALUE_ID].filter(Boolean).join(' ')}
|
|
91
|
+
>
|
|
92
|
+
<span id={VALUE_ID} hidden>Current rating: {value} out of {_max}.</span>
|
|
93
|
+
{#each items as item (item)}
|
|
94
|
+
{@const fill = fillFor(item)}
|
|
95
|
+
<span class="rating-item">
|
|
96
|
+
{#if custom}
|
|
97
|
+
{@const Outline = iconOutline ?? StarRegular}
|
|
98
|
+
{@const Filled = iconFilled ?? StarFilled}
|
|
99
|
+
<span class="rating-icon custom" class:selected={fill > 0} style="--rating-fill: {fill * 100}%">
|
|
100
|
+
<Outline aria-hidden="true" />
|
|
101
|
+
<span class="filled"><Filled aria-hidden="true" /></span>
|
|
102
|
+
</span>
|
|
103
|
+
{:else}
|
|
104
|
+
{@const Icon = iconFor(fill)}
|
|
105
|
+
<span class="rating-icon" class:selected={fill > 0}><Icon aria-hidden="true" /></span>
|
|
106
|
+
{/if}
|
|
107
|
+
{#if step === 0.5}
|
|
108
|
+
<input
|
|
109
|
+
class="half-value"
|
|
110
|
+
type="radio"
|
|
111
|
+
{name}
|
|
112
|
+
value={item - 0.5}
|
|
113
|
+
checked={checkedValue === item - 0.5}
|
|
114
|
+
aria-label={itemLabel(item - 0.5)}
|
|
115
|
+
onclick={() => value !== item - 0.5 && select(item - 0.5)}
|
|
116
|
+
onpointerenter={() => (hovered = item - 0.5)}
|
|
117
|
+
/>
|
|
118
|
+
{/if}
|
|
119
|
+
<input
|
|
120
|
+
class="full-value"
|
|
121
|
+
type="radio"
|
|
122
|
+
{name}
|
|
123
|
+
value={item}
|
|
124
|
+
checked={checkedValue === item}
|
|
125
|
+
aria-label={itemLabel(item)}
|
|
126
|
+
onclick={() => value !== item && select(item)}
|
|
127
|
+
onpointerenter={() => (hovered = item)}
|
|
128
|
+
/>
|
|
129
|
+
</span>
|
|
130
|
+
{/each}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<style>
|
|
134
|
+
.fs-rating {
|
|
135
|
+
--rating-size: 24px;
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
color: var(--fs-accent-fill-default);
|
|
139
|
+
|
|
140
|
+
&.size-small {
|
|
141
|
+
--rating-size: 12px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&.size-medium {
|
|
145
|
+
--rating-size: 16px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&.size-large {
|
|
149
|
+
--rating-size: 20px;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.rating-item {
|
|
154
|
+
display: inline-flex;
|
|
155
|
+
position: relative;
|
|
156
|
+
border-radius: var(--fs-control-border-radius);
|
|
157
|
+
padding: 2px;
|
|
158
|
+
|
|
159
|
+
&:has(input:focus-visible) {
|
|
160
|
+
box-shadow: var(--fs-focus-stroke-outer);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
& input {
|
|
164
|
+
appearance: none;
|
|
165
|
+
position: absolute;
|
|
166
|
+
inset-block: 0;
|
|
167
|
+
inset-inline-start: 0;
|
|
168
|
+
margin: 0;
|
|
169
|
+
outline: none;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
inline-size: 100%;
|
|
172
|
+
block-size: 100%;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
& .half-value {
|
|
176
|
+
z-index: 1;
|
|
177
|
+
inline-size: 50%;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.rating-icon {
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
color: var(--fs-text-secondary);
|
|
184
|
+
|
|
185
|
+
&.selected {
|
|
186
|
+
color: var(--fs-accent-fill-default);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
& :global(svg) {
|
|
190
|
+
inline-size: var(--rating-size);
|
|
191
|
+
block-size: var(--rating-size);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
&.custom {
|
|
195
|
+
position: relative;
|
|
196
|
+
|
|
197
|
+
& .filled {
|
|
198
|
+
display: inline-flex;
|
|
199
|
+
position: absolute;
|
|
200
|
+
inset: 0;
|
|
201
|
+
color: var(--fs-accent-fill-default);
|
|
202
|
+
clip-path: inset(0 calc(100% - var(--rating-fill)) 0 0);
|
|
203
|
+
|
|
204
|
+
&:dir(rtl) {
|
|
205
|
+
clip-path: inset(0 0 0 calc(100% - var(--rating-fill)));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RatingProps } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A rating lets users give a value to an item, using a row of symbols such as stars. It supports half-filled items and four sizes.
|
|
4
|
+
*
|
|
5
|
+
* - Usage:
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <script>
|
|
8
|
+
* import { Rating } from 'fluentui-svelte';
|
|
9
|
+
*
|
|
10
|
+
* let value = $state(3);
|
|
11
|
+
* </script>
|
|
12
|
+
*
|
|
13
|
+
* <Rating bind:value />
|
|
14
|
+
* <Rating bind:value step={0.5} size="medium" />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
declare const Rating: import("svelte").Component<RatingProps, {}, "ref" | "value">;
|
|
18
|
+
type Rating = ReturnType<typeof Rating>;
|
|
19
|
+
export default Rating;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
/** @propsmith RatingProps */
|
|
4
|
+
export type RatingProps = {
|
|
5
|
+
/** The current rating. Fractions are drawn in quarters until the user rates.
|
|
6
|
+
* @default 0
|
|
7
|
+
* @bindable
|
|
8
|
+
*/
|
|
9
|
+
value?: number;
|
|
10
|
+
/** The number of items displayed. Must be a whole number greater than 1.
|
|
11
|
+
* @default 5
|
|
12
|
+
*/
|
|
13
|
+
max?: number;
|
|
14
|
+
/** The precision of the rating. `0.5` allows half-filled items.
|
|
15
|
+
* @default 1
|
|
16
|
+
*/
|
|
17
|
+
step?: 1 | 0.5;
|
|
18
|
+
/** The size of the rating items.
|
|
19
|
+
* @default 'extra-large'
|
|
20
|
+
*/
|
|
21
|
+
size?: 'small' | 'medium' | 'large' | 'extra-large';
|
|
22
|
+
/** The icon of a filled item. Falls back to a filled star. Partially filled items are clipped, so it also works with half steps.
|
|
23
|
+
* @type Component
|
|
24
|
+
*/
|
|
25
|
+
iconFilled?: Component;
|
|
26
|
+
/** The icon of an empty item. Falls back to an outlined star.
|
|
27
|
+
* @type Component
|
|
28
|
+
*/
|
|
29
|
+
iconOutline?: Component;
|
|
30
|
+
/** The name of the radio inputs. Generated when not provided. */
|
|
31
|
+
name?: string;
|
|
32
|
+
/** Generates the accessible label of the radio input for a rating value.
|
|
33
|
+
* @default (rating) => `${rating}`
|
|
34
|
+
*/
|
|
35
|
+
itemLabel?: (rating: number) => string;
|
|
36
|
+
/** Called when the user changes the rating. */
|
|
37
|
+
onChange?: (value: number) => void;
|
|
38
|
+
/** The class to apply to the rating. */
|
|
39
|
+
class?: string;
|
|
40
|
+
/** The DOM reference of the rating element.
|
|
41
|
+
* @bindable
|
|
42
|
+
*/
|
|
43
|
+
ref?: HTMLElement;
|
|
44
|
+
} & Omit<HTMLAttributes<HTMLDivElement>, 'onchange'>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|