svelte-fluentui 1.0.0 → 1.1.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,14 @@
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.1.0
6
+
7
+ - **`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.
8
+ - **`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.
9
+ - **`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.
10
+ - **`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
+ - **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
+
5
13
  ## What's New in v1.0.0
6
14
 
7
15
  The first stable release. Headline changes since the rc series:
@@ -24,17 +32,6 @@ The first stable release. Headline changes since the rc series:
24
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.
25
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).
26
34
 
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
37
-
38
35
  ## Features
39
36
 
40
37
  - 🎨 **Complete FluentUI Component Set** - Wraps all major FluentUI web components
@@ -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
- readOnly: readonly ?? false,
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]`. */
@@ -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(() => readonly || disabled)) {
175
+ if (untrack(() => disabled)) {
178
176
  selectedOptions.set(untrack(() => $state.snapshot(value)))
179
177
  return
180
178
  }
@@ -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}
@@ -10,7 +10,6 @@ type Props = {
10
10
  placeholder?: string;
11
11
  position?: "above" | "below";
12
12
  disabled?: boolean;
13
- readonly?: boolean;
14
13
  appearance?: "outline" | "filled";
15
14
  required?: boolean;
16
15
  autofocus?: boolean;
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  * DatePicker Component
3
3
  * Inspired by FluentUI Blazor DatePicker component
4
- * https://www.fluentui-blazor.net/DatePicker
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
- <div class="datepicker-wrapper" bind:this={wrapperElement}>
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;
@@ -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,
@@ -3,7 +3,6 @@ type ValueType = string | string[] | null | undefined;
3
3
  type Props = {
4
4
  value: ValueType;
5
5
  multi?: boolean;
6
- readonly?: boolean;
7
6
  disabled?: boolean;
8
7
  autofocus?: boolean;
9
8
  name?: string;
@@ -5,6 +5,17 @@
5
5
 
6
6
  provideFluentDesignSystem().register(fluentTextArea())
7
7
 
8
+ const activeIndicatorOverride =
9
+ typeof CSSStyleSheet !== "undefined"
10
+ ? (() => {
11
+ const sheet = new CSSStyleSheet()
12
+ sheet.replaceSync(
13
+ ":host(:not([disabled]):active)::after { left: 0; width: 100%; transform: none; }"
14
+ )
15
+ return sheet
16
+ })()
17
+ : (undefined as unknown as CSSStyleSheet)
18
+
8
19
  type Props = {
9
20
  class?: string
10
21
  style?: string
@@ -88,6 +99,23 @@
88
99
  }
89
100
  })
90
101
 
102
+ // Neutralize FluentUI's `:active::after` rule, which collapses the focus
103
+ // indicator to a 40%-wide centered line while the user is dragging the
104
+ // resize handle. Make active match focus-within (full-width line).
105
+ $effect(() => {
106
+ if (!element || typeof CSSStyleSheet === "undefined") return
107
+ const adopt = () => {
108
+ const root = element?.shadowRoot
109
+ if (!root) {
110
+ setTimeout(adopt, 10)
111
+ return
112
+ }
113
+ if (root.adoptedStyleSheets.includes(activeIndicatorOverride)) return
114
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, activeIndicatorOverride]
115
+ }
116
+ adopt()
117
+ })
118
+
91
119
  // Sizing: by default the host opts out of parent flex stretching (matches the
92
120
  // Blazor demo, where a default textarea renders at its intrinsic ~20ch width).
93
121
  // `fullWidth` opts back into stretching. `cols` sets the host attribute that
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  * TimePicker Component
3
3
  * Inspired by FluentUI Blazor TimePicker component
4
- * https://www.fluentui-blazor.net/TimePicker
4
+ * https://www.fluentui-blazor.net/DateTime
5
5
  *
6
6
  * Provides time selection with hour, minute, and optional second/millisecond inputs
7
7
  -->
@@ -40,6 +40,8 @@
40
40
  disabledTimes?: string[]
41
41
  /** When true (default), the popup closes when the user clicks OK. */
42
42
  autoClose?: boolean
43
+ /** When true (default), clicking anywhere on the input area opens the popup, matching FluentUI Blazor. Set false to require clicking the clock icon. */
44
+ openOnInputClick?: boolean
43
45
  /** Bindable popup open state. */
44
46
  open?: boolean
45
47
  openClockIconAriaLabel?: string
@@ -72,6 +74,7 @@
72
74
  disabledTimeFunc = undefined,
73
75
  disabledTimes = undefined,
74
76
  autoClose = true,
77
+ openOnInputClick = true,
75
78
  open = $bindable(false),
76
79
  openClockIconAriaLabel = "Open time picker",
77
80
  title = undefined,
@@ -278,13 +281,21 @@
278
281
 
279
282
  let displayValue = $derived(formatTime())
280
283
 
281
- // Handle input click
284
+ // Handle clock-icon click — toggles the popup. Always available regardless of openOnInputClick.
282
285
  function handleInputClick(e: MouseEvent) {
283
286
  if (!disabled && !readonly) {
284
287
  setOpen(!isOpen)
285
288
  }
286
289
  }
287
290
 
291
+ // Handle click anywhere on the input area — opens (does not toggle) so
292
+ // clicking inside an already-open picker keeps it open. Matches Blazor.
293
+ function handleWrapperClick() {
294
+ if (!openOnInputClick) return
295
+ if (disabled || readonly) return
296
+ if (!isOpen) setOpen(true)
297
+ }
298
+
288
299
  // The wrapper has its own onclick for "click anywhere in field to open"; without these the click bubbles
289
300
  // from the inner button → wrapper, toggling the popup twice (clock) or re-opening after clear (clear).
290
301
  function handleClockButtonClick(e: MouseEvent) {
@@ -363,7 +374,7 @@
363
374
 
364
375
  <!-- svelte-ignore a11y_click_events_have_key_events -->
365
376
  <!-- svelte-ignore a11y_no_static_element_interactions -->
366
- <div class="timepicker-wrapper" bind:this={wrapperElement} onclick={handleInputClick} style="cursor: pointer;">
377
+ <div class="timepicker-wrapper" bind:this={wrapperElement} onclick={handleWrapperClick} style:cursor={openOnInputClick && !disabled && !readonly ? "pointer" : undefined}>
367
378
  <TextField
368
379
  {id}
369
380
  value={displayValue}
@@ -27,6 +27,8 @@ type Props = {
27
27
  disabledTimes?: string[];
28
28
  /** When true (default), the popup closes when the user clicks OK. */
29
29
  autoClose?: boolean;
30
+ /** When true (default), clicking anywhere on the input area opens the popup, matching FluentUI Blazor. Set false to require clicking the clock icon. */
31
+ openOnInputClick?: boolean;
30
32
  /** Bindable popup open state. */
31
33
  open?: boolean;
32
34
  openClockIconAriaLabel?: string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.0.0";
1
+ export declare const VERSION = "1.1.0";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // Auto-generated by scripts/pre-package.js — do NOT edit by hand.
2
2
  // In dev this file holds whatever value was committed last; on each `npm run package`
3
3
  // the pre-package script overwrites it with the current package.json version.
4
- export const VERSION = "1.0.0";
4
+ export const VERSION = "1.1.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-fluentui",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A Svelte wrapper library for Microsoft FluentUI web components",
5
5
  "license": "MIT",
6
6
  "author": "KeenMate",