diff --git a/.changeset/good-rivers-train.md b/.changeset/good-rivers-train.md new file mode 100644 index 00000000..c2a8c9b4 --- /dev/null +++ b/.changeset/good-rivers-train.md @@ -0,0 +1,6 @@ +--- +'@crowdstrike/ember-toucan-core': patch +'@crowdstrike/ember-toucan-form': patch +--- + +(internal) Updated both packages to use the ` } diff --git a/packages/ember-toucan-core/src/components/form/controls/input.hbs b/packages/ember-toucan-core/src/components/form/controls/input.hbs deleted file mode 100644 index 98c51fd2..00000000 --- a/packages/ember-toucan-core/src/components/form/controls/input.hbs +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/controls/multiselect.ts b/packages/ember-toucan-core/src/components/form/controls/multiselect.gts similarity index 76% rename from packages/ember-toucan-core/src/components/form/controls/multiselect.ts rename to packages/ember-toucan-core/src/components/form/controls/multiselect.gts index 38874f41..9040c57c 100644 --- a/packages/ember-toucan-core/src/components/form/controls/multiselect.ts +++ b/packages/ember-toucan-core/src/components/form/controls/multiselect.gts @@ -1,12 +1,15 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; +import { fn, hash } from '@ember/helper'; +import { on } from '@ember/modifier'; import { action } from '@ember/object'; import { guidFor } from '@ember/object/internals'; import { next } from '@ember/runloop'; import { isEqual as emberIsEqual } from '@ember/utils'; import { offset, size } from '@floating-ui/dom'; +import { Velcro } from 'ember-velcro'; import ChipComponent from '../../../-private/components/form/controls/multiselect/chip'; import OptionComponent, { @@ -110,7 +113,7 @@ export interface ToucanFormMultiselectControlComponentSignature { typeof RemoveComponent, 'isVisible' | 'onClick' | 'onMouseDown' >; - } + }, ]; default: [ { @@ -132,7 +135,7 @@ export interface ToucanFormMultiselectControlComponentSignature { | 'onMouseover' | 'popoverId' >; - } + }, ]; }; Element: HTMLInputElement; @@ -307,7 +310,7 @@ export default class ToucanFormMultiselectControlComponent extends Component + {{#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 }} +
+
+ {{#each this.selected as |option index|}} + {{yield + (hash + index=index + option=option + Chip=(component this.ChipComponent index=index) + Remove=(component + this.RemoveComponent + onClick=(fn this.removeSelection index) + onMouseDown=this.handleRemoveMouseDown + isVisible=this.isEnabled + ) + ) + to="chip" + }} + {{/each}} + + {{! template-lint-disable no-redundant-role }} + +
+ + +
+ + {{#if this.isPopoverOpen}} + + {{/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 }} -
-
- {{#each this.selected as |option index|}} - {{yield - (hash - index=index - option=option - Chip=(component - (ensure-safe-component this.ChipComponent) index=index - ) - Remove=(component - (ensure-safe-component this.RemoveComponent) - onClick=(fn this.removeSelection index) - onMouseDown=this.handleRemoveMouseDown - isVisible=this.isEnabled - ) - ) - to="chip" - }} - {{/each}} - - {{! template-lint-disable no-redundant-role }} - -
- - -
- - {{#if this.isPopoverOpen}} - - {{/if}} -
-{{/if}} \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/controls/radio.ts b/packages/ember-toucan-core/src/components/form/controls/radio.gts similarity index 68% rename from packages/ember-toucan-core/src/components/form/controls/radio.ts rename to packages/ember-toucan-core/src/components/form/controls/radio.gts index 66571a9d..73819294 100644 --- a/packages/ember-toucan-core/src/components/form/controls/radio.ts +++ b/packages/ember-toucan-core/src/components/form/controls/radio.gts @@ -1,5 +1,6 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; +import { on } from '@ember/modifier'; import { action } from '@ember/object'; import type { OnChangeCallback } from '../../../-private/types'; @@ -37,7 +38,7 @@ export interface ToucanFormRadioControlComponentSignature { export default class ToucanFormRadioControlComponent extends Component { constructor( owner: unknown, - args: ToucanFormRadioControlComponentSignature['Args'] + args: ToucanFormRadioControlComponentSignature['Args'], ) { assert('A "@value" argument is required', args.value); super(owner, args); @@ -75,4 +76,28 @@ export default class ToucanFormRadioControlComponent extends Component +
+ + + {{#if @isChecked}} +
+ {{/if}} +
+ } diff --git a/packages/ember-toucan-core/src/components/form/controls/radio.hbs b/packages/ember-toucan-core/src/components/form/controls/radio.hbs deleted file mode 100644 index e4800631..00000000 --- a/packages/ember-toucan-core/src/components/form/controls/radio.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
- - - {{#if @isChecked}} -
- {{/if}} -
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/controls/textarea.ts b/packages/ember-toucan-core/src/components/form/controls/textarea.gts similarity index 81% rename from packages/ember-toucan-core/src/components/form/controls/textarea.ts rename to packages/ember-toucan-core/src/components/form/controls/textarea.gts index 5b8e9386..d3e25569 100644 --- a/packages/ember-toucan-core/src/components/form/controls/textarea.ts +++ b/packages/ember-toucan-core/src/components/form/controls/textarea.gts @@ -1,5 +1,6 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; +import { on } from '@ember/modifier'; import { action } from '@ember/object'; import type { OnChangeCallback } from '../../../-private/types'; @@ -57,9 +58,20 @@ export default class ToucanFormTextareaControlComponent extends Component + + } diff --git a/packages/ember-toucan-core/src/components/form/controls/textarea.hbs b/packages/ember-toucan-core/src/components/form/controls/textarea.hbs deleted file mode 100644 index 546adfdb..00000000 --- a/packages/ember-toucan-core/src/components/form/controls/textarea.hbs +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/field.ts b/packages/ember-toucan-core/src/components/form/field.gts similarity index 83% rename from packages/ember-toucan-core/src/components/form/field.ts rename to packages/ember-toucan-core/src/components/form/field.gts index a6db5ae7..4c02429c 100644 --- a/packages/ember-toucan-core/src/components/form/field.ts +++ b/packages/ember-toucan-core/src/components/form/field.gts @@ -1,9 +1,11 @@ import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; import Control from '../../-private/components/control'; import Error from '../../-private/components/error'; import Hint from '../../-private/components/hint'; import Label from '../../-private/components/label'; +import { uniqueId } from '../../-private/utils'; interface ToucanFormFieldComponentSignature { Element: null; @@ -58,7 +60,7 @@ interface ToucanFormFieldComponentSignature { * Renders a Toucan-styled error block. */ Error: typeof Error; - } + }, ]; }; } @@ -68,4 +70,20 @@ export default class ToucanFormFieldComponent extends Component + {{#let (uniqueId) (uniqueId) (uniqueId) as |id hintId errorId|}} + {{yield + (hash + Label=this.Label + Hint=this.Hint + Control=this.Control + Error=this.Error + id=id + hintId=hintId + errorId=errorId + ) + }} + {{/let}} + } diff --git a/packages/ember-toucan-core/src/components/form/field.hbs b/packages/ember-toucan-core/src/components/form/field.hbs deleted file mode 100644 index 198df2e0..00000000 --- a/packages/ember-toucan-core/src/components/form/field.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#let (unique-id) (unique-id) (unique-id) as |uniqueId hintId errorId|}} - {{yield - (hash - Label=this.Label - Hint=this.Hint - Control=this.Control - Error=this.Error - id=uniqueId - hintId=hintId - errorId=errorId - ) - }} -{{/let}} \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/autocomplete.ts b/packages/ember-toucan-core/src/components/form/fields/autocomplete.gts similarity index 56% rename from packages/ember-toucan-core/src/components/form/fields/autocomplete.ts rename to packages/ember-toucan-core/src/components/form/fields/autocomplete.gts index 447e6b48..8c7c70dc 100644 --- a/packages/ember-toucan-core/src/components/form/fields/autocomplete.ts +++ b/packages/ember-toucan-core/src/components/form/fields/autocomplete.gts @@ -1,7 +1,10 @@ import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; import LockIcon from '../../../-private/icons/lock'; +import Autocomplete from '../controls/autocomplete'; +import Field from '../field'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ErrorMessage } from '../../../-private/types'; @@ -44,7 +47,7 @@ export interface ToucanFormAutocompleteFieldComponentSignature { /** * A string to display when there are no results after filtering. */ - noResultsText: string; + noResultsText: string; /** * The function called when a new selection is made. @@ -101,4 +104,82 @@ export default class ToucanFormAutocompleteFieldComponent extends Component +
+ + {{#if + (this.assertBlockOrArgumentExists + (hash + blockExists=(has-block "label") + argName="label" + arg=@label + isRequired=true + ) + ) + }} + + {{#if (has-block "label")}} + {{yield to="label"}} + {{else}} + {{@label}} + {{/if}} + + {{#if this.isReadOnlyOrDisabled}} + + {{/if}} + + {{/if}} + + {{#if + (this.assertBlockOrArgumentExists + (hash blockExists=(has-block "hint") argName="hint" arg=@hint) + ) + }} + + {{#if (has-block "hint")}} + {{yield to="hint"}} + {{else}} + {{@hint}} + {{/if}} + + {{/if}} + + + {{yield select}} + + + + {{#if @error}} + + {{/if}} + +
+ } diff --git a/packages/ember-toucan-core/src/components/form/fields/autocomplete.hbs b/packages/ember-toucan-core/src/components/form/fields/autocomplete.hbs deleted file mode 100644 index 8cbd941d..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/autocomplete.hbs +++ /dev/null @@ -1,72 +0,0 @@ -
- - {{#if - (this.assertBlockOrArgumentExists - (hash - blockExists=(has-block "label") - argName="label" - arg=@label - isRequired=true - ) - ) - }} - - {{#if (has-block "label")}} - {{yield to="label"}} - {{else}} - {{@label}} - {{/if}} - - {{#if this.isReadOnlyOrDisabled}} - - {{/if}} - - {{/if}} - - {{#if - (this.assertBlockOrArgumentExists - (hash blockExists=(has-block "hint") argName="hint" arg=@hint) - ) - }} - - {{#if (has-block "hint")}} - {{yield to="hint"}} - {{else}} - {{@hint}} - {{/if}} - - {{/if}} - - - {{yield select}} - - - - {{#if @error}} - - {{/if}} - -
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.gts similarity index 58% rename from packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts rename to packages/ember-toucan-core/src/components/form/fields/checkbox-group.gts index ef588fba..84b4c63b 100644 --- a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts +++ b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.gts @@ -1,8 +1,10 @@ import Component from '@glimmer/component'; +import { 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 Field from '../field'; import CheckboxFieldComponent from './checkbox'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; @@ -66,7 +68,7 @@ export interface ToucanFormCheckboxGroupFieldComponentSignature { typeof CheckboxFieldComponent, 'isDisabled' | 'isReadOnly' | 'name' | 'onChange' | 'selectedValues' >; - } + }, ]; label: []; hint: []; @@ -103,4 +105,80 @@ export default class ToucanFormCheckboxGroupFieldComponent extends Component + +
+ {{#if + (this.assertBlockOrArgumentExists + (hash + blockExists=(has-block "label") + argName="label" + arg=@label + isRequired=true + ) + ) + }} + + {{#if (has-block "label")}} + {{yield to="label"}} + {{else}} + {{@label}} + {{/if}} + + {{#if this.isReadOnlyOrDisabled}} + + {{/if}} + + {{/if}} + + {{#if + (this.assertBlockOrArgumentExists + (hash blockExists=(has-block "hint") argName="hint" arg=@hint) + ) + }} + + {{#if (has-block "hint")}} + {{yield to="hint"}} + {{else}} + {{@hint}} + {{/if}} + + {{/if}} + + + {{yield + (hash + CheckboxField=(component + this.CheckboxFieldComponent + name=@name + onChange=this.handleInput + isDisabled=@isDisabled + isReadOnly=@isReadOnly + selectedValues=@value + isGrouped=true + ) + ) + }} + + + {{#if @error}} + + {{/if}} +
+
+ } diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.hbs b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.hbs deleted file mode 100644 index c4d014f7..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.hbs +++ /dev/null @@ -1,73 +0,0 @@ - -
- {{#if - (this.assertBlockOrArgumentExists - (hash - blockExists=(has-block "label") - argName="label" - arg=@label - isRequired=true - ) - ) - }} - - {{#if (has-block "label")}} - {{yield to="label"}} - {{else}} - {{@label}} - {{/if}} - - {{#if this.isReadOnlyOrDisabled}} - - {{/if}} - - {{/if}} - - {{#if - (this.assertBlockOrArgumentExists - (hash blockExists=(has-block "hint") argName="hint" arg=@hint) - ) - }} - - {{#if (has-block "hint")}} - {{yield to="hint"}} - {{else}} - {{@hint}} - {{/if}} - - {{/if}} - - - {{yield - (hash - CheckboxField=(component - (ensure-safe-component this.CheckboxFieldComponent) - name=@name - onChange=this.handleInput - isDisabled=@isDisabled - isReadOnly=@isReadOnly - selectedValues=@value - isGrouped=true - ) - ) - }} - - - {{#if @error}} - - {{/if}} -
-
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox.ts b/packages/ember-toucan-core/src/components/form/fields/checkbox.gts similarity index 62% rename from packages/ember-toucan-core/src/components/form/fields/checkbox.ts rename to packages/ember-toucan-core/src/components/form/fields/checkbox.gts index e8e389de..f6f296d9 100644 --- a/packages/ember-toucan-core/src/components/form/fields/checkbox.ts +++ b/packages/ember-toucan-core/src/components/form/fields/checkbox.gts @@ -1,8 +1,11 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; +import { hash } from '@ember/helper'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; import LockIcon from '../../../-private/icons/lock'; +import Checkbox from '../controls/checkbox'; +import Field from '../field'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ErrorMessage } from '../../../-private/types'; @@ -109,11 +112,11 @@ export default class ToucanFormCheckboxFieldComponent extends Component +
+ + + + + + + {{#if @error}} + + {{/if}} + +
+ } diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox.hbs b/packages/ember-toucan-core/src/components/form/fields/checkbox.hbs deleted file mode 100644 index 078e67a1..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/checkbox.hbs +++ /dev/null @@ -1,77 +0,0 @@ -
- - - - - - - {{#if @error}} - - {{/if}} - -
\ 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); + } + } + + +} diff --git a/packages/ember-toucan-core/src/components/form/fields/file-input.hbs b/packages/ember-toucan-core/src/components/form/fields/file-input.hbs deleted file mode 100644 index 4aeb6756..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/file-input.hbs +++ /dev/null @@ -1,103 +0,0 @@ -
- - - {{#if - (this.assertBlockOrArgumentExists - (hash - blockExists=(has-block "label") - argName="label" - arg=@label - isRequired=true - ) - ) - }} - - {{#if (has-block "label")}} - {{yield to="label"}} - {{else}} - {{@label}} - {{/if}} - - {{/if}} - - {{#if - (this.assertBlockOrArgumentExists - (hash blockExists=(has-block "hint") argName="hint" arg=@hint) - ) - }} - - {{#if (has-block "hint")}} - {{yield to="hint"}} - {{else}} - {{@hint}} - {{/if}} - - {{/if}} - - - - - - - {{#if @error}} - - {{/if}} - {{#if @files.length}} - - {{/if}} - -
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/file-input.ts b/packages/ember-toucan-core/src/components/form/fields/file-input.ts deleted file mode 100644 index d48ce40e..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/file-input.ts +++ /dev/null @@ -1,148 +0,0 @@ -import Component from '@glimmer/component'; -import { assert } from '@ember/debug'; -import { action } from '@ember/object'; - -import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/icons/lock'; - -import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; -import type { ErrorMessage } from '../../../-private/types'; - -type FileTarget = EventTarget & { files?: FileList }; -export type FileEvent = (Event | MouseEvent) & { target: FileTarget | null }; - -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?: (files: File[], event: FileEvent) => void; - - /** - * 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); - } - } -} diff --git a/packages/ember-toucan-core/src/components/form/fields/input.gts b/packages/ember-toucan-core/src/components/form/fields/input.gts new file mode 100644 index 00000000..602fed5d --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/fields/input.gts @@ -0,0 +1,202 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { assert } from '@ember/debug'; +import { hash } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; + +import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; +import LockIcon from '../../../-private/icons/lock'; +import CharacterCount from '../../../components/form/controls/character-count'; +import CoreInput from '../controls/input'; +import Field from '../field'; + +import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; +import type { ErrorMessage, OnChangeCallback } from '../../../-private/types'; +import type { WithBoundArgs } from '@glint/template'; + +export interface ToucanFormInputFieldComponentSignature { + Element: HTMLInputElement; + Args: { + /** + * Provide a string or array of strings to this argument to render an error message and apply error styling to the Field. + */ + error?: ErrorMessage; + + /** + * Provide a string to this argument to render a hint message to help describe the control. + */ + hint?: string; + + /** + * Sets the disabled attribute on the control. + */ + isDisabled?: boolean; + + /** + * Sets the readonly attribute of the input. + */ + isReadOnly?: boolean; + + /** + * Provide a string to this argument to render inside of the label tag. + */ + label?: string; + + /** + * The function called when the element is typed into. + */ + onChange?: OnChangeCallback; + + /** + * A test selector for targeting the root element of the field. In this case, the wrapping div element. + */ + rootTestSelector?: string; + + /** + * Sets the value attribute of the input. + */ + value?: string; + }; + Blocks: { + default: []; + label: []; + hint: []; + secondary: [ + { + CharacterCount: WithBoundArgs; + }, + ]; + }; +} + +export default class ToucanFormInputFieldComponent extends Component { + @tracked count = this.args.value?.length ?? 0; + + CharacterCount = CharacterCount; + LockIcon = LockIcon; + + assertBlockOrArgumentExists = ({ + blockExists, + argName, + arg, + isRequired, + }: AssertBlockOrArg) => + assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired }); + + get hasError() { + return Boolean(this.args?.error); + } + + get isReadOnlyOrDisabled() { + return this.args?.isDisabled || this.args?.isReadOnly; + } + + @action + handleCount(event: Event | InputEvent): void { + assert( + 'Expected HTMLInputElement', + event.target instanceof HTMLInputElement, + ); + + this.count = event.target?.value.length ?? 0; + } + + +} diff --git a/packages/ember-toucan-core/src/components/form/fields/input.hbs b/packages/ember-toucan-core/src/components/form/fields/input.hbs deleted file mode 100644 index f65ac97d..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/input.hbs +++ /dev/null @@ -1,92 +0,0 @@ -
- - {{#if - (this.assertBlockOrArgumentExists - (hash - blockExists=(has-block "label") - argName="label" - arg=@label - isRequired=true - ) - ) - }} - - {{#if (has-block "label")}} - {{yield to="label"}} - {{else}} - {{@label}} - {{/if}} - - {{#if this.isReadOnlyOrDisabled}} - - {{/if}} - - {{/if}} - - {{#if - (this.assertBlockOrArgumentExists - (hash blockExists=(has-block "hint") argName="hint" arg=@hint) - ) - }} - - {{#if (has-block "hint")}} - {{yield to="hint"}} - {{else}} - {{@hint}} - {{/if}} - - {{/if}} - - - - - - {{#if (has-block "secondary")}} -
- {{#if @error}} - - {{/if}} - -
- {{yield - (hash - CharacterCount=(component - (ensure-safe-component this.CharacterCount) current=this.count - ) - ) - to="secondary" - }} -
-
- {{else if @error}} - - {{/if}} -
-
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/input.ts b/packages/ember-toucan-core/src/components/form/fields/input.ts deleted file mode 100644 index c77680c9..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/input.ts +++ /dev/null @@ -1,100 +0,0 @@ -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { assert } from '@ember/debug'; -import { action } from '@ember/object'; - -import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/icons/lock'; -import CharacterCount from '../../../components/form/controls/character-count'; - -import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; -import type { ErrorMessage, OnChangeCallback } from '../../../-private/types'; -import type { WithBoundArgs } from '@glint/template'; - -export interface ToucanFormInputFieldComponentSignature { - Element: HTMLInputElement; - Args: { - /** - * Provide a string or array of strings to this argument to render an error message and apply error styling to the Field. - */ - error?: ErrorMessage; - - /** - * Provide a string to this argument to render a hint message to help describe the control. - */ - hint?: string; - - /** - * Sets the disabled attribute on the control. - */ - isDisabled?: boolean; - - /** - * Sets the readonly attribute of the input. - */ - isReadOnly?: boolean; - - /** - * Provide a string to this argument to render inside of the label tag. - */ - label?: string; - - /** - * The function called when the element is typed into. - */ - onChange?: OnChangeCallback; - - /** - * A test selector for targeting the root element of the field. In this case, the wrapping div element. - */ - rootTestSelector?: string; - - /** - * Sets the value attribute of the input. - */ - value?: string; - }; - Blocks: { - default: []; - label: []; - hint: []; - secondary: [ - { - CharacterCount: WithBoundArgs; - } - ]; - }; -} - -export default class ToucanFormInputFieldComponent extends Component { - @tracked count = this.args.value?.length ?? 0; - - CharacterCount = CharacterCount; - LockIcon = LockIcon; - - assertBlockOrArgumentExists = ({ - blockExists, - argName, - arg, - isRequired, - }: AssertBlockOrArg) => - assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired }); - - get hasError() { - return Boolean(this.args?.error); - } - - get isReadOnlyOrDisabled() { - return this.args?.isDisabled || this.args?.isReadOnly; - } - - @action - handleCount(event: Event | InputEvent): void { - assert( - 'Expected HTMLInputElement', - event.target instanceof HTMLInputElement - ); - - this.count = event.target?.value.length ?? 0; - } -} diff --git a/packages/ember-toucan-core/src/components/form/fields/multiselect.ts b/packages/ember-toucan-core/src/components/form/fields/multiselect.gts similarity index 56% rename from packages/ember-toucan-core/src/components/form/fields/multiselect.ts rename to packages/ember-toucan-core/src/components/form/fields/multiselect.gts index 0bcf5cf5..35096c97 100644 --- a/packages/ember-toucan-core/src/components/form/fields/multiselect.ts +++ b/packages/ember-toucan-core/src/components/form/fields/multiselect.gts @@ -1,7 +1,10 @@ import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; import LockIcon from '../../../-private/icons/lock'; +import Multiselect from '../controls/multiselect'; +import Field from '../field'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ErrorMessage } from '../../../-private/types'; @@ -114,4 +117,102 @@ export default class ToucanFormMultiselectFieldComponent extends Component +
+ + {{#if + (this.assertBlockOrArgumentExists + (hash + blockExists=(has-block "label") + argName="label" + arg=@label + isRequired=true + ) + ) + }} + + {{#if (has-block "label")}} + {{yield to="label"}} + {{else}} + {{@label}} + {{/if}} + + {{#if this.isReadOnlyOrDisabled}} + + {{/if}} + + {{/if}} + + {{#if + (this.assertBlockOrArgumentExists + (hash blockExists=(has-block "hint") argName="hint" arg=@hint) + ) + }} + + {{#if (has-block "hint")}} + {{yield to="hint"}} + {{else}} + {{@hint}} + {{/if}} + + {{/if}} + + + + <:chip as |chip|> + {{yield + (hash + index=chip.index + option=chip.option + Chip=(component chip.Chip) + Remove=(component chip.Remove) + ) + to="chip" + }} + + + <:default as |multiselect|> + {{yield + (hash + Option=(component multiselect.Option) + option=multiselect.option + ) + }} + + + + + {{#if @error}} + + {{/if}} + +
+ } diff --git a/packages/ember-toucan-core/src/components/form/fields/multiselect.hbs b/packages/ember-toucan-core/src/components/form/fields/multiselect.hbs deleted file mode 100644 index f395d0ea..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/multiselect.hbs +++ /dev/null @@ -1,91 +0,0 @@ -
- - {{#if - (this.assertBlockOrArgumentExists - (hash - blockExists=(has-block "label") - argName="label" - arg=@label - isRequired=true - ) - ) - }} - - {{#if (has-block "label")}} - {{yield to="label"}} - {{else}} - {{@label}} - {{/if}} - - {{#if this.isReadOnlyOrDisabled}} - - {{/if}} - - {{/if}} - - {{#if - (this.assertBlockOrArgumentExists - (hash blockExists=(has-block "hint") argName="hint" arg=@hint) - ) - }} - - {{#if (has-block "hint")}} - {{yield to="hint"}} - {{else}} - {{@hint}} - {{/if}} - - {{/if}} - - - <:chip as |chip|> - {{yield - (hash - index=chip.index - option=chip.option - Chip=(component (ensure-safe-component chip.Chip)) - Remove=(component (ensure-safe-component chip.Remove)) - ) - to="chip" - }} - - - <:default as |multiselect|> - {{yield - (hash - Option=(component (ensure-safe-component multiselect.Option)) - option=multiselect.option - ) - }} - - - - - {{#if @error}} - - {{/if}} - -
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/radio-group.ts b/packages/ember-toucan-core/src/components/form/fields/radio-group.gts similarity index 56% rename from packages/ember-toucan-core/src/components/form/fields/radio-group.ts rename to packages/ember-toucan-core/src/components/form/fields/radio-group.gts index 3d24866a..5cba6894 100644 --- a/packages/ember-toucan-core/src/components/form/fields/radio-group.ts +++ b/packages/ember-toucan-core/src/components/form/fields/radio-group.gts @@ -1,8 +1,10 @@ import Component from '@glimmer/component'; +import { 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 Field from '../field'; import RadioFieldComponent from './radio'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; @@ -67,7 +69,7 @@ export interface ToucanFormRadioGroupFieldComponentSignature { typeof RadioFieldComponent, 'isDisabled' | 'isReadOnly' | 'name' | 'onChange' | 'selectedValue' >; - } + }, ]; label: []; hint: []; @@ -94,4 +96,82 @@ export default class ToucanFormRadioGroupFieldComponent extends Component + +
+ {{#if + (this.assertBlockOrArgumentExists + (hash + blockExists=(has-block "label") + argName="label" + arg=@label + isRequired=true + ) + ) + }} + + {{#if (has-block "label")}} + {{yield to="label"}} + {{else}} + {{@label}} + {{/if}} + + {{#if this.isReadOnlyOrDisabled}} + + {{/if}} + + {{/if}} + + {{#if + (this.assertBlockOrArgumentExists + (hash blockExists=(has-block "hint") argName="hint" arg=@hint) + ) + }} + + {{#if (has-block "hint")}} + {{yield to="hint"}} + {{else}} + {{@hint}} + {{/if}} + + {{/if}} + + + {{yield + (hash + RadioField=(component + this.RadioFieldComponent + name=@name + onChange=this.handleInput + selectedValue=@value + isDisabled=@isDisabled + isReadOnly=@isReadOnly + ) + ) + }} + + + {{#if @error}} + + {{/if}} +
+
+ } diff --git a/packages/ember-toucan-core/src/components/form/fields/radio-group.hbs b/packages/ember-toucan-core/src/components/form/fields/radio-group.hbs deleted file mode 100644 index f242a62b..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/radio-group.hbs +++ /dev/null @@ -1,75 +0,0 @@ - -
- {{#if - (this.assertBlockOrArgumentExists - (hash - blockExists=(has-block "label") - argName="label" - arg=@label - isRequired=true - ) - ) - }} - - {{#if (has-block "label")}} - {{yield to="label"}} - {{else}} - {{@label}} - {{/if}} - - {{#if this.isReadOnlyOrDisabled}} - - {{/if}} - - {{/if}} - - {{#if - (this.assertBlockOrArgumentExists - (hash blockExists=(has-block "hint") argName="hint" arg=@hint) - ) - }} - - {{#if (has-block "hint")}} - {{yield to="hint"}} - {{else}} - {{@hint}} - {{/if}} - - {{/if}} - - - {{yield - (hash - RadioField=(component - (ensure-safe-component this.RadioFieldComponent) - name=@name - onChange=this.handleInput - selectedValue=@value - isDisabled=@isDisabled - isReadOnly=@isReadOnly - ) - ) - }} - - - {{#if @error}} - - {{/if}} -
-
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/radio.ts b/packages/ember-toucan-core/src/components/form/fields/radio.gts similarity index 52% rename from packages/ember-toucan-core/src/components/form/fields/radio.ts rename to packages/ember-toucan-core/src/components/form/fields/radio.gts index 0951ff51..1a823a3b 100644 --- a/packages/ember-toucan-core/src/components/form/fields/radio.ts +++ b/packages/ember-toucan-core/src/components/form/fields/radio.gts @@ -1,7 +1,10 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; +import { hash } from '@ember/helper'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; +import Radio from '../controls/radio'; +import Field from '../field'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ToucanFormRadioControlComponentSignature } from '../controls/radio'; @@ -74,7 +77,7 @@ export default class ToucanFormRadioFieldComponent extends Component +
+ + + + + + +
+ } diff --git a/packages/ember-toucan-core/src/components/form/fields/radio.hbs b/packages/ember-toucan-core/src/components/form/fields/radio.hbs deleted file mode 100644 index ddabf017..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/radio.hbs +++ /dev/null @@ -1,62 +0,0 @@ -
- - - - - - -
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/textarea.gts b/packages/ember-toucan-core/src/components/form/fields/textarea.gts new file mode 100644 index 00000000..3449d364 --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/fields/textarea.gts @@ -0,0 +1,208 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { assert } from '@ember/debug'; +import { hash } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; + +import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; +import LockIcon from '../../../-private/icons/lock'; +import CharacterCount from '../../../components/form/controls/character-count'; +import CoreTextarea from '../controls/textarea'; +import Field from '../field'; + +import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; +import type { ErrorMessage } from '../../../-private/types'; +import type { ToucanFormTextareaControlComponentSignature } from '../controls/textarea'; +import type { WithBoundArgs } from '@glint/template'; + +export interface ToucanFormTextareaFieldComponentSignature { + Element: HTMLTextAreaElement; + Args: { + /** + * Provide a string or array of strings to this argument to render an error message and apply error styling to the Field. + */ + error?: ErrorMessage; + + /** + * Provide a string to this argument to render a hint message to help describe the control. + */ + hint?: string; + + /** + * Sets the disabled attribute on the control. + */ + isDisabled?: boolean; + + /** + * Sets the readonly attribute of the textarea. + */ + isReadOnly?: boolean; + + /** + * Provide a string to this argument to render inside of the label tag. + */ + label?: string; + + /** + * The function called when the element is typed into. + */ + onChange?: ToucanFormTextareaControlComponentSignature['Args']['onChange']; + + /** + * A test selector for targeting the root element of the field. In this case, the wrapping div element. + */ + rootTestSelector?: string; + + /** + * Sets the value attribute of the textarea. + */ + value?: ToucanFormTextareaControlComponentSignature['Args']['value']; + }; + Blocks: { + label: []; + hint: []; + secondary: [ + { + CharacterCount: WithBoundArgs; + }, + ]; + }; +} + +export default class ToucanFormTextareaFieldComponent extends Component { + @tracked count = this.args.value?.length ?? 0; + + CharacterCount = CharacterCount; + LockIcon = LockIcon; + + assertBlockOrArgumentExists = ({ + blockExists, + argName, + arg, + isRequired, + }: AssertBlockOrArg) => + assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired }); + + constructor( + owner: unknown, + args: ToucanFormTextareaFieldComponentSignature['Args'], + ) { + super(owner, args); + } + + get hasError() { + return Boolean(this.args?.error); + } + + get isReadOnlyOrDisabled() { + return this.args?.isDisabled || this.args?.isReadOnly; + } + + @action + handleCount(event: Event | InputEvent): void { + assert( + 'Expected HTMLTextAreaElement', + event.target instanceof HTMLTextAreaElement, + ); + this.count = event.target?.value.length ?? 0; + } + + +} diff --git a/packages/ember-toucan-core/src/components/form/fields/textarea.hbs b/packages/ember-toucan-core/src/components/form/fields/textarea.hbs deleted file mode 100644 index 8d8b4622..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/textarea.hbs +++ /dev/null @@ -1,92 +0,0 @@ -
- - {{#if - (this.assertBlockOrArgumentExists - (hash - blockExists=(has-block "label") - argName="label" - arg=@label - isRequired=true - ) - ) - }} - - {{#if (has-block "label")}} - {{yield to="label"}} - {{else}} - {{@label}} - {{/if}} - - {{#if this.isReadOnlyOrDisabled}} - - {{/if}} - - {{/if}} - - {{#if - (this.assertBlockOrArgumentExists - (hash blockExists=(has-block "hint") argName="hint" arg=@hint) - ) - }} - - {{#if (has-block "hint")}} - {{yield to="hint"}} - {{else}} - {{@hint}} - {{/if}} - - {{/if}} - - - - - - {{#if (has-block "secondary")}} -
- {{#if @error}} - - {{/if}} - -
- {{yield - (hash - CharacterCount=(component - (ensure-safe-component this.CharacterCount) current=this.count - ) - ) - to="secondary" - }} -
-
- {{else if @error}} - - {{/if}} -
-
\ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/textarea.ts b/packages/ember-toucan-core/src/components/form/fields/textarea.ts deleted file mode 100644 index efffb641..00000000 --- a/packages/ember-toucan-core/src/components/form/fields/textarea.ts +++ /dev/null @@ -1,106 +0,0 @@ -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { assert } from '@ember/debug'; -import { action } from '@ember/object'; - -import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/icons/lock'; -import CharacterCount from '../../../components/form/controls/character-count'; - -import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; -import type { ErrorMessage } from '../../../-private/types'; -import type { ToucanFormTextareaControlComponentSignature } from '../controls/textarea'; -import type { WithBoundArgs } from '@glint/template'; - -export interface ToucanFormTextareaFieldComponentSignature { - Element: HTMLTextAreaElement; - Args: { - /** - * Provide a string or array of strings to this argument to render an error message and apply error styling to the Field. - */ - error?: ErrorMessage; - - /** - * Provide a string to this argument to render a hint message to help describe the control. - */ - hint?: string; - - /** - * Sets the disabled attribute on the control. - */ - isDisabled?: boolean; - - /** - * Sets the readonly attribute of the textarea. - */ - isReadOnly?: boolean; - - /** - * Provide a string to this argument to render inside of the label tag. - */ - label?: string; - - /** - * The function called when the element is typed into. - */ - onChange?: ToucanFormTextareaControlComponentSignature['Args']['onChange']; - - /** - * A test selector for targeting the root element of the field. In this case, the wrapping div element. - */ - rootTestSelector?: string; - - /** - * Sets the value attribute of the textarea. - */ - value?: ToucanFormTextareaControlComponentSignature['Args']['value']; - }; - Blocks: { - label: []; - hint: []; - secondary: [ - { - CharacterCount: WithBoundArgs; - } - ]; - }; -} - -export default class ToucanFormTextareaFieldComponent extends Component { - @tracked count = this.args.value?.length ?? 0; - - CharacterCount = CharacterCount; - LockIcon = LockIcon; - - assertBlockOrArgumentExists = ({ - blockExists, - argName, - arg, - isRequired, - }: AssertBlockOrArg) => - assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired }); - - constructor( - owner: unknown, - args: ToucanFormTextareaFieldComponentSignature['Args'] - ) { - super(owner, args); - } - - get hasError() { - return Boolean(this.args?.error); - } - - get isReadOnlyOrDisabled() { - return this.args?.isDisabled || this.args?.isReadOnly; - } - - @action - handleCount(event: Event | InputEvent): void { - assert( - 'Expected HTMLTextAreaElement', - event.target instanceof HTMLTextAreaElement - ); - this.count = event.target?.value.length ?? 0; - } -} diff --git a/packages/ember-toucan-core/src/components/form/file-input/delete-button.gts b/packages/ember-toucan-core/src/components/form/file-input/delete-button.gts new file mode 100644 index 00000000..e05dfc6d --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/file-input/delete-button.gts @@ -0,0 +1,78 @@ +import Component from '@glimmer/component'; +import { assert } from '@ember/debug'; +import { fn } from '@ember/helper'; +import { on } from '@ember/modifier'; + +export type onDeleteFileHandler = ( + file: File, + event: Event | InputEvent, +) => void; + +interface ToucanFormFileInputDeleteButtonComponentSignature { + Element: HTMLButtonElement; + Args: { + /** + * The current file associated with this delete button. + */ + file: File; + + /** + * Called when the delete button is pressed. + */ + onDelete: onDeleteFileHandler; + + /** + * The label for the delete button. + */ + deleteLabel: string; + }; +} + +export default class ToucanFormFileInputDeleteButtonComponent extends Component { + constructor( + owner: unknown, + args: ToucanFormFileInputDeleteButtonComponentSignature['Args'], + ) { + assert( + 'A "@file" argument is required for Form::FileInput::DeleteButton. If using the Form::Fields::FileInput this should be provided automatically.', + args.file !== undefined, + ); + + assert( + 'A "@onDelete" argument is required for Form::FileInput::DeleteButton. If using the Form::Fields::FileInput this should be provided automatically.', + args.onDelete !== undefined, + ); + + assert( + 'A "@deleteLabel" argument is required for Form::FileInput::DeleteButton. If using the Form::Fields::FileInput this should be provided automatically.', + args.deleteLabel !== undefined, + ); + + super(owner, args); + } + + +} diff --git a/packages/ember-toucan-core/src/components/form/file-input/delete-button.hbs b/packages/ember-toucan-core/src/components/form/file-input/delete-button.hbs deleted file mode 100644 index ede30293..00000000 --- a/packages/ember-toucan-core/src/components/form/file-input/delete-button.hbs +++ /dev/null @@ -1,22 +0,0 @@ - \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/file-input/delete-button.ts b/packages/ember-toucan-core/src/components/form/file-input/delete-button.ts deleted file mode 100644 index e85793bb..00000000 --- a/packages/ember-toucan-core/src/components/form/file-input/delete-button.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Component from '@glimmer/component'; -import { assert } from '@ember/debug'; - -export type onDeleteFileHandler = ( - file: File, - event: Event | InputEvent -) => void; - -interface ToucanFormFileInputDeleteButtonComponentSignature { - Element: HTMLButtonElement; - Args: { - file: File; - onDelete: onDeleteFileHandler; - deleteLabel: string; - }; -} - -export default class ToucanFormFileInputDeleteButtonComponent extends Component { - constructor( - owner: unknown, - args: ToucanFormFileInputDeleteButtonComponentSignature['Args'] - ) { - assert( - 'A "@file" argument is required for Form::FileInput::DeleteButton. If using the Form::Fields::FileInput this should be provided automatically.', - args.file !== undefined - ); - - assert( - 'A "@onDelete" argument is required for Form::FileInput::DeleteButton. If using the Form::Fields::FileInput this should be provided automatically.', - args.onDelete !== undefined - ); - - assert( - 'A "@deleteLabel" argument is required for Form::FileInput::DeleteButton. If using the Form::Fields::FileInput this should be provided automatically.', - args.deleteLabel !== undefined - ); - - super(owner, args); - } -} diff --git a/packages/ember-toucan-core/src/components/form/file-input/list-item.gts b/packages/ember-toucan-core/src/components/form/file-input/list-item.gts new file mode 100644 index 00000000..17c512a8 --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/file-input/list-item.gts @@ -0,0 +1,95 @@ +import Component from '@glimmer/component'; +import { assert } from '@ember/debug'; + +import DeleteButton from './delete-button'; + +import type { onDeleteFileHandler } from './delete-button'; + +interface ToucanFormFileInputListItemComponentSignature { + Element: HTMLLIElement; + Args: { + /** + * The label for the delete button. + */ + deleteLabel: string; + + /** + * The current file associated with this delete button. + */ + file: File; + + /** + * Sets the element to an errored-state via styling. + */ + hasError?: boolean; + + /** + * Sets the disabled attribute on the element. + */ + isDisabled?: boolean; + + /** + * The event called when an item is deleted. + */ + onDelete: onDeleteFileHandler; + }; +} + +export default class ToucanFormFileInputListComponent extends Component { + constructor( + owner: unknown, + args: ToucanFormFileInputListItemComponentSignature['Args'], + ) { + assert( + 'An "@onDelete" argument is required for Form::FileInput::List. If using Form::Fields::FileInput, this should be provided automatically.', + args.onDelete !== undefined, + ); + + assert( + 'An "@file" argument is required for Form::FileInput::List. If using Form::Fields::FileInput, this should be provided automatically.', + args.file !== undefined, + ); + + assert( + 'An "@deleteLabel" argument is required for Form::FileInput::List. If using Form::Fields::FileInput, this should be provided automatically.', + args.deleteLabel !== undefined, + ); + super(owner, args); + } + + formatSize(size: number) { + return `${Math.round(size / 1000)} KB`; + } + + +} diff --git a/packages/ember-toucan-core/src/components/form/file-input/list-item.hbs b/packages/ember-toucan-core/src/components/form/file-input/list-item.hbs deleted file mode 100644 index bfaad2fc..00000000 --- a/packages/ember-toucan-core/src/components/form/file-input/list-item.hbs +++ /dev/null @@ -1,26 +0,0 @@ -
  • - {{! The `pl-2` added here is to offset the spacing added by the delete button below so that the horizontal axis spacing appears identical }} -
    -

    {{@file.name}}

    - {{(this.formatSize @file.size)}} -
    - - -
  • \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/file-input/list-item.ts b/packages/ember-toucan-core/src/components/form/file-input/list-item.ts deleted file mode 100644 index bab0ff35..00000000 --- a/packages/ember-toucan-core/src/components/form/file-input/list-item.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Component from '@glimmer/component'; -import { assert } from '@ember/debug'; - -import type { onDeleteFileHandler } from './delete-button'; - -interface ToucanFormFileInputListItemComponentSignature { - Element: HTMLLIElement; - Args: { - file: File; - onDelete: onDeleteFileHandler; - deleteLabel: string; - hasError?: boolean; - isDisabled?: boolean; - }; -} - -export default class ToucanFormFileInputListComponent extends Component { - constructor( - owner: unknown, - args: ToucanFormFileInputListItemComponentSignature['Args'] - ) { - assert( - 'An "@onDelete" argument is required for Form::FileInput::List. If using Form::Fields::FileInput, this should be provided automatically.', - args.onDelete !== undefined - ); - - assert( - 'An "@file" argument is required for Form::FileInput::List. If using Form::Fields::FileInput, this should be provided automatically.', - args.file !== undefined - ); - - assert( - 'An "@deleteLabel" argument is required for Form::FileInput::List. If using Form::Fields::FileInput, this should be provided automatically.', - args.deleteLabel !== undefined - ); - super(owner, args); - } - - formatSize(size: number) { - return `${Math.round(size / 1000)} KB`; - } -} diff --git a/packages/ember-toucan-core/src/components/form/file-input/list.ts b/packages/ember-toucan-core/src/components/form/file-input/list.gts similarity index 62% rename from packages/ember-toucan-core/src/components/form/file-input/list.ts rename to packages/ember-toucan-core/src/components/form/file-input/list.gts index 94b42157..3a315c97 100644 --- a/packages/ember-toucan-core/src/components/form/file-input/list.ts +++ b/packages/ember-toucan-core/src/components/form/file-input/list.gts @@ -1,5 +1,6 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; +import { hash } from '@ember/helper'; import ListItem from './list-item'; @@ -21,7 +22,7 @@ interface ToucanFormFileInputListComponentSignature { 'file' | 'onDelete' | 'deleteLabel' >; file: File; - } + }, ]; }; } @@ -30,20 +31,20 @@ export default class ToucanFormFileInputListComponent extends Component +
      + {{#each @files as |file|}} + {{#if (has-block)}} + {{yield + (hash + ListItem=(component + this.ListItem + file=file + onDelete=@onDelete + deleteLabel=@deleteLabel + ) + file=file + ) + }} + {{else}} + + {{/if}} + {{/each}} +
    + } diff --git a/packages/ember-toucan-core/src/components/form/file-input/list.hbs b/packages/ember-toucan-core/src/components/form/file-input/list.hbs deleted file mode 100644 index f1b06314..00000000 --- a/packages/ember-toucan-core/src/components/form/file-input/list.hbs +++ /dev/null @@ -1,23 +0,0 @@ -
      - {{#each @files as |file|}} - {{#if (has-block)}} - {{yield - (hash - ListItem=(component - (ensure-safe-component this.ListItem) - file=file - onDelete=@onDelete - deleteLabel=@deleteLabel - ) - file=file - ) - }} - {{else}} - - {{/if}} - {{/each}} -
    \ No newline at end of file diff --git a/packages/ember-toucan-core/tsconfig.json b/packages/ember-toucan-core/tsconfig.json index b2dd193a..33809bb5 100644 --- a/packages/ember-toucan-core/tsconfig.json +++ b/packages/ember-toucan-core/tsconfig.json @@ -2,6 +2,6 @@ "extends": "@tsconfig/ember/tsconfig.json", "include": ["src/**/*", "unpublished-development-types/**/*"], "glint": { - "environment": "ember-loose" + "environment": ["ember-loose", "ember-template-imports"] } } diff --git a/packages/ember-toucan-core/unpublished-development-types/index.d.ts b/packages/ember-toucan-core/unpublished-development-types/index.d.ts index eb266d25..399f3454 100644 --- a/packages/ember-toucan-core/unpublished-development-types/index.d.ts +++ b/packages/ember-toucan-core/unpublished-development-types/index.d.ts @@ -3,36 +3,10 @@ import '@glint/environment-ember-loose'; -import Helper from '@ember/component/helper'; - -import { ComponentLike } from '@glint/template'; - import type ToucanCoreRegistry from '../src/template-registry'; -import type TemplateRegistry from '@glint/environment-ember-loose/registry'; -import type EmberVelcroRegistry from 'ember-velcro/template-registry'; - -// importing this directly from the published types (https://github.com/embroider-build/embroider/blob/main/packages/util/index.d.ts) does not work, -// see point 3 in Dan's comment here: https://github.com/typed-ember/glint/issues/518#issuecomment-1400306133 -declare class EnsureSafeComponentHelper< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - C extends string | ComponentLike -> extends Helper<{ - Args: { - Positional: [component: C]; - }; - Return: C extends keyof TemplateRegistry - ? TemplateRegistry[C] - : C extends string - ? ComponentLike - : C; -}> {} declare module '@glint/environment-ember-loose/registry' { - export default interface Registry - extends ToucanCoreRegistry, - EmberVelcroRegistry { + export default interface Registry extends ToucanCoreRegistry { // local entries - - 'ensure-safe-component': typeof EnsureSafeComponentHelper; } } diff --git a/packages/ember-toucan-form/babel.config.json b/packages/ember-toucan-form/babel.config.json index 739f8261..04da3ac3 100644 --- a/packages/ember-toucan-form/babel.config.json +++ b/packages/ember-toucan-form/babel.config.json @@ -1,6 +1,7 @@ { "presets": [["@babel/preset-typescript"]], "plugins": [ + "ember-template-imports/src/babel-plugin", "@embroider/addon-dev/template-colocation-plugin", ["@babel/plugin-proposal-decorators", { "legacy": true }], "@babel/plugin-proposal-class-properties" diff --git a/packages/ember-toucan-form/package.json b/packages/ember-toucan-form/package.json index 3803bf78..d48c87b5 100644 --- a/packages/ember-toucan-form/package.json +++ b/packages/ember-toucan-form/package.json @@ -62,6 +62,7 @@ "@glimmer/tracking": "^1.1.2", "@glint/core": "^1.0.2", "@glint/environment-ember-loose": "^1.0.2", + "@glint/environment-ember-template-imports": "^1.0.2", "@glint/template": "^1.0.2", "@nullvoxpopuli/eslint-configs": "^3.0.4", "@tsconfig/ember": "^2.0.0", @@ -73,6 +74,7 @@ "@types/ember__debug": "^4.0.0", "@types/ember__engine": "^4.0.0", "@types/ember__error": "^4.0.0", + "@types/ember__helper": "^4.0.2", "@types/ember__modifier": "^4.0.3", "@types/ember__object": "^4.0.0", "@types/ember__polyfills": "^4.0.0", @@ -90,6 +92,7 @@ "ember-cli-htmlbars": "^6.1.1", "ember-headless-form": "^1.0.0-beta.3", "ember-source": "~5.1.0", + "ember-template-imports": "^3.4.2", "ember-template-lint": "^5.8.0", "eslint": "^8.32.0", "eslint-config-prettier": "^8.3.0", @@ -99,11 +102,12 @@ "eslint-plugin-prettier": "^4.0.0", "fractal-page-object": "^0.4.1", "postcss": "^8.2.14", - "prettier": "^2.8.3", + "prettier": "^3.0.1", "prettier-plugin-ember-template-tag": "^1.0.0", "prettier-plugin-tailwindcss": "^0.4.0", "rollup": "^3.12.1", "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-glimmer-template-tag": "^0.4.1", "rollup-plugin-ts": "^3.0.2", "tailwindcss": "^2.2.15", "typescript": "^5.0.0" diff --git a/packages/ember-toucan-form/rollup.config.mjs b/packages/ember-toucan-form/rollup.config.mjs index f88be5fe..94118601 100644 --- a/packages/ember-toucan-form/rollup.config.mjs +++ b/packages/ember-toucan-form/rollup.config.mjs @@ -1,6 +1,7 @@ import typescript from 'rollup-plugin-ts'; import copy from 'rollup-plugin-copy'; import { Addon } from '@embroider/addon-dev/rollup'; +import { glimmerTemplateTag } from 'rollup-plugin-glimmer-template-tag'; const addon = new Addon({ srcDir: 'src', @@ -26,11 +27,15 @@ export default { // not everything in publicEntrypoints necessarily needs to go here. addon.appReexports(['components/**/*.js']), + // compile