svelte-fluentui 1.3.1 → 1.3.3

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,21 +2,17 @@
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.3.1
5
+ ## What's New in v1.3.3
6
6
 
7
- - **`Panel` new `pinned` prop renders the panel as an inline `<aside>` instead of a portal'd overlay** flips the rendering entirely: no portal, no backdrop, no transforms, no escape/outside-click handlers, `position: relative` so it participates in the parent's flex/grid layout, `align-self: stretch` so it fills its grid cell vertically. `open` is still respected so the panel can hide and let its grid cell collapse. Explicit z-index forms its own stacking context so overlay Panels opened on top still layer above as expected.
8
- - **`TopNav` — new `drawerPinned` + `drawerWidth` props turn the drawer into a permanent left rail with an always-visible hamburger toggle** — composes Panel's `pinned` so the same `drawerContent` snippet that renders the mobile overlay drawer also renders the desktop rail. `drawerWidth` (default `"280px"`) propagates to both modes. The hamburger stays visible when pinned so the user can collapse the rail back away, and an effect re-syncs the drawer's `open` state to `drawerPinned` on viewport flips (narrow→wide auto-opens, wide→narrow auto-closes).
9
- - **Docs layout switched to a single nav surface at every width via `drawerPinned`** — the duplicate desktop sidebar `GridItem` is gone; TopNav's drawer now renders as a pinned `<aside>` at ≥1024px and collapses to the hamburger overlay below. The layout uses CSS Grid with `grid-template-areas` and a `matchMedia` listener to drive `drawerPinned`. The pinned rail is `position: sticky` below the topnav and the scrollbar sits flush with the rail's inside edge.
10
- - **`TopNav` — tightened horizontal padding from `1.5rem` to `0.5rem`** — the bar's inside edges felt cramped relative to the rest of the content. Brand + hamburger now sit closer to the inline-start edge and the action slot sits closer to the inline-end edge.
7
+ - **Custom-element attribute bindings stop stringifying `undefined` / `null` / `false`** Recurring regression (last fixed ~5 months ago, reintroduced during the structural rework) that affected 22 wrappers including `TextField`, `Textarea`, `Switch`, `Slider`, `Checkbox`, `Button`, `Anchor`, `Accordion(Item)`, `BreadcrumbItem`, `DataGrid(Row/Cell)`, `Dialog`, `Listbox`, `MenuButton`, `Option`, `TabPanel`, `Toolbar`, `NumberField`, `Combobox`, plus `Paginator` and `QuickGrid` sub-buttons. Svelte 5 sets properties on custom elements rather than attributes, and FAST's `@attr` decorators stringify whatever they receive — so unset props were rendering as `title="undefined"`, `readonly="false"`, `disabled="false"`, etc. The `readonly`/`disabled` cases were the worst symptom because `[readonly]` and `[disabled]` CSS attribute-presence selectors match regardless of value, leaving an enabled field with a not-allowed cursor. All affected wrappers converted to the conditional-spread pattern (`{...(title ? { title } : {})}`) which physically omits the attribute from the template when unset. Variants `{...(value != null ? { value } : {})}` for inputs (so `value=""` still renders) and `{...(attr !== undefined ? { attr } : {})}` for numeric props (so `0` survives) are used where falsy values are meaningful.
11
8
 
12
- ## What's New in v1.3.0
9
+ - **`TextField` demo page — new Readonly and Disabled example sections** — The `/components/forms/text-field` showcase previously demonstrated only the basic input and `autocomplete` variants; the readonly and disabled states (the surface that exposed the bug above) had no live example. Added Readonly with outline + filled variants and Disabled with three variants (outline + placeholder, outline + value, filled + value).
13
10
 
