@simplybusiness/mobius 5.5.0 → 5.6.0

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/components/Combobox/Combobox.js +56 -33
  3. package/dist/cjs/components/Combobox/Combobox.js.map +1 -1
  4. package/dist/cjs/components/Combobox/Listbox.js +58 -0
  5. package/dist/cjs/components/Combobox/Listbox.js.map +1 -0
  6. package/dist/cjs/components/Combobox/Option.js +44 -0
  7. package/dist/cjs/components/Combobox/Option.js.map +1 -0
  8. package/dist/cjs/components/Combobox/fixtures.js +115 -0
  9. package/dist/cjs/components/Combobox/fixtures.js.map +1 -1
  10. package/dist/cjs/components/Combobox/useComboboxHighlight.js +86 -0
  11. package/dist/cjs/components/Combobox/useComboboxHighlight.js.map +1 -0
  12. package/dist/cjs/components/Combobox/utils.js +46 -0
  13. package/dist/cjs/components/Combobox/utils.js.map +1 -0
  14. package/dist/cjs/components/TextArea/TextArea.js +4 -4
  15. package/dist/cjs/components/TextArea/TextArea.js.map +1 -1
  16. package/dist/cjs/components/TextAreaInput/TextAreaInput.js +6 -3
  17. package/dist/cjs/components/TextAreaInput/TextAreaInput.js.map +1 -1
  18. package/dist/cjs/components/TextField/TextField.js +4 -3
  19. package/dist/cjs/components/TextField/TextField.js.map +1 -1
  20. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  21. package/dist/esm/components/Combobox/Combobox.js +55 -32
  22. package/dist/esm/components/Combobox/Combobox.js.map +1 -1
  23. package/dist/esm/components/Combobox/Listbox.js +43 -0
  24. package/dist/esm/components/Combobox/Listbox.js.map +1 -0
  25. package/dist/esm/components/Combobox/Option.js +29 -0
  26. package/dist/esm/components/Combobox/Option.js.map +1 -0
  27. package/dist/esm/components/Combobox/fixtures.js +109 -0
  28. package/dist/esm/components/Combobox/fixtures.js.map +1 -1
  29. package/dist/esm/components/Combobox/types.js.map +1 -1
  30. package/dist/esm/components/Combobox/useComboboxHighlight.js +76 -0
  31. package/dist/esm/components/Combobox/useComboboxHighlight.js.map +1 -0
  32. package/dist/esm/components/Combobox/utils.js +20 -0
  33. package/dist/esm/components/Combobox/utils.js.map +1 -0
  34. package/dist/esm/components/TextArea/TextArea.js +4 -4
  35. package/dist/esm/components/TextArea/TextArea.js.map +1 -1
  36. package/dist/esm/components/TextAreaInput/TextAreaInput.js +6 -3
  37. package/dist/esm/components/TextAreaInput/TextAreaInput.js.map +1 -1
  38. package/dist/esm/components/TextField/TextField.js +4 -3
  39. package/dist/esm/components/TextField/TextField.js.map +1 -1
  40. package/dist/types/components/Combobox/Combobox.d.ts +1 -1
  41. package/dist/types/components/Combobox/Combobox.stories.d.ts +4 -1
  42. package/dist/types/components/Combobox/Listbox.d.ts +10 -0
  43. package/dist/types/components/Combobox/Option.d.ts +2 -0
  44. package/dist/types/components/Combobox/fixtures.d.ts +5 -0
  45. package/dist/types/components/Combobox/types.d.ts +17 -2
  46. package/dist/types/components/Combobox/useComboboxHighlight.d.ts +10 -0
  47. package/dist/types/components/Combobox/useComboboxHighlight.test.d.ts +1 -0
  48. package/dist/types/components/Combobox/utils.d.ts +6 -0
  49. package/dist/types/components/Combobox/utils.test.d.ts +1 -0
  50. package/dist/types/components/TextArea/TextArea.d.ts +2 -2
  51. package/dist/types/components/TextAreaInput/TextAreaInput.d.ts +3 -1
  52. package/package.json +1 -1
  53. package/src/components/Combobox/Combobox.css +51 -4
  54. package/src/components/Combobox/Combobox.mdx +147 -0
  55. package/src/components/Combobox/Combobox.stories.tsx +47 -2
  56. package/src/components/Combobox/Combobox.test.tsx +535 -316
  57. package/src/components/Combobox/Combobox.tsx +78 -58
  58. package/src/components/Combobox/Listbox.tsx +74 -0
  59. package/src/components/Combobox/Option.tsx +41 -0
  60. package/src/components/Combobox/fixtures.tsx +111 -0
  61. package/src/components/Combobox/types.tsx +22 -4
  62. package/src/components/Combobox/useComboboxHighlight.test.tsx +242 -0
  63. package/src/components/Combobox/useComboboxHighlight.tsx +88 -0
  64. package/src/components/Combobox/utils.test.tsx +120 -0
  65. package/src/components/Combobox/utils.tsx +50 -0
  66. package/src/components/TextArea/TextArea.tsx +6 -6
  67. package/src/components/TextAreaInput/TextAreaInput.tsx +16 -4
  68. package/src/components/TextField/TextField.test.tsx +8 -0
  69. package/src/components/TextField/TextField.tsx +3 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["\"use client\";\n\nimport { Ref, forwardRef, RefAttributes } from \"react\";\nimport classNames from \"classnames/dedupe\";\nimport { DOMProps } from \"../../types/dom\";\nimport { TextAreaInput } from \"../TextAreaInput\";\nimport { Label } from \"../Label\";\nimport { ForwardedRefComponent } from \"../../types/components\";\nimport { ErrorMessage } from \"../ErrorMessage\";\nimport {\n UseTextFieldProps,\n useTextField,\n useValidationClasses,\n} from \"../../hooks\";\nimport { Stack } from \"../Stack\";\n\nexport type TextAreaElementType = HTMLTextAreaElement;\n\nexport interface TextAreaProps\n extends UseTextFieldProps,\n DOMProps,\n RefAttributes<TextAreaElementType> {\n className?: string;\n errorMessage?: string;\n}\n\nexport type TextAreaRef = Ref<TextAreaElementType>;\n\nconst TextArea: ForwardedRefComponent<TextAreaProps, TextAreaElementType> =\n forwardRef((props: TextAreaProps, ref: TextAreaRef) => {\n const {\n isDisabled,\n className,\n errorMessage,\n label,\n validationState,\n isInvalid,\n ...otherProps\n } = props;\n\n const { inputProps, labelProps, errorMessageProps } = useTextField(props);\n\n // Set class name for atom, including interaction state\n const classes = classNames(\"mobius\", \"mobius-text-area\", className);\n const validationClasses = useValidationClasses({\n validationState,\n isInvalid,\n });\n const inputClasses = classNames(\n \"mobius-text-area__input\",\n validationClasses,\n );\n const labelClasses = classNames(\n {\n \"--is-disabled\": isDisabled,\n },\n validationClasses,\n );\n\n return (\n <Stack className={classes} gap=\"xs\">\n {label && (\n <Label {...labelProps} className={labelClasses}>\n {props.label}\n </Label>\n )}\n <TextAreaInput\n {...otherProps}\n {...inputProps}\n ref={ref}\n className={inputClasses}\n isDisabled={isDisabled}\n aria-invalid={errorMessage != null}\n />\n <ErrorMessage {...errorMessageProps} errorMessage={errorMessage} />\n </Stack>\n );\n });\n\nTextArea.displayName = \"TextArea\";\nexport { TextArea };\n"],"names":["forwardRef","classNames","TextAreaInput","Label","ErrorMessage","useTextField","useValidationClasses","Stack","TextArea","props","ref","isDisabled","className","errorMessage","label","validationState","isInvalid","otherProps","inputProps","labelProps","errorMessageProps","classes","validationClasses","inputClasses","labelClasses","gap","aria-invalid","displayName"],"mappings":"AAAA;;AAEA,SAAcA,UAAU,QAAuB,QAAQ;AACvD,OAAOC,gBAAgB,oBAAoB;AAE3C,SAASC,aAAa,QAAQ,mBAAmB;AACjD,SAASC,KAAK,QAAQ,WAAW;AAEjC,SAASC,YAAY,QAAQ,kBAAkB;AAC/C,SAEEC,YAAY,EACZC,oBAAoB,QACf,cAAc;AACrB,SAASC,KAAK,QAAQ,WAAW;AAcjC,MAAMC,yBACJR,WAAW,CAACS,OAAsBC;IAChC,MAAM,EACJC,UAAU,EACVC,SAAS,EACTC,YAAY,EACZC,KAAK,EACLC,eAAe,EACfC,SAAS,EACT,GAAGC,YACJ,GAAGR;IAEJ,MAAM,EAAES,UAAU,EAAEC,UAAU,EAAEC,iBAAiB,EAAE,GAAGf,aAAaI;IAEnE,uDAAuD;IACvD,MAAMY,UAAUpB,WAAW,UAAU,oBAAoBW;IACzD,MAAMU,oBAAoBhB,qBAAqB;QAC7CS;QACAC;IACF;IACA,MAAMO,eAAetB,WACnB,2BACAqB;IAEF,MAAME,eAAevB,WACnB;QACE,iBAAiBU;IACnB,GACAW;IAGF,qBACE,MAACf;QAAMK,WAAWS;QAASI,KAAI;;YAC5BX,uBACC,KAACX;gBAAO,GAAGgB,UAAU;gBAAEP,WAAWY;0BAC/Bf,MAAMK,KAAK;;0BAGhB,KAACZ;gBACE,GAAGe,UAAU;gBACb,GAAGC,UAAU;gBACdR,KAAKA;gBACLE,WAAWW;gBACXZ,YAAYA;gBACZe,gBAAcb,gBAAgB;;0BAEhC,KAACT;gBAAc,GAAGgB,iBAAiB;gBAAEP,cAAcA;;;;AAGzD;AAEFL,SAASmB,WAAW,GAAG;AACvB,SAASnB,QAAQ,GAAG"}
