coreui / coreui/coreui-react

Add ReactNode support for dropdown / placeholders in the Select component

Open
#539 0 comments 0 reactions 0 assignees View on GitHub

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:

Image

(which is what we had with react-select which we're finally porting over so we're fully CoreUI)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.