react-lightning-design-system
Version:
Salesforce Lightning Design System components built with React
542 lines (519 loc) • 20.2 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
import _typeof from "@babel/runtime/helpers/typeof";
var _excluded = ["id", "className", "value", "defaultValue", "opened", "defaultOpened", "multiSelect", "selectedText", "optionsSelectedText", "menuSize", "menuStyle", "disabled", "label", "required", "error", "cols", "tooltip", "tooltipIcon", "elementRef", "buttonRef", "dropdownRef", "onSelect", "onComplete", "onValueChange", "onBlur", "onKeyDown", "children"];
import React, { createContext, useContext, useRef, useId, useState, useEffect, useCallback, useMemo } from 'react';
import classnames from 'classnames';
import { FormElement } from './FormElement';
import { Icon } from './Icon';
import { AutoAlign } from './AutoAlign';
import { isElInChildren } from './util';
import { ComponentSettingsContext } from './ComponentSettings';
import { useControlledValue, useEventCallback, useMergeRefs } from './hooks';
import { createFC } from './common';
/**
* Recursively collect option values from PicklistItem components
*/
function collectOptionValues(children) {
return React.Children.map(children, function (child) {
if (! /*#__PURE__*/React.isValidElement(child)) {
return [];
}
var props = child.props;
var isPropsObject = _typeof(props) === 'object' && props !== null;
if (!isPropsObject) {
return [];
}
// Recursively check children for nested PicklistItems
if (child.type !== PicklistItem) {
return !('children' in props) ? [] : collectOptionValues(props.children);
}
// Check if this is specifically a PicklistItem component
if (!('value' in props) || typeof props.value !== 'string' && typeof props.value !== 'number') {
return [];
}
// Skip disabled items
if ('disabled' in props && props.disabled === true) {
return [];
}
return [props.value];
}).flat();
}
/**
* Recursively find selected item label from PicklistItem components
*/
function findSelectedItemLabel(children, selectedValue) {
return React.Children.map(children, function (child) {
if (! /*#__PURE__*/React.isValidElement(child)) {
return null;
}
var props = child.props;
var isPropsObject = _typeof(props) === 'object' && props !== null;
if (!isPropsObject) {
return null;
}
// Recursively check children for nested PicklistItems
if (child.type !== PicklistItem) {
return !('children' in props) ? null : findSelectedItemLabel(props.children, selectedValue);
}
// Check if this is specifically a PicklistItem component
if (!('value' in props) || props.value !== selectedValue) {
return null;
}
// Skip disabled items
if ('disabled' in props && props.disabled === true) {
return null;
}
// Safely access label and children properties with proper type checking
var label = 'label' in props ? props.label : undefined;
var itemChildren = 'children' in props ? props.children : undefined;
// Simple type check for React.ReactNode values
var labelValue = typeof label === 'string' || typeof label === 'number' || /*#__PURE__*/React.isValidElement(label) ? label : undefined;
var childrenValue = typeof itemChildren === 'string' || typeof itemChildren === 'number' || /*#__PURE__*/React.isValidElement(itemChildren) || Array.isArray(itemChildren) ? itemChildren : undefined;
return labelValue || childrenValue;
});
}
/**
*
*/
/**
*
*/
var PicklistContext = /*#__PURE__*/createContext({
values: [],
onSelect: function onSelect() {
// noop
},
optionIdPrefix: ''
});
/**
*
*/
/**
*
*/
export var Picklist = createFC(function (props) {
var id_ = props.id,
className = props.className,
value_ = props.value,
defaultValue = props.defaultValue,
opened_ = props.opened,
defaultOpened = props.defaultOpened,
multiSelect = props.multiSelect,
_props$selectedText = props.selectedText,
selectedText = _props$selectedText === void 0 ? '' : _props$selectedText,
_props$optionsSelecte = props.optionsSelectedText,
optionsSelectedText = _props$optionsSelecte === void 0 ? '' : _props$optionsSelecte,
menuSize = props.menuSize,
menuStyle = props.menuStyle,
disabled = props.disabled,
label = props.label,
required = props.required,
error = props.error,
cols = props.cols,
tooltip = props.tooltip,
tooltipIcon = props.tooltipIcon,
elementRef_ = props.elementRef,
buttonRef_ = props.buttonRef,
dropdownRef_ = props.dropdownRef,
onSelect = props.onSelect,
onComplete = props.onComplete,
onValueChange = props.onValueChange,
onBlur_ = props.onBlur,
onKeyDown_ = props.onKeyDown,
children = props.children,
rprops = _objectWithoutProperties(props, _excluded);
var fallbackId = useId();
var id = id_ !== null && id_ !== void 0 ? id_ : fallbackId;
var listboxId = "".concat(id, "-listbox");
var optionIdPrefix = "".concat(useId(), "-option");
var values_ = typeof value_ === 'undefined' ? undefined : value_ == null ? [] : Array.isArray(value_) ? value_ : [value_];
var defaultValues = typeof defaultValue === 'undefined' ? undefined : defaultValue == null ? [] : Array.isArray(defaultValue) ? defaultValue : [defaultValue];
var _useControlledValue = useControlledValue(values_, defaultValues !== null && defaultValues !== void 0 ? defaultValues : []),
_useControlledValue2 = _slicedToArray(_useControlledValue, 2),
values = _useControlledValue2[0],
setValues = _useControlledValue2[1];
var _useControlledValue3 = useControlledValue(opened_, defaultOpened !== null && defaultOpened !== void 0 ? defaultOpened : false),
_useControlledValue4 = _slicedToArray(_useControlledValue3, 2),
opened = _useControlledValue4[0],
setOpened = _useControlledValue4[1];
var _useState = useState(),
_useState2 = _slicedToArray(_useState, 2),
focusedValue = _useState2[0],
setFocusedValue = _useState2[1];
var _useContext = useContext(ComponentSettingsContext),
getActiveElement = _useContext.getActiveElement;
// Memoized option values - recursively collected from PicklistItem components (excluding disabled items)
var optionValues = useMemo(function () {
return collectOptionValues(children);
}, [children]);
// Get next option value for keyboard navigation
var getNextValue = useCallback(function (currentValue) {
if (optionValues.length === 0) return undefined;
if (!currentValue) return optionValues[0];
var currentIndex = optionValues.indexOf(currentValue);
return optionValues[Math.min(currentIndex + 1, optionValues.length - 1)]; // not wrap around
}, [optionValues]);
// Get previous option value for keyboard navigation
var getPrevValue = useCallback(function (currentValue) {
if (optionValues.length === 0) return undefined;
if (!currentValue) return optionValues[optionValues.length - 1];
var currentIndex = optionValues.indexOf(currentValue);
return optionValues[Math.max(currentIndex - 1, 0)]; // not wrap around
}, [optionValues]);
// Scroll focused element into view
var scrollFocusedElementIntoView = useEventCallback(function (nextFocusedValue) {
if (!nextFocusedValue || !dropdownElRef.current) {
return;
}
var dropdownContainer = dropdownElRef.current;
var targetElement = dropdownContainer.querySelector("#".concat(CSS.escape("".concat(optionIdPrefix, "-").concat(nextFocusedValue))));
if (!(targetElement instanceof HTMLElement)) {
return;
}
targetElement.focus();
});
// Set initial focus when dropdown opens
useEffect(function () {
if (opened && !focusedValue) {
// Focus on first selected value or first option
var initialFocus = values.length > 0 ? values[0] : optionValues[0];
setFocusedValue(initialFocus);
scrollFocusedElementIntoView(initialFocus);
} else if (!opened) {
// Reset focus when dropdown closes
setFocusedValue(undefined);
}
}, [opened, values, optionValues, focusedValue, scrollFocusedElementIntoView]);
var elRef = useRef(null);
var elementRef = useMergeRefs([elRef, elementRef_]);
var comboboxElRef = useRef(null);
var buttonRef = useMergeRefs([comboboxElRef, buttonRef_]);
var dropdownElRef = useRef(null);
var dropdownRef = useMergeRefs([dropdownElRef, dropdownRef_]);
var setPicklistValues = useEventCallback(function (newValues) {
var prevValues = values;
setValues(newValues);
if (onValueChange && prevValues !== newValues) {
if (multiSelect) {
onValueChange(newValues, prevValues);
} else {
onValueChange(newValues.length > 0 ? newValues[0] : null, prevValues.length > 0 ? prevValues[0] : null);
}
}
});
var updateItemValue = useEventCallback(function (itemValue) {
if (multiSelect) {
var newValues = _toConsumableArray(values);
// toggle value
if (newValues.indexOf(itemValue) === -1) {
// add value to array
newValues.push(itemValue);
} else {
// remove from array
newValues.splice(newValues.indexOf(itemValue), 1);
}
setPicklistValues(newValues);
} else {
// set only one value
setPicklistValues([itemValue]);
setOpened(false);
setTimeout(function () {
var _comboboxElRef$curren;
(_comboboxElRef$curren = comboboxElRef.current) === null || _comboboxElRef$curren === void 0 || _comboboxElRef$curren.focus();
onComplete === null || onComplete === void 0 || onComplete();
}, 10);
}
});
var isFocusedInComponent = useEventCallback(function () {
var targetEl = getActiveElement();
return isElInChildren(elRef.current, targetEl) || isElInChildren(dropdownElRef.current, targetEl);
});
var onClick = useEventCallback(function () {
if (!disabled) {
setOpened(function (opened) {
return !opened;
});
}
});
var onPicklistItemSelect = useEventCallback(function (value) {
updateItemValue(value);
onSelect === null || onSelect === void 0 || onSelect(value);
});
var onBlur = useEventCallback(function () {
setTimeout(function () {
if (!isFocusedInComponent()) {
setOpened(false);
onBlur_ === null || onBlur_ === void 0 || onBlur_();
onComplete === null || onComplete === void 0 || onComplete();
}
}, 10);
});
var onKeyDown = useEventCallback(function (e) {
if (e.keyCode === 40) {
// down
e.preventDefault();
e.stopPropagation();
if (!opened) {
setOpened(true);
} else {
// Navigate to next option
var nextValue = getNextValue(focusedValue);
setFocusedValue(nextValue);
scrollFocusedElementIntoView(nextValue);
}
} else if (e.keyCode === 38) {
// up
e.preventDefault();
e.stopPropagation();
if (!opened) {
setOpened(true);
} else {
// Navigate to previous option
var _prevValue = getPrevValue(focusedValue);
setFocusedValue(_prevValue);
scrollFocusedElementIntoView(_prevValue);
}
} else if (e.keyCode === 9) {
// Tab or Shift+Tab
if (opened) {
e.preventDefault();
e.stopPropagation();
var currentIndex = focusedValue ? optionValues.indexOf(focusedValue) : -1;
if (e.shiftKey) {
// Shift+Tab - Navigate to previous option or close if at first
if (currentIndex <= 0) {
// At first option or no focus, close the picklist
setOpened(false);
onComplete === null || onComplete === void 0 || onComplete();
} else {
var _prevValue2 = getPrevValue(focusedValue);
setFocusedValue(_prevValue2);
scrollFocusedElementIntoView(_prevValue2);
}
} else {
// Tab - Navigate to next option or close if at last
if (currentIndex >= optionValues.length - 1) {
// At last option, close the picklist
setOpened(false);
onComplete === null || onComplete === void 0 || onComplete();
} else {
var _nextValue = getNextValue(focusedValue);
setFocusedValue(_nextValue);
scrollFocusedElementIntoView(_nextValue);
}
}
}
} else if (e.keyCode === 27) {
// ESC
e.preventDefault();
e.stopPropagation();
setOpened(false);
onComplete === null || onComplete === void 0 || onComplete();
} else if (e.keyCode === 13 || e.keyCode === 32) {
// Enter or Space
e.preventDefault();
e.stopPropagation();
if (opened && focusedValue != null) {
// Select focused option
onPicklistItemSelect(focusedValue);
} else {
setOpened(function (opened) {
return !opened;
});
}
}
onKeyDown_ === null || onKeyDown_ === void 0 || onKeyDown_(e);
});
// Memoized selected item label - displays count for multiple selections, label for single selection, or placeholder text
var selectedItemLabel = useMemo(function () {
// many items selected
if (values.length > 1) {
return optionsSelectedText;
}
// one item
if (values.length === 1) {
var selectedValue = values[0];
var selected = findSelectedItemLabel(children, selectedValue);
return selected || selectedValue;
}
// zero items
return selectedText;
}, [values, optionsSelectedText, selectedText, children]);
var hasValue = values.length > 0;
var portalClassNames = classnames(className, 'react-slds-picklist', 'slds-combobox_container');
var containerClassNames = classnames(portalClassNames, 'slds-size_small');
var comboboxClassNames = classnames('slds-combobox', 'slds-dropdown-trigger', 'slds-dropdown-trigger_click', {
'slds-is-open': opened
});
var inputClassNames = classnames('slds-input_faux', 'slds-combobox__input', {
'slds-has-focus': opened && !disabled,
'slds-combobox__input-value': hasValue,
'slds-is-disabled': disabled
});
var createDropdownClassNames = useCallback(function (alignment) {
var _alignment = _slicedToArray(alignment, 2),
vertAlign = _alignment[0],
align = _alignment[1];
return classnames('slds-dropdown', vertAlign ? "slds-dropdown_".concat(vertAlign) : undefined, align ? "slds-dropdown_".concat(align) : undefined, menuSize ? "slds-dropdown_".concat(menuSize) : 'slds-dropdown_fluid');
}, [menuSize]);
var formElemProps = {
id: id,
label: label,
required: required,
error: error,
cols: cols,
tooltip: tooltip,
tooltipIcon: tooltipIcon,
elementRef: elementRef
};
var contextValue = {
values: values,
multiSelect: multiSelect,
onSelect: onPicklistItemSelect,
focusedValue: focusedValue,
optionIdPrefix: optionIdPrefix
};
return /*#__PURE__*/React.createElement(FormElement, formElemProps, /*#__PURE__*/React.createElement("div", {
className: containerClassNames
}, /*#__PURE__*/React.createElement("div", {
className: comboboxClassNames,
ref: elementRef
}, /*#__PURE__*/React.createElement("div", {
className: "slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right",
role: "none"
}, /*#__PURE__*/React.createElement("button", _extends({
type: "button",
ref: buttonRef,
role: "combobox",
tabIndex: disabled ? -1 : 0,
className: inputClassNames,
"aria-controls": listboxId,
"aria-expanded": opened,
"aria-haspopup": "listbox",
"aria-disabled": disabled,
"aria-activedescendant": focusedValue ? "".concat(optionIdPrefix, "-").concat(focusedValue) : undefined,
onClick: onClick,
onKeyDown: onKeyDown,
onBlur: onBlur,
disabled: disabled
}, rprops), /*#__PURE__*/React.createElement("span", {
className: "slds-truncate"
}, selectedItemLabel)), /*#__PURE__*/React.createElement(Icon, {
containerClassName: "slds-input__icon slds-input__icon_right",
category: "utility",
icon: "down",
size: "x-small",
textColor: "default"
})), opened && /*#__PURE__*/React.createElement(AutoAlign, {
triggerSelector: ".react-slds-picklist",
alignmentStyle: "menu",
portalClassName: portalClassNames,
size: menuSize
}, function (_ref) {
var alignment = _ref.alignment,
autoAlignContentRef = _ref.autoAlignContentRef;
return /*#__PURE__*/React.createElement("div", {
id: listboxId,
className: createDropdownClassNames(alignment),
role: "listbox",
"aria-label": "Options",
tabIndex: 0,
"aria-busy": false,
ref: useMergeRefs([dropdownRef, autoAlignContentRef]),
style: menuStyle
}, /*#__PURE__*/React.createElement("ul", {
className: "slds-listbox slds-listbox_vertical",
role: "presentation",
onKeyDown: onKeyDown,
onBlur: onBlur
}, /*#__PURE__*/React.createElement(PicklistContext.Provider, {
value: contextValue
}, children)));
}))));
}, {
isFormElement: true
});
/**
*
*/
/**
*
*/
export var PicklistItem = function PicklistItem(_ref2) {
var label = _ref2.label,
selected_ = _ref2.selected,
value = _ref2.value,
disabled = _ref2.disabled,
icon = _ref2.icon,
iconRight = _ref2.iconRight,
divider = _ref2.divider,
onClick_ = _ref2.onClick,
children = _ref2.children;
var _useContext2 = useContext(PicklistContext),
values = _useContext2.values,
multiSelect = _useContext2.multiSelect,
onSelect = _useContext2.onSelect,
focusedValue = _useContext2.focusedValue,
optionIdPrefix = _useContext2.optionIdPrefix;
var selected = selected_ !== null && selected_ !== void 0 ? selected_ : value != null ? values.indexOf(value) >= 0 : false;
var isFocused = value != null && focusedValue === value;
var onClick = useEventCallback(function (e) {
if (disabled) {
return;
}
if (value != null) {
onSelect(value);
}
onClick_ === null || onClick_ === void 0 || onClick_(e);
});
var itemClassNames = classnames('slds-media', 'slds-listbox__option', 'slds-listbox__option_plain', 'slds-media_small', {
'slds-is-selected': selected,
'slds-has-focus': isFocused
});
var mainListItem = /*#__PURE__*/React.createElement("li", {
role: "presentation",
className: "slds-listbox__item"
}, /*#__PURE__*/React.createElement("div", {
id: value ? "".concat(optionIdPrefix, "-").concat(value) : undefined,
className: itemClassNames,
role: "option",
"aria-selected": selected,
"aria-checked": multiSelect ? selected : undefined,
"aria-disabled": disabled,
tabIndex: disabled ? undefined : 0,
onClick: onClick
}, /*#__PURE__*/React.createElement("span", {
className: "slds-media__figure slds-listbox__option-icon"
}, icon ? /*#__PURE__*/React.createElement(Icon, {
category: "utility",
icon: icon,
size: "x-small"
}) : selected ? /*#__PURE__*/React.createElement(Icon, {
category: "utility",
icon: "check",
size: "x-small",
textColor: "currentColor"
}) : null), /*#__PURE__*/React.createElement("span", {
className: "slds-media__body"
}, /*#__PURE__*/React.createElement("span", {
className: "slds-truncate",
title: String(label || children)
}, label || children)), iconRight && /*#__PURE__*/React.createElement("span", {
className: "slds-media__figure slds-media__figure_reverse"
}, /*#__PURE__*/React.createElement(Icon, {
category: "utility",
icon: iconRight,
size: "x-small"
}))));
return /*#__PURE__*/React.createElement(React.Fragment, null, divider === 'top' && /*#__PURE__*/React.createElement("li", {
className: "slds-has-divider_".concat(divider, "-space"),
role: "separator"
}), mainListItem, divider === 'bottom' && /*#__PURE__*/React.createElement("li", {
className: "slds-has-divider_".concat(divider, "-space"),
role: "separator"
}));
};
//# sourceMappingURL=Picklist.js.map