@deque/cauldron-react 5.7.1-canary.024ab594 → 5.7.1-canary.2562efe5

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.
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import type { ListboxOption } from './ListboxContext';
3
+ import type { ListboxValue } from './ListboxOption';
4
+ interface ListboxProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onSelect'> {
5
+ as?: React.ElementType | string;
6
+ value?: ListboxValue;
7
+ navigation?: 'cycle' | 'bound';
8
+ onSelectionChange?: <T extends HTMLElement = HTMLElement>({ value }: {
9
+ target: T;
10
+ previousValue: ListboxValue;
11
+ value: ListboxValue;
12
+ }) => void;
13
+ onActiveChange?: (option: ListboxOption) => void;
14
+ }
15
+ declare const Listbox: React.ForwardRefExoticComponent<ListboxProps & React.RefAttributes<HTMLElement>>;
16
+ export default Listbox;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ type UnknownElement<T> = T extends Element ? T : HTMLElement;
3
+ type UnknownValue<T> = T extends string ? T : number;
4
+ type ListboxOption<Element = HTMLElement, Value = string | number> = {
5
+ element: UnknownElement<Element>;
6
+ value?: UnknownValue<Value>;
7
+ };
8
+ type ListboxContext<T extends ListboxOption> = {
9
+ options: T[];
10
+ active: T | null;
11
+ selected: T | null;
12
+ setOptions: React.Dispatch<React.SetStateAction<T[]>>;
13
+ onSelect: (option: T) => void;
14
+ };
15
+ type ListboxProvider<T extends ListboxOption> = {
16
+ children: React.ReactNode;
17
+ } & ListboxContext<T>;
18
+ declare const ListboxContext: React.Context<{
19
+ options: never[];
20
+ active: null;
21
+ selected: null;
22
+ setOptions: () => null;
23
+ onSelect: () => null;
24
+ }>;
25
+ declare function ListboxProvider<T extends ListboxOption>({ options, active, selected, setOptions, onSelect, children }: ListboxProvider<T>): JSX.Element;
26
+ declare function useListboxContext<T extends ListboxOption>(): ListboxContext<T>;
27
+ export { ListboxProvider, useListboxContext, ListboxOption };
@@ -0,0 +1,9 @@
1
+ import { ContentNode } from '../../types';
2
+ import React from 'react';
3
+ interface ListboxGroupProps extends React.HTMLAttributes<HTMLElement> {
4
+ as?: React.ElementType | string;
5
+ groupLabelProps?: React.HTMLAttributes<HTMLLIElement>;
6
+ label: ContentNode;
7
+ }
8
+ declare const ListboxGroup: React.ForwardRefExoticComponent<ListboxGroupProps & React.RefAttributes<HTMLElement>>;
9
+ export default ListboxGroup;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ export type ListboxValue = Readonly<string | number | undefined>;
3
+ interface ListboxOptionsProps extends React.HTMLAttributes<HTMLElement> {
4
+ as?: React.ElementType | string;
5
+ value?: ListboxValue;
6
+ disabled?: boolean;
7
+ activeClass?: string;
8
+ }
9
+ declare const ListboxOption: React.ForwardRefExoticComponent<ListboxOptionsProps & React.RefAttributes<HTMLElement>>;
10
+ export default ListboxOption;
@@ -0,0 +1,4 @@
1
+ export { default } from './Listbox';
2
+ export { default as ListboxOption } from './ListboxOption';
3
+ export { default as ListboxGroup } from './ListboxGroup';
4
+ export { ListboxProvider, useListboxContext } from './ListboxContext';
package/lib/index.d.ts CHANGED
@@ -52,6 +52,7 @@ export { default as FieldWrap } from './components/FieldWrap';
52
52
  export { default as Breadcrumb, BreadcrumbItem, BreadcrumbLink } from './components/Breadcrumb';
53
53
  export { default as TwoColumnPanel, ColumnHeader, ColumnGroupHeader, ColumnLeft, ColumnRight, ColumnList } from './components/TwoColumnPanel';
54
54
  export { default as Notice } from './components/Notice';
55
+ export { default as Listbox, ListboxOption, ListboxGroup } from './components/Listbox';
55
56
  /**
56
57
  * Helpers / Utils
57
58
  */
package/lib/index.js CHANGED
@@ -3943,6 +3943,225 @@ Notice.propTypes = {
3943
3943
  icon: PropTypes__default["default"].string
3944
3944
  };
3945
3945
 
