svelte-fluentui 1.0.0 → 1.2.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/README.md +16 -32
- package/dist/assets/styles/components.scss +23 -3
- package/dist/components/Calendar.svelte +41 -4
- package/dist/components/Calendar.svelte.d.ts +2 -0
- package/dist/components/Checkbox.svelte +21 -15
- package/dist/components/Combobox.svelte +2 -13
- package/dist/components/Combobox.svelte.d.ts +0 -1
- package/dist/components/DatePicker.svelte +22 -5
- package/dist/components/DatePicker.svelte.d.ts +2 -0
- package/dist/components/Divider.svelte +9 -2
- package/dist/components/Divider.svelte.d.ts +2 -0
- package/dist/components/Listbox.svelte +0 -2
- package/dist/components/Listbox.svelte.d.ts +0 -1
- package/dist/components/PositioningRegion.svelte +23 -5
- package/dist/components/PositioningRegion.svelte.d.ts +6 -0
- package/dist/components/Select.svelte +665 -188
- package/dist/components/Select.svelte.d.ts +6 -3
- package/dist/components/Switch.svelte +19 -13
- package/dist/components/Textarea.svelte +28 -0
- package/dist/components/TimePicker.svelte +14 -3
- package/dist/components/TimePicker.svelte.d.ts +2 -0
- package/dist/main.css +15 -0
- package/dist/main.css.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,38 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2.6.x), providing a seamless way to use FluentUI components in Svelte applications.
|
|
4
4
|
|
|
5
|
-
## What's New in v1.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **`
|
|
10
|
-
-
|
|
11
|
-
- **`
|
|
12
|
-
- **`
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **`
|
|
20
|
-
|
|
21
|
-
### Previously (rc22)
|
|
22
|
-
|
|
23
|
-
- **`InputFile` chips mode now has an overall progress footer** — the same `Pause all` / `Resume all` / `Retry all` controls plus the aggregate progress bar that list/popover modes already had. Works for both `chipsPosition` values; rendered as a sibling below the chips so layout stays sane regardless of position.
|
|
24
|
-
- **`package.json` `homepage` points at the docs site** — npm's "Homepage" sidebar link now goes to `svelte-fluentui.keenmate.dev` instead of duplicating the GitHub repo link. Two distinct destinations on the package page.
|
|
25
|
-
- **InputFile demo playground persists in localStorage** — the live selector × list × cardSize × chipsPosition mixer at `/components/inputfile` remembers your last configuration across page reloads. Files themselves are not persisted (File objects don't survive JSON).
|
|
26
|
-
|
|
27
|
-
## What's New in v1.0.0-rc21
|
|
28
|
-
|
|
29
|
-
- **`InputFile` — `appearance` split into `selectorAppearance` + `listAppearance` + new `cardSize` prop** — the single `appearance` union baked the trigger visual and the file-list visual together, so combinations like "button trigger with popover list" or "card with chips below" weren't expressible. Now `selectorAppearance: "card" | "button" | "minimal"` and `listAppearance: "list" | "chips" | "popover" | "none"` are independent. All previous presets still expressible (`card + list`, `button + list`, `minimal + popover`, `card + chips`). New `cardSize: "minimal" | "compact" | "big"` resizes the card — `big` is the original tall vertical card, `compact` is a 2-row grid layout for forms where 200px felt enormous, and `minimal` collapses everything to a single inline wrapping row (icon · message · hint · hint · Browse) with `·` separators between hints
|
|
30
|
-
- **`InputFile` — `expandOnDrag` + `expandOnDragTarget` props for drag-expanded overlay** — when an external file is dragged onto the page, the card temporarily expands into a larger portaled overlay so the user has a generous hit area. Rendered above the form via `use:portal` so the surrounding layout doesn't shift up and down. Overlay centre is anchored to the selector's (or `expandOnDragTarget`'s) centre via `transform: translate(-50%, -50%)` and grows symmetrically in all four directions — clamped to `90vw` / `90vh` so it never escapes the viewport. Dismissal paths cover Esc keydown (since OS-originated drags don't reliably fire `dragend` on cancel), cursor leaving the viewport (`document.dragleave` with `relatedTarget === null`), drop, and window blur. The overlay's own `dragleave` uses `relatedTarget` containment rather than `target === currentTarget` to avoid a flicker bug (`dragleave` fires when moving from a parent element into a child, same as `mouseout`)
|
|
31
|
-
- **`InputFile` — popover sticky header + sticky footer with progress bar, batch actions, and three CSS-variable dimensions** — Long file lists in minimal-mode popovers used to scroll the header (file count, +Add more, Clear all) away as the user navigated; aggregate progress was nowhere visible without picking through individual rows. New sticky header pins the file count + actions + limits hint to the top; new sticky footer carries an overall progress bar (`uploadedBytes / totalBytes`), a stats line via `labels.totalProgress({completed, failed, total, uploadedBytes, totalBytes, percent})`, and conditional batch buttons — Pause all (any uploading), Resume all (any paused), Retry all (any failed). New exported instance methods `pauseAll()` / `resumeAll()` / `retryAll()` and `actions` snippet payload widened to receive them. Popover sizing is now bracketed by three CSS variables: `--fluent-inputfile-popover-width` (default `28rem`), `--fluent-inputfile-popover-min-height` (default `16rem`), `--fluent-inputfile-popover-max-height` (default `24rem`) — overridable per-theme or per-instance so the popover doesn't jump around as files are added
|
|
32
|
-
- **`InputFile` — `FileUploadResult` return type + `onItemRemove(item)` callback for clean server-side upload/cleanup flows** — Previously the upload handler's `Promise<void>` return meant capturing a server-returned guid required a WeakMap + post-success `onFileUploaded` patch (fragile, two-step). Handler return widened to `Promise<void | FileUploadResult>` where `FileUploadResult = Partial<Pick<InputFileItem, "metadata" | "downloadUrl" | "thumbnailUrl" | "name">>` — restricted on purpose so handlers can't clobber `id` / `status` / `progress`. Returned fields are spread into the same `patchItem` that flips status to `"completed"`, landing the server guid atomically with the completion flag. New `onItemRemove?(item)` callback fires once per item from `removeAt` / `removeById` / row `×` / `clear()` / `reset()`, receiving the removed item with its final metadata still intact. Together they cover the canonical scenario: file picked → auto-upload → server returns guid (now on `item.metadata`) → user removes the file before submit → consumer fires `DELETE /api/files/:guid` from `onItemRemove`. Demo 16 on `/components/inputfile` shows the full flow with an in-memory fake server + activity log
|
|
33
|
-
- **`InputFile` — `chipsPosition: "end" | "below"` + chip restructure to `[info | X]` with full-height remove hitbox** — When `listAppearance="chips"`, the chips previously rendered at the trailing edge of the same flex row as the selector card, leaving a tall card + short chips visually floating mid-row. New `chipsPosition` prop (default `"below"`) flips the layout: `"end"` keeps trailing-edge with an inner `.chips-wrap` so multi-row wrap doesn't anchor each row to the card's vertical centre, `"below"` stacks chips beneath the card. Chip itself restructured into `.chip-info` (filename / size / progress, fills width, non-interactive) and `.chip-remove` (full-height button covering everything from the info edge to the chip's right edge), so users no longer miss the small `×` and click empty chip space
|
|
34
|
-
- **`InputFile` playground demo on `/components/inputfile`** — live controls for `selectorAppearance` / `listAppearance` / `cardSize` / `chipsPosition` / `multiple` / `disabled` / `expandOnDrag` at the top of the examples grid so consumers can try every combination without rewriting code
|
|
35
|
-
- **Border-radius consolidated into a six-tier CSS-variable scale** — ~40 hardcoded `border-radius` declarations across 12 components were swept into `--fluent-border-radius-{sm,md,lg,xl,pill,circle}` (2/4/6/8/999/50%) on `:root`, backed by matching SCSS tokens (`$border-radius-sm/md/lg/xl/pill/circle`). The pre-existing `--fluent-border-radius` is preserved as a back-compat alias of `md`. Pixel-identical to the prior visual — drove tier choice off existing values per call site (4px → md, 2px → sm, etc.). Theming the library's entire corner-rounding is now a six-line variable override instead of a 40-line search-and-replace
|
|
36
|
-
- **`InputFile` popover `max-height` was being silently overridden — fixed** — Floating UI's `size` middleware ran on every reposition and unconditionally wrote `floating.style.maxHeight = ${availableHeight - 16}px`, so on any window taller than the CSS cap the inline style won and the popover ballooned to ~viewport height (sticky footer ended up pinned far below the visible scroll region, looking like it was floating mid-list). Middleware now clears the inline cap, reads the CSS-rule-driven `max-height` via `getComputedStyle`, and only re-applies an inline cap when the viewport is actually tighter than the ceiling. CSS variable wins on tall screens, viewport wins on short screens
|
|
5
|
+
## What's New in v1.2.0
|
|
6
|
+
|
|
7
|
+
- **`Divider` — `orientation` and `role` props now forwarded** — the wrapper previously only exposed `class` and `style`, so vertical dividers and `role="presentation"` (for decorative separators that should be skipped by screen readers) weren't expressible through the Svelte API. `<fluent-divider>` natively supports both via fast-foundation; the wrapper just forwards them. Defaults preserved (`orientation="horizontal"`, `role="separator"`) so every existing call site renders identically. First proper demo page at `/components/divider` — the previous `/divider` route was an HMR sandbox and the components-list link to `/components/divider` was a 404.
|
|
8
|
+
- **`Select` rewritten as a custom component** — the wrapped `<fluent-select>` rendered its dropdown listbox inside its own shadow DOM with a static `--max-height`, so inside Dialogs and other overflow-hidden ancestors the option list extended past the viewport with no scroll cutoff. New custom Select portals the listbox to `document.body` via `PositioningRegion` (Floating-UI), so it escapes any clipping ancestor and stays inside the viewport regardless of where the trigger sits. Full keyboard wiring (Arrow/Home/End/Enter/Space/Escape/Tab, single-char type-ahead with cycle-through-matches), multi-mode renders an always-expanded inline listbox matching FluentUI Blazor, and a hidden `<input>` keeps native form submission working. Public API mostly preserved; `value` widened to `string | string[]` for multi mode, `open` is now bindable, and `position` means "force placement" rather than the old static attribute.
|
|
9
|
+
- **`PositioningRegion` — new `availableHeight` prop** — writes the computed remaining viewport height into the `--available-height` CSS custom property on the floating element on every reposition, so dropdowns can cap their max-height with `max-height: var(--available-height, <fallback>)` instead of hard-coding a value that overflows on small screens or near viewport edges. Works alongside `matchWidth` in a single `size()` middleware pass.
|
|
10
|
+
- **`.fluent-value-label` — new canonical class for inline value labels next to a control** — separates field labels (the bold, stacked-above text that names a form field) from value labels (the inline text next to a single control that describes the value, e.g. "Remember me" next to a checkbox). Value labels are normal weight with a pointer cursor, matching FluentUI Blazor's pickers and `<fluent-radio>`'s slotted label. `data-disabled` / `data-readonly` on the wrapper drive a `not-allowed` cursor when the host is inactive.
|
|
11
|
+
- **`Checkbox` & `Switch` — inline label fixes** — labels were rendering bold with a text cursor (using the field-label class) and clicks on the label didn't reliably toggle the control when no `id` was passed. Switched to the new `.fluent-value-label` class, and a stable per-instance `fallbackId` is now used as `<label for>` / element `id` when the caller didn't supply one, so label-click-to-toggle works regardless of whether an `id` was passed in. Applies across all three `labelPosition` values (`top` / `start` / `end`).
|
|
12
|
+
- **`Select` & `Combobox` — field-label class consolidated to shared `.fluent-label`** — both components had component-scoped `.select-label` / `.combobox-label` rules that duplicated the canonical `.fluent-label` declarations from `assets/styles/components.scss`. A single library-wide tweak to label styling now lives in one place. After this change, every component that renders a stacked field label uses `.fluent-label`, Field uses its BEM-namespaced `fluent-field__label`, and Checkbox/Switch use `.fluent-value-label` — three classes total, each with a clear semantic role.
|
|
13
|
+
|
|
14
|
+
## What's New in v1.1.0
|
|
15
|
+
|
|
16
|
+
- **`Calendar` — new `disabled` prop for whole-control disabled state** — Calendar previously had `readonly` (blocks interaction, sets `aria-readonly`) but no host-level `disabled`. New `disabled?: boolean` blocks all interaction (day/month/year cells, title-button view switching, prev/next navigation) AND adds visual de-emphasis via `aria-disabled="true"`: drops opacity to `var(--disabled-opacity)`, sets `cursor: not-allowed` on interactive cells. Per-cell `aria-disabled` strikethrough rules are untouched — the new selectors only fire when the HOST has `aria-disabled`. Propagates through the recursive Calendar instances (month/year picker views) and through `DatePicker` to the inner Calendar. Demo "Calendar states" at `/components/forms/calendar` shows Normal / Readonly / Disabled side-by-side.
|
|
17
|
+
- **`DatePicker` & `TimePicker` — new `openOnInputClick` prop (default `true`)** — FluentUI Blazor's pickers open their popup whenever the user clicks the input field, not just the trigger icon. DatePicker previously required clicking the calendar icon; TimePicker had wrapper-click wired but it *toggled* (a second click inside the open picker closed it). New prop enables Blazor-equivalent behavior on both: clicking anywhere on the input opens the popup (does not toggle, so clicks inside an already-open picker keep it open), while the icon button keeps its explicit toggle role. Set `openOnInputClick={false}` to require the icon as the sole entry point.
|
|
18
|
+
- **`Listbox` & `Combobox` — `readonly` prop removed (BREAKING for type-checked consumers)** — Audit of input wrappers caught two cases where the Svelte wrapper exposed a `readonly` prop that the underlying FluentUI component silently ignored: `<fluent-listbox>` and `<fluent-combobox>` have no `readOnly` property in fast-foundation. In Listbox the prop was declared but never forwarded — pure dead code. In Combobox it was forwarded but ignored by the web component. Consumers passing `readonly` will see a TypeScript error; switch to `disabled` for non-interactive comboboxes. The remaining 14 input wrappers (TextField, Textarea, NumberField, Search, Checkbox, Switch, Radio, RadioGroup, Slider, Select, Autocomplete, DatePicker, TimePicker, InputFile) are unchanged.
|
|
19
|
+
- **`Textarea` — drag-resize no longer collapses the focus indicator** — FluentUI's shadow-DOM `:active::after` rule shrank the bottom indicator to a 40%-wide centered stub during any `:active` state, which fired during the resize drag and read as a focus loss. Override injected via `adoptedStyleSheets` restores the full-width indicator while dragging.
|
|
20
|
+
- **Playwright e2e suite** — New `playwright.config.ts` targets fixture pages under `docs/src/routes/test/<feature>/+page.svelte` (intentionally separate from showcase pages so specs stay stable). First feature covered is Autocomplete with 567 lines of specs across two fixture pages.
|
|
37
21
|
|
|
38
22
|
## Features
|
|
39
23
|
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
align-items: center;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Canonical label
|
|
36
|
-
//
|
|
37
|
-
//
|
|
35
|
+
// Canonical FIELD label — bold, block, sits ABOVE a stacked input
|
|
36
|
+
// (TextField, Select, Combobox, Autocomplete, NumberField, Textarea, Slider,
|
|
37
|
+
// DatePicker, TimePicker, RadioGroup, and Checkbox/Switch when labelPosition="top").
|
|
38
38
|
.fluent-label {
|
|
39
39
|
display: block;
|
|
40
40
|
font-size: 0.875rem;
|
|
@@ -44,3 +44,23 @@
|
|
|
44
44
|
margin-bottom: 0.25rem;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Canonical VALUE label — inline, normal weight, pointer cursor.
|
|
48
|
+
// Used for the text that sits next to a control and describes the value/option
|
|
49
|
+
// itself (Checkbox/Switch inline labels in labelPosition="start"/"end"). The
|
|
50
|
+
// pointer cursor is the sole hover affordance — no color shift, matching how
|
|
51
|
+
// fluent-radio's slotted default-slot label behaves natively.
|
|
52
|
+
.fluent-value-label {
|
|
53
|
+
display: inline;
|
|
54
|
+
font-size: 0.875rem;
|
|
55
|
+
font-weight: normal;
|
|
56
|
+
line-height: 1.25;
|
|
57
|
+
color: var(--neutral-foreground-rest);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
user-select: none;
|
|
60
|
+
}
|
|
61
|
+
// Disabled / readonly host swaps the cursor to indicate non-interactivity.
|
|
62
|
+
[data-disabled="true"] > .fluent-value-label,
|
|
63
|
+
[data-readonly="true"] > .fluent-value-label {
|
|
64
|
+
cursor: not-allowed;
|
|
65
|
+
}
|
|
66
|
+
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
/** Gap between cells in CSS units (default 2px). */
|
|
36
36
|
gap?: string | number
|
|
37
37
|
readonly?: boolean
|
|
38
|
+
/** When true, the calendar is non-interactive AND visually de-emphasized (opacity, not-allowed cursor on interactive cells, `aria-disabled="true"` on the host). Stricter than `readonly`, which blocks interaction without visual treatment. */
|
|
39
|
+
disabled?: boolean
|
|
38
40
|
/** Hover-preview function: given the day under the cursor, returns the set of dates to visually highlight. Defaults to `[date]`. */
|
|
39
41
|
highlightDates?: (date: Date) => Date[]
|
|
40
42
|
/** Click-selection function: given the clicked day, returns the dates to select. In multiple mode, the result is unioned with existing `selectedDates` (or removed wholesale if the clicked day was already selected). In range mode, the result replaces the range. Defaults to `[date]`. */
|
|
@@ -64,6 +66,7 @@
|
|
|
64
66
|
animatePeriodChanges = undefined,
|
|
65
67
|
disabledSelectable = undefined,
|
|
66
68
|
readonly = undefined,
|
|
69
|
+
disabled = undefined,
|
|
67
70
|
selectMode = "single",
|
|
68
71
|
checkIfSelectedValueHasChanged = undefined,
|
|
69
72
|
dayFormat = undefined,
|
|
@@ -107,7 +110,9 @@
|
|
|
107
110
|
// year fail the disabledDateFunc check, blocking the year picker entirely.
|
|
108
111
|
disabledCheckAllDaysOfMonthYear: disabledCheckAllDaysOfMonthYear ?? true,
|
|
109
112
|
disabledSelectable: disabledSelectable ?? false,
|
|
110
|
-
|
|
113
|
+
// Treat `disabled` as effectively-readonly for the title/month/year selection gates.
|
|
114
|
+
// The visual difference (opacity, not-allowed cursor) is layered via `aria-disabled` CSS.
|
|
115
|
+
readOnly: (disabled || readonly) ?? false,
|
|
111
116
|
selectMode,
|
|
112
117
|
value: value,
|
|
113
118
|
selectedDates: selectedDates,
|
|
@@ -470,6 +475,7 @@
|
|
|
470
475
|
class:fluent-month={view === "months"}
|
|
471
476
|
class:fluent-year={view === "years"}
|
|
472
477
|
aria-readonly={readonly ? "true" : null}
|
|
478
|
+
aria-disabled={disabled ? "true" : null}
|
|
473
479
|
style={sizeStyle || null}
|
|
474
480
|
>
|
|
475
481
|
{#if !_pickerView || _pickerView === "days"}
|
|
@@ -547,9 +553,9 @@
|
|
|
547
553
|
data-multi-end={multipleSelection.isMultiple && selectMode === "range" && sameDay(multipleSelection.max, day)}
|
|
548
554
|
aria-label={dayProperties.title}
|
|
549
555
|
data-value={dayProperties.dayIdentifier}
|
|
550
|
-
onkeydown={ev => (ev.key === "Enter" || ev.key === " ") && onSelectDayHandlerAsync(day, dayProperties.isDisabled || dayProperties.isInactive || readonly || false)}
|
|
551
|
-
onclick={ev => onSelectDayHandlerAsync(day, dayProperties.isDisabled || dayProperties.isInactive || readonly || false)}
|
|
552
|
-
onmouseover={ev => onSelectDayMouseOverAsync(day, dayProperties.isDisabled || dayProperties.isInactive || readonly || false)}
|
|
556
|
+
onkeydown={ev => (ev.key === "Enter" || ev.key === " ") && onSelectDayHandlerAsync(day, dayProperties.isDisabled || dayProperties.isInactive || readonly || disabled || false)}
|
|
557
|
+
onclick={ev => onSelectDayHandlerAsync(day, dayProperties.isDisabled || dayProperties.isInactive || readonly || disabled || false)}
|
|
558
|
+
onmouseover={ev => onSelectDayMouseOverAsync(day, dayProperties.isDisabled || dayProperties.isInactive || readonly || disabled || false)}
|
|
553
559
|
>
|
|
554
560
|
{#if daySnippet}
|
|
555
561
|
{@render daySnippet(dayProperties)}
|
|
@@ -619,6 +625,7 @@
|
|
|
619
625
|
onDateSelected={pickerMonthSelectAsync}
|
|
620
626
|
checkIfSelectedValueHasChanged={false}
|
|
621
627
|
{readonly}
|
|
628
|
+
{disabled}
|
|
622
629
|
{culture}
|
|
623
630
|
{disabledSelectable}
|
|
624
631
|
{animatePeriodChanges}
|
|
@@ -636,6 +643,7 @@
|
|
|
636
643
|
onDateSelected={pickerYearSelectAsync}
|
|
637
644
|
checkIfSelectedValueHasChanged={false}
|
|
638
645
|
{readonly}
|
|
646
|
+
{disabled}
|
|
639
647
|
{culture}
|
|
640
648
|
disabledSelectable={disabledSelectable}
|
|
641
649
|
{animatePeriodChanges}
|
|
@@ -646,3 +654,32 @@
|
|
|
646
654
|
/>
|
|
647
655
|
{/if}
|
|
648
656
|
</div>
|
|
657
|
+
|
|
658
|
+
<style>
|
|
659
|
+
/* Visual treatment for the host-level disabled state. Per-cell `aria-disabled` (e.g. dates filtered
|
|
660
|
+
out by minDate/maxDate/disabledDateFunc) already has its own strikethrough rule in
|
|
661
|
+
fluent-components.scss — these selectors only target the WHOLE-control disabled state
|
|
662
|
+
(`.fluent-calendar[aria-disabled="true"]`, etc., set when the consumer passes `disabled`). */
|
|
663
|
+
:global(.fluent-calendar[aria-disabled="true"]),
|
|
664
|
+
:global(.fluent-month[aria-disabled="true"]),
|
|
665
|
+
:global(.fluent-year[aria-disabled="true"]) {
|
|
666
|
+
opacity: var(--disabled-opacity, 0.5);
|
|
667
|
+
cursor: not-allowed;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
:global(.fluent-calendar[aria-disabled="true"] .day),
|
|
671
|
+
:global(.fluent-calendar[aria-disabled="true"] .title > .label),
|
|
672
|
+
:global(.fluent-calendar[aria-disabled="true"] .change-period > .previous),
|
|
673
|
+
:global(.fluent-calendar[aria-disabled="true"] .change-period > .next),
|
|
674
|
+
:global(.fluent-month[aria-disabled="true"] .month),
|
|
675
|
+
:global(.fluent-month[aria-disabled="true"] .title > .label),
|
|
676
|
+
:global(.fluent-month[aria-disabled="true"] .change-period > .previous),
|
|
677
|
+
:global(.fluent-month[aria-disabled="true"] .change-period > .next),
|
|
678
|
+
:global(.fluent-year[aria-disabled="true"] .year),
|
|
679
|
+
:global(.fluent-year[aria-disabled="true"] .title > .label),
|
|
680
|
+
:global(.fluent-year[aria-disabled="true"] .change-period > .previous),
|
|
681
|
+
:global(.fluent-year[aria-disabled="true"] .change-period > .next) {
|
|
682
|
+
cursor: not-allowed;
|
|
683
|
+
pointer-events: none;
|
|
684
|
+
}
|
|
685
|
+
</style>
|
|
@@ -24,6 +24,8 @@ type Props = {
|
|
|
24
24
|
/** Gap between cells in CSS units (default 2px). */
|
|
25
25
|
gap?: string | number;
|
|
26
26
|
readonly?: boolean;
|
|
27
|
+
/** When true, the calendar is non-interactive AND visually de-emphasized (opacity, not-allowed cursor on interactive cells, `aria-disabled="true"` on the host). Stricter than `readonly`, which blocks interaction without visual treatment. */
|
|
28
|
+
disabled?: boolean;
|
|
27
29
|
/** Hover-preview function: given the day under the cursor, returns the set of dates to visually highlight. Defaults to `[date]`. */
|
|
28
30
|
highlightDates?: (date: Date) => Date[];
|
|
29
31
|
/** Click-selection function: given the clicked day, returns the dates to select. In multiple mode, the result is unioned with existing `selectedDates` (or removed wholesale if the clicked day was already selected). In range mode, the result replaces the range. Defaults to `[date]`. */
|
|
@@ -56,6 +56,16 @@
|
|
|
56
56
|
onclick = undefined
|
|
57
57
|
}: Props = $props()
|
|
58
58
|
|
|
59
|
+
// Stable fallback id so <label for={...}> always links to the checkbox —
|
|
60
|
+
// without it, clicking the value label on an id-less <Checkbox /> wouldn't
|
|
61
|
+
// toggle the box.
|
|
62
|
+
const fallbackId = `fluent-checkbox-${Math.random().toString(36).slice(2, 11)}`
|
|
63
|
+
const effectiveId = $derived(id ?? fallbackId)
|
|
64
|
+
|
|
65
|
+
// Checkbox label always describes the value next to the control — never a
|
|
66
|
+
// field-label — so styling stays the same across all positions; only the
|
|
67
|
+
// wrapper's flex direction changes per labelPosition.
|
|
68
|
+
|
|
59
69
|
const currentMessage = $derived(
|
|
60
70
|
checked === true ? checkedMessage
|
|
61
71
|
: checked === false ? uncheckedMessage
|
|
@@ -126,9 +136,14 @@
|
|
|
126
136
|
}
|
|
127
137
|
</script>
|
|
128
138
|
|
|
129
|
-
<span
|
|
139
|
+
<span
|
|
140
|
+
class="fluent-checkbox-wrapper"
|
|
141
|
+
data-label-position={labelPosition}
|
|
142
|
+
data-disabled={disabled ? "true" : null}
|
|
143
|
+
data-readonly={readonly ? "true" : null}
|
|
144
|
+
>
|
|
130
145
|
{#if label || labelTemplate || children}
|
|
131
|
-
<label for={
|
|
146
|
+
<label for={effectiveId} class="fluent-value-label">
|
|
132
147
|
{#if label}{label}{/if}
|
|
133
148
|
{#if labelTemplate}{@render labelTemplate?.()}{/if}
|
|
134
149
|
{#if children}{@render children?.()}{/if}
|
|
@@ -146,7 +161,7 @@
|
|
|
146
161
|
{readonly}
|
|
147
162
|
{disabled}
|
|
148
163
|
{required}
|
|
149
|
-
{
|
|
164
|
+
id={effectiveId}
|
|
150
165
|
{name}
|
|
151
166
|
aria-label={ariaLabel || label || null}
|
|
152
167
|
class={className || null}
|
|
@@ -176,17 +191,8 @@
|
|
|
176
191
|
gap: 0.5rem;
|
|
177
192
|
}
|
|
178
193
|
|
|
179
|
-
/*
|
|
180
|
-
|
|
181
|
-
margin extends the label's flex-item box downward, so `align-items:
|
|
182
|
-
center` lifts the label's visible text above the checkbox midline. Zero
|
|
183
|
-
it out for start/end; keep it for top where the bottom margin is the
|
|
184
|
-
intended gap before the control. */
|
|
185
|
-
.fluent-checkbox-wrapper[data-label-position="start"] .fluent-label,
|
|
186
|
-
.fluent-checkbox-wrapper[data-label-position="end"] .fluent-label {
|
|
187
|
-
margin-bottom: 0;
|
|
188
|
-
}
|
|
189
|
-
|
|
194
|
+
/* "top" position: label stacks above the checkbox. Label keeps its
|
|
195
|
+
value-label styling — only the wrapper layout changes. */
|
|
190
196
|
.fluent-checkbox-wrapper[data-label-position="top"] {
|
|
191
197
|
flex-direction: column;
|
|
192
198
|
align-items: flex-start;
|
|
@@ -200,7 +206,7 @@
|
|
|
200
206
|
We use the `order` property rather than `flex-direction: row-reverse`
|
|
201
207
|
so the optional status message always trails the [checkbox, label] pair
|
|
202
208
|
instead of jumping to the front (which row-reverse would do). */
|
|
203
|
-
.fluent-checkbox-wrapper[data-label-position="end"] .fluent-label {
|
|
209
|
+
.fluent-checkbox-wrapper[data-label-position="end"] :global(.fluent-value-label) {
|
|
204
210
|
order: 1;
|
|
205
211
|
}
|
|
206
212
|
.fluent-checkbox-wrapper[data-label-position="end"] .checkbox-message {
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
placeholder?: string
|
|
23
23
|
position?: "above" | "below"
|
|
24
24
|
disabled?: boolean
|
|
25
|
-
readonly?: boolean
|
|
26
25
|
appearance?: "outline" | "filled"
|
|
27
26
|
required?: boolean
|
|
28
27
|
autofocus?: boolean
|
|
@@ -51,7 +50,6 @@
|
|
|
51
50
|
placeholder = undefined,
|
|
52
51
|
position = undefined,
|
|
53
52
|
disabled = undefined,
|
|
54
|
-
readonly = undefined,
|
|
55
53
|
appearance = undefined,
|
|
56
54
|
required = undefined,
|
|
57
55
|
autofocus = undefined,
|
|
@@ -174,7 +172,7 @@
|
|
|
174
172
|
// )
|
|
175
173
|
return
|
|
176
174
|
}
|
|
177
|
-
if (untrack(() =>
|
|
175
|
+
if (untrack(() => disabled)) {
|
|
178
176
|
selectedOptions.set(untrack(() => $state.snapshot(value)))
|
|
179
177
|
return
|
|
180
178
|
}
|
|
@@ -241,7 +239,7 @@
|
|
|
241
239
|
|
|
242
240
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
243
241
|
{#if label || labelTemplate}
|
|
244
|
-
<label class="
|
|
242
|
+
<label class="fluent-label" for={id}>
|
|
245
243
|
{#if label}
|
|
246
244
|
{label}
|
|
247
245
|
{/if}
|
|
@@ -264,7 +262,6 @@
|
|
|
264
262
|
placeholder={placeholder || null}
|
|
265
263
|
position={position}
|
|
266
264
|
disabled={disabled || null}
|
|
267
|
-
readonly={readonly || null}
|
|
268
265
|
appearance={appearance || null}
|
|
269
266
|
required={required || null}
|
|
270
267
|
autofocus={autofocus || null}
|
|
@@ -285,14 +282,6 @@
|
|
|
285
282
|
</fluent-combobox>
|
|
286
283
|
|
|
287
284
|
<style>
|
|
288
|
-
.combobox-label {
|
|
289
|
-
display: block;
|
|
290
|
-
font-size: 0.875rem;
|
|
291
|
-
font-weight: 600;
|
|
292
|
-
color: var(--neutral-foreground-rest);
|
|
293
|
-
margin-bottom: 0.25rem;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
285
|
.required-indicator {
|
|
297
286
|
color: var(--error-foreground-rest, #d13438);
|
|
298
287
|
margin-left: 0.25rem;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
* DatePicker Component
|
|
3
3
|
* Inspired by FluentUI Blazor DatePicker component
|
|
4
|
-
* https://www.fluentui-blazor.net/
|
|
4
|
+
* https://www.fluentui-blazor.net/DateTime
|
|
5
5
|
*
|
|
6
6
|
* Combines TextField with Calendar popup for date selection
|
|
7
7
|
-->
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
firstDayOfWeek?: number | null
|
|
49
49
|
/** When true (default), the popup closes after a date is picked. */
|
|
50
50
|
autoClose?: boolean
|
|
51
|
+
/** When true (default), clicking anywhere on the input area opens the calendar popup, matching FluentUI Blazor. Set false to require clicking the calendar icon. */
|
|
52
|
+
openOnInputClick?: boolean
|
|
51
53
|
/** Bindable open state of the calendar popup. */
|
|
52
54
|
open?: boolean
|
|
53
55
|
openCalendarIconAriaLabel?: string
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
animatePeriodChanges = undefined,
|
|
88
90
|
firstDayOfWeek = undefined,
|
|
89
91
|
autoClose = true,
|
|
92
|
+
openOnInputClick = true,
|
|
90
93
|
open = $bindable(false),
|
|
91
94
|
openCalendarIconAriaLabel = "Open calendar",
|
|
92
95
|
title = undefined,
|
|
@@ -168,12 +171,21 @@
|
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
// Handle input click to open calendar
|
|
171
|
-
function handleInputClick() {
|
|
174
|
+
function handleInputClick(event?: Event) {
|
|
175
|
+
event?.stopPropagation()
|
|
172
176
|
if (!disabled && !readonly) {
|
|
173
177
|
setOpen(!isOpen)
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
// Handle click anywhere on the input area — opens (does not toggle) so
|
|
182
|
+
// clicking inside an already-open picker keeps it open. Matches Blazor.
|
|
183
|
+
function handleWrapperClick() {
|
|
184
|
+
if (!openOnInputClick) return
|
|
185
|
+
if (disabled || readonly) return
|
|
186
|
+
if (!isOpen) setOpen(true)
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
// Handle input change (manual text entry)
|
|
178
190
|
function handleInputChange(event: Event) {
|
|
179
191
|
const target = event.target as HTMLInputElement
|
|
@@ -192,7 +204,8 @@
|
|
|
192
204
|
}
|
|
193
205
|
|
|
194
206
|
// Handle clear
|
|
195
|
-
function handleClear() {
|
|
207
|
+
function handleClear(event?: Event) {
|
|
208
|
+
event?.stopPropagation()
|
|
196
209
|
value = null
|
|
197
210
|
inputValue = ""
|
|
198
211
|
setOpen(false)
|
|
@@ -209,7 +222,9 @@
|
|
|
209
222
|
</label>
|
|
210
223
|
{/if}
|
|
211
224
|
|
|
212
|
-
|
|
225
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
226
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
227
|
+
<div class="datepicker-wrapper" bind:this={wrapperElement} onclick={handleWrapperClick}>
|
|
213
228
|
<TextField
|
|
214
229
|
{id}
|
|
215
230
|
value={inputValue}
|
|
@@ -221,7 +236,7 @@
|
|
|
221
236
|
{appearance}
|
|
222
237
|
{title}
|
|
223
238
|
oninput={handleInputChange}
|
|
224
|
-
style="width: 100%; cursor: pointer;"
|
|
239
|
+
style={openOnInputClick && !disabled && !readonly ? "width: 100%; cursor: pointer;" : "width: 100%;"}
|
|
225
240
|
>
|
|
226
241
|
{#snippet end()}
|
|
227
242
|
<span class="end-buttons">
|
|
@@ -268,6 +283,8 @@
|
|
|
268
283
|
pickerMonth={value || new Date()}
|
|
269
284
|
{culture}
|
|
270
285
|
{firstDayOfWeek}
|
|
286
|
+
{disabled}
|
|
287
|
+
{readonly}
|
|
271
288
|
{disabledSelectable}
|
|
272
289
|
{disabledCheckAllDaysOfMonthYear}
|
|
273
290
|
{dayFormat}
|
|
@@ -35,6 +35,8 @@ type Props = {
|
|
|
35
35
|
firstDayOfWeek?: number | null;
|
|
36
36
|
/** When true (default), the popup closes after a date is picked. */
|
|
37
37
|
autoClose?: boolean;
|
|
38
|
+
/** When true (default), clicking anywhere on the input area opens the calendar popup, matching FluentUI Blazor. Set false to require clicking the calendar icon. */
|
|
39
|
+
openOnInputClick?: boolean;
|
|
38
40
|
/** Bindable open state of the calendar popup. */
|
|
39
41
|
open?: boolean;
|
|
40
42
|
openCalendarIconAriaLabel?: string;
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
type Props = {
|
|
9
9
|
class?: string
|
|
10
10
|
style?: string
|
|
11
|
+
orientation?: "horizontal" | "vertical"
|
|
12
|
+
role?: "separator" | "presentation"
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
let {class: className = "", style = ""}: Props = $props()
|
|
15
|
+
let {class: className = "", style = "", orientation = "horizontal", role = "separator"}: Props = $props()
|
|
14
16
|
</script>
|
|
15
17
|
|
|
16
18
|
<!-- fluent-divider's shadow DOM borders itself with `--neutral-stroke-divider-rest`
|
|
@@ -18,5 +20,10 @@
|
|
|
18
20
|
page background and reads as invisible. Re-point the token at the heavier
|
|
19
21
|
`--neutral-stroke-rest` so the line actually shows up. The variable is
|
|
20
22
|
inherited into the shadow DOM, so setting it on the host suffices. -->
|
|
21
|
-
<fluent-divider
|
|
23
|
+
<fluent-divider
|
|
24
|
+
class={className}
|
|
25
|
+
{orientation}
|
|
26
|
+
{role}
|
|
27
|
+
style="--neutral-stroke-divider-rest: var(--neutral-stroke-rest); {style}"
|
|
28
|
+
></fluent-divider>
|
|
22
29
|
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
type Props = {
|
|
13
13
|
value: ValueType
|
|
14
14
|
multi?: boolean
|
|
15
|
-
readonly?: boolean
|
|
16
15
|
disabled?: boolean
|
|
17
16
|
autofocus?: boolean
|
|
18
17
|
name?: string
|
|
@@ -31,7 +30,6 @@
|
|
|
31
30
|
value = $bindable(),
|
|
32
31
|
multi = undefined,
|
|
33
32
|
disabled = undefined,
|
|
34
|
-
readonly = undefined,
|
|
35
33
|
autofocus = undefined,
|
|
36
34
|
name = undefined,
|
|
37
35
|
label = undefined,
|
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
* width via Floating UI's `size` middleware. Useful for dropdowns whose
|
|
29
29
|
* options should align under the input. */
|
|
30
30
|
matchWidth?: boolean
|
|
31
|
+
/** When true, exposes the remaining viewport height as the CSS custom
|
|
32
|
+
* property `--available-height` on the floating element (minus the
|
|
33
|
+
* shift padding). Consumers should set max-height: var(--available-height)
|
|
34
|
+
* on a scroll container inside, so the dropdown stays inside the viewport
|
|
35
|
+
* regardless of which side flip() picks. */
|
|
36
|
+
availableHeight?: boolean
|
|
31
37
|
children?: SlotType
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -39,6 +45,7 @@
|
|
|
39
45
|
position: positionProp = "bottom",
|
|
40
46
|
align: alignProp = "center",
|
|
41
47
|
matchWidth = true,
|
|
48
|
+
availableHeight = false,
|
|
42
49
|
children = undefined
|
|
43
50
|
}: Props = $props()
|
|
44
51
|
|
|
@@ -60,10 +67,12 @@
|
|
|
60
67
|
anchor: HTMLElement
|
|
61
68
|
placement: Placement
|
|
62
69
|
matchWidth: boolean
|
|
70
|
+
availableHeight: boolean
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
function position(node: HTMLElement, params: PositionParams) {
|
|
66
74
|
let cleanup: () => void = () => {}
|
|
75
|
+
const SHIFT_PADDING = 8
|
|
67
76
|
|
|
68
77
|
function attach(p: PositionParams) {
|
|
69
78
|
cleanup()
|
|
@@ -71,13 +80,22 @@
|
|
|
71
80
|
const middleware = [
|
|
72
81
|
offset(0),
|
|
73
82
|
flip(),
|
|
74
|
-
shift({padding:
|
|
83
|
+
shift({padding: SHIFT_PADDING})
|
|
75
84
|
]
|
|
76
|
-
if (p.matchWidth) {
|
|
85
|
+
if (p.matchWidth || p.availableHeight) {
|
|
77
86
|
middleware.push(
|
|
78
87
|
size({
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
padding: SHIFT_PADDING,
|
|
89
|
+
apply({rects, elements, availableHeight: h}) {
|
|
90
|
+
if (p.matchWidth) {
|
|
91
|
+
elements.floating.style.width = `${rects.reference.width}px`
|
|
92
|
+
}
|
|
93
|
+
if (p.availableHeight) {
|
|
94
|
+
elements.floating.style.setProperty(
|
|
95
|
+
"--available-height",
|
|
96
|
+
`${Math.max(0, Math.floor(h))}px`
|
|
97
|
+
)
|
|
98
|
+
}
|
|
81
99
|
}
|
|
82
100
|
})
|
|
83
101
|
)
|
|
@@ -110,7 +128,7 @@
|
|
|
110
128
|
{#if visible}
|
|
111
129
|
<div
|
|
112
130
|
use:portal
|
|
113
|
-
use:position={{anchor, placement, matchWidth}}
|
|
131
|
+
use:position={{anchor, placement, matchWidth, availableHeight}}
|
|
114
132
|
class="positioning-region positioning-region-floating"
|
|
115
133
|
{title}
|
|
116
134
|
style="position: fixed; top: 0; left: 0; {style}"
|
|
@@ -15,6 +15,12 @@ type Props = {
|
|
|
15
15
|
* width via Floating UI's `size` middleware. Useful for dropdowns whose
|
|
16
16
|
* options should align under the input. */
|
|
17
17
|
matchWidth?: boolean;
|
|
18
|
+
/** When true, exposes the remaining viewport height as the CSS custom
|
|
19
|
+
* property `--available-height` on the floating element (minus the
|
|
20
|
+
* shift padding). Consumers should set max-height: var(--available-height)
|
|
21
|
+
* on a scroll container inside, so the dropdown stays inside the viewport
|
|
22
|
+
* regardless of which side flip() picks. */
|
|
23
|
+
availableHeight?: boolean;
|
|
18
24
|
children?: SlotType;
|
|
19
25
|
};
|
|
20
26
|
declare const PositioningRegion: import("svelte").Component<Props, {}, "">;
|