svelte-fluentui 1.1.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 CHANGED
@@ -2,6 +2,15 @@
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.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
+
5
14
  ## What's New in v1.1.0
6
15
 
7
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.
@@ -10,28 +19,6 @@ A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2
10
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.
11
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.
12
21
 
13
- ## What's New in v1.0.0
14
-
15
- The first stable release. Headline changes since the rc series:
16
-
17
- - **`Calendar` — generic `highlightDates` + `selectDates` function-prop split (replaces `selectDatesHover`)** — two consumer-supplied pure functions of shape `(date: Date) => Date[]` drive the hover preview and the click selection separately, so you can preview a week on hover but commit only 3 days on click. Multiple mode UNIONS the returned group with the existing array (clicked day toggles the whole group); range mode replaces. Consumer functions are authoritative — the calendar no longer re-filters against `disabledDateFunc`, so consumers are free to skip disabled days, jump past them to reach a target count, or include them deliberately for special highlighting. Two new demos: "Range with SelectOneWeek" (hovering any day highlights Mon→Sun, click commits the whole week) and "Multiple with Select3Days" (hover/click selects the day ± 1 neighbor).
18
- - **`Calendar` — `maxSelectableDays` + `onSelectionError` callback with structured payload** — multiple/range modes now gate every selection against an upper bound. Overflow does NOT modify `selectedDates`; instead fires `onSelectionError({code, message, attempted, current, max})` with a discriminated-union `code` (`"max_selectable_days_exceeded"` for now, future codes won't break consumer dispatch). New exported `CalendarSelectionError` type. Demo "Multiple (max 5 days)" shows the Alert-remount pattern for repeat-error feedback.
19
- - **`Calendar` — `onDayHover?(date: Date | null) => void` pure-observation callback** — fires with the hovered `Date` regardless of `selectMode` / disabled state (so tooltips can react to inactive days too), and with `null` when the cursor leaves the grid. Distinct from `highlightDates` (which DRIVES rendering); `onDayHover` is for tooltips, analytics, "currently hovering" readouts.
20
- - **`Calendar` — public type re-exports** — `CalendarView`, `CalendarSelectMode`, `CalendarSelectionError`, `CalendarSelectionErrorCode` now importable from the package entry point.
21
- - **`Checkbox` — three state-driven message props (`checkedMessage` / `uncheckedMessage` / `intermediateMessage`)** — Switch already had `checkedMessage` / `uncheckedMessage`; Checkbox catches up plus the three-state variant for `withIntermediate`. All three accept `string | Snippet`, rendered as a sibling `.checkbox-message` after the box.
22
- - **`Checkbox` — `labelPosition="end"` (now the default)** — was `"start"` (label on the LEFT, atypical for forms). New default puts the label after the box `[☐] Label text`. Implemented via flex `order` so a trailing status message stays after the [box, label] pair without flipping.
23
- - **Docs — per-page SEO metadata via shared `<Meta>` component on all 63 routes** — every page now emits its own `<title>`, description, keywords, canonical, Open Graph, and Twitter Card tags. Browser tabs, bookmarks, and JS-capable crawlers (Google) get the per-page tags; non-JS crawlers (Slack, Discord, Facebook, LinkedIn, X) still see the static `app.html` fallback since the docs site runs as a pure SPA.
24
- - **Docs — every `<h1>` / `<h2>` / `<h3>` in component pages is now a shareable anchor with a copy-link button on hover** — pure post-render DOM decoration at the layout level, so it works on every existing and future component page automatically. Stable slugified ids, `-2`/`-3` suffixes on collisions, copy-to-clipboard with flash feedback.
25
- - **Docs — shared `<References>` component replaces hand-rolled references cards on 48 pages** — single source of truth for the references block; `na: true` / `custom: true` variants for "Not available in FluentUI Web Components" and "Custom component" disabled spans with tooltips. Format tweaks now live in one file instead of 48.
26
- - **Docs — API tables relocated to the BOTTOM of every component page, AFTER the Examples** — the previous layout forced visitors to scroll past dense reference material to reach the examples. New order: `[title + description] → [References] → [Examples] → [API tables]`.
27
- - **`InputFile` — 2547-line component split across six files for maintainability** — types extracted to `InputFile.types.ts`, utilities + default labels to `InputFile.utils.ts`, ~725 lines of inline CSS to an external SCSS partial, and the three `selectorAppearance` variants pulled into `InputFileSelectorCard.svelte` / `InputFileSelectorButton.svelte` / `InputFileSelectorMinimal.svelte`. Public API unchanged; type-check clean.
28
-
29
- ### Previously (rc22)
30
-
31
- - **`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.
32
- - **`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.
33
- - **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).
34
-
35
22
  ## Features
36
23
 
37
24
  - 🎨 **Complete FluentUI Component Set** - Wraps all major FluentUI web components
@@ -32,9 +32,9 @@
32
32
  align-items: center;
33
33
  }
34
34
 
35
- // Canonical label style used by form components (TextField, Select,
36
- // Autocomplete, Radio, RadioGroup) so labels stay visually consistent
37
- // regardless of whether the input is a fluent web component or a custom one.
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
+
@@ -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 class="fluent-checkbox-wrapper" data-label-position={labelPosition}>
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={id} class="fluent-label">
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
- {id}
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
- /* `.fluent-label` carries a 0.25rem bottom margin (set globally in
180
- components.scss for stacked form-field labels). In inline layouts that
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 {
@@ -239,7 +239,7 @@
239
239
 
240
240
  <!-- svelte-ignore a11y_label_has_associated_control -->
241
241
  {#if label || labelTemplate}
242
- <label class="combobox-label">
242
+ <label class="fluent-label" for={id}>
243
243
  {#if label}
244
244
  {label}
245
245
  {/if}
@@ -282,14 +282,6 @@
282
282
  </fluent-combobox>
283
283
 
284
284
  <style>
285
- .combobox-label {
286
- display: block;
287
- font-size: 0.875rem;
288
- font-weight: 600;
289
- color: var(--neutral-foreground-rest);
290
- margin-bottom: 0.25rem;
291
- }
292
-
293
285
  .required-indicator {
294
286
  color: var(--error-foreground-rest, #d13438);
295
287
  margin-left: 0.25rem;
@@ -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 class={className} style="--neutral-stroke-divider-rest: var(--neutral-stroke-rest); {style}"></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
 
@@ -1,6 +1,8 @@
1
1
  type Props = {
2
2
  class?: string;
3
3
  style?: string;
4
+ orientation?: "horizontal" | "vertical";
5
+ role?: "separator" | "presentation";
4
6
  };
5
7
  declare const Divider: import("svelte").Component<Props, {}, "">;
6
8
  type Divider = ReturnType<typeof Divider>;
@@ -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: 8})
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
- apply({rects, elements}) {
80
- elements.floating.style.width = `${rects.reference.width}px`
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, {}, "">;