diff --git a/.nvmrc b/.nvmrc index 6f7f377b..9a2a0e21 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16 +v20 diff --git a/package-lock.json b/package-lock.json index a16ba8c8..880907cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "classnames": "^2.3.2" }, "devDependencies": { + "@ariakit/react": "^0.4.15", "@babel/core": "^7.23.6", "@babel/plugin-syntax-flow": "^7.26.0", "@babel/preset-env": "^7.23.6", @@ -44,6 +45,7 @@ "eslint-plugin-autofix": "^1.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-storybook": "^0.11.1", + "match-sorter": "^8.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "storybook": "^8.4.7", @@ -80,6 +82,44 @@ "node": ">=6.0.0" } }, + "node_modules/@ariakit/core": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.14.tgz", + "integrity": "sha512-hpzZvyYzGhP09S9jW1XGsU/FD5K3BKsH1eG/QJ8rfgEeUdPS7BvHPt5lHbOeJ2cMrRzBEvsEzLi1ivfDifHsVA==", + "dev": true + }, + "node_modules/@ariakit/react": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.15.tgz", + "integrity": "sha512-0V2LkNPFrGRT+SEIiObx/LQjR6v3rR+mKEDUu/3tq7jfCZ+7+6Q6EMR1rFaK+XMkaRY1RWUcj/rRDWAUWnsDww==", + "dev": true, + "dependencies": { + "@ariakit/react-core": "0.4.15" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-core": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.15.tgz", + "integrity": "sha512-Up8+U97nAPJdyUh9E8BCEhJYTA+eVztWpHoo1R9zZfHd4cnBWAg5RHxEmMH+MamlvuRxBQA71hFKY/735fDg+A==", + "dev": true, + "dependencies": { + "@ariakit/core": "0.4.14", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -2058,9 +2098,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2649,6 +2689,31 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "dev": true, + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dev": true, + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "dev": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -8811,6 +8876,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/match-sorter": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.0.0.tgz", + "integrity": "sha512-bGJ6Zb+OhzXe+ptP5d80OLVx7AkqfRbtGEh30vNSfjNwllu+hHI+tcbMIT/fbkx/FKN1PmKuDb65+Oofg+XUxw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", @@ -10668,6 +10743,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "dev": true + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -11742,6 +11823,15 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "dev": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/package.json b/package.json index fb258d31..c63fe725 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "url": "git+https://github.com/miljodir/component-library" }, "devDependencies": { + "@ariakit/react": "^0.4.15", "@babel/core": "^7.23.6", "@babel/plugin-syntax-flow": "^7.26.0", "@babel/preset-env": "^7.23.6", @@ -50,6 +51,7 @@ "eslint-plugin-autofix": "^1.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-storybook": "^0.11.1", + "match-sorter": "^8.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "storybook": "^8.4.7", diff --git a/packages/css/src/colors.css b/packages/css/src/colors.css index fd37d6b7..d21774c0 100644 --- a/packages/css/src/colors.css +++ b/packages/css/src/colors.css @@ -3,6 +3,7 @@ --mdPrimaryColor160: #003b3a; --mdPrimaryColor120: #005251; --mdPrimaryColor80: #337e7d; + --mdPrimaryColor40: #99bebe; --mdPrimaryColor30: #b2cece; --mdPrimaryColor20: #ccdfde; --mdPrimaryColor10: #e5eeee; @@ -28,7 +29,7 @@ --mdGreyColor10: #e8e8e8; --mdGreyColor20: #d2d2d2; --mdGreyColor40: #a6a6a6; - --mdGreyColor60: #808080; + --mdGreyColor60: #767676; --mdGreyColor80: #4e4e4e; --mdGreenColor60: #b3e8c2; diff --git a/packages/css/src/formElements/combobox/combobox.css b/packages/css/src/formElements/combobox/combobox.css new file mode 100644 index 00000000..dc5a24b5 --- /dev/null +++ b/packages/css/src/formElements/combobox/combobox.css @@ -0,0 +1,170 @@ +.md-combobox { + font-family: 'Open sans'; + width: 100%; +} + +/* Label */ +.md-combobox__label-wrapper { + margin-bottom: 0.5rem; +} +.md-combobox__label { + display: flex; + align-items: center; + gap: 0.5rem; +} +.md-combobox__label label { + font-weight: 600; +} + +.md-combobox__input-wrapper { + position: relative; +} +.md-combobox__input { + width: 100%; + font-size: 1rem; + line-height: 150%; + border: 1px solid var(--mdPrimaryColor); + padding: 0.75rem 4rem 0.75rem 2.5rem; +} +.md-combobox--large .md-combobox__input { + padding: 1rem 4rem 1rem 2.5rem; +} +.md-combobox--small .md-combobox__input { + padding: 0.5rem 4rem 0.5rem 2.5rem; +} +.md-combobox__input--no-prefix-icon, +.md-combobox--large .md-combobox__input--no-prefix-icon, +.md-combobox--small .md-combobox__input--no-prefix-icon { + padding-left: 0.75rem; +} +.md-combobox__input::placeholder { + color: var(--mdTextColor); + font-size: 1rem; +} +.md-combobox__input[data-focus-visible], +.md-combobox__input[aria-expanded='true'] { + outline: 2px solid var(--mdPrimaryColor); + outline-offset: -2px; +} + +.md-combobox__input[data-focus-visible]::placeholder, +.md-combobox__input[aria-expanded='true']::placeholder { + color: var(--mdGreyColor60); +} +.md-combobox__input:disabled { + background-color: var(--mdGreyColor20); + border-color: var(--mdGreyColor60); +} +.md-combobox__input:disabled::placeholder { + color: var(--mdGreyColor60); +} +.md-combobox__input--before { + position: absolute; + display: flex; + top: 50%; + left: 0.75rem; + color: var(--mdPrimaryColor); + width: 1rem; + height: 1rem; + transform: translateY(-50%); +} +.md-combobox__input-wrapper--disabled .md-combobox__input--before { + color: var(--mdGreyColor60); +} +.md-combobox__input--after { + position: absolute; + top: 50%; + right: 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--mdTextColor); + transform: translateY(-50%); + pointer-events: none; + z-index: 0; +} +.md-combobox__input-wrapper--disabled .md-combobox__input--after { + color: var(--mdGreyColor60); +} +.md-combobox__input--after svg { + width: 1rem; + height: 1rem; + rotate: 90deg; + transition: rotate 0.2s ease-in; + color: var(--mdPrimaryColor); +} +.md-combobox__input-wrapper--disabled .md-combobox__input--after svg { + color: var(--mdGreyColor60); +} +.md-combobox__input[aria-expanded='true'] + .md-combobox__input--after svg { + rotate: -90deg; + transition: rotate 0.2s ease-in; +} + +/* Popover */ +.md-combobox__popover { + background-color: #fff; + max-height: min(var(--popover-available-height, 300px), 300px); + overflow: auto; + overscroll-behavior: contain; + border: 2px solid var(--mdPrimaryColor); + border-top: 0; + opacity: 0; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + animation-duration: 200ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transform: translateY(-5%); +} +.md-combobox__popover[data-enter] { + opacity: 1; + transform: translateY(0%); + z-index: 3; +} + +/* Combobox item */ +.md-combobox__checkbox-item { + padding: 0.75rem; + cursor: pointer; +} +.md-combobox--large .md-combobox__checkbox-item { + padding: 1rem 0.75rem; +} +.md-combobox--small .md-combobox__checkbox-item { + padding: 0.5rem 0.75rem; +} +.md-combobox__checkbox-item[aria-selected='true'] { + background-color: var(--mdPrimaryColor20); +} +.md-combobox__checkbox-item[data-focus-visible], +.md-combobox__checkbox-item[data-active-item] { + background-color: var(--mdPrimaryColor40); + + .md-checkbox__label::before { + background-color: #fff; + } +} + +/* Help text */ +.md-combobox__help-text { + max-height: 0; + overflow: hidden; + transition: max-height 0.15s ease-out; +} +.md-combobox__help-text--open { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + max-height: 2000px; + transition: max-height 0.5s ease-in; +} + +/* Error */ +.md-combobox__error { + color: var(--mdErrorColor); + font-size: 0.88em; +} +.md-combobox--has-error .md-combobox__input, +.md-combobox--has-error .md-combobox__popover { + outline-color: var(--mdErrorColor); + border-color: var(--mdErrorColor); +} diff --git a/packages/css/src/index.css b/packages/css/src/index.css index c91ef897..5a39f15d 100644 --- a/packages/css/src/index.css +++ b/packages/css/src/index.css @@ -26,6 +26,7 @@ @import './formElements/multiselect/multiselect.css'; @import './formElements/multiautocomplete/multiautocomplete.css'; @import './formElements/fileupload/fileupload.css'; +@import './formElements/combobox/combobox.css'; @import './utils.css'; html { diff --git a/packages/react/package.json b/packages/react/package.json index c854baf1..f881b265 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -17,7 +17,9 @@ "url": "git+https://github.com/miljodir/md-components" }, "dependencies": { - "classnames": "^2.3.2" + "@ariakit/react": "^0.4.15", + "classnames": "^2.3.2", + "match-sorter": "^8.0.0" }, "devDependencies": { "@types/node": "^20.10.5", diff --git a/packages/react/src/formElements/MdAutocomplete.tsx b/packages/react/src/formElements/MdAutocomplete.tsx index 385c8b2b..28c4a61a 100644 --- a/packages/react/src/formElements/MdAutocomplete.tsx +++ b/packages/react/src/formElements/MdAutocomplete.tsx @@ -1,5 +1,5 @@ import classnames from 'classnames'; -import React, { useId, useRef, useState } from 'react'; +import React, { useEffect, useId, useRef, useState } from 'react'; import MdHelpButton from '../help/MdHelpButton'; import MdHelpText from '../help/MdHelpText'; import useDropdown from '../hooks/useDropdown'; @@ -108,6 +108,13 @@ const MdAutocomplete = React.forwardRef( displayValue = ''; } + useEffect(() => { + // eslint-disable-next-line no-console + console.warn( + 'Notice: MdAutocomplete and MdMultiAutocomplete are deprecated and will be removed in a future version. use MdCombobox instead.', + ); + }, []); + const handleOptionClick = (option: MdAutocompleteOption) => { onSelectOption(option); setOpen(false); diff --git a/packages/react/src/formElements/MdComboBox.tsx b/packages/react/src/formElements/MdComboBox.tsx new file mode 100644 index 00000000..1c3c6dab --- /dev/null +++ b/packages/react/src/formElements/MdComboBox.tsx @@ -0,0 +1,235 @@ +import * as Ariakit from '@ariakit/react'; + +import React, { useEffect, useMemo, useState, useTransition, useId } from 'react'; +import MdHelpButton from '../help/MdHelpButton'; +import MdHelpText from '../help/MdHelpText'; +import MdChevronIcon from '../icons/MdChevronIcon'; +import MdZoomIcon from '../icons/MdZoomIcon'; +import MdCheckbox from './MdCheckbox'; + +export interface MdComboBoxOption { + value: string; + text: string; +} + +export interface MdComboBoxProps extends React.InputHTMLAttributes { + id?: string; + label?: string; + options: MdComboBoxOption[]; + defaultOptions?: MdComboBoxOption[]; + value: string | string[]; + disabled?: boolean; + errorText?: string; + placeholder?: string; + helpText?: string; + mode?: 'large' | 'medium' | 'small'; + noResultsText?: string; + dropdownHeight?: number; + prefixIcon?: React.ReactNode; + hidePrefixIcon?: boolean; + onSelectOption(_value: string[] | string): void; +} + +/** + * MdComboBox. + * + * @type {React.FC} + * @returns {React.ReactElement} MdCombobox. + * @params id {string=} - The id of the combobox. + * @params label {string=} - The label of the combobox. + * @params options {MdComboBoxOption[]} - The options of the combobox. + * @params value {string[] | string} - The value of the combobox. string for single select, string[] for multi select. + * @params disabled {boolean=} - The disabled state of the combobox. + * @params errorText {string=} - The error text of the combobox. + * @params placeholder {string=} - The placeholder of the combobox. + * @params mode {string=} - The size of the combobox. 'large' | 'medium' | 'small' + * @params onSelectOption {function} - The onSelectOption handler for change events. + * @params helpText {string=} - The help text of the combobox. + * @params noResultsText {string=} - The text to display if no results are found. + * @params dropdownHeight {number=} - The height of the dropdown. + * @params prefixIcon {React.ReactNode=} - The prefix icon of the combobox. + * @params hidePrefixIcon {boolean=} - The hide prefix icon of the combobox. + */ + +const MdComboBox: React.FC = React.forwardRef( + ( + { + id, + label, + options, + defaultOptions, + value, + disabled = false, + placeholder = 'Søk', + mode = 'medium', + helpText, + errorText, + noResultsText = 'Ingen treff', + dropdownHeight, + prefixIcon, + hidePrefixIcon = false, + onSelectOption, + ...otherProps + }, + ref, + ) => { + const uuid = `combobox_${useId()}`; + const comboBoxId = id || uuid; + const isMultiSelect = Array.isArray(value); + const [isPending, startTransition] = useTransition(); + const [searchValue, setSearchValue] = useState(''); + const [selectedValues, setSelectedValues] = useState(value); + const [helpOpen, setHelpOpen] = useState(false); + + useEffect(() => { + onSelectOption(selectedValues); + }, [selectedValues]); + + const matches = useMemo(() => { + if (!searchValue && defaultOptions && defaultOptions.length > 0) { + return defaultOptions; + } + + // return matchSorter(options, searchValue, { keys: ['value'], threshold: matchSorter.rankings.CONTAINS }); + const results = options?.filter(o => { + return o.text?.toLowerCase().includes(searchValue.toLowerCase() || ''); + }); + return results; + }, [searchValue]); + + const getValueById = (value: string) => { + const option = options.find(option => { + return option.value === value; + }); + return option ? option.text : placeholder; + }; + + let displayValue: string | string[] = placeholder; + if (isMultiSelect) { + displayValue = selectedValues.length > 0 ? getValueById(selectedValues[0]) : placeholder; + } else if (selectedValues !== '') { + displayValue = getValueById(selectedValues as string); + } + + let ariaDescribedBy = helpText && helpText !== '' ? `md-combobox_help-text_${comboBoxId}` : undefined; + ariaDescribedBy = errorText && errorText !== '' ? `md-combobox_error_${comboBoxId}` : ariaDescribedBy; + + return ( +
+ { + startTransition(() => { + setSearchValue(val); + }); + }} + > + {label && label !== '' && ( +
+
+ {label} + {helpText && helpText !== '' && ( +
+ { + return setHelpOpen(!helpOpen); + }} + expanded={helpOpen} + /> +
+ )} +
+ + {helpText && helpText !== '' && ( +
+ + {helpText} + +
+ )} +
+ )} + +
+ {!hidePrefixIcon && ( +
{prefixIcon ? prefixIcon : }
+ )} + +
+
{isMultiSelect && selectedValues.length > 0 && `+${selectedValues.length}`}
+ +
+
+ + + {matches && + matches.map(option => { + let isChecked = false; + if (isMultiSelect) { + isChecked = selectedValues.includes(option.value); + } else { + isChecked = selectedValues === option.value; + } + + return ( + + {isMultiSelect ? ( + + ) : ( + option.text + )} + + ); + })} + {!matches.length &&
{noResultsText}
} +
+
+ + {errorText && errorText !== '' && ( +
+ {errorText} +
+ )} +
+ ); + }, +); + +MdComboBox.displayName = 'MdComboBox'; + +export default MdComboBox; diff --git a/packages/react/src/formElements/MdMultiAutocomplete.tsx b/packages/react/src/formElements/MdMultiAutocomplete.tsx index 0ab70246..8072da65 100644 --- a/packages/react/src/formElements/MdMultiAutocomplete.tsx +++ b/packages/react/src/formElements/MdMultiAutocomplete.tsx @@ -1,5 +1,5 @@ import classnames from 'classnames'; -import React, { useId, useRef, useState } from 'react'; +import React, { useEffect, useId, useRef, useState } from 'react'; import MdInputChip from '../chips/MdInputChip'; import MdHelpButton from '../help/MdHelpButton'; import MdHelpText from '../help/MdHelpText'; @@ -91,6 +91,13 @@ const MdMultiAutocomplete = React.forwardRef { + // eslint-disable-next-line no-console + console.warn( + 'Notice: MdAutocomplete and MdMultiAutocomplete are deprecated and will be removed in a future version. use MdCombobox instead.', + ); + }, []); + const optionClass = (option: MdMultiAutocompleteOption) => { return classnames('md-multiautocomplete__dropdown-item', { 'md-multiautocomplete__dropdown-item--selected': optionIsChecked(option), diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 74931e52..cb0f3c7b 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -6,6 +6,7 @@ import MdFileList, { MdFileListProps } from './fileList/MdFileList'; import MdAutocomplete, { MdAutocompleteOption, MdAutocompleteProps } from './formElements/MdAutocomplete'; import MdCheckbox, { MdCheckboxProps } from './formElements/MdCheckbox'; import MdCheckboxGroup, { MdCheckboxGroupProps } from './formElements/MdCheckboxGroup'; +import MdComboBox, { MdComboBoxOption } from './formElements/MdComboBox'; import MdFileUpload, { MdFileUploadProps } from './formElements/MdFileUpload'; import MdInput, { MdInputProps } from './formElements/MdInput'; import MdMultiAutocomplete, { MdMultiAutocompleteOption } from './formElements/MdMultiAutocomplete'; @@ -284,4 +285,6 @@ export { MdStep, MdStepProps, MdStepperProps, + MdComboBox, + MdComboBoxOption, }; diff --git a/stories/ComboBox.stories.tsx b/stories/ComboBox.stories.tsx new file mode 100644 index 00000000..384db825 --- /dev/null +++ b/stories/ComboBox.stories.tsx @@ -0,0 +1,216 @@ +import { Title, Subtitle, Description, Controls, Primary } from '@storybook/addon-docs'; +import { useArgs } from '@storybook/preview-api'; +import React from 'react'; +import MdComboBox from '../packages/react/src/formElements/MdComboBox'; + +import MdCalendarIcon from '../packages/react/src/icons/MdCalendarIcon'; +import MdZoomIcon from '../packages/react/src/icons/MdZoomIcon'; +import type { Args } from '@storybook/react'; + +export default { + title: 'Form/Combobox', + component: MdComboBox, + parameters: { + docs: { + page: () => { + return ( + <> + + <Subtitle /> + <Description /> + <Primary /> + <Controls /> + {/* <Markdown>{Readme.toString()}</Markdown> */} + </> + ); + }, + description: { + component: + // eslint-disable-next-line quotes + "A component for combobox.<br/>Can handle single or mulitple selections. For single selection set `value` to a string, for multiselect, set `value` to an array of strings.<br/><br/>`import { MdComboBox } from '@miljodirektoratet/md-react'`", + }, + }, + }, + argTypes: { + options: { + type: { name: 'array', required: true }, + description: 'Array with data objects for select options', + table: { + type: { + summary: '[{ value: string | number, text: string }, ...]', + }, + }, + }, + defaultOptions: { + type: { name: 'array' }, + description: 'Array with data objects for default autocomplete options', + table: { + type: { + summary: '[{ value: string | number, text: string }, ...]', + }, + }, + }, + value: { + type: { name: 'string[] | string', required: true }, + description: + 'The currently selected values. Either an array of strings or a single string. For multiselect, value needs to be an array.', + table: { + type: { + summary: '[string, string, ...] or string', + }, + }, + }, + onSelectOption: { + type: { name: 'function', required: true }, + description: + 'The handler for change events. Returns an array of strings for multiselect, or a single string value for single select.', + table: { + type: { + summary: 'function', + }, + }, + }, + label: { + type: { name: 'string' }, + description: 'The label for the combobox.', + table: { + defaultValue: { summary: 'null' }, + type: { + summary: 'string', + }, + }, + control: { type: 'text' }, + }, + disabled: { + type: { name: 'boolean' }, + description: 'Is the Combobox disabled?', + table: { + defaultValue: { summary: 'false' }, + type: { + summary: 'boolean', + }, + }, + control: { type: 'boolean' }, + }, + mode: { + description: 'Set size of combobox', + options: ['small', 'medium', 'large'], + table: { + defaultValue: { summary: 'medium' }, + type: { + summary: 'string', + }, + }, + control: { type: 'inline-radio' }, + }, + helpText: { + type: { name: 'string' }, + description: 'Help text for the combobox', + table: { + defaultValue: { summary: 'null' }, + type: { + summary: 'string', + }, + }, + control: { type: 'text' }, + }, + errorText: { + type: { name: 'string' }, + description: 'Display error text, and style the combobox as an error state', + table: { + defaultValue: { summary: 'null' }, + type: { + summary: 'string', + }, + }, + control: { type: 'text' }, + }, + noResultsText: { + type: { name: 'string' }, + description: 'The text to display when no results are found', + table: { + defaultValue: { summary: 'Ingen treff' }, + type: { + summary: 'string', + }, + }, + control: { type: 'text' }, + }, + dropdownHeight: { + type: { name: 'number' }, + description: 'Set max height of dropdown in pixels. Deafults to `300px` if not set.', + table: { + defaultValue: { summary: 'variable' }, + type: { + summary: 'number', + }, + }, + control: { type: 'number' }, + }, + }, +}; + +const Template = (args: Args) => { + const [, updateArgs] = useArgs(); + + const handleSelect = (values: string[] | string) => { + updateArgs({ ...args, value: values }); + }; + + return ( + <div style={{ minHeight: '340px' }}> + <MdComboBox + {...args} + value={args.value} + options={args.options} + onSelectOption={values => { + handleSelect(values); + }} + aria-required="true" + /> + </div> + ); +}; + +export const Multi = Template.bind({}); +export const Single = Template.bind({}); + +Multi.args = { + options: [ + { value: 'optionA', text: 'A option' }, + { value: 'optionB', text: 'B option' }, + { value: 'optionC', text: 'Et valg' }, + { value: 'optionD', text: 'Et annet valg som er litt langt' }, + ], + value: ['optionA', 'optionC'], + onSelectOption: () => {}, + defaultOptions: [], + label: 'Label', + disabled: false, + mode: 'medium', + helpText: 'This is a help text', + prefixIcon: <MdZoomIcon />, + errorText: '', + hidePrefixIcon: true, +}; + +Single.args = { + options: [ + { value: 'optionA', text: 'A option' }, + { value: 'optionB', text: 'B option' }, + { value: 'optionC', text: 'Et valg' }, + { value: 'optionD', text: 'Et annet valg som er litt langt' }, + ], + value: 'optionA', + onSelectOption: () => {}, + defaultOptions: [ + { value: 'optionB', text: 'B option' }, + { value: 'optionD', text: 'Et annet valg som er litt langt' }, + ], + label: 'Label', + disabled: false, + mode: 'medium', + helpText: 'This is a help text', + prefixIcon: <MdCalendarIcon />, + errorText: 'This is an example of an error text', +};