1
+ {"version":3,"sources":["../../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["\"use client\";\n\nimport classNames from \"classnames/dedupe\";\nimport { Ref, RefAttributes, forwardRef } from \"react\";\nimport {\n UseTextFieldProps,\n useTextField,\n useValidationClasses,\n} from \"../../hooks\";\nimport { ForwardedRefComponent } from \"../../types/components\";\nimport { DOMProps } from \"../../types/dom\";\nimport { ErrorMessage } from \"../ErrorMessage\";\nimport { Label } from \"../Label\";\nimport { Stack } from \"../Stack\";\nimport { TextAreaInput } from \"../TextAreaInput\";\n\nexport type TextAreaElementType = HTMLTextAreaElement;\n\nexport interface TextAreaProps\n extends UseTextFieldProps,\n DOMProps,\n RefAttributes<TextAreaElementType> {\n className?: string;\n errorMessage?: string;\n}\n\nexport type TextAreaRef = Ref<TextAreaElementType>;\n\nconst TextArea: ForwardedRefComponent<TextAreaProps, TextAreaElementType> =\n forwardRef((props: TextAreaProps, ref: TextAreaRef) => {\n const {\n isDisabled,\n className,\n errorMessage,\n label,\n validationState,\n isInvalid,\n ...otherProps\n } = props;\n\n const { inputProps, labelProps, errorMessageProps } = useTextField(props);\n\n // Set class name for atom, including interaction state\n const classes = classNames(\"mobius\", \"mobius-text-area\", className);\n const validationClasses = useValidationClasses({\n validationState,\n isInvalid,\n });\n const inputClasses = classNames(\n \"mobius-text-area__input\",\n validationClasses,\n );\n const labelClasses = classNames(\n {\n \"--is-disabled\": isDisabled,\n },\n validationClasses,\n );\n\n return (\n <Stack className={classes} gap=\"xs\">\n {label && (\n <Label {...labelProps} className={labelClasses}>\n {props.label}\n </Label>\n )}\n <TextAreaInput\n {...otherProps}\n {...inputProps}\n ref={ref}\n className={inputClasses}\n isDisabled={isDisabled}\n aria-invalid={errorMessage != null}\n />\n <ErrorMessage {...errorMessageProps} errorMessage={errorMessage} />\n </Stack>\n );\n });\n\nTextArea.displayName = \"TextArea\";\nexport { TextArea };\n"],"names":["classNames","forwardRef","useTextField","useValidationClasses","ErrorMessage","Label","Stack","TextAreaInput","TextArea","props","ref","isDisabled","className","errorMessage","label","validationState","isInvalid","otherProps","inputProps","labelProps","errorMessageProps","classes","validationClasses","inputClasses","labelClasses","gap","aria-invalid","displayName"],"mappings":"AAAA;;AAEA,OAAOA,gBAAgB,oBAAoB;AAC3C,SAA6BC,UAAU,QAAQ,QAAQ;AACvD,SAEEC,YAAY,EACZC,oBAAoB,QACf,cAAc;AAGrB,SAASC,YAAY,QAAQ,kBAAkB;AAC/C,SAASC,KAAK,QAAQ,WAAW;AACjC,SAASC,KAAK,QAAQ,WAAW;AACjC,SAASC,aAAa,QAAQ,mBAAmB;AAcjD,MAAMC,yBACJP,WAAW,CAACQ,OAAsBC;IAChC,MAAM,EACJC,UAAU,EACVC,SAAS,EACTC,YAAY,EACZC,KAAK,EACLC,eAAe,EACfC,SAAS,EACT,GAAGC,YACJ,GAAGR;IAEJ,MAAM,EAAES,UAAU,EAAEC,UAAU,EAAEC,iBAAiB,EAAE,GAAGlB,aAAaO;IAEnE,uDAAuD;IACvD,MAAMY,UAAUrB,WAAW,UAAU,oBAAoBY;IACzD,MAAMU,oBAAoBnB,qBAAqB;QAC7CY;QACAC;IACF;IACA,MAAMO,eAAevB,WACnB,2BACAsB;IAEF,MAAME,eAAexB,WACnB;QACE,iBAAiBW;IACnB,GACAW;IAGF,qBACE,MAAChB;QAAMM,WAAWS;QAASI,KAAI;;YAC5BX,uBACC,KAACT;gBAAO,GAAGc,UAAU;gBAAEP,WAAWY;0BAC/Bf,MAAMK,KAAK;;0BAGhB,KAACP;gBACE,GAAGU,UAAU;gBACb,GAAGC,UAAU;gBACdR,KAAKA;gBACLE,WAAWW;gBACXZ,YAAYA;gBACZe,gBAAcb,gBAAgB;;0BAEhC,KAACT;gBAAc,GAAGgB,iBAAiB;gBAAEP,cAAcA;;;;AAGzD;AAEFL,SAASmB,WAAW,GAAG;AACvB,SAASnB,QAAQ,GAAG"}
@@ -1,16 +1,19 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef } from "react";
3
2
  import classNames from "classnames/dedupe";