14
- - **`Panel` new slide-in side-panel primitive** — fixed-position, edge-anchored container with a single `children` slot and CSS-driven open/close transitions. Props cover physical sides (`left`/`right`), logical sides (`start`/`end` that follow RTL), `width`, `overlay`, `closeOnOutsideClick`, `closeOnEscape`, and a new `top` offset so the panel + overlay can sit below a sticky header (e.g. TopNav) and leave the trigger button clickable. Portals to `document.body` with direction captured before portal so locally-RTL wrappers still anchor correctly. First-class demo at `/components/navigation-layout/panel`.
15
- - **`TopNav` drawer rebuilt on `Panel`; container queries replace media queries** — the bespoke `.mobile-sidebar` div with hand-rolled `left: -280px` / `transition` CSS is gone, replaced with `<Panel side="start" width="280px" top="{height}px">` so Escape-close, portal, overlay outside-click, and `aria-modal` all come for free. The collapse breakpoint is now an `@container topnav (max-width: 960px)` query keyed on the nav's own width (aligns with Grid's `md`), so behavior is intrinsic to the bar regardless of where it's mounted. `items` auto-populate the drawer, the hamburger only renders when there's content to open, and the action slot now stays visible at every width.
16
- - **`TopNav` — `brandTemplate`, `drawerContent`, and `collapse` snippets/props** — `brandTemplate` replaces the default `<a>` brand rendering so consumers can compose logo + wordmark + version badge as one unit; `drawerContent: Snippet<[() => void]>` renders custom content inside the mobile drawer and receives a `closeDrawer` callback so link clicks can dismiss the drawer before navigation; `collapse: "auto" | "always" | "never"` overrides the container-query state when the consumer drives sidebar visibility from its own responsive breakpoint. First demo page added at `/components/navigation-layout/topnav`.
17
- - **`Footer` — `start` / `center` / `end` named snippets with a truly-centered grid layout** — Footer previously had a single `children` slot, so a "© left, brand center, version right" arrangement required inline `justify-content: space-between` and hoping the middle item floated where you wanted. The new snippets switch Footer into a CSS Grid (`1fr auto 1fr`) where the center column is genuinely centered regardless of side-section widths, with explicit `grid-column` placement so a missing section doesn't reflow the others. New `stack` prop (default `true`) collapses to a single inline-start column below 600px via a container query keyed on the footer itself. RTL flips `start` `end` automatically.
18
- - **`Footer` — class namespaced as `.fluent-footer`, wired to the dedicated theme tokens, thin-strip bug fixed** — the rendered class went from generic `.footer` (collided with anything else using that name) to `.fluent-footer`, the rule now reads from `--fluent-footer-bg/text/border` (which were defined in the theme system but unused) with the previous neutral-layer values as fallback, a duplicate Blazor-port `.footer` rule was deleted, and `min-height: 48px` + `padding: 0.75rem 1rem` replaces the original horizontal-only padding so a default Footer is no longer a ~24px thin strip. First dedicated demo page at `/components/navigation-layout/footer`.
19
- - **Docs site dogfoods the new primitives** — the hand-rolled `<div class="topnav">` with ~90 lines of parallel CSS is replaced with `<TopNav class="docs-topnav" collapse="always">` using `brandTemplate` for the brand+version pair and `drawerContent` for the mobile sidebar nav. The sidebar nav is extracted into a single `{#snippet sidebarNav(closeDrawer)}` rendered both as the desktop sidebar and inside the TopNav drawer, with the sidebar/hamburger handoff at `lg` (1280px) instead of `md` so laptops at 1280×800 stay roomy. The footer at the bottom of every page now uses the three-part layout (© + MIT / attribution / GitHub + npm + version chip).
11
+ ## What's New in v1.3.2
12
+
13
+ - **`TopNav` — hamburger flexibility via `menuToggleSize` + `menuToggleTemplate`** — `menuToggleSize` (default `42`) flows through a `--topnav-toggle-size` CSS variable so the glyph and close-icon scale proportionally with the box. `menuToggleTemplate: Snippet<[{open, toggle}]>` lets consumers render any button shape text + chevron, brand-styled accent button, custom SVG and still wire up state correctly. Both paths render inside a wrapper that owns the visibility/order/collapse rules, so custom templates automatically inherit hide-when-wide / show-when-narrow / stay-visible-when-pinned behavior.
14
+ - **`TopNav` — default hamburger is now a plain `<button>` instead of a fluent-button** — the previous default rendered `<Button appearance="stealth">` which still carried button chrome (hover background fill, boxed visual weight). Replaced with a vanilla `<button>` styled `background: transparent; border: none; padding: 0`, with `opacity: 0.7` on hover and a `:focus-visible` accent outline for keyboard nav. Reads as just the icon, not a button containing an icon. Click target still fills the full `menuToggleSize` box for touch reachability.
15
+ - **`Badge` — new `radius` prop overrides the binary rounded-rect / pill shape with an arbitrary border-radius** — Badge had exactly two shape modes (default 4px corner / `circular={true}` pill) mirroring Microsoft's FluentUI Badge `shape` prop. The new `radius?: string` accepts any CSS length (`8px`, `0.5rem`, `var(--my-radius)`) and emits inline `border-radius`, winning over both class rules via CSS specificity. Plays nicely with `circular={true}` the circular sizing wins, the radius wins, so you get pill-sized badges with non-pill corners if you want.
20
16
 
21
17
  ## Features
22
18
 
@@ -84,6 +84,6 @@
84
84
  }
85
85
  </script>
86
86
 
87
- <fluent-accordion expand-mode={multi} onchange={handleAccordionChange}>
87
+ <fluent-accordion {...(multi ? { "expand-mode": multi } : {})} onchange={handleAccordionChange}>
88
88
  {@render children?.()}
89
89
  </fluent-accordion>
@@ -43,14 +43,18 @@
43
43
 
44
44
  onchange?.(ev, target.getAttribute("expanded") === "")
45
45
  }
46
+
47
+ const isExpanded = $derived(
48
+ expanded === undefined && ctx.value !== undefined ? !!ctx.value?.includes(id) : expanded
49
+ )
46
50
  </script>
47
51
 
48
52
  <!-- todo: investigate flicker during switching of accordions -->
49
53
  <fluent-accordion-item
50
- {id}
54
+ {...(id ? { id } : {})}
51
55
  data-custom-id={id}
52
- heading-level={headingLevel}
53
- expanded={expanded === undefined && ctx.value !== undefined ? !!ctx.value?.includes(id) : expanded}
56
+ {...(headingLevel !== undefined ? { "heading-level": headingLevel } : {})}
57
+ {...(isExpanded ? { expanded: isExpanded } : {})}
54
58
  onchange={handleOnChange}