3946
+ /* istanbul ignore next */
3947
+ var ListboxContext = React.createContext({
3948
+ options: [],
3949
+ active: null,
3950
+ selected: null,
3951
+ setOptions: function () { return null; },
3952
+ onSelect: function () { return null; }
3953
+ });
3954
+ function ListboxProvider(_a) {
3955
+ var options = _a.options, active = _a.active, selected = _a.selected, setOptions = _a.setOptions, onSelect = _a.onSelect, children = _a.children;
3956
+ var Provider = ListboxContext.Provider;
3957
+ var value = React.useMemo(function () { return ({
3958
+ options: options,
3959
+ active: active,
3960
+ selected: selected,
3961
+ setOptions: setOptions,
3962
+ onSelect: onSelect
3963
+ }); }, [options, active, selected, setOptions]);
3964
+ return React__default["default"].createElement(Provider, { value: value }, children);
3965
+ }
3966
+ function useListboxContext() {
3967
+ return React.useContext(ListboxContext);
3968
+ }
3969
+
3970
+ var keys = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', ' '];
3971
+ // id for listbox options should always be defined since it should
3972
+ // be provide via the author, or auto-generated via the component
3973
+ var getOptionId = function (option) {
3974
+ return option.element.getAttribute('id');
3975
+ };
3976
+ var isDisabledOption = function (option) {
3977
+ return option.element.getAttribute('aria-disabled') === 'true';
3978
+ };
3979
+ var optionMatchesValue = function (option, value) {
3980
+ return typeof option.value !== null &&
3981
+ typeof option.value !== 'undefined' &&
3982
+ option.value === value;
3983
+ };
3984
+ var Listbox = React.forwardRef(function (_a, ref) {
3985
+ var _b = _a.as, Component = _b === void 0 ? 'ul' : _b, children = _a.children, defaultValue = _a.defaultValue, value = _a.value, _c = _a.navigation, navigation = _c === void 0 ? 'bound' : _c, onKeyDown = _a.onKeyDown, onFocus = _a.onFocus, onSelectionChange = _a.onSelectionChange, onActiveChange = _a.onActiveChange, props = tslib.__rest(_a, ["as", "children", "defaultValue", "value", "navigation", "onKeyDown", "onFocus", "onSelectionChange", "onActiveChange"]);
3986
+ var _d = tslib.__read(React.useState([]), 2), options = _d[0], setOptions = _d[1];
3987
+ var _e = tslib.__read(React.useState(null), 2), activeOption = _e[0], setActiveOption = _e[1];
3988
+ var _f = tslib.__read(React.useState(null), 2), selectedOption = _f[0], setSelectedOption = _f[1];
3989
+ var listboxRef = useSharedRef(ref);
3990
+ var isControlled = typeof value !== 'undefined';
3991
+ React.useLayoutEffect(function () {
3992
+ if (!isControlled && selectedOption) {
3993
+ return;
3994
+ }
3995
+ var listboxValue = isControlled ? value : defaultValue;
3996
+ var matchingOption = options.find(function (option) {
3997
+ return optionMatchesValue(option, listboxValue);
3998
+ });
3999
+ setSelectedOption(matchingOption || null);
4000
+ setActiveOption(matchingOption || null);
4001
+ }, [isControlled, options, value]);
4002
+ React.useEffect(function () {
4003
+ if (activeOption) {
4004
+ onActiveChange === null || onActiveChange === void 0 ? void 0 : onActiveChange(activeOption);
4005
+ }
4006
+ }, [activeOption]);
4007
+ var handleSelect = React.useCallback(function (option) {
4008
+ setActiveOption(option);
4009
+ // istanbul ignore else
4010
+ if (!isControlled) {
4011
+ setSelectedOption(option);
4012
+ }
4013
+ onSelectionChange === null || onSelectionChange === void 0 ? void 0 : onSelectionChange({
4014
+ target: option.element,
4015
+ value: option.value,
4016
+ previousValue: selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.value
4017
+ });
4018
+ }, [isControlled, selectedOption]);
4019
+ var handleKeyDown = React.useCallback(function (event) {
4020
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
4021
+ if (!keys.includes(event.key)) {
4022
+ return;
4023
+ }
4024
+ event.preventDefault();
4025
+ var enabledOptions = options.filter(function (option) { return !isDisabledOption(option); });
4026
+ // istanbul ignore next
4027
+ if (!enabledOptions.length) {
4028
+ return;
4029
+ }
4030
+ var _a = tslib.__read(keys, 6), up = _a[0], down = _a[1], home = _a[2], end = _a[3], enter = _a[4], space = _a[5];
4031
+ var firstOption = enabledOptions[0];
4032
+ if (!activeOption) {
4033
+ setActiveOption(firstOption);
4034
+ return;
4035
+ }
4036
+ var lastOption = enabledOptions[enabledOptions.length - 1];
4037
+ var currentOption = activeOption;
4038
+ var currentIndex = enabledOptions.findIndex(function (_a) {
4039
+ var element = _a.element;
4040
+ return element === currentOption.element;
4041
+ });
4042
+ var allowCyclicalNavigation = navigation === 'cycle';
4043
+ switch (event.key) {
4044
+ case up:
4045
+ var previousOption = currentIndex === 0 && allowCyclicalNavigation
4046
+ ? lastOption
4047
+ : enabledOptions[Math.max(currentIndex - 1, 0)];
4048
+ setActiveOption(previousOption);
4049
+ break;
4050
+ case down:
4051
+ var nextOption = currentIndex === enabledOptions.length - 1 &&
4052
+ allowCyclicalNavigation
4053
+ ? firstOption
4054
+ : enabledOptions[Math.min(currentIndex + 1, enabledOptions.length - 1)];
4055
+ setActiveOption(nextOption);
4056
+ break;
4057
+ case home:
4058
+ setActiveOption(firstOption);
4059
+ break;
4060
+ case end:
4061
+ setActiveOption(lastOption);
4062
+ break;
4063
+ case enter:
4064
+ case space:
4065
+ activeOption && handleSelect(activeOption);
4066
+ break;
4067
+ }
4068
+ }, [options, activeOption, navigation]);
4069
+ var handleFocus = React.useCallback(function (event) {
4070
+ if (!activeOption && !selectedOption) {
4071
+ var firstOption = options.find(function (option) { return !isDisabledOption(option); });
4072
+ // istanbul ignore else
4073
+ if (firstOption) {
4074
+ setActiveOption(firstOption);
4075
+ }
4076
+ // istanbul ignore else
4077
+ }
4078
+ else if (event.target === listboxRef.current) {
4079
+ setActiveOption(selectedOption);
4080
+ }
4081
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
4082
+ }, [options, activeOption, selectedOption]);
4083
+ return (React__default["default"].createElement(Component, tslib.__assign({ role: "listbox", ref: listboxRef, tabIndex: "0", onKeyDown: handleKeyDown, onFocus: handleFocus, "aria-activedescendant": activeOption ? getOptionId(activeOption) : undefined }, props),
4084
+ React__default["default"].createElement(ListboxProvider, { options: options, active: activeOption, selected: selectedOption, setOptions: setOptions, onSelect: handleSelect }, children)));
4085
+ });
4086
+ Listbox.displayName = 'Listbox';
4087
+
4088
+ function isElementPreceding(a, b) {
4089
+ return !!(b.compareDocumentPosition(a) & Node.DOCUMENT_POSITION_PRECEDING);
4090
+ }
4091
+ var ListboxOption = React.forwardRef(function (_a, ref) {
4092
+ var _b;
4093
+ var _c;
4094
+ var propId = _a.id, className = _a.className, _d = _a.as, Component = _d === void 0 ? 'li' : _d, children = _a.children, value = _a.value, disabled = _a.disabled, _e = _a.activeClass, activeClass = _e === void 0 ? 'ListboxOption--active' : _e, onClick = _a.onClick, props = tslib.__rest(_a, ["id", "className", "as", "children", "value", "disabled", "activeClass", "onClick"]);
4095
+ var _f = useListboxContext(), active = _f.active, selected = _f.selected, setOptions = _f.setOptions, onSelect = _f.onSelect;
4096
+ var listboxOptionRef = useSharedRef(ref);
4097
+ var _g = tslib.__read(propId ? [propId] : nextId.useId(1, 'listbox-option'), 1), id = _g[0];
4098
+ var isActive = active !== null && active.element === listboxOptionRef.current;
4099
+ var isSelected = selected !== null && selected.element === listboxOptionRef.current;
4100
+ var optionValue = typeof value !== 'undefined'
4101
+ ? value
4102
+ : (_c = listboxOptionRef.current) === null || _c === void 0 ? void 0 : _c.innerText;
4103
+ React.useEffect(function () {
4104
+ var element = listboxOptionRef.current;
4105
+ setOptions(function (options) {
4106
+ var e_1, _a;
4107
+ var option = { element: element, value: optionValue };
4108
+ // istanbul ignore next
4109
+ if (!element)
4110
+ return options;
4111
+ // Elements are frequently appended, so check to see if the newly rendered
4112
+ // element follows the last element first before any other checks
4113
+ if (!options.length ||
4114
+ isElementPreceding(options[options.length - 1].element, option.element)) {
4115
+ return tslib.__spreadArray(tslib.__spreadArray([], tslib.__read(options), false), [option], false);
4116
+ }
4117
+ try {
4118
+ for (var options_1 = tslib.__values(options), options_1_1 = options_1.next(); !options_1_1.done; options_1_1 = options_1.next()) {
4119
+ var opt = options_1_1.value;
4120
+ if (isElementPreceding(element, opt.element)) {
4121
+ var index = options.indexOf(opt);
4122
+ return tslib.__spreadArray(tslib.__spreadArray(tslib.__spreadArray([], tslib.__read(options.slice(0, index)), false), [
4123
+ option
4124
+ ], false), tslib.__read(options.slice(index)), false);
4125
+ }
4126
+ }
4127
+ }
4128
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
4129
+ finally {
4130
+ try {
4131
+ if (options_1_1 && !options_1_1.done && (_a = options_1.return)) _a.call(options_1);
4132
+ }
4133
+ finally { if (e_1) throw e_1.error; }
4134
+ }
4135
+ // istanbul ignore next
4136
+ // this should never happen, but just in case fall back to options
4137
+ return options;
4138
+ });
4139
+ return function () {
4140
+ setOptions(function (opts) { return opts.filter(function (opt) { return opt.element !== element; }); });
4141
+ };
4142
+ }, [optionValue]);
4143
+ var handleClick = React.useCallback(function (event) {
4144
+ if (disabled) {
4145
+ return;
4146
+ }
4147
+ onSelect({ element: listboxOptionRef.current, value: optionValue });
4148
+ onClick === null || onClick === void 0 ? void 0 : onClick(event);
4149
+ }, [optionValue]);
4150
+ return (React__default["default"].createElement(Component, tslib.__assign({ id: id, className: classNames__default["default"](className, (_b = {},
4151
+ _b[activeClass] = isActive,
4152
+ _b)), role: "option", ref: listboxOptionRef, "aria-disabled": typeof disabled === 'boolean' ? disabled : undefined, "aria-selected": isSelected, onClick: handleClick }, props), children));
4153
+ });
4154
+ ListboxOption.displayName = 'ListboxOption';
4155
+
4156
+ var ListboxGroup = React.forwardRef(function (_a, ref) {
4157
+ var _b = _a.as, Component = _b === void 0 ? 'ul' : _b, children = _a.children, propId = _a.id, label = _a.label, groupLabelProps = _a.groupLabelProps, props = tslib.__rest(_a, ["as", "children", "id", "label", "groupLabelProps"]);
4158
+ var _c = tslib.__read(propId ? [propId] : nextId.useId(1, 'listbox-group-label'), 1), id = _c[0];
4159
+ return (React__default["default"].createElement(Component, tslib.__assign({ role: "group", ref: ref, "aria-labelledby": id }, props),
4160
+ React__default["default"].createElement("li", tslib.__assign({ role: "presentation", id: id }, groupLabelProps), label),
4161
+ children));
4162
+ });
4163
+ ListboxGroup.displayName = 'ListboxGroup';
4164
+
3946
4165
  var LIGHT_THEME_CLASS = 'cauldron--theme-light';
3947
4166
  var DARK_THEME_CLASS = 'cauldron--theme-dark';
3948
4167
  var ThemeContext = React.createContext({
@@ -4048,6 +4267,9 @@ exports.IssuePanel = IssuePanel;
4048
4267
  exports.Layout = Layout;
4049
4268
  exports.Line = Line;
4050
4269
  exports.Link = Link;
4270
+ exports.Listbox = Listbox;
4271
+ exports.ListboxGroup = ListboxGroup;
4272
+ exports.ListboxOption = ListboxOption;
4051
4273
  exports.Loader = Loader;
4052
4274
  exports.LoaderOverlay = LoaderOverlay;
4053
4275
  exports.Main = Main;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deque/cauldron-react",
3
- "version": "5.7.1-canary.024ab594",
3
+ "version": "5.7.1-canary.2562efe5",
4
4
  "description": "Fully accessible react components library for Deque Cauldron",
5
5
  "homepage": "https://cauldron.dequelabs.com/",
6
6
  "publishConfig": {