Add ReactNode support for dropdown / placeholders in the Select component
Open
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 715
- Forks
- 276
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 8
Description
This is how we've done it:
iff --git a/dist/esm/components/multi-select/CMultiSelect.js b/dist/esm/components/multi-select/CMultiSelect.js
index 1eec708..10836ab 100644
--- a/dist/esm/components/multi-select/CMultiSelect.js
+++ b/dist/esm/components/multi-select/CMultiSelect.js
@@ -122,7 +122,10 @@ const CMultiSelect = forwardRef((_a, ref) => {
nativeSelectRef.current.dispatchEvent(new Event('change', { bubbles: true }));
}
updatePopper();
- }, [JSON.stringify(selected)]);
+ // Keyed on the selected values rather than JSON.stringify(selected). An option may now carry a `labelNode`, and
+ // a React element created during render holds an `_owner` fiber, which makes JSON.stringify throw
+ // ("Converting circular structure to JSON"). The values are what this effect actually depends on.
+ }, [selected.map((option) => option.value).join(',')]);
useEffect(() => {
visible ? openDropdown() : closeDropdown();
}, [visible]);
@@ -333,13 +336,11 @@ const CMultiSelect = forwardRef((_a, ref) => {
? selected.map((option) => option.value.toString())
: selected.map((option) => option.value)[0], onChange: () => onChange && onChange(selected), onKeyDown: handleNativeSelectKeyDown, ref: nativeSelectRef }),
React.createElement("div", Object.assign({ className: "form-multi-select-input-group" }, (!search && !disabled && { tabIndex: 0 }), { onClick: () => !disabled && openDropdown(), onKeyDown: handleTogglerKeyDown, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": `multiselect-listbox-${uniqueId}` }, (portal && { 'aria-owns': `multiselect-listbox-${uniqueId}` }), (disabled && { 'aria-disabled': true }), { ref: dropdownRefElement }),
- React.createElement(CMultiSelectSelection, { ariaTagDeleteLabel: ariaTagDeleteLabel, disabled: disabled, multiple: multiple, onRemove: (option) => !disabled && handleOnOptionClick(option), placeholder: placeholder, search: search, selected: selected, selectionType: selectionType, selectionTypeCounterText: selectionTypeCounterText },
+ React.createElement(CMultiSelectSelection, { ariaTagDeleteLabel: ariaTagDeleteLabel, disabled: disabled, multiple: multiple, onRemove: (option) => !disabled && handleOnOptionClick(option), placeholder: placeholder, search: search, searchValue: searchValue, selected: selected, selectionType: selectionType, selectionTypeCounterText: selectionTypeCounterText },
search && (React.createElement("input", Object.assign({ type: "text", className: "form-multi-select-search", disabled: disabled, id: `search${id !== null && id !== void 0 ? id : uniqueId}`, name: `search${name !== null && name !== void 0 ? name : uniqueId}`, autoComplete: "off", "aria-label": ariaSearchLabel, "aria-autocomplete": "list", "aria-controls": `multiselect-listbox-${uniqueId}`, onChange: handleSearchChange, onKeyDown: handleSearchKeyDown }, (selected.length === 0 && { placeholder: placeholder }), (selected.length > 0 &&
selectionType === 'counter' && {
placeholder: `${selected.length} ${selectionTypeCounterText}`,
}), (selected.length > 0 &&
- !multiple && { placeholder: selected.map((option) => option.label)[0] }), (multiple &&
- selected.length > 0 &&
selectionType !== 'counter' && { size: searchValue.length + 2 }), { ref: searchRef }))),
!search && selected.length === 0 && (React.createElement("span", { className: "form-multi-select-placeholder" }, placeholder))),
React.createElement("div", { className: "form-multi-select-buttons" },
diff --git a/dist/esm/components/multi-select/CMultiSelectSelection.js b/dist/esm/components/multi-select/CMultiSelectSelection.js
index 36a2be1..2568d9c 100644
--- a/dist/esm/components/multi-select/CMultiSelectSelection.js
+++ b/dist/esm/components/multi-select/CMultiSelectSelection.js
@@ -2,7 +2,7 @@ import React, { forwardRef } from 'react';
import classNames from '../../_virtual/index.js';
import PropTypes from 'prop-types';
-const CMultiSelectSelection = forwardRef(({ ariaTagDeleteLabel = 'Remove', children, disabled, multiple, placeholder, onRemove, search, selected = [], selectionType, selectionTypeCounterText, }, ref) => {
+const CMultiSelectSelection = forwardRef(({ ariaTagDeleteLabel = 'Remove', children, disabled, multiple, placeholder, onRemove, search, searchValue = '', selected = [], selectionType, selectionTypeCounterText, }, ref) => {
return (React.createElement("span", { className: classNames('form-multi-select-selection', {
'form-multi-select-selection-tags': multiple && selectionType === 'tags',
}), "aria-live": "polite", ref: ref },
@@ -17,7 +17,7 @@ const CMultiSelectSelection = forwardRef(({ ariaTagDeleteLabel = 'Remove', child
selected.map((option, index) => {
if (selectionType === 'tags') {
return (React.createElement("span", { className: "form-multi-select-tag", key: index },
- option.label,
+ option.labelNode ?? option.label,
!disabled && !option.disabled && (React.createElement("button", { className: "form-multi-select-tag-delete", type: "button", "aria-label": `${ariaTagDeleteLabel} ${option.label}`.trim(), onClick: () => onRemove === null || onRemove === void 0 ? void 0 : onRemove(option) }))));
}
return;
@@ -25,10 +25,13 @@ const CMultiSelectSelection = forwardRef(({ ariaTagDeleteLabel = 'Remove', child
multiple &&
selectionType === 'text' &&
selected.map((option, index) => (React.createElement("span", { key: index },
- option.label,
+ option.labelNode ?? option.label,
index === selected.length - 1 ? '' : ',',
"\u00A0"))),
- !multiple && !search && selected.map((option) => option.label)[0],
+ // Rendered as content rather than the search input's placeholder attribute, so a single select's value
+ // takes normal text colour and can carry a `labelNode` badge. Stepping aside once the user types is what
+ // the placeholder gave us for free.
+ !multiple && !searchValue && selected.map((option) => option.labelNode ?? option.label)[0],
children));
});
CMultiSelectSelection.propTypes = {
@@ -38,6 +41,7 @@ CMultiSelectSelection.propTypes = {
multiple: PropTypes.bool,
onRemove: PropTypes.func,
placeholder: PropTypes.string,
+ searchValue: PropTypes.string,
search: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.oneOf(['external', 'global']),
diff --git a/dist/esm/components/multi-select/types.d.ts b/dist/esm/components/multi-select/types.d.ts
index 8015721..8248e3a 100644
--- a/dist/esm/components/multi-select/types.d.ts
+++ b/dist/esm/components/multi-select/types.d.ts
@@ -3,6 +3,13 @@ export type Option = {
disabled?: boolean;
selected?: boolean;
label: string;
+ /**
+ * Patched in by workforce. Rendered instead of `label` wherever CoreUI renders the selection as content
+ * (multi-select tags, `selectionType="text"`, and a single select with `search` off), so a selected value can
+ * carry markup such as a status badge. `label` is still required, and remains what CoreUI uses for the
+ * `placeholder` and `aria-label` attributes, for the mirrored native <select>, and for its own search matching.
+ */
+ labelNode?: import('react').ReactNode;
value: number | string;
[key: string]: number | string | any;
};
Which lets us build something like this:
(which is what we had with react-select which we're finally porting over so we're fully CoreUI)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the affected dist/esm/components/multi-select/CMultiSelect.js, CMultiSelectSelection.js, and types.d.ts files to trace the Select option and selection rendering paths. No test entry point is named; done means ReactNode content works for dropdown selections and placeholders while the existing label, native select, search, and accessibility behavior remains intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100