55
59
  >
56
60
  <div slot="heading">
@@ -68,18 +68,18 @@
68
68
  <!-- svelte-ignore a11y_click_events_have_key_events -->
69
69
  <fluent-anchor
70
70
  bind:this={element}
71
- {id}
72
- class={className}
73
- {style}
74
- {download}
75
- {href}
76
- {hreflang}
77
- {ping}
78
- {referrerpolicy}
79
- {rel}
80
- {target}
81
- {type}
82
- {appearance}
71
+ {...(id ? { id } : {})}
72
+ {...(className ? { class: className } : {})}
73
+ {...(style ? { style } : {})}
74
+ {...(download ? { download } : {})}
75
+ {...(href ? { href } : {})}
76
+ {...(hreflang ? { hreflang } : {})}
77
+ {...(ping ? { ping } : {})}
78
+ {...(referrerpolicy ? { referrerpolicy } : {})}
79
+ {...(rel ? { rel } : {})}
80
+ {...(target ? { target } : {})}
81
+ {...(type ? { type } : {})}
82
+ {...(appearance ? { appearance } : {})}
83
83
  onclick={handleClick}
84
84
  >
85
85
  {#if iconStart}
@@ -18,6 +18,7 @@
18
18
  fill?: string
19
19
  appearance?: BadgeAppearance
20
20
  circular?: boolean
21
+ radius?: string
21
22
  children?: SlotType
22
23
  onclick?: (ev: MouseEvent) => void
23
24
  class?: string
@@ -29,6 +30,7 @@
29
30
  fill = undefined,
30
31
  appearance = "lightweight",
31
32
  circular = false,
33
+ radius = undefined,
32
34
  children = undefined,
33
35
  onclick = undefined,
34
36
  class: className = "",
@@ -51,7 +53,9 @@
51
53
  return styles
52
54
  })
53
55
 
54
- let computedStyle = $derived([colorStyles, style].filter(Boolean).join(" "))
56
+ let radiusStyle = $derived(radius ? `border-radius: ${radius};` : "")
57
+
58
+ let computedStyle = $derived([colorStyles, radiusStyle, style].filter(Boolean).join(" "))
55
59
 
