fluentui-extended 2026.2.12 → 2026.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -54,6 +54,16 @@ interface LookupProps extends Omit<InputProps, 'onChange' | 'value'> {
54
54
  header?: React.ReactNode;
55
55
  /** Footer content rendered at the bottom of the dropdown */
56
56
  footer?: React.ReactNode;
57
+ /**
58
+ * Controlled open state. When provided, the component will use this value
59
+ * instead of its internal state. Use together with `onOpenChange`.
60
+ */
61
+ open?: boolean;
62
+ /**
63
+ * Callback fired when the dropdown open state changes.
64
+ * Use together with `open` for controlled mode, or standalone to observe changes.
65
+ */
66
+ onOpenChange?: (open: boolean) => void;
57
67
  }
58
68
 
59
69
  declare const Lookup: React.FC<LookupProps>;
package/dist/index.d.ts CHANGED
@@ -54,6 +54,16 @@ interface LookupProps extends Omit<InputProps, 'onChange' | 'value'> {
54
54
  header?: React.ReactNode;
55
55
  /** Footer content rendered at the bottom of the dropdown */
56
56
  footer?: React.ReactNode;
57
+ /**
58
+ * Controlled open state. When provided, the component will use this value
59
+ * instead of its internal state. Use together with `onOpenChange`.
60
+ */
61
+ open?: boolean;
62
+ /**
63
+ * Callback fired when the dropdown open state changes.
64
+ * Use together with `open` for controlled mode, or standalone to observe changes.
65
+ */
66
+ onOpenChange?: (open: boolean) => void;
57
67
  }
58
68
 
59
69
  declare const Lookup: React.FC<LookupProps>;
