Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Description } from "./Description";
import { AddressCompleteOptions } from "./AddressCompleteOptions";
import { FormattedDateOptions } from "./FormattedDateOptions";
import { RequiredOptions } from "./RequiredOptions";
import { StrictValue } from "./StrictValue";
import { SortOptions } from "./SortOptions";
import { DynamicRowOptions } from "./DynamicRowOptions";
import { TextFieldOptions } from "./TextFieldOptions";
Expand Down Expand Up @@ -123,6 +124,8 @@ export const MoreDialog = () => {

<RequiredOptions item={item} setItem={setItem} />

<StrictValue item={item} setItem={setItem} />

<DynamicRowOptions item={item} setItem={setItem} />

<TextFieldOptions item={item} setItem={setItem} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useTranslation } from "@i18n/client";
import { Checkbox } from "@formBuilder/components/shared/MultipleChoice";
import { FormElement } from "@lib/types";

export const StrictValue = ({
item,
setItem,
}: {
item: FormElement;
setItem: (item: FormElement) => void;
}) => {
const { t } = useTranslation("form-builder");

// ⚠️ Early return if not combobox
if (item.type !== "combobox") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could use the enum FormElementTypes.combobox

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return null;
}

const checked = item.properties.strictValue;

return (
<section className="mb-4">
<div className="mb-2">
<h3>{t("strictValue.title")}</h3>
</div>
<div>
<Checkbox
data-testid="strictValue"
id={`required-${item.id}-id-modal`}
value={`required-${item.id}-value-modal-` + checked}
key={`required-${item.id}-modal-` + checked}
defaultChecked={checked}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({
...item,
properties: {
...item.properties,
strictValue: e.target.checked,
},
});
}}
label={t("strictValue.label")}
></Checkbox>
</div>
</section>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,36 @@ export const SelectedElement = ({
if (elIndex !== -1) {
element = (
<>
<ShortAnswer>{t("addElementDialog.combobox.title")}</ShortAnswer>
<ShortAnswer>
<>
{t("addElementDialog.combobox.title")}

{item.properties.strictValue && (
<div className="ml-2 inline-block text-sm text-slate-600">
- {t("strictValue.description")}
</div>
)}
</>
</ShortAnswer>

{!item.properties.managedChoices && <SubOptions item={item} />}
</>
);
} else {
element = (
<>
<ShortAnswer>{t("addElementDialog.combobox.title")}</ShortAnswer>
<ShortAnswer>
<>
{t("addElementDialog.combobox.title")}

{item.properties.strictValue && (
<div className="ml-2 inline-block text-sm text-slate-600">
- {t("strictValue.description")}
</div>
)}
</>
</ShortAnswer>

{!item.properties.managedChoices && <Options item={item} formId={formId} />}
</>
);
Expand Down
21 changes: 20 additions & 1 deletion components/clientComponents/forms/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { cn } from "@lib/utils";

interface ComboboxProps extends InputFieldProps {
choices?: string[];
strictValue?: boolean;
}

export const Combobox = (props: ComboboxProps): React.ReactElement => {
Expand All @@ -18,6 +19,21 @@ export const Combobox = (props: ComboboxProps): React.ReactElement => {
const { setValue } = helpers;

const [items, setItems] = React.useState(choices);

// Clear the field on blur if value does not match a choice (full match only)
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
const hasFullMatch = choices.some(
(choice) => choice.toLowerCase() === inputValue.toLowerCase()
);
if (inputValue && !hasFullMatch) {
setValue("");
}
if (typeof field.onBlur === "function") {
field.onBlur(e);
}
};

const { isOpen, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectedItem } =
useCombobox({
onInputValueChange({ inputValue }) {
Expand All @@ -41,7 +57,9 @@ export const Combobox = (props: ComboboxProps): React.ReactElement => {
{meta.error && <ErrorMessage>{meta.error}</ErrorMessage>}

<input
{...getInputProps()}
{...getInputProps(
props.strictValue ? { onBlur: handleBlur, value: field.value || "" } : {}
)}
aria-describedby={ariaDescribedBy}
id={id}
required={required}
Expand All @@ -54,6 +72,7 @@ export const Combobox = (props: ComboboxProps): React.ReactElement => {
className={`${!(isOpen && items.length >= 1 && items[0] !== "") ? "hidden" : ""}`}
{...getMenuProps()}
data-testid="combobox-listbox"
data-is-strict={props.strictValue}
hidden={!isOpen}
>
{isOpen &&
Expand Down
7 changes: 6 additions & 1 deletion i18n/translations/en/form-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -1366,5 +1366,10 @@
"formDescription": "Form description"
},
"en": "English",
"fr": "Français"
"fr": "Français",
"strictValue": {
"title": "Ensure choice value",
"label": "Strict value",
"description": "strict value"
}
}
7 changes: 6 additions & 1 deletion i18n/translations/fr/form-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -1366,5 +1366,10 @@
"formDescription": "Description du formulaire"
},
"en": "English",
"fr": "Français"
"fr": "Français",
"strictValue": {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anikbrazeau couple of missing strings here if you get a chance

"title": "Ensure choice value",
"label": "Strict value",
"description": "strict value"
}
}
1 change: 1 addition & 0 deletions lib/formBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ function _buildForm(element: FormElement, lang: string): ReactElement {
{labelComponent}
{description && <Description id={`${id}`}>{description}</Description>}
<Combobox
strictValue={element.properties.strictValue}
id={`${id}`}
name={`${id}`}
ariaDescribedBy={description ? `desc-${id}` : undefined}
Expand Down
4 changes: 4 additions & 0 deletions lib/middleware/schemas/templates.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@
"type": "integer",
"minimum": 1
},
"strictValue": {
"description": "If set to true, the element will only accept values that are in the choices list.",
"type": "boolean"
},
"validation": {
"type": "object",
"properties": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"copy-to-clipboard": "3.3.3",
"csv-writer": "^1.6.0",
"d3-hierarchy": "^3.1.2",
"downshift": "^8.3.1",
"downshift": "^9.0.9",
"file-type": "^19.0.0",
"formik": "2.4.6",
"htmlparser2": "8.0.2",
Expand Down
7 changes: 7 additions & 0 deletions packages/types/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [1.0.9] - 2025-05-22

### Changed

- Add strictValue to ElementProperties for use with ComboBox / Searchable list

## [1.0.8] - 2025-05-20

### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gcforms/types",
"version": "1.0.8",
"version": "1.0.9",
"author": "Canadian Digital Service",
"license": "MIT",
"publishConfig": {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/form-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export interface ElementProperties {
addressComponents?: AddressComponents | undefined;
dynamicRow?: dynamicRowType;
sortOrder?: SortValue;
strictValue?: boolean;
[key: string]:
| string
| string[]
Expand Down
29 changes: 18 additions & 11 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2145,7 +2145,7 @@ __metadata:
languageName: node
linkType: hard

"@babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.9, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.22.15, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
"@babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.9, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
version: 7.27.1
resolution: "@babel/runtime@npm:7.27.1"
checksum: 10c0/530a7332f86ac5a7442250456823a930906911d895c0b743bf1852efc88a20a016ed4cd26d442d0ca40ae6d5448111e02a08dd638a4f1064b47d080e2875dc05
Expand Down Expand Up @@ -10239,7 +10239,7 @@ __metadata:
languageName: node
linkType: hard

"compute-scroll-into-view@npm:^3.0.3":
"compute-scroll-into-view@npm:^3.1.0":
version: 3.1.1
resolution: "compute-scroll-into-view@npm:3.1.1"
checksum: 10c0/59761ed62304a9599b52ad75d0d6fbf0669ee2ab7dd472fdb0ad9da36628414c014dea7b5810046560180ad30ffec52a953d19297f66a1d4f3aa0999b9d2521d
Expand Down Expand Up @@ -11163,18 +11163,18 @@ __metadata:
languageName: node
linkType: hard

"downshift@npm:^8.3.1":
version: 8.5.0
resolution: "downshift@npm:8.5.0"
"downshift@npm:^9.0.9":
version: 9.0.9
resolution: "downshift@npm:9.0.9"
dependencies:
"@babel/runtime": "npm:^7.22.15"
compute-scroll-into-view: "npm:^3.0.3"
"@babel/runtime": "npm:^7.24.5"
compute-scroll-into-view: "npm:^3.1.0"
prop-types: "npm:^15.8.1"
react-is: "npm:^18.2.0"
react-is: "npm:18.2.0"
tslib: "npm:^2.6.2"
peerDependencies:
react: ">=16.12.0"
checksum: 10c0/f572affe0884d6fb6523352fcb230db42875b557d972a74d15ef8a1b72702733bb5f329911d2810f9880e4b8ba1c6b6e10eb4bf6d8160ab4975c1e65c42c6da2
checksum: 10c0/9102d498286e7ec41d0036aa753c3865096d307e91b56e5eb466e0f3c54b13cfcb900f91bae50e079ab0888eb00fd46dcd83b78ec126a8dd3ea86d94a26685cc
languageName: node
linkType: hard

Expand Down Expand Up @@ -12724,7 +12724,7 @@ __metadata:
cypress-terminal-report: "npm:^7.1.0"
cypress-wait-until: "npm:^3.0.2"
d3-hierarchy: "npm:^3.1.2"
downshift: "npm:^8.3.1"
downshift: "npm:^9.0.9"
eslint: "npm:^9.17.0"
eslint-config-next: "npm:15.3.2"
eslint-config-prettier: "npm:^9.1.0"
Expand Down Expand Up @@ -17391,6 +17391,13 @@ __metadata:
languageName: node
linkType: hard

"react-is@npm:18.2.0":
version: 18.2.0
resolution: "react-is@npm:18.2.0"
checksum: 10c0/6eb5e4b28028c23e2bfcf73371e72cd4162e4ac7ab445ddae2afe24e347a37d6dc22fae6e1748632cd43c6d4f9b8f86dcf26bf9275e1874f436d129952528ae0
languageName: node
linkType: hard

"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
Expand All @@ -17405,7 +17412,7 @@ __metadata:
languageName: node
linkType: hard

"react-is@npm:^18.0.0, react-is@npm:^18.2.0":
"react-is@npm:^18.0.0":
version: 18.3.1
resolution: "react-is@npm:18.3.1"
checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072
Expand Down
Loading