56
60
  let classes = $derived.by(() => {
57
61
  let cls = ["badge"]
@@ -15,6 +15,7 @@ type Props = {
15
15
  fill?: string;
16
16
  appearance?: BadgeAppearance;
17
17
  circular?: boolean;
18
+ radius?: string;
18
19
  children?: SlotType;
19
20
  onclick?: (ev: MouseEvent) => void;
20
21
  class?: string;
@@ -20,7 +20,7 @@
20
20
  </script>
21
21
 
22
22
  <fluent-breadcrumb-item
23
- {href}
23
+ {...(href ? { href } : {})}
24
24
  >
25
25
  {#if separator}
26
26
  <span slot="separator">
@@ -56,22 +56,22 @@
56
56
  <!-- svelte-ignore a11y_click_events_have_key_events -->
57
57
  <!-- svelte-ignore a11y_autofocus -->
58
58
  <fluent-button
59
- {type}
60
- {name}
61
- {value}
62
- {appearance}
63
- {autofocus}
64
- {formaction}
65
- {form}
66
- {formenctype}
67
- {formmethod}
68
- {formnovalidate}
69
- {formtarget}
70
- {disabled}
71
- {style}
72
- class={className}
73
- aria-label={ariaLabel}
74
- {title}
59
+ {...(type ? { type } : {})}
60
+ {...(name ? { name } : {})}
61
+ {...(value != null ? { value } : {})}
62
+ {...(appearance ? { appearance } : {})}
63
+ {...(autofocus ? { autofocus } : {})}
64
+ {...(formaction ? { formaction } : {})}
65
+ {...(form ? { form } : {})}
66
+ {...(formenctype ? { formenctype } : {})}
67
+ {...(formmethod ? { formmethod } : {})}
68
+ {...(formnovalidate ? { formnovalidate } : {})}
69
+ {...(formtarget ? { formtarget } : {})}
70
+ {...(disabled ? { disabled } : {})}
71
+ {...(style ? { style } : {})}
72
+ {...(className ? { class: className } : {})}
73
+ {...(ariaLabel ? { "aria-label": ariaLabel } : {})}
74
+ {...(title ? { title } : {})}
75
75
  {onclick}
76
76
  >
77
77
  {#if start}
@@ -155,17 +155,17 @@
155
155
  <!-- svelte-ignore a11y_autofocus -->
156
156
  <fluent-checkbox
157
157
  bind:this={element}
158
- checked={checked === true}
159
- indeterminate={checked === null}
160
- {autofocus}
161
- {readonly}
162
- {disabled}
163
- {required}
158
+ {...(checked === true ? { checked: true } : {})}
159
+ {...(checked === null ? { indeterminate: true } : {})}
160
+ {...(autofocus ? { autofocus } : {})}
161
+ {...(readonly ? { readonly } : {})}
162
+ {...(disabled ? { disabled } : {})}
163
+ {...(required ? { required } : {})}
164
164
  id={effectiveId}
165
- {name}
166
- aria-label={ariaLabel || label || null}
167
- class={className || null}
168
- style={style || null}
165
+ {...(name ? { name } : {})}
166
+ {...(ariaLabel || label ? { "aria-label": ariaLabel || label } : {})}
167
+ {...(className ? { class: className } : {})}
168
+ {...(style ? { style } : {})}
169
169
  onclick={handleOnClick}
170
170
  ></fluent-checkbox>
171
171
 
@@ -254,19 +254,19 @@
254
254
  <fluent-combobox
255
255
  bind:this={element}
256
256
  class={className}
257
- style={computedStyle()}
258
- {id}
259
- autocomplete={autocomplete || null}
260
- open={open || null}
261
- current-value={currentValue || null}
262
- placeholder={placeholder || null}
263
- position={position}
264
- disabled={disabled || null}
265
- appearance={appearance || null}
266
- required={required || null}
267
- autofocus={autofocus || null}
268
- name={name || null}
269
- aria-label={ariaLabel || label || null}
257
+ {...(computedStyle() ? { style: computedStyle() } : {})}
258
+ {...(id ? { id } : {})}
259
+ {...(autocomplete ? { autocomplete } : {})}
260
+ {...(open ? { open } : {})}
261
+ {...(currentValue ? { "current-value": currentValue } : {})}
262
+ {...(placeholder ? { placeholder } : {})}
263
+ {...(position ? { position } : {})}
264
+ {...(disabled ? { disabled } : {})}
265
+ {...(appearance ? { appearance } : {})}
266
+ {...(required ? { required } : {})}
267
+ {...(autofocus ? { autofocus } : {})}
268
+ {...(name ? { name } : {})}
269
+ {...(ariaLabel || label ? { "aria-label": ariaLabel ?? label } : {})}
270
270
  {...titleProps}
271
271
  onchange={handleChange}
272
272
  >
@@ -39,10 +39,10 @@
39
39
 
40
40
  <fluent-data-grid
41
41
  class={className}
42
- {style}
43
- {id}
44
- aria-rowcount={ariaRowCount}
45
- generate-header={generateHeader}
42
+ {...(style ? { style } : {})}
43
+ {...(id ? { id } : {})}
44
+ {...(ariaRowCount !== undefined ? { "aria-rowcount": ariaRowCount } : {})}
45
+ {...(generateHeader ? { "generate-header": generateHeader } : {})}
46
46
  role={role}
47
47
  onclosecolumnoptions={handleCloseColumnOptions}
48
48
  onclosecolumnresize={handleCloseColumnResize}
@@ -52,14 +52,14 @@
52
52
  <!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_click_events_have_key_events -->
53
53
  <fluent-data-grid-cell
54
54
  class={className}
55
- {style}
56
- grid-column={gridColumn}
57
- cell-type={cellType}
58
- col-index={colIndex}
59
- {role}
60
- tabindex={tabIndex}
61
- {title}
62
- aria-label={ariaLabel}
55
+ {...(style ? { style } : {})}
56
+ {...(gridColumn !== undefined ? { "grid-column": gridColumn } : {})}
57
+ {...(cellType ? { "cell-type": cellType } : {})}
58
+ {...(colIndex !== undefined ? { "col-index": colIndex } : {})}
59
+ {...(role ? { role } : {})}
60
+ {...(tabIndex !== undefined ? { tabindex: tabIndex } : {})}
61
+ {...(title ? { title } : {})}
62
+ {...(ariaLabel ? { "aria-label": ariaLabel } : {})}
63
63
  onkeydown={handleKeyDown}
64
64
  onclick={handleClick}
65
65
  onfocus={handleFocus}
@@ -48,9 +48,9 @@
48
48
  <!-- svelte-ignore a11y_interactive_supports_focus -->
49
49
  <fluent-data-grid-row
50
50
  class={className}
51
- {style}
52
- data-row-index={rowIndex}
53
- row-type={rowType}
51
+ {...(style ? { style } : {})}
52
+ {...(rowIndex !== undefined ? { "data-row-index": rowIndex } : {})}
53
+ {...(rowType ? { "row-type": rowType } : {})}
54
54
  role="row"
55
55
  onkeydown={handleKeyDown}
56
56
  onclick={handleClick}
@@ -229,12 +229,12 @@
229
229
  <fluent-dialog
230
230
  use:portal
231
231
  bind:this={element}
232
- {modal}
232
+ {...(modal ? { modal } : {})}
233
233
  hidden={!visible}
234
- {trapFocus}
235
- {ariaDescribedby}
236
- {ariaLabelledby}
237
- {ariaLabel}
234
+ {...(trapFocus ? { trapFocus } : {})}
235
+ {...(ariaDescribedby ? { ariaDescribedby } : {})}
236
+ {...(ariaLabelledby ? { ariaLabelledby } : {})}
237
+ {...(ariaLabel ? { ariaLabel } : {})}
238
238
  class="dialog-positioned"
239
239
  style={dialogStyle + (style ? ` ${style}` : '')}
240
240
  >
@@ -97,9 +97,9 @@
97
97
  <fluent-listbox
98
98
  bind:this={element}
99
99
  selected-options={selectedOptionsAttr}
100
- {disabled}
101
- {autofocus}
102
- {name}
100
+ {...(disabled ? { disabled } : {})}
101
+ {...(autofocus ? { autofocus } : {})}
102
+ {...(name ? { name } : {})}
103
103
  {...(size !== undefined ? { size } : {})}
104
104
  aria-label={ariaLabel || null}
105
105
  class={className || null}
@@ -207,10 +207,10 @@
207
207
  <span bind:this={anchorEl} class="fluent-menu-button-anchor">
208
208
  <!-- svelte-ignore a11y_click_events_have_key_events -->
209
209
  <fluent-button
210
- {appearance}
211
- {disabled}
210
+ {...(appearance ? { appearance } : {})}
211
+ {...(disabled ? { disabled } : {})}
212
212
  class={className}
213
- {style}
213
+ {...(style ? { style } : {})}
214
214
  aria-haspopup="menu"
215
215
  aria-expanded={open}
216
216
  onclick={toggleMenu}
@@ -187,27 +187,27 @@
187
187
  <!-- svelte-ignore a11y_autofocus -->
188
188
  <fluent-number-field
189
189
  bind:this={element}
190
- {id}
191
- class={className || null}
192
- style={computedStyle}
193
- placeholder={placeholder || null}
194
- appearance={appearance || null}
195
- disabled={disabled || null}
196
- readonly={readonly || null}
197
- required={required || null}
198
- autofocus={autofocus || null}
199
- autocomplete={autocomplete || null}
190
+ {...(id ? { id } : {})}
191
+ {...(className ? { class: className } : {})}
192
+ {...(computedStyle ? { style: computedStyle } : {})}
193
+ {...(placeholder ? { placeholder } : {})}
194
+ {...(appearance ? { appearance } : {})}
195
+ {...(disabled ? { disabled } : {})}
196
+ {...(readonly ? { readonly } : {})}
197
+ {...(required ? { required } : {})}
198
+ {...(autofocus ? { autofocus } : {})}
199
+ {...(autocomplete ? { autocomplete } : {})}
200
200
  {...(step !== undefined ? { step } : {})}
201
201
  {...(min !== undefined ? { min } : {})}
202
202
  {...(max !== undefined ? { max } : {})}
203
203
  {...(minlength !== undefined ? { minlength } : {})}
204
204
  {...(maxlength !== undefined ? { maxlength } : {})}
205
205
  {...(size !== undefined ? { size } : {})}
206
- list={list || null}
207
- hide-step={hideStep || null}
208
- name={name || null}
209
- aria-label={ariaLabel ?? (label || null)}
210
- value={value ?? ""}
206
+ {...(list ? { list } : {})}
207
+ {...(hideStep ? { "hide-step": hideStep } : {})}
208
+ {...(name ? { name } : {})}
209
+ {...(ariaLabel || label ? { "aria-label": ariaLabel ?? label } : {})}
210
+ {...(value != null ? { value } : {})}
211
211
  {...titleProps}
212
212
  oninput={handleOnInput}
213
213
  onchange={handleOnChange}
@@ -55,13 +55,13 @@
55
55
  <!-- svelte-ignore a11y_no_static_element_interactions -->
56
56
  <!-- svelte-ignore a11y_click_events_have_key_events -->
57
57
  <fluent-option
58
- {value}
59
- selected={selected !== undefined ? selected : (selectedValue?.value?.includes(value) || undefined)}
58
+ {...(value != null ? { value } : {})}
59
+ {...((selected !== undefined ? selected : selectedValue?.value?.includes(value)) ? { selected: true } : {})}
60
60
  class={className}
61
- {style}
62
- data-option-label={label || null}
63
- data-option-context={data ? JSON.stringify(data) : undefined}
64
- {disabled}
61
+ {...(style ? { style } : {})}
62
+ {...(label ? { "data-option-label": label } : {})}
63
+ {...(data ? { "data-option-context": JSON.stringify(data) } : {})}
64
+ {...(disabled ? { disabled } : {})}
65
65
  onclick={handleOnClick}
66
66
  >
67
67
  {#if icon}
@@ -55,7 +55,7 @@
55
55
  <!-- svelte-ignore a11y_no_static_element_interactions -->
56
56
  <fluent-button
57
57
  onclick={() => onFirst?.()}
58
- disabled={!canGoBack || disabled}
58
+ {...(!canGoBack || disabled ? { disabled: true } : {})}
59
59
  title="Go to first page"
60
60
  aria-label="Go to first page"
61
61
  >
@@ -66,7 +66,7 @@
66
66
  <!-- svelte-ignore a11y_no_static_element_interactions -->
67
67
  <fluent-button
68
68
  onclick={() => onPrevious?.()}
69
- disabled={!canGoBack || disabled}
69
+ {...(!canGoBack || disabled ? { disabled: true } : {})}
70
70
  title="Go to previous page"
71
71
  aria-label="Go to previous page"
72
72
  >
@@ -85,7 +85,7 @@
85
85
  <!-- svelte-ignore a11y_no_static_element_interactions -->
86
86
  <fluent-button
87
87
  onclick={() => onNext?.()}
88
- disabled={!canGoForwards || disabled}
88
+ {...(!canGoForwards || disabled ? { disabled: true } : {})}
89
89
  title="Go to next page"
90
90
  aria-label="Go to next page"
91
91
  >
@@ -96,7 +96,7 @@
96
96
  <!-- svelte-ignore a11y_no_static_element_interactions -->
97
97
  <fluent-button
98
98
  onclick={() => onLast?.()}
99
- disabled={!canGoForwards || disabled}
99
+ {...(!canGoForwards || disabled ? { disabled: true } : {})}
100
100
  title="Go to last page"
101
101
  aria-label="Go to last page"
102
102
  >
@@ -2194,7 +2194,7 @@
2194
2194
  <!-- svelte-ignore a11y_click_events_have_key_events -->
2195
2195
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2196
2196
  <fluent-menu-item
2197
- disabled={isDisabled || undefined}
2197
+ {...(isDisabled ? { disabled: true } : {})}
2198
2198
  class:danger={menuItem.danger}
2199
2199
  onclick={() => handleContextMenuItemClick(menuItem)}
2200
2200
  >
@@ -764,7 +764,7 @@ declare function $$render<T>(): {
764
764
  row: T;
765
765
  rowIndex: number;
766
766
  }) => void | Promise<void>) | undefined;
767
- }, "title" | "group" | "icon" | "id" | "row">> & Omit<{
767
+ }, "group" | "title" | "icon" | "id" | "row">> & Omit<{
768
768
  id: string;
769
769
  icon: string;
770
770
  title: string;
@@ -778,7 +778,7 @@ declare function $$render<T>(): {
778
778
  row: T;
779
779
  rowIndex: number;
780
780
  }) => void | Promise<void>) | undefined;
781
- }, "title" | "group" | "icon" | "id" | "row">;
781
+ }, "group" | "title" | "icon" | "id" | "row">;
782
782
  rowIndex: number;