3
+ import { forwardRef } from "react";
4
4
  const TextAreaInput = /*#__PURE__*/ forwardRef((props, ref)=>{
5
5
  // Extract interaction props:
6
- const { isSelected, isDisabled, ...otherProps } = props;
6
+ const { isSelected, isDisabled, isReadOnly, isRequired, ...otherProps } = props;
7
7
  const classes = classNames("mobius", "mobius-text-area__input", {
8
8
  "--is-disabled": isDisabled,
9
- "--is-selected": isSelected
9
+ "--is-selected": isSelected,
10
+ "--is-required": isRequired
10
11
  }, otherProps.className);
11
12
  return /*#__PURE__*/ _jsx("textarea", {
12
13
  ref: ref,
13
14
  ...otherProps,
15
+ required: isRequired,
16
+ readOnly: isReadOnly,
14
17
  className: classes
15
18
  });
16
19
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/TextAreaInput/TextAreaInput.tsx"],"sourcesContent":["import { Ref, forwardRef, RefAttributes } from \"react\";\nimport classNames from \"classnames/dedupe\";\nimport { DOMProps } from \"../../types/dom\";\nimport { ForwardedRefComponent } from \"../../types/components\";\n\nexport type TextAreaInputElementType = HTMLTextAreaElement;\n\nexport interface InteractionStateProps {\n isSelected?: boolean;\n isDisabled?: boolean;\n}\n\nexport interface TextAreaInputProps\n extends DOMProps,\n InteractionStateProps,\n RefAttributes<TextAreaInputElementType> {\n className?: string;\n}\n\nexport type TextAreaInputRef = Ref<TextAreaInputElementType>;\n\nconst TextAreaInput: ForwardedRefComponent<\n TextAreaInputProps,\n TextAreaInputElementType\n> = forwardRef((props: TextAreaInputProps, ref: TextAreaInputRef) => {\n // Extract interaction props:\n const { isSelected, isDisabled, ...otherProps } = props;\n\n const classes = classNames(\n \"mobius\",\n \"mobius-text-area__input\",\n {\n \"--is-disabled\": isDisabled,\n \"--is-selected\": isSelected,\n },\n otherProps.className,\n );\n\n return <textarea ref={ref} {...otherProps} className={classes} />;\n});\n\nTextAreaInput.displayName = \"TextAreaInput\";\nexport { TextAreaInput };\n"],"names":["forwardRef","classNames","TextAreaInput","props","ref","isSelected","isDisabled","otherProps","classes","className","textarea","displayName"],"mappings":";AAAA,SAAcA,UAAU,QAAuB,QAAQ;AACvD,OAAOC,gBAAgB,oBAAoB;AAoB3C,MAAMC,8BAGFF,WAAW,CAACG,OAA2BC;IACzC,6BAA6B;IAC7B,MAAM,EAAEC,UAAU,EAAEC,UAAU,EAAE,GAAGC,YAAY,GAAGJ;IAElD,MAAMK,UAAUP,WACd,UACA,2BACA;QACE,iBAAiBK;QACjB,iBAAiBD;IACnB,GACAE,WAAWE,SAAS;IAGtB,qBAAO,KAACC;QAASN,KAAKA;QAAM,GAAGG,UAAU;QAAEE,WAAWD;;AACxD;AAEAN,cAAcS,WAAW,GAAG;AAC5B,SAAST,aAAa,GAAG"}
1
+ {"version":3,"sources":["../../../../src/components/TextAreaInput/TextAreaInput.tsx"],"sourcesContent":["import classNames from \"classnames/dedupe\";\nimport { forwardRef, Ref, RefAttributes } from \"react\";\nimport { ForwardedRefComponent } from \"../../types/components\";\nimport { DOMProps } from \"../../types/dom\";\n\nexport type TextAreaInputElementType = HTMLTextAreaElement;\n\nexport interface InteractionStateProps {\n isSelected?: boolean;\n isDisabled?: boolean;\n}\n\nexport interface TextAreaInputProps\n extends DOMProps,\n InteractionStateProps,\n RefAttributes<TextAreaInputElementType> {\n className?: string;\n isReadOnly?: boolean;\n isRequired?: boolean;\n}\n\nexport type TextAreaInputRef = Ref<TextAreaInputElementType>;\n\nconst TextAreaInput: ForwardedRefComponent<\n TextAreaInputProps,\n TextAreaInputElementType\n> = forwardRef((props: TextAreaInputProps, ref: TextAreaInputRef) => {\n // Extract interaction props:\n const { isSelected, isDisabled, isReadOnly, isRequired, ...otherProps } =\n props;\n\n const classes = classNames(\n \"mobius\",\n \"mobius-text-area__input\",\n {\n \"--is-disabled\": isDisabled,\n \"--is-selected\": isSelected,\n \"--is-required\": isRequired,\n },\n otherProps.className,\n );\n\n return (\n <textarea\n ref={ref}\n {...otherProps}\n required={isRequired}\n readOnly={isReadOnly}\n className={classes}\n />\n );\n});\n\nTextAreaInput.displayName = \"TextAreaInput\";\nexport { TextAreaInput };\n"],"names":["classNames","forwardRef","TextAreaInput","props","ref","isSelected","isDisabled","isReadOnly","isRequired","otherProps","classes","className","textarea","required","readOnly","displayName"],"mappings":";AAAA,OAAOA,gBAAgB,oBAAoB;AAC3C,SAASC,UAAU,QAA4B,QAAQ;AAsBvD,MAAMC,8BAGFD,WAAW,CAACE,OAA2BC;IACzC,6BAA6B;IAC7B,MAAM,EAAEC,UAAU,EAAEC,UAAU,EAAEC,UAAU,EAAEC,UAAU,EAAE,GAAGC,YAAY,GACrEN;IAEF,MAAMO,UAAUV,WACd,UACA,2BACA;QACE,iBAAiBM;QACjB,iBAAiBD;QACjB,iBAAiBG;IACnB,GACAC,WAAWE,SAAS;IAGtB,qBACE,KAACC;QACCR,KAAKA;QACJ,GAAGK,UAAU;QACdI,UAAUL;QACVM,UAAUP;QACVI,WAAWD;;AAGjB;AAEAR,cAAca,WAAW,GAAG;AAC5B,SAASb,aAAa,GAAG"}
@@ -5,10 +5,10 @@ import { forwardRef } from "react";
5
5
  import { useTextField, useValidationClasses } from "../../hooks";
