\ No newline at end of file
diff --git a/packages/ember-toucan-core/src/-private/components/error.ts b/packages/ember-toucan-core/src/-private/components/error.ts
deleted file mode 100644
index 75a618f7..00000000
--- a/packages/ember-toucan-core/src/-private/components/error.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import Component from '@glimmer/component';
-import { assert } from '@ember/debug';
-
-import type { ErrorMessage } from '../../-private/types';
-
-export interface ToucanFormErrorComponentSignature {
- Element: HTMLDivElement;
- Args: {
- /**
- * Provide a string or array of strings to this argument to render styled errors.
- */
- error?: ErrorMessage;
- };
- Blocks: {};
-}
-
-export default class ToucanFormErrorComponent extends Component {
- /**
- * Used to help us determine if we should render a single error
- * or render a ul list of errors.
- */
- get hasMoreThanOneError() {
- return Array.isArray(this.args.error) && this.args.error?.length > 1;
- }
-
- get errors(): Array {
- let { error } = this.args;
-
- assert(
- '"@error" must be either a string or an array of strings',
- typeof error === 'string' || Array.isArray(error)
- );
-
- if (typeof error === 'string') {
- return [error];
- }
-
- return error;
- }
-}
diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/autocomplete/option.ts b/packages/ember-toucan-core/src/-private/components/form/controls/autocomplete/option.gts
similarity index 55%
rename from packages/ember-toucan-core/src/-private/components/form/controls/autocomplete/option.ts
rename to packages/ember-toucan-core/src/-private/components/form/controls/autocomplete/option.gts
index 8149f696..f4024bb8 100644
--- a/packages/ember-toucan-core/src/-private/components/form/controls/autocomplete/option.ts
+++ b/packages/ember-toucan-core/src/-private/components/form/controls/autocomplete/option.gts
@@ -1,9 +1,10 @@
import Component from '@glimmer/component';
+import { on } from '@ember/modifier';
import { action } from '@ember/object';
import Check from '../../../../../-private/icons/check';
-interface ToucanFormAutocompleteOptionControlComponentSignature {
+interface ToucanCoreAutocompleteOptionComponentSignature {
Args: {
/**
* When true, means that the option is currently hovered over with a mouse
@@ -62,7 +63,7 @@ const className = 'toucan-form-select-option-control';
export const selector = `.${className}`;
-export default class ToucanFormAutocompleteOptionControlComponent extends Component {
+export default class ToucanCoreAutocompleteOptionComponent extends Component {
className = className;
Check = Check;
@@ -91,4 +92,43 @@ export default class ToucanFormAutocompleteOptionControlComponent extends Compon
// Both "click" and "mousedown" steal focus, which we want to remain on the input.
event.preventDefault();
}
+
+
+ {{! template-lint-disable require-presentational-children }}
+
+
+ {{yield}}
+
+ {{! TODO: Do we make \`@value\` required? }}
+ {{!
+ We'll need Form::Controls::Select to work with real forms and the native \`FormData\` API.
+ That means it'll need to be backed by a real INPUT. Screenreaders already have all the information
+ they need, which means the INPUT is superfluous and is thus noise?
+ }}
+ {{! template-lint-disable no-nested-interactive }}
+ {{! template-lint-disable require-input-label }}
+
+
-
- {{yield}}
-
- {{! TODO: Do we make `@value` required? }}
- {{!
- We'll need Form::Controls::Select to work with real forms and the native `FormData` API.
- That means it'll need to be backed by a real INPUT. Screenreaders already have all the information
- they need, which means the INPUT is superfluous and is thus noise?
- }}
- {{! template-lint-disable no-nested-interactive }}
- {{! template-lint-disable require-input-label }}
-
-
\ No newline at end of file
diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/multiselect/chip.gts b/packages/ember-toucan-core/src/-private/components/form/controls/multiselect/chip.gts
new file mode 100644
index 00000000..9fd412c4
--- /dev/null
+++ b/packages/ember-toucan-core/src/-private/components/form/controls/multiselect/chip.gts
@@ -0,0 +1,28 @@
+import type { TemplateOnlyComponent } from '@ember/component/template-only';
+
+export interface ToucanCoreMultiselectChipComponentSignature {
+ Args: {
+ /**
+ * The index of the chip based on the selected options.
+ */
+ index?: number;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLDivElement;
+}
+
+const ToucanCoreMultiselectChipComponent: TemplateOnlyComponent =
+
+
+ {{/if}}
+
+ {{/if}}
+
}
diff --git a/packages/ember-toucan-core/src/components/form/controls/multiselect.hbs b/packages/ember-toucan-core/src/components/form/controls/multiselect.hbs
deleted file mode 100644
index a0155358..00000000
--- a/packages/ember-toucan-core/src/components/form/controls/multiselect.hbs
+++ /dev/null
@@ -1,164 +0,0 @@
-{{#if
- (this.assertRequiredBlocksExist (hash chipBlockExists=(has-block "chip")))
-}}
-
- {{! Disabling this rule as the user interacts with the input directly. The click on the div is simply for convenience. }}
- {{! template-lint-disable no-invalid-interactive }}
-
\ No newline at end of file
diff --git a/packages/ember-toucan-core/src/components/form/fields/file-input.gts b/packages/ember-toucan-core/src/components/form/fields/file-input.gts
new file mode 100644
index 00000000..1cdb3b41
--- /dev/null
+++ b/packages/ember-toucan-core/src/components/form/fields/file-input.gts
@@ -0,0 +1,272 @@
+import Component from '@glimmer/component';
+import { assert } from '@ember/debug';
+import { fn, hash } from '@ember/helper';
+import { action } from '@ember/object';
+
+import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists';
+import LockIcon from '../../../-private/icons/lock';
+import Button from '../../button';
+import FileInput from '../controls/file-input';
+import Field from '../field';
+import FileList from '../file-input/list';
+
+import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists';
+import type { ErrorMessage } from '../../../-private/types';
+import type {
+ FileEvent as FileInputFileEvent,
+ ToucanFormControlsFileInputComponentSignature,
+} from '../controls/file-input';
+
+export type FileEvent = FileInputFileEvent;
+
+export interface ToucanFormFileInputFieldComponentSignature {
+ Element: HTMLInputElement;
+ Args: {
+ /**
+ * A comma separated list of file types
+ * @example: `@accept="video/*"`
+ * @example: `@accept="image/png, image/jpeg"`
+ */
+ accept?: string;
+
+ /**
+ * The delete button a11y text.
+ */
+ deleteLabel: string;
+
+ /**
+ * The error message for the file input field. Linked to the input with aria-describedby.
+ */
+ error?: ErrorMessage;
+
+ /**
+ * This array is created automatically when a user uploads files.
+ * Note that this is not a FileList object, but an array of File objects. This is for convenience as FileList does not have common array methods like filter.
+ */
+ files?: File[];
+
+ /**
+ * Render a hint message to help describe the control. Linked to the input with aria-describedby.
+ */
+ hint?: string;
+
+ /**
+ * Renders inside the link tag.
+ */
+ label?: string;
+
+ /**
+ * Used to replace the "Select Files" text, and used for internationalization purposes.
+ */
+ trigger: string;
+
+ /**
+ * Sets the disabled attribute on the fieldset.
+ */
+ isDisabled?: boolean;
+
+ /**
+ * Sets the readonly attribute of the checkbox.
+ */
+ isReadOnly?: boolean;
+
+ /**
+ * Sets the multiple attribute on the file input.
+ * If not set, defaults no multiple attribute. In this case (single file upload) a file upload will REPLACE any existing file that is in the files array.
+ * If set as multiple file upload, new files are adding to the existing files array.
+ */
+ multiple?: boolean;
+
+ /**
+ * A callback to be notified when files change.
+ */
+ onChange?: ToucanFormControlsFileInputComponentSignature['Args']['onChange'];
+
+ /**
+ * Used for an alternate named test rootTestSelector.
+ */
+ rootTestSelector?: string;
+ };
+ Blocks: {
+ label: [];
+ hint: [];
+ };
+}
+
+export default class ToucanFormFileInputFieldComponent extends Component {
+ LockIcon = LockIcon;
+
+ assertBlockOrArgumentExists = ({
+ blockExists,
+ argName,
+ arg,
+ isRequired,
+ }: AssertBlockOrArg) =>
+ assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired });
+
+ constructor(
+ owner: unknown,
+ args: ToucanFormFileInputFieldComponentSignature['Args'],
+ ) {
+ assert(
+ 'A "@deleteLabel" argument is required for Form::FileInput::Field. This provides an accessible label for the delete button.',
+ args.deleteLabel !== undefined,
+ );
+
+ assert(
+ 'A "@trigger" argument is required for FileInputField, this prompts the user to select files',
+ args.trigger !== undefined,
+ );
+
+ super(owner, args);
+ }
+
+ get hasError() {
+ return Boolean(this.args?.error);
+ }
+
+ get isReadOnlyOrDisabled() {
+ return this.args?.isDisabled || this.args?.isReadOnly;
+ }
+
+ @action
+ handleChange(field: { id: string }) {
+ if (this.args.isReadOnly) {
+ return;
+ }
+
+ const input = document.getElementById(`${field.id}`);
+
+ // https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method
+ input?.click();
+ }
+
+ @action
+ handleDelete(file: File, event: Event | InputEvent) {
+ if (!this.args.files) {
+ // unlikely to happen since having a list of files is
+ // associated with a visible delete button, but to satisfy typescript
+ return;
+ }
+
+ const files = [...this.args.files].filter(
+ (currentFile) => currentFile !== file,
+ );
+
+ if (this.args.onChange) {
+ this.args.onChange(files, event);
+ }
+ }
+
+
+