783
783
  row: T;
784
784
  }) => void) | undefined;
@@ -91,18 +91,18 @@
91
91
  <!-- svelte-ignore a11y_no_static_element_interactions -->
92
92
  <fluent-slider
93
93
  bind:this={element}
94
- {id}
95
- {min}
96
- {max}
97
- {step}
98
- {orientation}
99
- {disabled}
100
- {readonly}
101
- {required}
102
- {name}
103
- aria-label={ariaLabel || label || null}
104
- class={className || null}
105
- style={style || null}
94
+ {...(id ? { id } : {})}
95
+ {...(min !== undefined ? { min } : {})}
96
+ {...(max !== undefined ? { max } : {})}
97
+ {...(step !== undefined ? { step } : {})}
98
+ {...(orientation ? { orientation } : {})}
99
+ {...(disabled ? { disabled } : {})}
100
+ {...(readonly ? { readonly } : {})}
101
+ {...(required ? { required } : {})}
102
+ {...(name ? { name } : {})}
103
+ {...(ariaLabel || label ? { "aria-label": ariaLabel || label } : {})}
104
+ {...(className ? { class: className } : {})}
105
+ {...(style ? { style } : {})}
106
106
  onchange={handleChange}
107
107
  oninput={handleInput}
108
108
  >
@@ -99,15 +99,15 @@
99
99
  mere presence as `true`. -->