6
6
  import { ErrorMessage } from "../ErrorMessage";
7
7
  import { Label } from "../Label";
8
- import { adornmentWithClassName } from "./adornmentWithClassName";
9
8
  import { Stack } from "../Stack";
9
+ import { adornmentWithClassName } from "./adornmentWithClassName";
10
10
  const TextField = /*#__PURE__*/ forwardRef((props, ref)=>{
11
- const { isDisabled, type = "text", validationState, isInvalid, className, label, errorMessage, children, isRequired, prefixInside, prefixOutside, suffixInside, suffixOutside, ...otherProps } = props;
11
+ const { isDisabled, isReadOnly, type = "text", validationState, isInvalid, className, label, errorMessage, children, isRequired, prefixInside, prefixOutside, suffixInside, suffixOutside, ...otherProps } = props;
12
12
  const { inputProps, labelProps, errorMessageProps } = useTextField({
13
13
  ...props,
14
14
  "aria-errormessage": errorMessage
@@ -54,7 +54,8 @@ const TextField = /*#__PURE__*/ forwardRef((props, ref)=>{
54
54
  ...inputProps,
55
55
  ref: ref,
56
56
  type: type,
57
- className: inputClasses
57
+ className: inputClasses,
58
+ readOnly: isReadOnly
58
59
  }),
59
60
  adornmentWithClassName(suffixInside, "mobius-text-field__suffix-inside")
60
61
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/TextField/TextField.tsx"],"sourcesContent":["\"use client\";\n\nimport classNames from \"classnames/dedupe\";\nimport {\n HTMLInputTypeAttribute,\n ReactElement,\n ReactNode,\n Ref,\n RefAttributes,\n forwardRef,\n} from \"react\";\nimport {\n UseTextFieldProps,\n useTextField,\n useValidationClasses,\n} from \"../../hooks\";\nimport { DOMProps, FocusEvents } from \"../../types\";\nimport { ForwardedRefComponent } from \"../../types/components\";\nimport { ErrorMessage } from \"../ErrorMessage\";\nimport { Label } from \"../Label\";\nimport { adornmentWithClassName } from \"./adornmentWithClassName\";\nimport { Stack } from \"../Stack\";\n\nexport type TextFieldElementType = HTMLInputElement;\nexport interface TextFieldProps\n extends DOMProps,\n FocusEvents,\n UseTextFieldProps,\n RefAttributes<TextFieldElementType> {\n className?: string;\n errorMessage?: string;\n children?: ReactNode;\n label?: string;\n type?: Exclude<\n HTMLInputTypeAttribute,\n | \"button\"\n | \"checkbox\"\n | \"color\"\n | \"date\"\n | \"datetime-local\"\n | \"file\"\n | \"image\"\n | \"month\"\n | \"radio\"\n | \"range\"\n | \"reset\"\n | \"submit\"\n | \"week\"\n >;\n prefixInside?: ReactElement;\n prefixOutside?: ReactElement;\n suffixInside?: ReactElement;\n suffixOutside?: ReactElement;\n}\n\nexport type TextFieldRef = Ref<TextFieldElementType>;\n\nconst TextField: ForwardedRefComponent<TextFieldProps, TextFieldElementType> =\n forwardRef((props: TextFieldProps, ref: TextFieldRef) => {\n const {\n isDisabled,\n type = \"text\",\n validationState,\n isInvalid,\n className,\n label,\n errorMessage,\n children,\n isRequired,\n prefixInside,\n prefixOutside,\n suffixInside,\n suffixOutside,\n ...otherProps\n } = props;\n\n const { inputProps, labelProps, errorMessageProps } = useTextField({\n ...props,\n \"aria-errormessage\": errorMessage,\n });\n\n const hidden = type === \"hidden\";\n\n const validationClasses = useValidationClasses({\n validationState,\n isInvalid,\n });\n\n const textfieldClasses = {\n \"--is-disabled\": isDisabled,\n \"--is-required\": typeof isRequired === \"boolean\" && isRequired,\n \"--is-optional\": typeof isRequired === \"boolean\" && !isRequired,\n \"--is-hidden\": hidden,\n [className || \"\"]: true,\n };\n\n const sharedClasses = classNames(validationClasses, textfieldClasses);\n\n const labelClasses = classNames(\n {\n \"--is-disabled\": isDisabled,\n },\n validationClasses,\n );\n\n const containerClasses = classNames(\n \"mobius\",\n \"mobius-text-field\",\n sharedClasses,\n );\n\n const inputClasses = classNames(\n \"mobius\",\n \"mobius-text-field__input\",\n sharedClasses,\n );\n\n const inputWrapperClasses = classNames(\n \"mobius-text-field__input-wrapper\",\n sharedClasses,\n );\n\n return (\n <Stack gap=\"xs\" className={containerClasses}>\n {label && !hidden && (\n <Label {...labelProps} className={labelClasses}>\n {label}\n </Label>\n )}\n <div className=\"mobius-text-field__inner-container\">\n {adornmentWithClassName(\n prefixOutside,\n \"mobius-text-field__prefix-outside\",\n )}\n <div className={inputWrapperClasses}>\n {adornmentWithClassName(\n prefixInside,\n \"mobius-text-field__prefix-inside\",\n )}\n <input\n {...otherProps}\n {...inputProps}\n ref={ref}\n type={type}\n className={inputClasses}\n />\n {adornmentWithClassName(\n suffixInside,\n \"mobius-text-field__suffix-inside\",\n )}\n </div>\n {adornmentWithClassName(\n suffixOutside,\n \"mobius-text-field__suffix-outside\",\n )}\n </div>\n {children && (\n <div className=\"mobius-text-field__children\">{children}</div>\n )}\n\n <ErrorMessage {...errorMessageProps} errorMessage={errorMessage} />\n </Stack>\n );\n });\n\nTextField.displayName = \"TextField\";\nexport { TextField };\n"],"names":["classNames","forwardRef","useTextField","useValidationClasses","ErrorMessage","Label","adornmentWithClassName","Stack","TextField","props","ref","isDisabled","type","validationState","isInvalid","className","label","errorMessage","children","isRequired","prefixInside","prefixOutside","suffixInside","suffixOutside","otherProps","inputProps","labelProps","errorMessageProps","hidden","validationClasses","textfieldClasses","sharedClasses","labelClasses","containerClasses","inputClasses","inputWrapperClasses","gap","div","input","displayName"],"mappings":"AAAA;;AAEA,OAAOA,gBAAgB,oBAAoB;AAC3C,SAMEC,UAAU,QACL,QAAQ;AACf,SAEEC,YAAY,EACZC,oBAAoB,QACf,cAAc;AAGrB,SAASC,YAAY,QAAQ,kBAAkB;AAC/C,SAASC,KAAK,QAAQ,WAAW;AACjC,SAASC,sBAAsB,QAAQ,2BAA2B;AAClE,SAASC,KAAK,QAAQ,WAAW;AAoCjC,MAAMC,0BACJP,WAAW,CAACQ,OAAuBC;IACjC,MAAM,EACJC,UAAU,EACVC,OAAO,MAAM,EACbC,eAAe,EACfC,SAAS,EACTC,SAAS,EACTC,KAAK,EACLC,YAAY,EACZC,QAAQ,EACRC,UAAU,EACVC,YAAY,EACZC,aAAa,EACbC,YAAY,EACZC,aAAa,EACb,GAAGC,YACJ,GAAGf;IAEJ,MAAM,EAAEgB,UAAU,EAAEC,UAAU,EAAEC,iBAAiB,EAAE,GAAGzB,aAAa;QACjE,GAAGO,KAAK;QACR,qBAAqBQ;IACvB;IAEA,MAAMW,SAAShB,SAAS;IAExB,MAAMiB,oBAAoB1B,qBAAqB;QAC7CU;QACAC;IACF;IAEA,MAAMgB,mBAAmB;QACvB,iBAAiBnB;QACjB,iBAAiB,OAAOQ,eAAe,aAAaA;QACpD,iBAAiB,OAAOA,eAAe,aAAa,CAACA;QACrD,eAAeS;QACf,CAACb,aAAa,GAAG,EAAE;IACrB;IAEA,MAAMgB,gBAAgB/B,WAAW6B,mBAAmBC;IAEpD,MAAME,eAAehC,WACnB;QACE,iBAAiBW;IACnB,GACAkB;IAGF,MAAMI,mBAAmBjC,WACvB,UACA,qBACA+B;IAGF,MAAMG,eAAelC,WACnB,UACA,4BACA+B;IAGF,MAAMI,sBAAsBnC,WAC1B,oCACA+B;IAGF,qBACE,MAACxB;QAAM6B,KAAI;QAAKrB,WAAWkB;;YACxBjB,SAAS,CAACY,wBACT,KAACvB;gBAAO,GAAGqB,UAAU;gBAAEX,WAAWiB;0BAC/BhB;;0BAGL,MAACqB;gBAAItB,WAAU;;oBACZT,uBACCe,eACA;kCAEF,MAACgB;wBAAItB,WAAWoB;;4BACb7B,uBACCc,cACA;0CAEF,KAACkB;gCACE,GAAGd,UAAU;gCACb,GAAGC,UAAU;gCACdf,KAAKA;gCACLE,MAAMA;gCACNG,WAAWmB;;4BAEZ5B,uBACCgB,cACA;;;oBAGHhB,uBACCiB,eACA;;;YAGHL,0BACC,KAACmB;gBAAItB,WAAU;0BAA+BG;;0BAGhD,KAACd;gBAAc,GAAGuB,iBAAiB;gBAAEV,cAAcA;;;;AAGzD;AAEFT,UAAU+B,WAAW,GAAG;AACxB,SAAS/B,SAAS,GAAG"}
1
+ {"version":3,"sources":["../../../../src/components/TextField/TextField.tsx"],"sourcesContent":["\"use client\";\n\nimport classNames from \"classnames/dedupe\";\nimport {\n HTMLInputTypeAttribute,\n ReactElement,\n ReactNode,\n Ref,\n RefAttributes,\n forwardRef,\n} from \"react\";\nimport {\n UseTextFieldProps,\n useTextField,\n useValidationClasses,\n} from \"../../hooks\";\nimport { DOMProps, FocusEvents } from \"../../types\";\nimport { ForwardedRefComponent } from \"../../types/components\";\nimport { ErrorMessage } from \"../ErrorMessage\";\nimport { Label } from \"../Label\";\nimport { Stack } from \"../Stack\";\nimport { adornmentWithClassName } from \"./adornmentWithClassName\";\n\nexport type TextFieldElementType = HTMLInputElement;\nexport interface TextFieldProps\n extends DOMProps,\n FocusEvents,\n UseTextFieldProps,\n RefAttributes<TextFieldElementType> {\n className?: string;\n errorMessage?: string;\n children?: ReactNode;\n label?: string;\n type?: Exclude<\n HTMLInputTypeAttribute,\n | \"button\"\n | \"checkbox\"\n | \"color\"\n | \"date\"\n | \"datetime-local\"\n | \"file\"\n | \"image\"\n | \"month\"\n | \"radio\"\n | \"range\"\n | \"reset\"\n | \"submit\"\n | \"week\"\n >;\n prefixInside?: ReactElement;\n prefixOutside?: ReactElement;\n suffixInside?: ReactElement;\n suffixOutside?: ReactElement;\n}\n\nexport type TextFieldRef = Ref<TextFieldElementType>;\n\nconst TextField: ForwardedRefComponent<TextFieldProps, TextFieldElementType> =\n forwardRef((props: TextFieldProps, ref: TextFieldRef) => {\n const {\n isDisabled,\n isReadOnly,\n type = \"text\",\n validationState,\n isInvalid,\n className,\n label,\n errorMessage,\n children,\n isRequired,\n prefixInside,\n prefixOutside,\n suffixInside,\n suffixOutside,\n ...otherProps\n } = props;\n\n const { inputProps, labelProps, errorMessageProps } = useTextField({\n ...props,\n \"aria-errormessage\": errorMessage,\n });\n\n const hidden = type === \"hidden\";\n\n const validationClasses = useValidationClasses({\n validationState,\n isInvalid,\n });\n\n const textfieldClasses = {\n \"--is-disabled\": isDisabled,\n \"--is-required\": typeof isRequired === \"boolean\" && isRequired,\n \"--is-optional\": typeof isRequired === \"boolean\" && !isRequired,\n \"--is-hidden\": hidden,\n [className || \"\"]: true,\n };\n\n const sharedClasses = classNames(validationClasses, textfieldClasses);\n\n const labelClasses = classNames(\n {\n \"--is-disabled\": isDisabled,\n },\n validationClasses,\n );\n\n const containerClasses = classNames(\n \"mobius\",\n \"mobius-text-field\",\n sharedClasses,\n );\n\n const inputClasses = classNames(\n \"mobius\",\n \"mobius-text-field__input\",\n sharedClasses,\n );\n\n const inputWrapperClasses = classNames(\n \"mobius-text-field__input-wrapper\",\n sharedClasses,\n );\n\n return (\n <Stack gap=\"xs\" className={containerClasses}>\n {label && !hidden && (\n <Label {...labelProps} className={labelClasses}>\n {label}\n </Label>\n )}\n <div className=\"mobius-text-field__inner-container\">\n {adornmentWithClassName(\n prefixOutside,\n \"mobius-text-field__prefix-outside\",\n )}\n <div className={inputWrapperClasses}>\n {adornmentWithClassName(\n prefixInside,\n \"mobius-text-field__prefix-inside\",\n )}\n <input\n {...otherProps}\n {...inputProps}\n ref={ref}\n type={type}\n className={inputClasses}\n readOnly={isReadOnly}\n />\n {adornmentWithClassName(\n suffixInside,\n \"mobius-text-field__suffix-inside\",\n )}\n </div>\n {adornmentWithClassName(\n suffixOutside,\n \"mobius-text-field__suffix-outside\",\n )}\n </div>\n {children && (\n <div className=\"mobius-text-field__children\">{children}</div>\n )}\n\n <ErrorMessage {...errorMessageProps} errorMessage={errorMessage} />\n </Stack>\n );\n });\n\nTextField.displayName = \"TextField\";\nexport { TextField };\n"],"names":["classNames","forwardRef","useTextField","useValidationClasses","ErrorMessage","Label","Stack","adornmentWithClassName","TextField","props","ref","isDisabled","isReadOnly","type","validationState","isInvalid","className","label","errorMessage","children","isRequired","prefixInside","prefixOutside","suffixInside","suffixOutside","otherProps","inputProps","labelProps","errorMessageProps","hidden","validationClasses","textfieldClasses","sharedClasses","labelClasses","containerClasses","inputClasses","inputWrapperClasses","gap","div","input","readOnly","displayName"],"mappings":"AAAA;;AAEA,OAAOA,gBAAgB,oBAAoB;AAC3C,SAMEC,UAAU,QACL,QAAQ;AACf,SAEEC,YAAY,EACZC,oBAAoB,QACf,cAAc;AAGrB,SAASC,YAAY,QAAQ,kBAAkB;AAC/C,SAASC,KAAK,QAAQ,WAAW;AACjC,SAASC,KAAK,QAAQ,WAAW;AACjC,SAASC,sBAAsB,QAAQ,2BAA2B;AAoClE,MAAMC,0BACJP,WAAW,CAACQ,OAAuBC;IACjC,MAAM,EACJC,UAAU,EACVC,UAAU,EACVC,OAAO,MAAM,EACbC,eAAe,EACfC,SAAS,EACTC,SAAS,EACTC,KAAK,EACLC,YAAY,EACZC,QAAQ,EACRC,UAAU,EACVC,YAAY,EACZC,aAAa,EACbC,YAAY,EACZC,aAAa,EACb,GAAGC,YACJ,GAAGhB;IAEJ,MAAM,EAAEiB,UAAU,EAAEC,UAAU,EAAEC,iBAAiB,EAAE,GAAG1B,aAAa;QACjE,GAAGO,KAAK;QACR,qBAAqBS;IACvB;IAEA,MAAMW,SAAShB,SAAS;IAExB,MAAMiB,oBAAoB3B,qBAAqB;QAC7CW;QACAC;IACF;IAEA,MAAMgB,mBAAmB;QACvB,iBAAiBpB;QACjB,iBAAiB,OAAOS,eAAe,aAAaA;QACpD,iBAAiB,OAAOA,eAAe,aAAa,CAACA;QACrD,eAAeS;QACf,CAACb,aAAa,GAAG,EAAE;IACrB;IAEA,MAAMgB,gBAAgBhC,WAAW8B,mBAAmBC;IAEpD,MAAME,eAAejC,WACnB;QACE,iBAAiBW;IACnB,GACAmB;IAGF,MAAMI,mBAAmBlC,WACvB,UACA,qBACAgC;IAGF,MAAMG,eAAenC,WACnB,UACA,4BACAgC;IAGF,MAAMI,sBAAsBpC,WAC1B,oCACAgC;IAGF,qBACE,MAAC1B;QAAM+B,KAAI;QAAKrB,WAAWkB;;YACxBjB,SAAS,CAACY,wBACT,KAACxB;gBAAO,GAAGsB,UAAU;gBAAEX,WAAWiB;0BAC/BhB;;0BAGL,MAACqB;gBAAItB,WAAU;;oBACZT,uBACCe,eACA;kCAEF,MAACgB;wBAAItB,WAAWoB;;4BACb7B,uBACCc,cACA;0CAEF,KAACkB;gCACE,GAAGd,UAAU;gCACb,GAAGC,UAAU;gCACdhB,KAAKA;gCACLG,MAAMA;gCACNG,WAAWmB;gCACXK,UAAU5B;;4BAEXL,uBACCgB,cACA;;;oBAGHhB,uBACCiB,eACA;;;YAGHL,0BACC,KAACmB;gBAAItB,WAAU;0BAA+BG;;0BAGhD,KAACf;gBAAc,GAAGwB,iBAAiB;gBAAEV,cAAcA;;;;AAGzD;AAEFV,UAAUiC,WAAW,GAAG;AACxB,SAASjC,SAAS,GAAG"}
@@ -1,3 +1,3 @@
1
- import type { ComboboxProps, ComboboxElementType } from "./types";
2
1
  import type { ForwardedRefComponent } from "../../types";
2
+ import type { ComboboxElementType, ComboboxProps } from "./types";
3
3
  export declare const Combobox: ForwardedRefComponent<ComboboxProps, ComboboxElementType>;
@@ -3,5 +3,8 @@ import { Combobox } from ".";
3
3
  type StoryType = StoryObj<typeof Combobox>;
4
4
  declare const meta: Meta<typeof Combobox>;
5
5
  export declare const Default: StoryType;
6
- export declare const ObjectOptions: StoryType;
6
+ export declare const OptionsObject: StoryType;
7
+ export declare const GroupedOptions: StoryType;
8
+ export declare const WithIcon: StoryType;
9
+ export declare const StateSelector: StoryType;
7
10
  export default meta;
@@ -0,0 +1,10 @@
1
+ import type { ComboboxOption, ComboboxOptions } from "./types";
2
+ export type ListboxProps = {
3
+ id: string;
4
+ options: ComboboxOptions;
5
+ highlightedIndex: number;
6
+ highlightedGroupIndex: number;
7
+ onOptionSelect: (option: ComboboxOption) => void;
8
+ expanded?: boolean;
9
+ };
10
+ export declare const Listbox: ({ id, options, highlightedIndex, highlightedGroupIndex, onOptionSelect, expanded, }: ListboxProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import type { ComboboxOptionProps } from "./types";
2
+ export declare const Option: ({ option, index, groupIndex, isHighlighted, onOptionSelect, id, }: ComboboxOptionProps) => import("react/jsx-runtime").JSX.Element;
@@ -3,3 +3,8 @@ export declare const FRUITS_OBJECTS: {
3
3
  label: string;
4
4
  value: string;
5
5
  }[];
6
+ export declare const FRUITS_GROUPS: {
7
+ heading: string;
8
+ options: string[];
9
+ }[];
10
+ export declare const US_STATES: string[];
@@ -1,8 +1,10 @@
1
- import type { Ref } from "react";
1
+ import type { ReactElement, Ref } from "react";
2
2
  import type { TextFieldElementType, TextFieldProps } from "../TextField";
3
3
  export type ComboboxProps = TextFieldProps & {
4
4
  /** The list of options to display in the dropdown */
5
- options: ComboboxOption[] | ((filter: string) => ComboboxOption[]) | ((filter: string) => Promise<ComboboxOption[]>);
5
+ options: ComboboxOptions | ((filter: string) => ComboboxOptions) | ((filter: string) => Promise<ComboboxOptions>);
6
+ /** An icon to display in TextField (left side) */
7
+ icon?: ReactElement;
6
8
  /** The default value of the selected option */
7
9
  defaultValue?: string | undefined;
8
10
  /** Callback when the selected option changes */
@@ -12,5 +14,18 @@ export type ComboboxOption = string | {
12
14
  label: string;
13
15
  value: string;
14
16
  };
17
+ export type ComboboxOptionGroup = {
18
+ heading: string;
19
+ options: ComboboxOption[];
20
+ };
21
+ export type ComboboxOptions = ComboboxOption[] | ComboboxOptionGroup[];
15
22
  export type ComboboxElementType = TextFieldElementType;
16
23
  export type ComboboxRef = Ref<ComboboxElementType>;
24
+ export type ComboboxOptionProps = {
25
+ option: ComboboxOption;
26
+ index: number;
27
+ groupIndex?: number;
28
+ isHighlighted: boolean;
29
+ onOptionSelect: (option: ComboboxOption) => void;
30
+ id: string;
31
+ };
@@ -0,0 +1,10 @@
1
+ import type { ComboboxOptions } from "./types";
2
+ export declare function useComboboxHighlight(options: ComboboxOptions): {
3
+ highlightedIndex: number;
4
+ highlightedGroupIndex: number;
5
+ highlightPreviousOption: () => void;
6
+ highlightNextOption: () => void;
7
+ highlightFirstOption: () => void;
8
+ highlightLastOption: () => void;
9
+ clearHighlight: () => void;
10
+ };
@@ -0,0 +1,6 @@
1
+ import type { ComboboxOption, ComboboxOptionGroup, ComboboxOptions } from "./types";
2
+ export declare function isOptionGroup(options: ComboboxOptions): options is ComboboxOptionGroup[];
3
+ export declare const getOptionValue: (option: ComboboxOption | undefined) => string | undefined;
4
+ export declare const getOptionLabel: (option: ComboboxOption | undefined) => string | undefined;
5
+ export declare function filterOptions(options: ComboboxOptions, inputValue: string): ComboboxOptions;
6
+ export declare function clamp(value: number, min: number, max: number): number;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { Ref, RefAttributes } from "react";
2
- import { DOMProps } from "../../types/dom";
3
- import { ForwardedRefComponent } from "../../types/components";
4
2
  import { UseTextFieldProps } from "../../hooks";
3
+ import { ForwardedRefComponent } from "../../types/components";
4
+ import { DOMProps } from "../../types/dom";
5
5
  export type TextAreaElementType = HTMLTextAreaElement;
6
6
  export interface TextAreaProps extends UseTextFieldProps, DOMProps, RefAttributes<TextAreaElementType> {
7
7
  className?: string;
@@ -1,6 +1,6 @@
1
1
  import { Ref, RefAttributes } from "react";
2
- import { DOMProps } from "../../types/dom";
3
2
  import { ForwardedRefComponent } from "../../types/components";
3
+ import { DOMProps } from "../../types/dom";
4
4
  export type TextAreaInputElementType = HTMLTextAreaElement;
5
5
  export interface InteractionStateProps {
6
6
  isSelected?: boolean;
@@ -8,6 +8,8 @@ export interface InteractionStateProps {
8
8
  }
9
9
  export interface TextAreaInputProps extends DOMProps, InteractionStateProps, RefAttributes<TextAreaInputElementType> {
10
10
  className?: string;
11
+ isReadOnly?: boolean;
12
+ isRequired?: boolean;
11
13
  }
12
14
  export type TextAreaInputRef = Ref<TextAreaInputElementType>;
13
15
  declare const TextAreaInput: ForwardedRefComponent<TextAreaInputProps, TextAreaInputElementType>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@simplybusiness/mobius",
3
3
  "license": "UNLICENSED",
4
- "version": "5.5.0",
4
+ "version": "5.6.0",
5
5
  "description": "Core library of Mobius react components",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1,25 +1,51 @@
1
+ :root {
2
+ /* --combobox-max-width: 562px; */
3
+ interpolate-size: allow-keywords;
4
+ --combobox-border-color: #ccc;
5
+ --combobox-group-color: var(--color-text);
6
+ --combobox-group-background-color: #dad6f7;
7
+ --combobox-selected-background-color: var(--color-primary);
8
+ --listbox-height: 200px;
9
+ --listbox-gap: 4px;
10
+ --option-padding: var(--size-xs) var(--size-lg);
11
+ }
12
+
1
13
  .mobius-combobox {
2
14
  position: relative;
15
+ width: 100%;
3
16
  }
4
17
 
5
18
  .mobius-combobox__list {
6
19
  position: absolute;
7
20
  margin: 0;
21
+ margin-top: var(--listbox-gap);
8
22
  padding: 0;
9
23
  top: 100%;
10
24
  left: 0;
11
25
  right: 0;
12
26
  z-index: 1000;
13
27
  background-color: #fff;
14
- border: 1px solid #ccc;
15
- border-top: 0;
28
+ border: 1px solid var(--combobox-border-color);
29
+ border-radius: var(--radius-1);
16
30
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
17
- max-height: 200px;
31
+ max-height: var(--listbox-height);
18
32
  overflow-y: auto;
33
+ height: 0;
34
+ opacity: 0;
35
+ transition:
36
+ box-shadow 0.3s ease,
37
+ height 0.3s ease,
38
+ opacity 0.3s ease;
39
+ transition-behavior: allow-discrete;
40
+
41
+ &.expanded {
42
+ opacity: 1;
43
+ height: auto;
44
+ }
19
45
  }
20
46
 
21
47
  .mobius-combobox__option {
22
- padding: 0.5rem 1rem;
48
+ padding: var(--option-padding);
23
49
  cursor: pointer;
24
50
  }
25
51
 
@@ -28,3 +54,24 @@
28
54
  background-color: var(--color-primary);
29
55
  color: var(--color-neutral-100);
30
56
  }
57
+
58
+ /* Group styles */
59
+ [role="group"] {
60
+ margin: 0;
61
+ padding: 0;
62
+ }
63
+
64
+ [role="group"] > [role="presentation"] {
65
+ display: block;
66
+ margin: 0;
67
+ padding: var(--option-padding);
68
+ font-weight: bold;
69
+ color: var(--combobox-group-color);
70
+ background-color: var(--combobox-group-background-color);
71
+ }
72
+
73
+ [role="option"] {
74
+ position: relative;
75
+ display: block;
76
+ cursor: pointer;
77
+ }
@@ -0,0 +1,147 @@
1
+ import { ArgTypes, Meta, Story } from "@storybook/blocks";
2
+ import { Combobox } from "./Combobox";
3
+ import * as ComboboxStories from "./Combobox.stories";
4
+
5
+ <Meta of={ComboboxStories} />
6
+
7
+ # Combobox
8
+
9
+ A `Combobox` is an enhanced `Select` input, which allows users to filter results as they type. It supports all of the common input props, with the main difference being that the `options` prop is supplied as data (or a function), rather than nested `children`.
10
+
11
+ `Combobox` is fully accessible, and supports keyboard navigation and screen readers.
12
+
13
+ ### Static / synchronous use
14
+
15
+ When you know all of the options data upfront, you can provide it as either:
16
+
17
+ - A flat array of strings
18
+ - An array of objects with type `Option` (see below)
19
+ - An array of groups of Options, with headings
20
+
21
+ The default `onChange` behaviour, while typing, is to filter the options list based on input value.
22
+
23
+ The `onSelected` prop is called when an option is selected, passing `Option` as its parameter.
24
+
25
+ ```ts
26
+ type Option = {
27
+ id: string;
28
+ label: string;
29
+ value?: string | number | boolean;
30
+ };
31
+
32
+ type OptionGroup = {
33
+ category: string;
34
+ options: Option[];
35
+ };
36
+
37
+ type Options = (string | Option | OptionGroup)[];
38
+ ```
39
+
40
+ ### Dynamic / asynchronous use
41
+
42
+ `Combobox` provides a useful primitive with which to build more dynamic selector components, such as `Trade Selector` and `Adddress Lookup`.
43
+
44
+ For these kinds of component, options (list) data is not known and must be fetched, typically from an API, as the user types. In this case, the `options` prop can be overloaded with a function that returns a promise.
45
+
46
+ ```ts
47
+ type OptionsFetcher = (inputValue: string) => Promise<Options>;
48
+ ```
49
+
50
+ In asynchronous mode, you are strongly encouraged to provide a `delay` value in milliseconds, to debounce API requests.
51
+
52
+ ### Overriding selection behaviour
53
+
54
+ While is it often sufficient to fetch data, present it in the list and use an `onSelected` handler to do something with the chosen option, there are times when you may want to override the default behaviour. E.g. Address Lookup can return results that are not full addresses, but are groups, which need further drill down.
55
+
56
+ In these cases, you can enhance the `onSelected` handler with a `callback` function. If a selected option has a callback, then it will be invoked, passing the option as params.
57
+
58
+ When a selected option does not have a callback, the standard handler is invoked.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ yarn add @simplybusiness/mobius
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ ```js
69
+ import { ComboboxStories } from "@simplybusiness/mobius";
70
+ ```
71
+
72
+ ## Examples
73
+
74
+ ### Default
75
+
76
+ <Story of={ComboboxStories.Default} />
77
+
78
+ {/* prettier-ignore */}
79
+ ```jsx
80
+ import { Combobox } from "@simplybusiness/mobius";
81
+
82
+ <Combobox options={["Apple", "Apricot", "etc."]} />
83
+ ```
84
+
85
+ ### With an options object
86
+
87
+ <Story of={ComboboxStories.OptionsObject} />
88
+
89
+ {/* prettier-ignore */}
90
+ ```jsx
91
+ import { Combobox } from "@simplybusiness/mobius";
92
+
93
+ <Combobox options={[
94
+ { id: "apple", label: "Apple" },
95
+ { id: "apricot", label: "Apricot" },
96
+ { id: "etc", label: "etc." }
97
+ ]} />
98
+ ```
99
+
100
+ ### With Icon
101
+
102
+ <Story of={ComboboxStories.WithIcon} />
103
+
104
+ {/* prettier-ignore */}
105
+ ```jsx
106
+ import { Combobox, Icon } from "@simplybusiness/mobius";
107
+ import { search } from "@simplybusiness/icons";
108
+
109
+ <Combobox
110
+ options={[...OPTIONS]}
111
+ icon={<Icon icon={search}/>}
112
+ />
113
+ ```
114
+
115
+ ### State Selector
116
+
117
+ This is a simple example of using `Combobox` to create a US State Selector, which uses a flat list of state strings as the options array.
118
+
119
+ <Story of={ComboboxStories.StateSelector} />
120
+
121
+ {/* prettier-ignore */}
122
+ ```jsx
123
+ import { Combobox, Icon } from "@simplybusiness/mobius";
124
+ import { search } from "@simplybusiness/icons";
125
+
126
+ <Combobox
127
+ options={[...US_STATES]}
128
+ placeholder="Choose your state"
129
+ icon={<Icon icon={search}/>}
130
+ />
131
+ ```
132
+
133
+ ## Accessibility
134
+
135
+ TODO: Add accessibility information
136
+
137
+ ## Props
138
+
139
+ <ArgTypes of={Combobox} />
140
+
141
+ ## Component HTML Structure and Class names
142
+
143
+ The following HTML is rendered for a Combobox:
144
+
145
+ ```html
146
+ <label class="mobius-label" htmlFor="{input-id}">{Label text}</label>
147
+ ```
@@ -1,26 +1,71 @@
1
+ import { search } from "@simplybusiness/icons";
1
2
  import type { Meta, StoryObj } from "@storybook/react";
2
3
  import { Combobox, type ComboboxProps } from ".";
3
- import { FRUITS, FRUITS_OBJECTS } from "./fixtures";
4
+ import { excludeControls } from "../../utils";
5
+ import { StoryContainer } from "../../utils/StoryContainer";
6
+ import { Icon } from "../Icon";
7
+ import { FRUITS, FRUITS_OBJECTS, FRUITS_GROUPS, US_STATES } from "./fixtures";
4
8
 
5
9
  type StoryType = StoryObj<typeof Combobox>;
6
10
 
7
11
  const meta: Meta<typeof Combobox> = {
8
12
  title: "Forms/Combobox",
9
13
  component: Combobox,
14
+ argTypes: {
15
+ type: {
16
+ options: [],
17
+ },
18
+ ...excludeControls("icon"), // options
19
+ },
20
+ decorators: [
21
+ Story => (
22
+ <StoryContainer>
23
+ <Story />
24
+ </StoryContainer>
25
+ ),
26
+ ],
10
27
  };
11
28
 
12
29
  export const Default: StoryType = {
13
30
  render: (args: ComboboxProps) => <Combobox {...args} />,
14
31
  args: {
32
+ label: "Fruits",
15
33
  options: FRUITS,
16
34
  },
17
35
  };
18
36
 
19
- export const ObjectOptions: StoryType = {
37
+ export const OptionsObject: StoryType = {
20
38
  render: (args: ComboboxProps) => <Combobox {...args} />,
21
39
  args: {
40
+ label: "Fruits",
22
41
  options: FRUITS_OBJECTS,
23
42
  },
24
43
  };
25
44
 
45
+ export const GroupedOptions: StoryType = {
46
+ render: (args: ComboboxProps) => <Combobox {...args} />,
47
+ args: {
48
+ options: FRUITS_GROUPS,
49
+ },
50
+ };
51
+
52
+ export const WithIcon: StoryType = {
53
+ render: (args: ComboboxProps) => <Combobox {...args} />,
54
+ args: {
55
+ label: "Fruits",
56
+ icon: <Icon icon={search} />,
57
+ options: FRUITS_OBJECTS,
58
+ },
59
+ };
60
+
61
+ export const StateSelector: StoryType = {
62
+ render: (args: ComboboxProps) => <Combobox {...args} />,
63
+ args: {
64
+ label: "US State Selector",
65
+ placeholder: "Choose your state",
66
+ icon: <Icon icon={search} />,
67
+ options: US_STATES,
68
+ },
69
+ };
70
+
26
71
  export default meta;