package/dist/index.js CHANGED
@@ -59,11 +59,17 @@ var useLookupStyles = reactComponents.makeStyles({
59
59
  width: "20px",
60
60
  height: "20px"
61
61
  },
62
- dropdownSurface: {
62
+ dropdownPortal: {
63
+ position: "absolute",
63
64
  padding: 0,
64
- marginTop: "4px",
65
65
  minWidth: "220px",
66
- zIndex: 1e3
66
+ zIndex: 1e6,
67
+ backgroundColor: reactComponents.tokens.colorNeutralBackground1,
68
+ borderRadius: reactComponents.tokens.borderRadiusXLarge,
69
+ boxShadow: reactComponents.tokens.shadow16,
70
+ border: `1px solid ${reactComponents.tokens.colorNeutralStroke1}`,
71
+ boxSizing: "border-box",
72
+ overflow: "hidden"
67
73
  },
68
74
  dropdownContent: {
69
75
  maxHeight: "350px",
@@ -205,7 +211,8 @@ var useLookupStyles = reactComponents.makeStyles({
205
211
  alignItems: "center",
206
212
  justifyContent: "space-between",
207
213
  borderRadius: reactComponents.tokens.borderRadiusMedium,
208
- padding: "4px 12px",
214
+ padding: "6px 12px",
215
+ minHeight: "24px",
209
216
  backgroundColor: reactComponents.tokens.colorNeutralBackground3,
210
217
  backgroundImage: `repeating-linear-gradient(
211
218
  -45deg,
@@ -226,6 +233,7 @@ var useLookupStyles = reactComponents.makeStyles({
226
233
  alignItems: "center",
227
234
  justifyContent: "space-between",
228
235
  padding: "6px 12px",
236
+ minHeight: "24px",
229
237
  borderTop: `1px solid ${reactComponents.tokens.colorNeutralStroke2}`,
230
238
  backgroundColor: reactComponents.tokens.colorNeutralBackground1,
231
239
  fontSize: reactComponents.tokens.fontSizeBase200,
@@ -257,6 +265,8 @@ var Lookup = ({
257
265
  disabled,
258
266
  header,
259
267
  footer,
268
+ open: controlledOpen,
269
+ onOpenChange,
260
270
  ...inputProps
261
271
  }) => {
262
272
  const styles = useLookupStyles();
@@ -264,7 +274,19 @@ var Lookup = ({
264
274
  const ariaLabelledBy = inputProps["aria-labelledby"];
265
275
  const autoId = reactComponents.useId("lookup-");
266
276
  const lookupId = id ?? autoId;
267
- const [isOpen, setIsOpen] = React3__namespace.useState(false);
277
+ const [internalOpen, setInternalOpen] = React3__namespace.useState(false);
278
+ const isControlled = controlledOpen !== void 0;
279
+ const isOpen = isControlled ? controlledOpen : internalOpen;
280
+ const setIsOpen = React3__namespace.useCallback(
281
+ (value) => {
282
+ const nextValue = typeof value === "function" ? value(isOpen) : value;
283
+ if (!isControlled) {
284
+ setInternalOpen(nextValue);
285
+ }
286
+ onOpenChange?.(nextValue);
287
+ },
288
+ [isControlled, isOpen, onOpenChange]
289
+ );
268
290
  const [searchText, setSearchText] = React3__namespace.useState("");
269
291
  const [highlightedIndex, setHighlightedIndex] = React3__namespace.useState(-1);
270
292
  const [expandedKeys, setExpandedKeys] = React3__namespace.useState(/* @__PURE__ */ new Set());
@@ -274,7 +296,7 @@ var Lookup = ({
274
296
  const dropdownRef = React3__namespace.useRef(null);
275
297
  const debounceRef = React3__namespace.useRef();
276
298
  const justSelectedRef = React3__namespace.useRef(false);
277
- const [dropdownWidth, setDropdownWidth] = React3__namespace.useState();
299
+ const [dropdownPosition, setDropdownPosition] = React3__namespace.useState({ top: 0, left: 0, width: 0 });
278
300
  const selectedOption = React3__namespace.useMemo(
279
301
  () => selectedOptionProp ?? options.find((opt) => opt.key === selectedKey) ?? internalSelectedOption,
280
302
  [selectedOptionProp, options, selectedKey, internalSelectedOption]
@@ -300,6 +322,15 @@ var Lookup = ({
300
322
  }
301
323
  return selectedOption?.text ?? "";
302
324
  }, [isOpen, searchText, selectedOption]);
325
+ const openDropdown = React3__namespace.useCallback(() => {
326
+ if (disabled) return;
327
+ setIsOpen(true);
328
+ }, [disabled]);
329
+ const closeDropdown = React3__namespace.useCallback(() => {
330
+ setIsOpen(false);
331
+ setSearchText("");
332
+ setHighlightedIndex(-1);
333
+ }, []);
303
334
  const handleSearchChange = React3__namespace.useCallback(
304
335
  (value) => {
305
336
  setSearchText(value);
@@ -320,10 +351,10 @@ var Lookup = ({
320
351
  const value = e.target.value;
321
352
  handleSearchChange(value);
322
353
  if (!isOpen) {
323
- setIsOpen(true);
354
+ openDropdown();
324
355
  }
325
356
  },
326
- [handleSearchChange, isOpen]
357
+ [handleSearchChange, isOpen, openDropdown]
327
358
  );
328
359
  const handleSelectOption = React3__namespace.useCallback(
329
360
  (option) => {
@@ -353,13 +384,10 @@ var Lookup = ({
353
384
  (e) => {
354
385
  if (disabled) return;
355
386
  switch (e.key) {
356
- case " ":
357
- e.stopPropagation();
358
- break;
359
387
  case "ArrowDown":
360
388
  e.preventDefault();
361
389
  if (!isOpen) {
362
- setIsOpen(true);
390
+ openDropdown();
363
391
  } else {
364
392
  setHighlightedIndex(
365
393
  (prev) => prev < filteredOptions.length - 1 ? prev + 1 : 0
@@ -382,27 +410,34 @@ var Lookup = ({
382
410
  break;
383
411
  case "Escape":
384
412
  e.preventDefault();
385
- setIsOpen(false);
386
- setSearchText("");
387
- setHighlightedIndex(-1);
413
+ closeDropdown();
388
414
  break;
389
415
  case "Tab":
390
- setIsOpen(false);
391
- setSearchText("");
416
+ closeDropdown();
392
417
  break;
393
418
  }
394
419
  },
395
- [disabled, isOpen, highlightedIndex, filteredOptions, handleSelectOption]
420
+ [disabled, isOpen, highlightedIndex, filteredOptions, handleSelectOption, openDropdown, closeDropdown]
396
421
  );
397
- const handleFocus = React3__namespace.useCallback(() => {
398
- if (!disabled) {
399
- if (justSelectedRef.current) {
400
- justSelectedRef.current = false;
401
- return;
422
+ const handleFocus = React3__namespace.useCallback(
423
+ (e) => {
424
+ if (!disabled) {
425
+ if (justSelectedRef.current) {
426
+ justSelectedRef.current = false;
427
+ return;
428
+ }
429
+ openDropdown();
402
430
  }
403
- setIsOpen(true);
431
+ inputProps.onFocus?.(e);
432
+ },
433
+ [disabled, openDropdown, inputProps.onFocus]
434
+ );
435
+ const handleWrapperClick = React3__namespace.useCallback(() => {
436
+ if (!disabled && !isOpen) {
437
+ openDropdown();
438
+ inputRef.current?.focus();
404
439
  }
405
- }, [disabled]);
440
+ }, [disabled, isOpen, openDropdown]);
406
441
  React3__namespace.useEffect(() => {
407
442
  return () => {
408
443
  if (debounceRef.current) {
@@ -419,74 +454,56 @@ var Lookup = ({
419
454
  }
420
455
  }, [highlightedIndex]);
421
456
  React3__namespace.useEffect(() => {
422
- if (!isOpen || !matchInputWidth) {
423
- return;
424
- }
425
- const updateWidth = () => {
426
- const width = inputWrapperRef.current?.getBoundingClientRect().width;
427
- if (width && width > 0) {
428
- setDropdownWidth(width);
457
+ if (!isOpen) return;
458
+ const updatePosition = () => {
459
+ const rect = inputWrapperRef.current?.getBoundingClientRect();
460
+ if (rect) {
461
+ setDropdownPosition({
462
+ top: rect.bottom + 4 + window.scrollY,
463
+ left: rect.left + window.scrollX,
464
+ width: matchInputWidth ? rect.width : 0
465
+ });
429
466
  }
430
467
  };
431
- updateWidth();
468
+ updatePosition();
469
+ let resizeObserver;
432
470
  if (typeof ResizeObserver !== "undefined" && inputWrapperRef.current) {
433
- const resizeObserver = new ResizeObserver(() => updateWidth());
471
+ resizeObserver = new ResizeObserver(() => updatePosition());
434
472
  resizeObserver.observe(inputWrapperRef.current);
435
- return () => resizeObserver.disconnect();
436
473
  }
437
- window.addEventListener("resize", updateWidth);
438
- return () => window.removeEventListener("resize", updateWidth);
439
- }, [isOpen, matchInputWidth]);
440
- React3__namespace.useEffect(() => {
441
- if (!isOpen) {
442
- return;
443
- }
444
- const closeOnScroll = (event) => {
445
- const target = event.target;
446
- if (target && (dropdownRef.current?.contains(target) || inputWrapperRef.current?.contains(target))) {
447
- return;
448
- }
449
- setIsOpen(false);
450
- setSearchText("");
451
- setHighlightedIndex(-1);
474
+ window.addEventListener("resize", updatePosition);
475
+ window.addEventListener("scroll", updatePosition, true);
476
+ return () => {
477
+ resizeObserver?.disconnect();
478
+ window.removeEventListener("resize", updatePosition);
479
+ window.removeEventListener("scroll", updatePosition, true);
452
480
  };
453
- window.addEventListener("scroll", closeOnScroll, true);
454
- return () => window.removeEventListener("scroll", closeOnScroll, true);
455
- }, [isOpen]);
481
+ }, [isOpen, matchInputWidth]);
456
482
  React3__namespace.useEffect(() => {
457
- if (!isOpen) {
458
- return;
459
- }
483
+ if (!isOpen) return;
460
484
  const handlePointerDownOutside = (event) => {
461
485
  const target = event.target;
462
- if (!target) {
463
- return;
464
- }
486
+ if (!target) return;
465
487
  const insideInput = inputWrapperRef.current?.contains(target);
466
488
  const insideDropdown = dropdownRef.current?.contains(target);
467
489
  if (!insideInput && !insideDropdown) {
468
- setIsOpen(false);
469
- setSearchText("");
470
- setHighlightedIndex(-1);
490
+ closeDropdown();
471
491
  }
472
492
  };
473
493
  document.addEventListener("mousedown", handlePointerDownOutside);
474
- return () => document.removeEventListener("mousedown", handlePointerDownOutside);
475
- }, [isOpen]);
494
+ return () => {
495
+ document.removeEventListener("mousedown", handlePointerDownOutside);
496
+ };
497
+ }, [isOpen, closeDropdown]);
498
+ const showDropdown = isOpen && !disabled;
476
499
  return /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.root }, /* @__PURE__ */ React3__namespace.createElement(
477
- reactComponents.Popover,
500
+ "div",
478
501
  {
479
- open: isOpen && !disabled,
480
- onOpenChange: (_e, data) => {
481
- if (data.open && !disabled) {
482
- setIsOpen(true);
483
- }
484
- },
485
- trapFocus: false,
486
- withArrow: false,
487
- positioning: "below-start"
502
+ className: styles.inputWrapper,
503
+ ref: inputWrapperRef,
504
+ onClick: handleWrapperClick
488
505
  },
489
- /* @__PURE__ */ React3__namespace.createElement(reactComponents.PopoverTrigger, { disableButtonEnhancement: true }, /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.inputWrapper, ref: inputWrapperRef }, /* @__PURE__ */ React3__namespace.createElement(
506
+ /* @__PURE__ */ React3__namespace.createElement(
490
507
  reactComponents.Input,
491
508
  {
492
509
  ...inputProps,
@@ -517,78 +534,81 @@ var Lookup = ({
517
534
  }
518
535
  ), loading ? /* @__PURE__ */ React3__namespace.createElement(reactComponents.Spinner, { size: "tiny" }) : /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.chevronIcon }, /* @__PURE__ */ React3__namespace.createElement(reactIcons.ChevronDownRegular, null)))
519
536
  }
520
- ))),
537
+ )
538
+ ), showDropdown && /* @__PURE__ */ React3__namespace.createElement(reactComponents.Portal, null, /* @__PURE__ */ React3__namespace.createElement(
539
+ "div",
540
+ {
541
+ className: styles.dropdownPortal,
542
+ style: {
543
+ top: dropdownPosition.top,
544
+ left: dropdownPosition.left,
545
+ ...matchInputWidth && dropdownPosition.width > 0 ? { width: dropdownPosition.width } : { minWidth: 220 }
546
+ }
547
+ },
521
548
  /* @__PURE__ */ React3__namespace.createElement(
522
- reactComponents.PopoverSurface,
549
+ "div",
523
550
  {
524
- className: styles.dropdownSurface,
525
- style: matchInputWidth && dropdownWidth ? { width: `${dropdownWidth}px` } : void 0
551
+ id: `${lookupId}-listbox`,
552
+ className: styles.dropdownContent,
553
+ ref: dropdownRef,
554
+ role: "listbox",
555
+ "aria-labelledby": lookupId
526
556
  },
527
- /* @__PURE__ */ React3__namespace.createElement(
528
- "div",
529
- {
530
- id: `${lookupId}-listbox`,
531
- className: styles.dropdownContent,
532
- ref: dropdownRef,
533
- role: "listbox",
534
- "aria-labelledby": lookupId
535
- },
536
- header && /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.headerWrapper }, /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.header }, header)),
537
- loading ? /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.loadingContainer }, /* @__PURE__ */ React3__namespace.createElement(reactComponents.Spinner, { size: "small", label: "Loading..." })) : filteredOptions.length === 0 ? /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.noResults }, noResultsMessage) : /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.optionsContainer }, /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.optionsList }, filteredOptions.map((option, index) => {
538
- const isExpanded = expandedKeys.has(option.key);
539
- const hasDetails = option.details && option.details.length > 0;
540
- const handleExpandClick = (e) => {
541
- e.stopPropagation();
542
- setExpandedKeys((prev) => {
543
- const next = new Set(prev);
544
- if (next.has(option.key)) {
545
- next.delete(option.key);
546
- } else {
547
- next.add(option.key);
548
- }
549
- return next;
550
- });
551
- };
552
- return /* @__PURE__ */ React3__namespace.createElement(
553
- "div",
557
+ header && /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.headerWrapper }, /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.header }, header)),
558
+ loading ? /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.loadingContainer }, /* @__PURE__ */ React3__namespace.createElement(reactComponents.Spinner, { size: "small", label: "Loading..." })) : filteredOptions.length === 0 ? /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.noResults }, noResultsMessage) : /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.optionsContainer }, /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.optionsList }, filteredOptions.map((option, index) => {
559
+ const isExpanded = expandedKeys.has(option.key);
560
+ const hasDetails = option.details && option.details.length > 0;
561
+ const handleExpandClick = (e) => {
562
+ e.stopPropagation();
563
+ setExpandedKeys((prev) => {
564
+ const next = new Set(prev);
565
+ if (next.has(option.key)) {
566
+ next.delete(option.key);
567
+ } else {
568
+ next.add(option.key);
569
+ }
570
+ return next;
571
+ });
572
+ };
573
+ return /* @__PURE__ */ React3__namespace.createElement(
574
+ "div",
575
+ {
576
+ key: option.key,
577
+ id: `${lookupId}-option-${option.key}`,
578
+ role: "option",
579
+ "data-index": index,
580
+ "aria-selected": option.key === selectedOption?.key ? true : false,
581
+ "aria-disabled": option.disabled === true ? true : void 0,
582
+ className: reactComponents.mergeClasses(
583
+ styles.option,
584
+ index === highlightedIndex && styles.optionHighlighted,
585
+ option.key === selectedOption?.key && styles.optionSelected,
586
+ option.disabled && styles.optionDisabled
587
+ ),
588
+ onClick: () => handleSelectOption(option),
589
+ onMouseEnter: () => setHighlightedIndex(index)
590
+ },
591
+ option.icon && /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionIcon }, option.icon),
592
+ /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionContent }, /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionText }, option.text), option.secondaryText && /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionSecondaryText }, option.secondaryText), isExpanded && hasDetails && /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.optionDetails }, option.details.map((detail, detailIndex) => /* @__PURE__ */ React3__namespace.createElement("div", { key: detailIndex, className: styles.optionDetailRow }, detail.label && /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionDetailLabel }, detail.label, ":"), /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionDetailValue }, detail.value))))),
593
+ hasDetails && /* @__PURE__ */ React3__namespace.createElement(
594
+ "span",
554
595
  {
555
- key: option.key,
556
- id: `${lookupId}-option-${option.key}`,
557
- role: "option",
558
- "data-index": index,
559
- "aria-selected": option.key === selectedOption?.key ? "true" : "false",
560
- "aria-disabled": option.disabled ? "true" : void 0,
596
+ role: "button",
597
+ tabIndex: -1,
561
598
  className: reactComponents.mergeClasses(
562
- styles.option,
563
- index === highlightedIndex && styles.optionHighlighted,
564
- option.key === selectedOption?.key && styles.optionSelected,
565
- option.disabled && styles.optionDisabled
599
+ styles.optionExpandButton,
600
+ isExpanded && styles.optionExpandButtonExpanded
566
601
  ),
567
- onClick: () => handleSelectOption(option),
568
- onMouseEnter: () => setHighlightedIndex(index)
602
+ onClick: handleExpandClick,
603
+ "aria-label": isExpanded ? "Collapse details" : "Expand details"
569
604
  },
570
- option.icon && /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionIcon }, option.icon),
571
- /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionContent }, /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionText }, option.text), option.secondaryText && /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionSecondaryText }, option.secondaryText), isExpanded && hasDetails && /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.optionDetails }, option.details.map((detail, detailIndex) => /* @__PURE__ */ React3__namespace.createElement("div", { key: detailIndex, className: styles.optionDetailRow }, detail.label && /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionDetailLabel }, detail.label, ":"), /* @__PURE__ */ React3__namespace.createElement("span", { className: styles.optionDetailValue }, detail.value))))),
572
- hasDetails && /* @__PURE__ */ React3__namespace.createElement(
573
- "span",
574
- {
575
- role: "button",
576
- tabIndex: -1,
577
- className: reactComponents.mergeClasses(
578
- styles.optionExpandButton,
579
- isExpanded && styles.optionExpandButtonExpanded
580
- ),
581
- onClick: handleExpandClick,
582
- "aria-label": isExpanded ? "Collapse details" : "Expand details"
583
- },
584
- /* @__PURE__ */ React3__namespace.createElement(reactIcons.ChevronDownRegular, null)
585
- )
586
- );
587
- }))),
588
- footer && /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.footerWrapper }, /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.footer }, footer))
589
- )
605
+ /* @__PURE__ */ React3__namespace.createElement(reactIcons.ChevronDownRegular, null)
606
+ )
607
+ );
608
+ }))),
609
+ footer && /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.footerWrapper }, /* @__PURE__ */ React3__namespace.createElement("div", { className: styles.footer }, footer))
590
610
  )
591
- ));
611
+ )));
592
612
  };
593
613
  Lookup.displayName = "Lookup";
594
614
  var gridWithBetween = "minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) auto";