100
100
  <fluent-switch
101
101
  class={className}
102
- {style}
103
- {readonly}
102
+ {...(style ? { style } : {})}
103
+ {...(readonly ? { readonly } : {})}
104
104
  id={effectiveId}
105
- {disabled}
106
- {autofocus}
107
- {name}
108
- aria-label={ariaLabel || label}
109
- {required}
110
- checked={checked || null}
105
+ {...(disabled ? { disabled } : {})}
106
+ {...(autofocus ? { autofocus } : {})}
107
+ {...(name ? { name } : {})}
108
+ {...(ariaLabel || label ? { "aria-label": ariaLabel || label } : {})}
109
+ {...(required ? { required } : {})}
110
+ {...(checked ? { checked } : {})}
111
111
  aria-checked={checked ? "true" : "false"}
112
112
  onchange={handleChange}
113
113
  role="switch"
@@ -23,7 +23,7 @@
23
23
  }: Props = $props()
24
24
  </script>
25
25
 
26
- <fluent-tab-panel class={className} {style} {id} hidden={!active} >
26
+ <fluent-tab-panel class={className} {...(style ? { style } : {})} {...(id ? { id } : {})} hidden={!active} >
27
27
  {#if children}
28
28
  {@render children?.()}
29
29
  {/if}
@@ -179,20 +179,20 @@
179
179
  <!-- svelte-ignore a11y_autofocus -->
180
180
  <fluent-text-field
181
181
  bind:this={element}
182
- {id}
183
- {value}
184
- {placeholder}
185
- {appearance}
186
- {disabled}
187
- {readonly}
188
- {required}
189
- {type}
190
- {name}
191
- {autofocus}
192
- {autocomplete}
193
- {style}
194
- {title}
195
- aria-label={!label && !labelTemplate ? undefined : label || undefined}
182
+ {...(id ? { id } : {})}
183
+ {...(value != null ? { value } : {})}
184
+ {...(placeholder ? { placeholder } : {})}
185
+ {...(appearance ? { appearance } : {})}
186
+ {...(disabled ? { disabled } : {})}
187
+ {...(readonly ? { readonly } : {})}
188
+ {...(required ? { required } : {})}
189
+ {...(type ? { type } : {})}
190
+ {...(name ? { name } : {})}
191
+ {...(autofocus ? { autofocus } : {})}
192
+ {...(autocomplete ? { autocomplete } : {})}
193
+ {...(style ? { style } : {})}
194
+ {...(title ? { title } : {})}
195
+ {...(label || labelTemplate ? { "aria-label": label } : {})}
196
196
  oninput={handleOnInput}
197
197
  onchange={handleOnChange}
198
198
  onkeydown={handleOnKeyDown}
@@ -164,26 +164,26 @@
164
164
  <fluent-text-area
165
165
  bind:this={element}
166
166
  class={className}
167
- {style}
168
- {readonly}
169
- {resize}
170
- {autofocus}
171
- autocomplete={autocomplete}
172
- {form}
173
- {list}
167
+ {...(style ? { style } : {})}
168
+ {...(readonly ? { readonly } : {})}
169
+ {...(resize ? { resize } : {})}
170
+ {...(autofocus ? { autofocus } : {})}
171
+ {...(autocomplete ? { autocomplete } : {})}
172
+ {...(form ? { form } : {})}
173
+ {...(list ? { list } : {})}
174
174
  {...(maxlength !== undefined ? { maxlength } : {})}
175
175
  {...(minlength !== undefined ? { minlength } : {})}
176
- {placeholder}
176
+ {...(placeholder ? { placeholder } : {})}
177
177
  {...(cols !== undefined ? { cols } : {})}
178
178
  {...(rows !== undefined ? { rows } : {})}
179
- {spellcheck}
180
- {id}
181
- {name}
182
- {disabled}
183
- {required}
184
- {appearance}
185
- {value}
186
- aria-label={ariaLabel}
179
+ {...(spellcheck != null ? { spellcheck } : {})}
180
+ {...(id ? { id } : {})}
181
+ {...(name ? { name } : {})}
182
+ {...(disabled ? { disabled } : {})}
183
+ {...(required ? { required } : {})}
184
+ {...(appearance ? { appearance } : {})}
185
+ {...(value != null ? { value } : {})}
186
+ {...(ariaLabel ? { "aria-label": ariaLabel } : {})}
187
187
  oninput={handleInput}
188
188
  onchange={handleChange}
189
189
  >
@@ -22,10 +22,10 @@
22
22
  </script>
23
23
 
24
24
  <fluent-toolbar
25
- id={id}
25
+ {...(id ? { id } : {})}
26
26
  class={className}
27
- style={style}
28
- orientation={orientation}
27
+ {...(style ? { style } : {})}
28
+ {...(orientation ? { orientation } : {})}
29
29
  >
30
30
  {#if children}
31
31
  {@render children?.()}
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import type {SlotType} from "../../types/index.js"
3
- import Button from "../Button.svelte"
4
3
  import Panel from "./Panel.svelte"
5
4
  import NavMenu from "../nav/NavMenu.svelte"
6
5
  import NavGroup from "../nav/NavGroup.svelte"
@@ -34,6 +33,8 @@
34
33
  drawerPinned?: boolean
35
34
  drawerWidth?: string
36
35
  height?: number
36
+ menuToggleSize?: number
37
+ menuToggleTemplate?: SlotType
37
38
  class?: string
38
39
  style?: string
39
40
  }
@@ -50,6 +51,8 @@
50
51
  drawerPinned = false,
51
52
  drawerWidth = "280px",
52
53
  height = 60,
54
+ menuToggleSize = 42,
55
+ menuToggleTemplate = undefined,
53
56
  class: className = "",
54
57
  style = ""
55
58
  }: Props = $props()
@@ -92,25 +95,29 @@
92
95
  class:topnav--force-collapse={collapse === "always"}
93
96
  class:topnav--force-expand={collapse === "never"}
94
97
  class:topnav--drawer-pinned={drawerPinned}
95
- style="height: {height}px; {style}"
98
+ style="height: {height}px; --topnav-toggle-size: {menuToggleSize}px; {style}"
96
99
  >
97
100
  <div class="topnav-container">
98
101
  {#if hasDrawerContent}
99
- <Button
100
- appearance="stealth"
101
- class="mobile-menu-toggle"
102
- onclick={toggleMobileMenu}
103
- aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
104
- aria-expanded={mobileMenuOpen}
105
- >
106
- <span class="hamburger-icon">
107
- {#if mobileMenuOpen}
108
- <DismissIcon size={20} />
109
- {:else}
110
-
111
- {/if}
112
- </span>
113
- </Button>
102
+ <div class="topnav-menu-toggle">
103
+ {#if menuToggleTemplate}
104
+ {@render menuToggleTemplate({open: mobileMenuOpen, toggle: toggleMobileMenu})}
105
+ {:else}
106
+ <button
107
+ type="button"
108
+ class="mobile-menu-toggle"
109
+ onclick={toggleMobileMenu}
110
+ aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
111
+ aria-expanded={mobileMenuOpen}
112
+ >
113
+ {#if mobileMenuOpen}
114
+ <DismissIcon size={Math.round(menuToggleSize * 0.48)} />
115
+ {:else}
116
+ <span class="hamburger-glyph">☰</span>
117
+ {/if}
118
+ </button>
119
+ {/if}
120
+ </div>
114
121
  {/if}
115
122
 
116
123
  {#if brandTemplate}
@@ -238,31 +245,46 @@
238
245
  flex-shrink: 0;
239
246
  }
240
247
 
241
- /* Mobile menu toggle button hidden by default, shown when container is narrow */
242
- :global(.topnav .mobile-menu-toggle) {
248
+ /* Wrapper slotowns visibility + position so a custom menuToggleTemplate
249
+ gets the same collapse behavior as the default button without consumers
250
+ needing to know about the internal rules. */
251
+ .topnav-menu-toggle {
243
252
  display: none;
244
253
  order: -1;
245
254
  flex-shrink: 0;
246
- width: 42px;
247
- min-width: 42px;
248
- height: 42px;
255
+ align-items: center;
249
256
  }
250
257
 
251
- /* Override fluent-button's internal control padding/min-height so the
252
- hamburger fills the fixed 42×42 box and the icon centers in it. */
253
- :global(.topnav .mobile-menu-toggle::part(control)) {
254
- width: 100%;
255
- height: 100%;
256
- min-width: 0;
257
- min-height: 0;
258
+ /* Default toggle plain icon target, no button chrome. Sized via
259
+ --topnav-toggle-size (set inline from the menuToggleSize prop).
260
+ Custom templates render at their natural size. */
261
+ .mobile-menu-toggle {
262
+ width: var(--topnav-toggle-size, 42px);
263
+ height: var(--topnav-toggle-size, 42px);
264
+ display: inline-flex;
265
+ align-items: center;
266
+ justify-content: center;
258
267
  padding: 0;
268
+ background: transparent;
269
+ border: none;
270
+ color: inherit;
271
+ cursor: pointer;
272
+ border-radius: var(--fluent-border-radius-md, 4px);
273
+ transition: opacity 0.15s, background-color 0.15s;
274
+ }
275
+
276
+ .mobile-menu-toggle:hover {
277
+ opacity: 0.7;
259
278
  }
260
279
 
261
- .hamburger-icon {
262
- font-size: 1.5rem;
280
+ .mobile-menu-toggle:focus-visible {
281
+ outline: 2px solid var(--accent-fill-rest, currentColor);
282
+ outline-offset: 2px;
283
+ }
284
+
285
+ .hamburger-glyph {
286
+ font-size: calc(var(--topnav-toggle-size, 42px) * 0.55);
263
287
  line-height: 1;
264
- display: flex;
265
- align-items: center;
266
288
  }
267
289
 
268
290
  /* Trailing flex group: items + divider + actions. `margin-left: auto`
@@ -343,7 +365,7 @@
343
365
  }
344
366
 
345
367
  @container topnav (max-width: 960px) {
346
- :global(.topnav .mobile-menu-toggle) {
368
+ .topnav-menu-toggle {
347
369
  display: flex;
348
370
  }
349
371
 
@@ -359,14 +381,14 @@
359
381
  }
360
382
  }
361
383
 
362
- /* collapse="always" — force hamburger visible + items hidden regardless of width */
363
- :global(.topnav.topnav--force-collapse .mobile-menu-toggle) {
384
+ /* collapse="always" — force toggle visible + items hidden regardless of width */
385
+ .topnav--force-collapse .topnav-menu-toggle {
364
386
  display: flex;
365
387
  }
366
388
 
367
- /* When drawer is pinned the hamburger acts as a show/hide toggle for the
389
+ /* When drawer is pinned the toggle acts as a show/hide control for the
368
390
  pinned rail itself, so it must stay visible at every width. */
369
- :global(.topnav.topnav--drawer-pinned .mobile-menu-toggle) {
391
+ .topnav--drawer-pinned .topnav-menu-toggle {
370
392
  display: flex;
371
393
  }
372
394
 
@@ -375,8 +397,8 @@
375
397
  display: none;
376
398
  }
377
399
 
378
- /* collapse="never" — force items/divider visible + hamburger hidden regardless of width */
379
- :global(.topnav.topnav--force-expand .mobile-menu-toggle) {
400
+ /* collapse="never" — force items/divider visible + toggle hidden regardless of width */
401
+ .topnav--force-expand .topnav-menu-toggle {
380
402
  display: none;
381
403
  }
382
404
 
@@ -26,6 +26,8 @@ type Props = {
26
26
  drawerPinned?: boolean;
27
27
  drawerWidth?: string;
28
28
  height?: number;
29
+ menuToggleSize?: number;
30
+ menuToggleTemplate?: SlotType;
29
31
  class?: string;
30
32
  style?: string;
31
33
  };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.3.1";
1
+ export declare const VERSION = "1.3.3";
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.3.1";
4
+ export const VERSION = "1.3.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-fluentui",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "A Svelte wrapper library for Microsoft FluentUI web components",
5
5
  "license": "MIT",
6
6
  "author": "KeenMate",