From 8e476068b4fa1e89a2c1160daab29ae0c40d8821 Mon Sep 17 00:00:00 2001 From: clintcs <114178960+clintcs@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:33:06 -0700 Subject: [PATCH 01/86] feat: Add Form::Controls::Select --- docs-app/package.json | 1 + .../components/select-field/demo/base-demo.md | 59 +++ docs/components/select-field/index.md | 325 ++++++++++++ docs/components/select/demo/multiple.md | 78 +++ docs/components/select/demo/single.md | 75 +++ docs/components/select/index.md | 15 + packages/ember-toucan-core/babel.config.json | 3 +- packages/ember-toucan-core/package.json | 7 +- .../form/controls/select/option.hbs | 34 ++ .../components/form/controls/select/option.ts | 58 ++ .../src/-private/icons/check.hbs | 11 + .../src/-private/icons/check.ts | 11 + .../src/-private/icons/chevron.hbs | 9 + .../src/-private/icons/chevron.ts | 11 + .../src/components/form/controls/select.hbs | 68 +++ .../src/components/form/controls/select.ts | 498 ++++++++++++++++++ .../src/components/form/fields/select.hbs | 61 +++ .../src/components/form/fields/select.ts | 83 +++ .../src/template-registry.ts | 2 + packages/ember-toucan-core/tsconfig.json | 1 + .../unpublished-development-types/index.d.ts | 5 +- .../src/-private/select-field.hbs | 87 +++ .../src/-private/select-field.ts | 63 +++ .../src/components/toucan-form.hbs | 3 + .../src/components/toucan-form.ts | 3 + 25 files changed, 1568 insertions(+), 3 deletions(-) create mode 100644 docs/components/select-field/demo/base-demo.md create mode 100644 docs/components/select-field/index.md create mode 100644 docs/components/select/demo/multiple.md create mode 100644 docs/components/select/demo/single.md create mode 100644 docs/components/select/index.md create mode 100644 packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs create mode 100644 packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts create mode 100644 packages/ember-toucan-core/src/-private/icons/check.hbs create mode 100644 packages/ember-toucan-core/src/-private/icons/check.ts create mode 100644 packages/ember-toucan-core/src/-private/icons/chevron.hbs create mode 100644 packages/ember-toucan-core/src/-private/icons/chevron.ts create mode 100644 packages/ember-toucan-core/src/components/form/controls/select.hbs create mode 100644 packages/ember-toucan-core/src/components/form/controls/select.ts create mode 100644 packages/ember-toucan-core/src/components/form/fields/select.hbs create mode 100644 packages/ember-toucan-core/src/components/form/fields/select.ts create mode 100644 packages/ember-toucan-form/src/-private/select-field.hbs create mode 100644 packages/ember-toucan-form/src/-private/select-field.ts diff --git a/docs-app/package.json b/docs-app/package.json index 03769c1f..f0275889 100644 --- a/docs-app/package.json +++ b/docs-app/package.json @@ -135,6 +135,7 @@ "ember-browser-services": "^4.0.4", "ember-modifier": "^4.1.0", "ember-resources": "^6.0.0", + "ember-velcro": "^2.1.0", "highlight.js": "^11.6.0", "highlightjs-glimmer": "^2.0.0", "tracked-built-ins": "^3.1.0" diff --git a/docs/components/select-field/demo/base-demo.md b/docs/components/select-field/demo/base-demo.md new file mode 100644 index 00000000..bafc5b15 --- /dev/null +++ b/docs/components/select-field/demo/base-demo.md @@ -0,0 +1,59 @@ +```hbs template + + {{#each this.colors as |color|}} + + {{/each}} + +``` + +```js component +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class extends Component { + colors = [ + { + label: "Blue", + name: "blue", + }, + { + label: "Green", + name: "green", + }, + { + label: "Yellow", + name: "yellow", + }, + { + label: "Orange", + name: "orange", + }, + { + label: "Red", + name: "red", + }, + { + label: "Purple", + name: "purple", + }, + { + label: "Teal", + name: "teal", + }, + ]; + + @action + onChange(values: string[]) { + console.log(values) + } +} +``` diff --git a/docs/components/select-field/index.md b/docs/components/select-field/index.md new file mode 100644 index 00000000..aa01d370 --- /dev/null +++ b/docs/components/select-field/index.md @@ -0,0 +1,325 @@ +# Select field + +Provides a Toucan-styled select with filtering that builds on top of the Field component. + +## Label + +Required. + +Use either the `@label` component argument or the `:label` named block. + +Provide a string to the `@label` component argument or content to the `:label` named block to render into the Label section of the Field. + +### `@label` + +```hbs + + + +``` + +### `:label` + +```hbs + + <:label>Here is a label + + + +``` + +## Hint + +Optional. + +Use either the `@hint` component argument or the `:hint` named block. + +Provide a string to the `@hint` component argument or content to `:hint` named block to render into the Hint section of the Field. + +### @hint + +```hbs + + + +``` + +### `:hint` + +```hbs + + <:hint>Here is a hint Link + + + +``` + +## Error + +Optional. + +Provide a string or array of strings to `@error` to render the text into the Error section of the Field. + +```hbs + +``` + +```hbs + + + +``` + +## `@onChange` + +Provide an `@onChange` callback to be notified when the user's selections have changed. +`@onChange` will receive an array of values as its only argument. + +```hbs + + + +``` + +```js +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + +export default class extends Component { + @action + onChange(values) { + console.log(values); + } +} +``` + +## Disabled State + +Set the `@isDisabled` argument to disable the input. + +## Read Only State + +Set the `@isReadOnly` argument to put the input in the read only state. + +## Attributes and Modifiers + +Consumers have direct access to the underlying [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input), so all attributes are supported. +Modifiers can also be added directly to the input as shown in the demo. + +## Test Selectors + +### Root Element + +Provide a custom selector via `@rootTestSelector`. +This test selector will be used as the value for the `data-root-field` attribute. +The Field can be targeted via: + +```hbs + +``` + +```js +assert.dom('[data-root-field="example"]'); +// targeting this field's specific label +assert.dom('[data-root-field="example"] > [data-label]'); +``` + +### Label + +Target the label element via `data-label`. + +### Hint + +Target the hint block via `data-hint`. + +### Error + +Target the error block via `data-error`. + +## UI States + +### SelectField with `@label` + +
+ + + + + +
+ +### SelectField with `@label` and `@hint` + +
+ + + + + +
+ +### SelectField with `:label` and `:hint` blocks + +
+ + <:label>Label + <:hint>Hint text link + <:default as |select|> + + + + + +
+ +### SelectField with `@label` and `@error` + +
+ + + + + +
+ +### SelectField with `@label`, `@hint`, and `@error` + +
+ + + + + +
+ +### SelectField with `@label` and `@isDisabled` + +
+ + + + + +
+ + +### SelectField with `@label`, `@value`, and `@isDisabled` + +
+ + + + + +
+ +### SelectField with `@label`, `@value`, `@isDisabled`, and `@initialSelectedValues` + +
+ + + + + +
+ +### SelectField with multiple errors + +
+ + + + + +
+ +### SelectField with `@isReadOnly` + +
+ + + + + +
+ +### SelectField with `@isReadOnly` and `@initialSelectedValues` + +
+ + + + + +
diff --git a/docs/components/select/demo/multiple.md b/docs/components/select/demo/multiple.md new file mode 100644 index 00000000..3f17bd0a --- /dev/null +++ b/docs/components/select/demo/multiple.md @@ -0,0 +1,78 @@ +```hbs template +

+ Multiple select +

+ +
+ + + {{#each this.colors as |color|}} + + {{/each}} + + + + + {{#each this.colors as |color|}} + + {{/each}} + +
+ +``` + +```js component +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + +export default class extends Component { + colors = [ + { + label: "Blue", + name: "blue", + }, + { + label: "Green", + name: "green", + }, + { + label: "Yellow", + name: "yellow", + }, + { + label: "Orange", + name: "orange", + }, + { + label: "Red", + name: "red", + }, + { + label: "Purple", + name: "purple", + }, + { + label: "Teal", + name: "teal", + }, + ]; + + @action + onChange(values: string[]) { + console.log(values) + } +} +``` diff --git a/docs/components/select/demo/single.md b/docs/components/select/demo/single.md new file mode 100644 index 00000000..ac364bce --- /dev/null +++ b/docs/components/select/demo/single.md @@ -0,0 +1,75 @@ +```hbs template +

+ Single select +

+ +
+ + + {{#each this.colors as |color|}} + + {{/each}} + + + + + {{#each this.colors as |color|}} + + {{/each}} + +
+``` + +```js component +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + +export default class extends Component { + colors = [ + { + label: "Blue", + name: "blue", + }, + { + label: "Green", + name: "green", + }, + { + label: "Yellow", + name: "yellow", + }, + { + label: "Orange", + name: "orange", + }, + { + label: "Red", + name: "red", + }, + { + label: "Purple", + name: "purple", + }, + { + label: "Teal", + name: "teal", + }, + ]; + + @action + onChange(values: string[]) { + console.log(values) + } +} +``` diff --git a/docs/components/select/index.md b/docs/components/select/index.md new file mode 100644 index 00000000..37f523f9 --- /dev/null +++ b/docs/components/select/index.md @@ -0,0 +1,15 @@ +# Select + +Provides a Toucan-styled select with filtering. +If you are building forms, you may be interested in the SelectField component instead. + +## `initialSelectedValues` + +To set values to be selected upon initial render, provide `@initialSelectedValues`. + +```hbs + + + + +``` diff --git a/packages/ember-toucan-core/babel.config.json b/packages/ember-toucan-core/babel.config.json index 739f8261..3d6b5125 100644 --- a/packages/ember-toucan-core/babel.config.json +++ b/packages/ember-toucan-core/babel.config.json @@ -3,6 +3,7 @@ "plugins": [ "@embroider/addon-dev/template-colocation-plugin", ["@babel/plugin-proposal-decorators", { "legacy": true }], - "@babel/plugin-proposal-class-properties" + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-private-methods" ] } diff --git a/packages/ember-toucan-core/package.json b/packages/ember-toucan-core/package.json index d6764d54..1a3e87fe 100644 --- a/packages/ember-toucan-core/package.json +++ b/packages/ember-toucan-core/package.json @@ -43,13 +43,16 @@ }, "dependencies": { "@babel/runtime": "^7.20.7", - "@embroider/addon-shim": "^1.0.0" + "@embroider/addon-shim": "^1.0.0", + "@floating-ui/dom": "^1.4.2", + "ember-velcro": "^2.1.0" }, "devDependencies": { "@babel/core": "^7.17.0", "@babel/eslint-parser": "^7.19.1", "@babel/plugin-proposal-class-properties": "^7.16.7", "@babel/plugin-proposal-decorators": "^7.17.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-syntax-decorators": "^7.17.0", "@babel/preset-typescript": "^7.18.6", "@crowdstrike/ember-toucan-styles": "^2.0.1", @@ -120,6 +123,7 @@ "./components/form/controls/file-input.js": "./dist/_app_/components/form/controls/file-input.js", "./components/form/controls/input.js": "./dist/_app_/components/form/controls/input.js", "./components/form/controls/radio.js": "./dist/_app_/components/form/controls/radio.js", + "./components/form/controls/select.js": "./dist/_app_/components/form/controls/select.js", "./components/form/controls/textarea.js": "./dist/_app_/components/form/controls/textarea.js", "./components/form/field.js": "./dist/_app_/components/form/field.js", "./components/form/fields/checkbox-group.js": "./dist/_app_/components/form/fields/checkbox-group.js", @@ -128,6 +132,7 @@ "./components/form/fields/input.js": "./dist/_app_/components/form/fields/input.js", "./components/form/fields/radio-group.js": "./dist/_app_/components/form/fields/radio-group.js", "./components/form/fields/radio.js": "./dist/_app_/components/form/fields/radio.js", + "./components/form/fields/select.js": "./dist/_app_/components/form/fields/select.js", "./components/form/fields/textarea.js": "./dist/_app_/components/form/fields/textarea.js", "./components/form/file-input/delete-button.js": "./dist/_app_/components/form/file-input/delete-button.js", "./components/form/file-input/list-item.js": "./dist/_app_/components/form/file-input/list-item.js", diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs new file mode 100644 index 00000000..86c17181 --- /dev/null +++ b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs @@ -0,0 +1,34 @@ +{{! template-lint-disable require-presentational-children }} +
  • +
  • \ No newline at end of file diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts new file mode 100644 index 00000000..890c7301 --- /dev/null +++ b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts @@ -0,0 +1,58 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +import Check from '../../../../../-private/icons/check'; + +// TODO: Should the directory structure of `-private` mirror the directory structure of the related component? +// Or should we simply put this subcomponent on the top level of `-private`? + +interface ToucanFormSelectOptionControlComponentSignature { + Args: { + activeOption: { label: string; value: string } | null; + label: string; + onClick: (value: string) => void; + onMouseover: (value: string) => void; + selectedOptions: { label: string; value: string }[]; + value: string; + }; + Blocks: { + default: []; + }; + Element: HTMLInputElement; +} + +const className = 'toucan-form-select-option-control'; + +export const selector = `.${className}`; + +export default class ToucanFormSelectOptionControlComponent extends Component { + @tracked isPopoverOpen = false; + + className = className; + Check = Check; + + get isActive() { + return this.args.value === this.args.activeOption?.value; + } + + get isSelected() { + return this.args.selectedOptions.some( + (option) => option.value === this.args.value + ); + } + + @action + onClick(value: string, event: Event) { + // Both "click" and "mousedown" steal focus, which we want to remain on the input. + event.preventDefault(); + + this.args.onClick(value); + } + + @action + onMousedown(event: Event) { + // Both "click" and "mousedown" steal focus, which we want to remain on the input. + event.preventDefault(); + } +} diff --git a/packages/ember-toucan-core/src/-private/icons/check.hbs b/packages/ember-toucan-core/src/-private/icons/check.hbs new file mode 100644 index 00000000..b00179bc --- /dev/null +++ b/packages/ember-toucan-core/src/-private/icons/check.hbs @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/packages/ember-toucan-core/src/-private/icons/check.ts b/packages/ember-toucan-core/src/-private/icons/check.ts new file mode 100644 index 00000000..35ad733f --- /dev/null +++ b/packages/ember-toucan-core/src/-private/icons/check.ts @@ -0,0 +1,11 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export interface ToucanCheckIconComponentSignature { + Args: {}; + Blocks: { + default: never; + }; + Element: SVGElement; +} + +export default templateOnlyComponent(); diff --git a/packages/ember-toucan-core/src/-private/icons/chevron.hbs b/packages/ember-toucan-core/src/-private/icons/chevron.hbs new file mode 100644 index 00000000..5fafbac0 --- /dev/null +++ b/packages/ember-toucan-core/src/-private/icons/chevron.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/ember-toucan-core/src/-private/icons/chevron.ts b/packages/ember-toucan-core/src/-private/icons/chevron.ts new file mode 100644 index 00000000..f69e885e --- /dev/null +++ b/packages/ember-toucan-core/src/-private/icons/chevron.ts @@ -0,0 +1,11 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export interface ToucanChevronIconComponentSignature { + Args: {}; + Blocks: { + default: never; + }; + Element: SVGElement; +} + +export default templateOnlyComponent(); diff --git a/packages/ember-toucan-core/src/components/form/controls/select.hbs b/packages/ember-toucan-core/src/components/form/controls/select.hbs new file mode 100644 index 00000000..48e8b298 --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/controls/select.hbs @@ -0,0 +1,68 @@ + +
    + + + +
    + + {{! TODO + + Another small trade-off to consider when deciding whether not supporting `@options` is worth it. + + In the case when `@isDisabled` is falsy, we can query the DOM after the popover has been opened (and thus inserted into the DOM) + to set `this.activeOption` and `this.selectedOptions`. + + In the case when `@isDisabled` is `true`, however, the popover would conventionally never be inserted in the DOM. + `this.activeOption` doesn't come into play when `@isDisabled` is `true`. But `this.selectedOptions` does because its used + to set the INPUT's value when `@isMultiple` is falsy and the pills when `@isMultiple` is `true`. + + The latter case is why the UL isn't wrapped in a conditonal based on `this.isPopoverOpen`. It has to remain in the DOM so that + it's queryable. + }} +
      + {{yield + (hash + Option=(component + (ensure-safe-component this.Option) + activeOption=this.activeOption + onMouseover=this.onOptionMouseover + onClick=this.onChange + selectedOptions=this.selectedOptions + ) + ) + }} +
    +
    \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts new file mode 100644 index 00000000..8653f866 --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -0,0 +1,498 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { assert } from '@ember/debug'; +import { action } from '@ember/object'; +import { guidFor } from '@ember/object/internals'; +import { next } from '@ember/runloop'; + +import { offset, size } from '@floating-ui/dom'; + +import Option, { + selector as optionSelector, +} from '../../../-private/components/form/controls/select/option'; +import Chevron from '../../../-private/icons/chevron'; + +import type { Middleware as VelcroMiddleware } from '@floating-ui/dom'; +import type { WithBoundArgs } from '@glint/template'; + +export interface ToucanFormSelectControlComponentSignature { + Args: { + /** + * Sets the input to an errored-state via styling. + */ + hasError?: boolean; + + /** + * Sets the values to be selected on render. + */ + initialSelectedValues?: string[]; + + /** + * Sets the `disabled` attribute of the input. + */ + isDisabled?: boolean; + + /** + * Sets the `readonly` attribute of the input. + */ + isReadOnly?: boolean; + + /** + * Set to allow multiple option to be selected at once. + */ + isMultiple?: boolean; + + // PR: callback type doesn't fit here as well as elsewhere. different events here. they don't make as much sense to pass along to the consumer. ditch callback type altogether? + /** + * The function called when a new selection is made. + */ + onChange?: (values: string[]) => void; + + /** + * A CSS class to add to the popover. + * Commonly used to specify a `z-index`. + */ + popoverClass?: string; + }; + Blocks: { + default: [ + { + Option: WithBoundArgs< + typeof Option, + 'activeOption' | 'onMouseover' | 'onClick' | 'selectedOptions' + >; + } + ]; + }; + Element: HTMLInputElement; +} + +type SelectedOption = { + label: string; + value: string; +}; + +// TODO +// +// The foundational work is done. And adding supporting for `@isMultiple` should (😬) be straightforward. +// There's still a decent amount of work left. Off the top of my head: +// +// - need final decisions from Mary on how filtering behaves with the single-select +// - need final visual design +// - tests +// - test with a screenreader +// - add loading feedback? +// - support for loading feedback? +// - probably a bunch of little things and a few bugs +// - `this.args.popoverClass` continues to bum me out 🤷‍♂️ + +// SOMETHING TO CONSIDER? +// +// This component's API is different from the other components in that consumers only set its +// initial value (via `this.args.initialSelectedValues`). We set `this.selectedOptions` based off of `this.args.initialSelectedValues` +// and from then on maintain the state of `this.selectedOptions` ourselves. We do this because we need to know when selections change +// so that we can go digging through the DOM to put together an updated array of labels and values that make up `this.selectedOptions`. +// +// This is a consequence of our API design, where we don't support an `this.args.options` array and instead let the consumer +// provide options freely in the template. If we supported `this.args.options` instead, we'd have no need to go digging through the +// DOM for labels and values and thus could let the consumer maintain what is selected and what isn't. +// +// There's also the changeset concern you brought up. Are changesets akin to controlled components in React? If so, I wonder if how +// we've implemented this component won't be incompatbile with changesets in spirit, if not practically. Managing selection state +// internally means consuming components won't control it. + +export default class ToucanFormSelectControlComponent extends Component { + constructor( + owner: unknown, + args: ToucanFormSelectControlComponentSignature['Args'] + ) { + super(owner, args); + + if (this.args.initialSelectedValues !== undefined) { + assert( + '`this.args.initialSelectedValues.length` can contain at most one value when `this.args.isMultiple` is falsy', + !(!this.args.isMultiple && this.args.initialSelectedValues.length > 1) + ); + } + + // PR: These functions can't be called until after first render because they query the DOM. + // Is there a better runloop callback to use? Haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalp. + next(() => { + // PR: Because they assert, these functions will throw if the consumer doesn't render options in the `{{yield}}`. + // What's the Ember way to ensure options are passed? + this.activeOption = this.#getActiveOption('[first]'); + + if ( + this.args.initialSelectedValues !== undefined && + this.args.initialSelectedValues.length > 0 + ) { + this.selectedOptions = this.#getSelectedOptions( + this.args.initialSelectedValues + ); + } + }); + } + + /** + * An active option is one that the user has soft-interacted with either by arrowing to it or hovering over it. + * `null` before render and after `null` render if the consumer doesn't pass options in the template. + **/ + @tracked activeOption: { + index: number; + label: string; + value: string; + } | null = null; + + @tracked isPopoverOpen = false; + + /** + * Selected options are options that the user has hard-interacted with by either pressing Enter on them or by clicking them. + * There's an assertion in the constructor to ensure that at most on selected option is allowed when `this.args.isMultiple` + * is falsy. + **/ + @tracked selectedOptions: SelectedOption[] = []; + + activeDescendantId = ''; + Chevron = Chevron; + Option = Option; + popoverId = `popover--${guidFor(this)}`; + + velcroMiddleware: VelcroMiddleware[] = [ + offset({ + mainAxis: 8, + }), + size({ + apply({ rects, elements }) { + Object.assign(elements.floating.style, { + width: `${rects.reference.width}px`, + }); + }, + }), + ]; + + get isDisabledOrReadOnly() { + return this.args.isDisabled || this.args.isReadOnly; + } + + get styles() { + let { isDisabled, isReadOnly, hasError } = this.args; + + if (isDisabled) { + return 'shadow-focusable-outline bg-overlay-1 text-disabled pointer-events-none'; + } + + if (isReadOnly) { + return 'focus:shadow-focus-outline bg-surface-xl shadow-read-only-outline text-titles-and-attributes'; + } + + if (hasError) { + return 'shadow-error-outline focus:shadow-error-focus-outline bg-overlay-1 text-titles-and-attributes'; + } + + return 'shadow-focusable-outline focus:shadow-focus-outline bg-overlay-1 text-titles-and-attributes'; + } + + /** + * @value A preset position that is a string literal or an option value that is a string. + * The former exists for the keyboard case (ArrowDown, etc.), where the previous or next + * active value isn't known by the input handler. + */ + #getActiveOption( + value: '[first]' | '[previous]' | '[next]' | '[last]' | string + ) { + assert( + "`value` cannot be `'[previous]'` if `this.activeOption` is `null`", + !(this.activeOption === null && value === '[previous]') + ); + + assert( + "`value` cannot be `'[next]'` if `this.activeOption` is `null`", + !(this.activeOption === null && value === '[next]') + ); + + const optionElements = document.querySelectorAll( + `#${this.popoverId} ${optionSelector}` + ); + + if (optionElements.length === 0) { + return null; + } + + let newActiveIndex: number; + + if (value === '[first]') { + newActiveIndex = 0; + } else if (value === '[last]') { + newActiveIndex = optionElements.length - 1; + } else if (value === '[previous]' || value === '[next]') { + const currentActiveIndex = Array.from(optionElements).findIndex( + (option) => { + assert( + '`option` must be an instance of `HTMLElement`', + option instanceof HTMLElement + ); + + return option.dataset['value'] === this.activeOption?.value; + } + ); + + newActiveIndex = + value === '[previous]' && currentActiveIndex === 0 + ? currentActiveIndex + : value === '[previous]' + ? currentActiveIndex - 1 + : value === '[next]' && + currentActiveIndex === optionElements.length - 1 + ? currentActiveIndex + : currentActiveIndex + 1; + } else { + newActiveIndex = Array.from(optionElements).findIndex((option) => { + assert( + '`option` must be an instance of `HTMLElement`', + option instanceof HTMLElement + ); + + return option.dataset['value'] === value; + }); + } + + const newActiveElement = optionElements[newActiveIndex]; + + assert( + '`newActiveElement` must be an instance of `HTMLElement`', + newActiveElement instanceof HTMLElement + ); + + const newActiveValue = newActiveElement.dataset['value']; + + assert( + '`newActiveValue` must be a `string`', + typeof newActiveValue === 'string' + ); + + return { + index: newActiveIndex, + label: newActiveValue, + value: newActiveValue, + }; + } + + /** + * @values Option values to add or remove depending on whether they're present in `this.selectedOptions`. + * Having `values` work this way adds complexity here but simplifies things for the internal consumer. + * Internal consumers, such as `this.onOptionClick`, don't need to figure out if they `value` they're dealing + * with should be selected or deselected. + * + * @todo The current behavior is deselect an already selected option when it's selected. I suggested this + * behavior to the team but we've yet to make a decision (go figure). You might want to get a final decision + * from Mary before opening a PR. + **/ + #getSelectedOptions(values: string[]): SelectedOption[] { + const allOptionsElements = document.querySelectorAll( + `#${this.popoverId} ${optionSelector}` + ); + + // When `this.args.isMultiple` is `true`, we want to keep every option in `this.selectedOptions` except + // those whose `value` is found in `values`. Those we want to remove because they represent deselections. + // When `this.args.isMultiple` is falsy, we want to discard every selected option be selected at a time. + const selectedOptions = this.args.isMultiple + ? this.selectedOptions.filter((option) => + values.every((value) => value !== option.value) + ) + : []; + + const filteredValues = values.filter((value) => { + return this.selectedOptions.every((option) => option.value !== value); + }); + + for (const value of filteredValues) { + const element = Array.from(allOptionsElements).find((element) => { + assert( + '`element` must be an instance of `HTMLElement`', + element instanceof HTMLElement + ); + + return element.dataset['value'] === value; + }); + + assert( + '`element` must be an instance of `HTMLElement`', + element instanceof HTMLElement + ); + + const label = element.dataset['label']; + + assert('`label` must be a `string`', typeof label === 'string'); + + selectedOptions.push({ + label, + value, + }); + } + + return selectedOptions; + } + + #scrollActiveOptionIntoView(alignToTop?: boolean) { + assert('`activeOption` cannot be `null`', this.activeOption !== null); + + const optionsElements = document.querySelectorAll( + `#${this.popoverId} ${optionSelector}` + ); + const optionElement = optionsElements[this.activeOption.index]; + + assert( + '`optionElement` an instance of `HTMLElement`', + optionElement instanceof HTMLElement + ); + + optionElement.scrollIntoView(alignToTop); + } + + @action + closePopover() { + this.isPopoverOpen = false; + } + + @action + onInput(event: Event) { + assert( + '`event.target` an instance of `HTMLInputElement`', + event.target instanceof HTMLInputElement + ); + + const options = document.querySelectorAll( + `#${this.popoverId} ${optionSelector}` + ); + + let firstFilteredValue: string | null = null; + let isCurrentActiveOptionFilteredOut = true; + + // We go through all the options that we've queried the DOM for. Then we show those + // that meet our simple filtering criterion and hide those that don't. + for (const option of options) { + assert( + '`option` must be an instance of `HTMLElement`', + option instanceof HTMLElement + ); + + assert( + "`option.dataset['value']` must be a `string`", + typeof option.dataset['value'] === 'string' + ); + + const isShow = + event.target.value === '' || + option.dataset['value'] + .toLowerCase() + .startsWith(event.target.value.toLowerCase().trim()); + + // It doesn't feel great doing stuff this imperative, especially on subcomponents, whose + // styling and implementation details should unknowable and untouched by the parent component. + // But I can't think of a better way. + option.style.display = isShow ? '' : 'none'; + + if (isShow) { + firstFilteredValue = option.dataset['value']; + } + + if (isShow && option.dataset['value'] === this.activeOption?.value) { + isCurrentActiveOptionFilteredOut = false; + } + } + + if (isCurrentActiveOptionFilteredOut && firstFilteredValue !== null) { + this.activeOption = this.#getActiveOption(firstFilteredValue); + } + } + + @action + noop() { + // eslint-disable @typescript-eslint/no-empty-function + // PR: This rather than mess around with Glint and `ember-composable-functions`. Is there a better way? + } + + @action + onChange(value: string) { + this.selectedOptions = this.#getSelectedOptions([value]); + this.closePopover(); + + const selectedValues = this.selectedOptions.map(({ value }) => value); + + this.args.onChange?.(selectedValues); + } + + @action + onKeydown(event: KeyboardEvent) { + if (!this.isPopoverOpen) { + this.openPopover(); + + return; + } + + if (event.key === 'Escape') { + this.closePopover(); + + return; + } + + if (event.key === 'Enter' && this.activeOption !== null) { + this.onChange(this.activeOption.value); + + return; + } + + if ( + (event.key === 'ArrowDown' && event.metaKey) || + event.key === 'PageDown' || + event.key === 'End' + ) { + // By default, arrowing up and down moves the insertion point to the beginning or end + // of an input field. We don't want this. We want to reserve arrowing for moving + // vertically through the list. + event.preventDefault(); + this.activeOption = this.#getActiveOption('[last]'); + this.#scrollActiveOptionIntoView(false); + + return; + } + + if (event.key === 'ArrowDown') { + event.preventDefault(); + this.activeOption = this.#getActiveOption('[next]'); + this.#scrollActiveOptionIntoView(false); + + return; + } + + if ( + (event.key === 'ArrowUp' && event.metaKey) || + event.key === 'PageUp' || + event.key === 'Home' + ) { + event.preventDefault(); + this.activeOption = this.#getActiveOption('[first]'); + this.#scrollActiveOptionIntoView(); + + return; + } + + if (event.key === 'ArrowUp') { + event.preventDefault(); + this.activeOption = this.#getActiveOption('[previous]'); + this.#scrollActiveOptionIntoView(); + + return; + } + } + + @action + onOptionMouseover(value: string) { + this.activeOption = this.#getActiveOption(value); + } + + @action + openPopover() { + this.isPopoverOpen = true; + } +} diff --git a/packages/ember-toucan-core/src/components/form/fields/select.hbs b/packages/ember-toucan-core/src/components/form/fields/select.hbs new file mode 100644 index 00000000..c7ec373a --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/fields/select.hbs @@ -0,0 +1,61 @@ +
    + + {{#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}} + + + {{yield select}} + + + + {{#if @error}} + + {{/if}} + +
    \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/fields/select.ts b/packages/ember-toucan-core/src/components/form/fields/select.ts new file mode 100644 index 00000000..2f1282e3 --- /dev/null +++ b/packages/ember-toucan-core/src/components/form/fields/select.ts @@ -0,0 +1,83 @@ +import Component from '@glimmer/component'; + +import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; + +import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; +import type { ErrorMessage } from '../../../-private/types'; +import type { ToucanFormSelectControlComponentSignature } from '../controls/select'; + +export interface ToucanFormSelectFieldComponentSignature { + 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 values to be selected on render. + */ + initialSelectedValues?: string[]; + + /** + * Sets the disabled attribute on the input. + */ + isDisabled?: boolean; + + /** + * Set to allow multiple option to be selected at once. + */ + isMultiple?: 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 a new selection is made. + */ + onChange?: (values: string[]) => void; + + /** + * A CSS class to add to the popover. + * Commonly used to specify a `z-index`. + */ + popoverClass?: string; + + /** + * A test selector for targeting the root element of the field. + * In this case, the wrapping div element. + */ + rootTestSelector?: string; + }; + Blocks: { + default: ToucanFormSelectControlComponentSignature['Blocks']['default']; + label: []; + hint: []; + }; +} + +export default class ToucanFormInputFieldComponent extends Component { + assertBlockOrArgumentExists = ({ + blockExists, + argName, + arg, + isRequired, + }: AssertBlockOrArg) => + assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired }); + + get hasError() { + return Boolean(this.args?.error); + } +} diff --git a/packages/ember-toucan-core/src/template-registry.ts b/packages/ember-toucan-core/src/template-registry.ts index 24694ea4..cb0f1786 100644 --- a/packages/ember-toucan-core/src/template-registry.ts +++ b/packages/ember-toucan-core/src/template-registry.ts @@ -4,6 +4,7 @@ import type CheckboxControlComponent from './components/form/controls/checkbox'; import type FormControlsFileInputComponent from './components/form/controls/file-input'; import type InputControlComponent from './components/form/controls/input'; import type RadioControlComponent from './components/form/controls/radio'; +import type SelectControlComponent from './components/form/controls/select'; import type TextareaControlComponent from './components/form/controls/textarea'; import type FieldComponent from './components/form/field'; import type CheckboxFieldComponent from './components/form/fields/checkbox'; @@ -26,6 +27,7 @@ export default interface Registry { 'Form::Controls::FileInput': typeof FormControlsFileInputComponent; 'Form::Controls::Input': typeof InputControlComponent; 'Form::Controls::Radio': typeof RadioControlComponent; + 'Form::Controls::Select': typeof SelectControlComponent; 'Form::Controls::Textarea': typeof TextareaControlComponent; 'Form::FileInput::List': typeof FormFileInputListComponent; 'Form::FileInput::DeleteButton': typeof FormFileInputDeleteButtonComponent; diff --git a/packages/ember-toucan-core/tsconfig.json b/packages/ember-toucan-core/tsconfig.json index b2dd193a..71ea6520 100644 --- a/packages/ember-toucan-core/tsconfig.json +++ b/packages/ember-toucan-core/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@tsconfig/ember/tsconfig.json", "include": ["src/**/*", "unpublished-development-types/**/*"], + "exclude": ["src/index.js"], // TODO: Works around a local issue. Note to self: Don't include it in your PR. "glint": { "environment": "ember-loose" } 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 3af066b6..eb266d25 100644 --- a/packages/ember-toucan-core/unpublished-development-types/index.d.ts +++ b/packages/ember-toucan-core/unpublished-development-types/index.d.ts @@ -9,6 +9,7 @@ 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 @@ -27,7 +28,9 @@ declare class EnsureSafeComponentHelper< }> {} declare module '@glint/environment-ember-loose/registry' { - export default interface Registry extends ToucanCoreRegistry { + export default interface Registry + extends ToucanCoreRegistry, + EmberVelcroRegistry { // local entries 'ensure-safe-component': typeof EnsureSafeComponentHelper; diff --git a/packages/ember-toucan-form/src/-private/select-field.hbs b/packages/ember-toucan-form/src/-private/select-field.hbs new file mode 100644 index 00000000..dfa81fce --- /dev/null +++ b/packages/ember-toucan-form/src/-private/select-field.hbs @@ -0,0 +1,87 @@ +{{! + Regarding Conditionals + + This looks really messy, but Form::Fields::Select exposes named blocks; HOWEVER, + we cannot conditionally render named blocks due to https://github.com/emberjs/rfcs/issues/735. + + We *can* conditionally render components though, based on the blocks and argument combinations + users provide us. This is very brittle, but until https://github.com/emberjs/rfcs/issues/735 + is resolved and a solution is found, this appears to be the only way to truly expose + conditional named blocks. + + --- + + Regarding glint-expect-error + + "@onChange" of the input only expects a string typed value, but field.setValue is generic, + accepting anything that DATA[KEY] could be. Similar case with "@value", but there casting to + a string is easy. +}} +<@form.Field @name={{@name}} as |field|> + {{#if (this.hasOnlyLabelBlock (has-block 'label') (has-block 'hint'))}} + + <:label>{{yield to='label'}} + + {{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint')) + }} + + <:label>{{yield to='label'}} + <:hint>{{yield to='hint'}} + + {{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}} + + <:hint>{{yield to='hint'}} + + {{else}} + {{! Argument-only case }} + + {{/if}} + \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/select-field.ts b/packages/ember-toucan-form/src/-private/select-field.ts new file mode 100644 index 00000000..8a25c8f1 --- /dev/null +++ b/packages/ember-toucan-form/src/-private/select-field.ts @@ -0,0 +1,63 @@ +import Component from '@glimmer/component'; +import { assert } from '@ember/debug'; +import { action } from '@ember/object'; + +import type { HeadlessFormBlock, UserData } from './types'; +import type { ToucanFormSelectFieldComponentSignature as BaseSelectFieldSignature } from '@crowdstrike/ember-toucan-core/components/form/fields/select'; +import type { FormData, FormKey, ValidationError } from 'ember-headless-form'; + +export interface ToucanFormSelectFieldComponentSignature< + DATA extends UserData, + KEY extends FormKey> = FormKey> +> { + Element: HTMLInputElement; + Args: Omit< + BaseSelectFieldSignature['Args'], + // TODO: `value` doesn't apply. But how to handle Select with changeset? + 'error' | 'onChange' + > & { + /** + * The name of your field, which must match a property of the `@data` passed to the form + */ + name: KEY; + + /* + * @internal + */ + form: HeadlessFormBlock; + }; + Blocks: BaseSelectFieldSignature['Blocks']; +} + +export default class ToucanFormSelectFieldComponent< + DATA extends UserData, + KEY extends FormKey> = FormKey> +> extends Component> { + hasOnlyLabelBlock = (hasLabel: boolean, hasHint: boolean) => + hasLabel && !hasHint; + hasHintAndLabelBlocks = (hasLabel: boolean, hasHint: boolean) => + hasLabel && hasHint; + hasLabelArgAndHintBlock = (hasLabel: string | undefined, hasHint: boolean) => + hasLabel && hasHint; + + mapErrors = (errors?: ValidationError[]) => { + if (!errors) { + return; + } + + // @todo we need to figure out what to do when message is undefined + return errors.map((error) => error.message ?? error.type); + }; + + @action + assertString(value: unknown): string | undefined { + assert( + `Only string values are expected for ${String( + this.args.name + )}, but you passed ${typeof value}`, + typeof value === 'undefined' || typeof value === 'string' + ); + + return value; + } +} diff --git a/packages/ember-toucan-form/src/components/toucan-form.hbs b/packages/ember-toucan-form/src/components/toucan-form.hbs index 83ee07cc..f5382f67 100644 --- a/packages/ember-toucan-form/src/components/toucan-form.hbs +++ b/packages/ember-toucan-form/src/components/toucan-form.hbs @@ -27,6 +27,9 @@ RadioGroup=(component (ensure-safe-component this.RadioGroupFieldComponent) form=form ) + Select=(component + (ensure-safe-component this.SelectFieldComponent) form=form + ) Textarea=(component (ensure-safe-component this.TextareaFieldComponent) form=form ) diff --git a/packages/ember-toucan-form/src/components/toucan-form.ts b/packages/ember-toucan-form/src/components/toucan-form.ts index 3a9e64cf..fa5ee025 100644 --- a/packages/ember-toucan-form/src/components/toucan-form.ts +++ b/packages/ember-toucan-form/src/components/toucan-form.ts @@ -5,6 +5,7 @@ import CheckboxGroupFieldComponent from '../-private/checkbox-group-field'; import FileInputFieldComponent from '../-private/file-input-field'; import InputFieldComponent from '../-private/input-field'; import RadioGroupFieldComponent from '../-private/radio-group-field'; +import SelectFieldComponent from '../-private/select-field'; import TextareaFieldComponent from '../-private/textarea-field'; import type { HeadlessFormBlock, UserData } from '../-private/types'; @@ -37,6 +38,7 @@ export interface ToucanFormComponentSignature< typeof RadioGroupFieldComponent, 'form' >; + Select: WithBoundArgs, 'form'>; Textarea: WithBoundArgs, 'form'>; /** @@ -68,6 +70,7 @@ export default class ToucanFormComponent< FileInputFieldComponent = FileInputFieldComponent; InputFieldComponent = InputFieldComponent; RadioGroupFieldComponent = RadioGroupFieldComponent; + SelectFieldComponent = SelectFieldComponent; TextareaFieldComponent = TextareaFieldComponent; get validateOn() { From 0ce8422c2b437f50b52b57ee37caf383f5119708 Mon Sep 17 00:00:00 2001 From: clintcs <114178960+clintcs@users.noreply.github.com> Date: Wed, 28 Jun 2023 09:36:20 -0700 Subject: [PATCH 02/86] fix: add placeholder styling --- .../ember-toucan-core/src/components/form/controls/select.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index 8653f866..3bb7a6c9 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -178,11 +178,11 @@ export default class ToucanFormSelectControlComponent extends Component Date: Wed, 28 Jun 2023 09:46:12 -0700 Subject: [PATCH 03/86] chore: post-rebase tweaks --- packages/ember-toucan-core/src/-private/icons/check.ts | 2 +- packages/ember-toucan-core/src/-private/icons/chevron.ts | 2 +- .../src/-private/{components/lock-icon.hbs => icons/lock.hbs} | 0 .../src/-private/{components/lock-icon.ts => icons/lock.ts} | 4 ++-- .../src/components/form/fields/checkbox-group.ts | 2 +- .../ember-toucan-core/src/components/form/fields/checkbox.ts | 2 +- .../src/components/form/fields/file-input.ts | 2 +- .../ember-toucan-core/src/components/form/fields/input.ts | 2 +- .../src/components/form/fields/radio-group.ts | 2 +- .../ember-toucan-core/src/components/form/fields/textarea.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) rename packages/ember-toucan-core/src/-private/{components/lock-icon.hbs => icons/lock.hbs} (100%) rename packages/ember-toucan-core/src/-private/{components/lock-icon.ts => icons/lock.ts} (52%) diff --git a/packages/ember-toucan-core/src/-private/icons/check.ts b/packages/ember-toucan-core/src/-private/icons/check.ts index 35ad733f..3fa3b84c 100644 --- a/packages/ember-toucan-core/src/-private/icons/check.ts +++ b/packages/ember-toucan-core/src/-private/icons/check.ts @@ -3,7 +3,7 @@ import templateOnlyComponent from '@ember/component/template-only'; export interface ToucanCheckIconComponentSignature { Args: {}; Blocks: { - default: never; + default: []; }; Element: SVGElement; } diff --git a/packages/ember-toucan-core/src/-private/icons/chevron.ts b/packages/ember-toucan-core/src/-private/icons/chevron.ts index f69e885e..27d3ec96 100644 --- a/packages/ember-toucan-core/src/-private/icons/chevron.ts +++ b/packages/ember-toucan-core/src/-private/icons/chevron.ts @@ -3,7 +3,7 @@ import templateOnlyComponent from '@ember/component/template-only'; export interface ToucanChevronIconComponentSignature { Args: {}; Blocks: { - default: never; + default: []; }; Element: SVGElement; } diff --git a/packages/ember-toucan-core/src/-private/components/lock-icon.hbs b/packages/ember-toucan-core/src/-private/icons/lock.hbs similarity index 100% rename from packages/ember-toucan-core/src/-private/components/lock-icon.hbs rename to packages/ember-toucan-core/src/-private/icons/lock.hbs diff --git a/packages/ember-toucan-core/src/-private/components/lock-icon.ts b/packages/ember-toucan-core/src/-private/icons/lock.ts similarity index 52% rename from packages/ember-toucan-core/src/-private/components/lock-icon.ts rename to packages/ember-toucan-core/src/-private/icons/lock.ts index e5789940..4381eea5 100644 --- a/packages/ember-toucan-core/src/-private/components/lock-icon.ts +++ b/packages/ember-toucan-core/src/-private/icons/lock.ts @@ -1,6 +1,6 @@ import templateOnlyComponent from '@ember/component/template-only'; -export interface ToucanLockIconComponentSignature { +export interface ToucanLockIconSignature { Element: SVGElement; Args: {}; Blocks: { @@ -8,4 +8,4 @@ export interface ToucanLockIconComponentSignature { }; } -export default templateOnlyComponent(); +export default templateOnlyComponent(); 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.ts index 8f2f28f9..ef588fba 100644 --- a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts +++ b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts @@ -2,7 +2,7 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/components/lock-icon'; +import LockIcon from '../../../-private/icons/lock'; import CheckboxFieldComponent from './checkbox'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox.ts b/packages/ember-toucan-core/src/components/form/fields/checkbox.ts index 59385374..e8e389de 100644 --- a/packages/ember-toucan-core/src/components/form/fields/checkbox.ts +++ b/packages/ember-toucan-core/src/components/form/fields/checkbox.ts @@ -2,7 +2,7 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/components/lock-icon'; +import LockIcon from '../../../-private/icons/lock'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ErrorMessage } from '../../../-private/types'; 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 index a011d4d1..d48ce40e 100644 --- a/packages/ember-toucan-core/src/components/form/fields/file-input.ts +++ b/packages/ember-toucan-core/src/components/form/fields/file-input.ts @@ -3,7 +3,7 @@ import { assert } from '@ember/debug'; import { action } from '@ember/object'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/components/lock-icon'; +import LockIcon from '../../../-private/icons/lock'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ErrorMessage } from '../../../-private/types'; diff --git a/packages/ember-toucan-core/src/components/form/fields/input.ts b/packages/ember-toucan-core/src/components/form/fields/input.ts index 334f2179..c77680c9 100644 --- a/packages/ember-toucan-core/src/components/form/fields/input.ts +++ b/packages/ember-toucan-core/src/components/form/fields/input.ts @@ -4,7 +4,7 @@ import { assert } from '@ember/debug'; import { action } from '@ember/object'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/components/lock-icon'; +import LockIcon from '../../../-private/icons/lock'; import CharacterCount from '../../../components/form/controls/character-count'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; 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.ts index 829d7a07..3d24866a 100644 --- a/packages/ember-toucan-core/src/components/form/fields/radio-group.ts +++ b/packages/ember-toucan-core/src/components/form/fields/radio-group.ts @@ -2,7 +2,7 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/components/lock-icon'; +import LockIcon from '../../../-private/icons/lock'; import RadioFieldComponent from './radio'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; diff --git a/packages/ember-toucan-core/src/components/form/fields/textarea.ts b/packages/ember-toucan-core/src/components/form/fields/textarea.ts index 6f91eebd..efffb641 100644 --- a/packages/ember-toucan-core/src/components/form/fields/textarea.ts +++ b/packages/ember-toucan-core/src/components/form/fields/textarea.ts @@ -4,7 +4,7 @@ import { assert } from '@ember/debug'; import { action } from '@ember/object'; import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists'; -import LockIcon from '../../../-private/components/lock-icon'; +import LockIcon from '../../../-private/icons/lock'; import CharacterCount from '../../../components/form/controls/character-count'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; From 6b70b36c315183eec50d1484f82a98b2c742bc56 Mon Sep 17 00:00:00 2001 From: clintcs <114178960+clintcs@users.noreply.github.com> Date: Wed, 28 Jun 2023 09:59:47 -0700 Subject: [PATCH 04/86] fix: Escape should never open the popover --- .../src/components/form/controls/select.hbs | 6 ++++-- .../src/components/form/controls/select.ts | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.hbs b/packages/ember-toucan-core/src/components/form/controls/select.hbs index 48e8b298..4d6b4fc9 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.hbs +++ b/packages/ember-toucan-core/src/components/form/controls/select.hbs @@ -7,13 +7,15 @@ Date: Wed, 28 Jun 2023 10:16:03 -0700 Subject: [PATCH 05/86] chore: add note about scrollbar styling --- .../ember-toucan-core/src/components/form/controls/select.hbs | 2 +- .../ember-toucan-core/src/components/form/controls/select.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.hbs b/packages/ember-toucan-core/src/components/form/controls/select.hbs index 4d6b4fc9..f93647be 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.hbs +++ b/packages/ember-toucan-core/src/components/form/controls/select.hbs @@ -48,7 +48,7 @@ it's queryable. }}
      Date: Wed, 28 Jun 2023 10:31:44 -0700 Subject: [PATCH 06/86] fix: shift arrow over --- .../ember-toucan-core/src/components/form/controls/select.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.hbs b/packages/ember-toucan-core/src/components/form/controls/select.hbs index f93647be..2d9a74e5 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.hbs +++ b/packages/ember-toucan-core/src/components/form/controls/select.hbs @@ -28,7 +28,7 @@ /> From 1d9469f22257c44c7beb4ae56d908d62a28eb233 Mon Sep 17 00:00:00 2001 From: clintcs <114178960+clintcs@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:50:25 -0700 Subject: [PATCH 07/86] chore: styling tweaks --- docs/components/select/demo/multiple.md | 2 +- docs/components/select/demo/single.md | 2 +- .../components/form/controls/select/option.hbs | 3 ++- .../components/form/controls/select/option.ts | 12 ++++++++++++ .../src/components/form/controls/select.hbs | 2 +- .../src/components/form/controls/select.ts | 8 +++----- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/components/select/demo/multiple.md b/docs/components/select/demo/multiple.md index 3f17bd0a..bf99f2c5 100644 --- a/docs/components/select/demo/multiple.md +++ b/docs/components/select/demo/multiple.md @@ -3,7 +3,7 @@ Multiple select -
      + - + Date: Wed, 28 Jun 2023 10:55:50 -0700 Subject: [PATCH 08/86] fix: add z-index to demo --- docs/components/select-field/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components/select-field/index.md b/docs/components/select-field/index.md index aa01d370..72f19467 100644 --- a/docs/components/select-field/index.md +++ b/docs/components/select-field/index.md @@ -194,7 +194,7 @@ Target the error block via `data-error`. ### SelectField with `:label` and `:hint` blocks
      - + <:label>Label <:hint>Hint text link <:default as |select|> From 7bd75dcfc71e1c61d8cfd48d3e5e397c40217489 Mon Sep 17 00:00:00 2001 From: clintcs <114178960+clintcs@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:59:04 -0700 Subject: [PATCH 09/86] fix: styling tweaks --- .../src/-private/components/form/controls/select/option.hbs | 4 ++-- .../ember-toucan-core/src/components/form/controls/select.hbs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs index 0c80c893..ba680483 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs +++ b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs @@ -1,10 +1,10 @@ {{! template-lint-disable require-presentational-children }}
    • Date: Wed, 28 Jun 2023 11:03:20 -0700 Subject: [PATCH 10/86] chore: remove active descendant support --- .../ember-toucan-core/src/components/form/controls/select.hbs | 1 - .../ember-toucan-core/src/components/form/controls/select.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.hbs b/packages/ember-toucan-core/src/components/form/controls/select.hbs index 2589af33..0cb4a248 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.hbs +++ b/packages/ember-toucan-core/src/components/form/controls/select.hbs @@ -5,7 +5,6 @@ >
      Date: Fri, 30 Jun 2023 15:20:54 -0400 Subject: [PATCH 11/86] Fix a11y issues and move to new API --- docs/components/select/demo/single.md | 92 ++-- .../form/controls/select/option.hbs | 18 +- .../components/form/controls/select/option.ts | 36 +- .../src/components/form/controls/select.hbs | 95 ++-- .../src/components/form/controls/select.ts | 444 ++++++------------ 5 files changed, 262 insertions(+), 423 deletions(-) diff --git a/docs/components/select/demo/single.md b/docs/components/select/demo/single.md index 5737adc2..e5e28b44 100644 --- a/docs/components/select/demo/single.md +++ b/docs/components/select/demo/single.md @@ -1,32 +1,22 @@ ```hbs template -

      - Single select -

      - - - + - - {{#each this.colors as |color|}} - - {{/each}} - - - - - {{#each this.colors as |color|}} - - {{/each}} + + {{select.option.label}} + ``` @@ -34,42 +24,56 @@ ```js component import Component from '@glimmer/component'; import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; export default class extends Component { - colors = [ + @tracked selected; + + options = [ { - label: "Blue", - name: "blue", + label: 'Blue', + name: 'blue', + value: 'blue', }, { - label: "Green", - name: "green", + label: 'Green', + name: 'green', + value: 'green', }, { - label: "Yellow", - name: "yellow", + label: 'Yellow', + name: 'yellow', + value: 'yellow', }, { - label: "Orange", - name: "orange", + label: 'Orange', + name: 'orange', + value: 'orange', }, { - label: "Red", - name: "red", + label: 'Red', + name: 'red', + value: 'red', }, { - label: "Purple", - name: "purple", + label: 'Purple', + name: 'purple', + value: 'purple', }, { - label: "Teal", - name: "teal", + label: 'Teal', + name: 'teal', + value: 'teal', }, ]; + isEqual(one: unknown, two: unknown) { + return Object.is(one, two); + } + @action - onChange(values: string[]) { - console.log(values) + onChange(option) { + this.selected = option; } } ``` diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs index ba680483..8f2efaf8 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs +++ b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs @@ -1,21 +1,19 @@ {{! template-lint-disable require-presentational-children }}
    • -
    • {{@noResultsText}}
    • + {{/if}}
    {{/if}} \ No newline at end of file diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index 778d0214..d566d3dc 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -39,6 +39,11 @@ export interface ToucanFormSelectControlComponentSignature { */ isReadOnly?: boolean; + /** + * A string to display when there are no results after the user's filter. + */ + noResultsText?: string; + /** * Called when the user makes a selection. * It is called with the selected option (derived from `@options`) as its only argument. diff --git a/packages/ember-toucan-core/src/components/form/fields/select.hbs b/packages/ember-toucan-core/src/components/form/fields/select.hbs index 8fea3d7c..62d536b3 100644 --- a/packages/ember-toucan-core/src/components/form/fields/select.hbs +++ b/packages/ember-toucan-core/src/components/form/fields/select.hbs @@ -54,6 +54,7 @@ @hasError={{this.hasError}} @isDisabled={{@isDisabled}} @isReadOnly={{@isReadOnly}} + @noResultsText={{@noResultsText}} @onChange={{@onChange}} @onFilter={{@onFilter}} @optionKey={{@optionKey}} diff --git a/packages/ember-toucan-core/src/components/form/fields/select.ts b/packages/ember-toucan-core/src/components/form/fields/select.ts index 2a958ef5..39820597 100644 --- a/packages/ember-toucan-core/src/components/form/fields/select.ts +++ b/packages/ember-toucan-core/src/components/form/fields/select.ts @@ -41,6 +41,11 @@ export interface ToucanFormSelectFieldComponentSignature { */ label?: string; + /** + * A string to display when there are no results after the user's filter. + */ + noResultsText?: string; + /** * The function called when a new selection is made. */ From 5c0dd0993a36fd8bbd9fb9a77f083a7ce867631f Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:42:16 -0400 Subject: [PATCH 36/86] toucan-core: Adjust optionElement logic --- .../src/components/form/controls/select.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index d566d3dc..1eceeebd 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -204,10 +204,9 @@ export default class ToucanFormSelectControlComponent extends Component Date: Wed, 5 Jul 2023 15:47:26 -0400 Subject: [PATCH 37/86] docs: Remove placeholder in demo --- docs/components/select-field/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/components/select-field/index.md b/docs/components/select-field/index.md index ec50ba3a..d6c277f3 100644 --- a/docs/components/select-field/index.md +++ b/docs/components/select-field/index.md @@ -286,7 +286,6 @@ Target the error block via `data-error`. @label='Label' @hint='With hint text' @isReadOnly={{true}} - placeholder='Colors' /> From 38a5e2c423f16e37bd035eb341383dee7dc071e2 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:48:11 -0400 Subject: [PATCH 38/86] toucan-core: Remove option chaining --- .../ember-toucan-core/src/components/form/controls/select.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index 1eceeebd..122b1c0e 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -208,7 +208,7 @@ export default class ToucanFormSelectControlComponent extends Component Date: Thu, 6 Jul 2023 08:09:11 -0400 Subject: [PATCH 39/86] toucan-core: Allow space key to be entered --- .../ember-toucan-core/src/components/form/controls/select.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index 122b1c0e..7d42cb7a 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -308,11 +308,6 @@ export default class ToucanFormSelectControlComponent extends Component Date: Thu, 6 Jul 2023 08:09:53 -0400 Subject: [PATCH 40/86] docs: Add noResultsText to select-field --- docs/components/select-field/demo/base-demo.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/components/select-field/demo/base-demo.md b/docs/components/select-field/demo/base-demo.md index 6e8f7054..260254b0 100644 --- a/docs/components/select-field/demo/base-demo.md +++ b/docs/components/select-field/demo/base-demo.md @@ -1,14 +1,15 @@ ```hbs template From 00bd8bf0a16bd966a6a38f8eab17750f79a8ba27 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 08:16:54 -0400 Subject: [PATCH 41/86] toucan-core: Adjust list option styling to match designs --- .../src/-private/components/form/controls/select/option.hbs | 2 +- .../src/-private/components/form/controls/select/option.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs index 6aff0d2e..9868ed1a 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs +++ b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs @@ -1,7 +1,7 @@ {{! template-lint-disable require-presentational-children }}
  • Date: Thu, 6 Jul 2023 08:16:59 -0400 Subject: [PATCH 42/86] Revert "toucan-core: Allow space key to be entered" This reverts commit 558b5b0b5ff121d45cd0c41657bc64d1d843ef4a. --- .../ember-toucan-core/src/components/form/controls/select.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index 7d42cb7a..122b1c0e 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -308,6 +308,11 @@ export default class ToucanFormSelectControlComponent extends Component Date: Thu, 6 Jul 2023 08:20:14 -0400 Subject: [PATCH 43/86] toucan-core: Allow space key to be entered --- .../ember-toucan-core/src/components/form/controls/select.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index 122b1c0e..7d42cb7a 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -308,11 +308,6 @@ export default class ToucanFormSelectControlComponent extends Component Date: Thu, 6 Jul 2023 08:29:02 -0400 Subject: [PATCH 44/86] docs: Remove unused component arg --- docs/components/select-field/demo/base-demo.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/components/select-field/demo/base-demo.md b/docs/components/select-field/demo/base-demo.md index 260254b0..14a63c93 100644 --- a/docs/components/select-field/demo/base-demo.md +++ b/docs/components/select-field/demo/base-demo.md @@ -9,7 +9,6 @@ @optionKey='label' @options={{this.options}} @selected={{this.selected}} - @selectedLabel={{this.selected.label}} placeholder='Colors' as |select| > From 1a0b7f4a00e877227676d23a352882255cef9558 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 09:07:05 -0400 Subject: [PATCH 45/86] cleanup: Remove TODO comments --- .../components/form/controls/select/option.hbs | 2 -- .../components/form/controls/select/option.ts | 3 --- .../src/components/form/controls/select.ts | 12 ------------ packages/ember-toucan-core/tsconfig.json | 1 - 4 files changed, 18 deletions(-) diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs index 9868ed1a..faf1e51a 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs +++ b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.hbs @@ -17,8 +17,6 @@ {{! TODO: Do we make `@value` required? }} {{! - TODO: Should this INPUT remain hidden from screenreaders? - 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? diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts index 03fc4cbf..bf99025e 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts +++ b/packages/ember-toucan-core/src/-private/components/form/controls/select/option.ts @@ -3,9 +3,6 @@ import { action } from '@ember/object'; import Check from '../../../../../-private/icons/check'; -// TODO: Should the directory structure of `-private` mirror the directory structure of the related component? -// Or should we simply put this subcomponent on the top level of `-private`? - interface ToucanFormSelectOptionControlComponentSignature { Args: { isActive?: boolean; diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/select.ts index 7d42cb7a..a0351995 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/select.ts @@ -96,18 +96,6 @@ export interface ToucanFormSelectControlComponentSignature { Element: HTMLInputElement; } -// TODO -// -// Off the top of my head: -// -// - tests -// - the popover's scrollbar track and thumb are at their defaults. Tailwind doesn't support scrollbar styling. do we need to add a stylesheet? -// - test with a screenreader -// - add loading feedback? or wait until v2? -// - inline TODOs throughout this component, its subcomponents, and its documentation -// - bugs and minor visual tweaks? -// - SelectField - export default class ToucanFormSelectControlComponent extends Component { @tracked activeIndex: number | null = null; @tracked inputValue: string | undefined; diff --git a/packages/ember-toucan-core/tsconfig.json b/packages/ember-toucan-core/tsconfig.json index 71ea6520..b2dd193a 100644 --- a/packages/ember-toucan-core/tsconfig.json +++ b/packages/ember-toucan-core/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "@tsconfig/ember/tsconfig.json", "include": ["src/**/*", "unpublished-development-types/**/*"], - "exclude": ["src/index.js"], // TODO: Works around a local issue. Note to self: Don't include it in your PR. "glint": { "environment": "ember-loose" } From 4cafbfc6d362042fbb8fe0d6064a31302ac057b8 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 09:08:00 -0400 Subject: [PATCH 46/86] Add SelectField to template-registry --- packages/ember-toucan-core/src/template-registry.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ember-toucan-core/src/template-registry.ts b/packages/ember-toucan-core/src/template-registry.ts index cb0f1786..5b0be21c 100644 --- a/packages/ember-toucan-core/src/template-registry.ts +++ b/packages/ember-toucan-core/src/template-registry.ts @@ -13,6 +13,7 @@ import type FormFileInputFieldComponent from './components/form/fields/file-inpu import type InputFieldComponent from './components/form/fields/input'; import type RadioFieldComponent from './components/form/fields/radio'; import type RadioGroupFieldComponent from './components/form/fields/radio-group'; +import type SelectFieldComponent from './components/form/fields/select'; import type TextareaFieldComponent from './components/form/fields/textarea'; import type FormFileInputDeleteButtonComponent from './components/form/file-input/delete-button'; import type FormFileInputListComponent from './components/form/file-input/list'; @@ -34,6 +35,7 @@ export default interface Registry { 'Form::Fields::FileInput': typeof FormFileInputFieldComponent; 'Form::Fields::Radio': typeof RadioFieldComponent; 'Form::Fields::RadioGroup': typeof RadioGroupFieldComponent; + 'Form::Fields::Select': typeof SelectFieldComponent; 'Form::Fields::Textarea': typeof TextareaFieldComponent; 'Form::Controls::CharacterCount': typeof FormControlsCharacterCount; } From 0692f974085ca7964cac3e22f1ae4c252534bc06 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 09:21:13 -0400 Subject: [PATCH 47/86] toucan-form: Wire up select-field to work with ToucanForm --- .../changeset-validation/demo/base-demo.md | 46 +++++++++++ .../src/-private/select-field.hbs | 79 +++++++++++++------ .../src/-private/select-field.ts | 13 +-- 3 files changed, 108 insertions(+), 30 deletions(-) diff --git a/docs/toucan-form/changeset-validation/demo/base-demo.md b/docs/toucan-form/changeset-validation/demo/base-demo.md index 6293538c..1b687db7 100644 --- a/docs/toucan-form/changeset-validation/demo/base-demo.md +++ b/docs/toucan-form/changeset-validation/demo/base-demo.md @@ -26,6 +26,20 @@ + + + {{select.option.label}} + + + {{#if (this.hasOnlyLabelBlock (has-block 'label') (has-block 'hint'))}} <:label>{{yield to='label'}} + <:default as |select|> + {{yield (hash Option=select.Option option=select.option)}} + {{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint')) }} <:label>{{yield to='label'}} <:hint>{{yield to='hint'}} + <:default as |select|> + {{yield (hash Option=select.Option option=select.option)}} + {{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}} <:hint>{{yield to='hint'}} + <:default as |select|> + {{yield (hash Option=select.Option option=select.option)}} + {{else}} {{! Argument-only case }} + as |select| + > + {{yield (hash Option=select.Option option=select.option)}} + {{/if}} \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/select-field.ts b/packages/ember-toucan-form/src/-private/select-field.ts index 8a25c8f1..3deae470 100644 --- a/packages/ember-toucan-form/src/-private/select-field.ts +++ b/packages/ember-toucan-form/src/-private/select-field.ts @@ -13,8 +13,7 @@ export interface ToucanFormSelectFieldComponentSignature< Element: HTMLInputElement; Args: Omit< BaseSelectFieldSignature['Args'], - // TODO: `value` doesn't apply. But how to handle Select with changeset? - 'error' | 'onChange' + 'error' | 'onChange' | 'selected' > & { /** * The name of your field, which must match a property of the `@data` passed to the form @@ -50,14 +49,16 @@ export default class ToucanFormSelectFieldComponent< }; @action - assertString(value: unknown): string | undefined { + assertSelected(value: unknown): string | Record | undefined { assert( - `Only string values are expected for ${String( + `Only string or object values are expected for ${String( this.args.name )}, but you passed ${typeof value}`, - typeof value === 'undefined' || typeof value === 'string' + typeof value === 'undefined' || + typeof value === 'string' || + typeof value === 'object' ); - return value; + return value as string | Record; } } From 0cdcc84d75b6f692ea05e2acd7cb849906590c8a Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:42:30 -0400 Subject: [PATCH 48/86] Rename from select to combobox --- .../demo/base-demo.md | 12 +- .../{select-field => combobox-field}/index.md | 169 +++++++++--------- .../{select => combobox}/demo/base-demo.md | 40 ++--- docs/components/{select => combobox}/index.md | 76 ++++---- .../changeset-validation/demo/base-demo.md | 12 +- packages/ember-toucan-core/package.json | 4 +- .../controls/{select => combobox}/option.hbs | 0 .../controls/{select => combobox}/option.ts | 4 +- .../controls/{select.hbs => combobox.hbs} | 0 .../form/controls/{select.ts => combobox.ts} | 8 +- .../form/fields/{select.hbs => combobox.hbs} | 4 +- .../form/fields/{select.ts => combobox.ts} | 18 +- .../src/template-registry.ts | 8 +- .../{select-field.hbs => combobox-field.hbs} | 18 +- .../{select-field.ts => combobox-field.ts} | 12 +- .../src/components/toucan-form.hbs | 6 +- .../src/components/toucan-form.ts | 6 +- 17 files changed, 199 insertions(+), 198 deletions(-) rename docs/components/{select-field => combobox-field}/demo/base-demo.md (89%) rename docs/components/{select-field => combobox-field}/index.md (62%) rename docs/components/{select => combobox}/demo/base-demo.md (81%) rename docs/components/{select => combobox}/index.md (80%) rename packages/ember-toucan-core/src/-private/components/form/controls/{select => combobox}/option.hbs (100%) rename packages/ember-toucan-core/src/-private/components/form/controls/{select => combobox}/option.ts (85%) rename packages/ember-toucan-core/src/components/form/controls/{select.hbs => combobox.hbs} (100%) rename packages/ember-toucan-core/src/components/form/controls/{select.ts => combobox.ts} (97%) rename packages/ember-toucan-core/src/components/form/fields/{select.hbs => combobox.hbs} (96%) rename packages/ember-toucan-core/src/components/form/fields/{select.ts => combobox.ts} (79%) rename packages/ember-toucan-form/src/-private/{select-field.hbs => combobox-field.hbs} (92%) rename packages/ember-toucan-form/src/-private/{select-field.ts => combobox-field.ts} (79%) diff --git a/docs/components/select-field/demo/base-demo.md b/docs/components/combobox-field/demo/base-demo.md similarity index 89% rename from docs/components/select-field/demo/base-demo.md rename to docs/components/combobox-field/demo/base-demo.md index 14a63c93..064880b4 100644 --- a/docs/components/select-field/demo/base-demo.md +++ b/docs/components/combobox-field/demo/base-demo.md @@ -1,5 +1,5 @@ ```hbs template - - - {{select.option.label}} - - + + {{combobox.option.label}} + + ``` ```js component diff --git a/docs/components/select-field/index.md b/docs/components/combobox-field/index.md similarity index 62% rename from docs/components/select-field/index.md rename to docs/components/combobox-field/index.md index d6c277f3..726101a2 100644 --- a/docs/components/select-field/index.md +++ b/docs/components/combobox-field/index.md @@ -1,6 +1,6 @@ -# Select Field +# Combobox Field -Provides a Toucan-styled select with filtering that builds on top of the Field component. +Provides a Toucan-styled combobox with filtering that builds on top of the Field component. ## Label @@ -13,43 +13,44 @@ Provide a string to the `@label` component argument or content to the `:label` n ### `@label` ```hbs - - - {{select.option}} - - + + {{combobox.option}} + + ``` ### `:label` ```hbs - <:label>Here is a label - - + + - <:label>Here is a label <:default> - - {{select.option}} - + + {{combobox.option}} + - + ``` ## Hint @@ -63,35 +64,35 @@ Provide a string to the `@hint` component argument or content to `:hint` named b ### @hint ```hbs - - - {{select.option}} - - + + {{combobox.option}} + + ``` ### `:hint` ```hbs - <:hint>Here is a hint Link <:default> - - {{select.option}} - + + {{combobox.option}} + - + ``` ## Error @@ -101,20 +102,20 @@ Optional. Provide a string or array of strings to `@error` to render the text into the Error section of the Field. ```hbs - + ``` ```hbs - - - {{select.option}} - - + + {{combobox.option}} + + ``` ## `@onChange` @@ -123,11 +124,15 @@ Provide an `@onChange` callback to be notified when the user's selections have c `@onChange` will receive the value as its only argument. ```hbs - - - {{select.option}} - - + + + {{combobox.option}} + + ``` ```js @@ -164,7 +169,7 @@ This test selector will be used as the value for the `data-root-field` attribute The Field can be targeted via: ```hbs - + ``` ```js @@ -187,50 +192,50 @@ Target the error block via `data-error`. ## UI States -### SelectField with `@label` +### ComboboxField with `@label`
    - +
    -### SelectField with `@label` and `@hint` +### ComboboxField with `@label` and `@hint`
    -
    -### SelectField with `:label` and `:hint` blocks +### ComboboxField with `:label` and `:hint` blocks
    - + <:label>Label <:hint>Hint text link - <:default as |select|> - - {{select.option}} - + <:default as |combobox|> + + {{combobox.option}} + - +
    -### SelectField with `@label` and `@error` +### ComboboxField with `@label` and `@error`
    -
    -### SelectField with `@label`, `@hint`, and `@error` +### ComboboxField with `@label`, `@hint`, and `@error`
    -
    -### SelectField with `@label` and `@isDisabled` +### ComboboxField with `@label` and `@isDisabled`
    -
    -### SelectField with `@label`, `@value`, and `@isDisabled` +### ComboboxField with `@label`, `@value`, and `@isDisabled`
    - - - {{select.option}} - - + as |combobox|> + + {{combobox.option}} + +
    -### SelectField with multiple errors +### ComboboxField with multiple errors
    - - - {{select.option}} - - + as |combobox|> + + {{combobox.option}} + +
    -### SelectField with `@isReadOnly` +### ComboboxField with `@isReadOnly`
    -
    -### SelectField with `@isReadOnly` and `@selected` +### ComboboxField with `@isReadOnly` and `@selected`
    - - - {{select.option}} - - + as |combobox|> + + {{combobox.option}} + +
    diff --git a/docs/components/select/demo/base-demo.md b/docs/components/combobox/demo/base-demo.md similarity index 81% rename from docs/components/select/demo/base-demo.md rename to docs/components/combobox/demo/base-demo.md index 8b62f4af..a84e190a 100644 --- a/docs/components/select/demo/base-demo.md +++ b/docs/components/combobox/demo/base-demo.md @@ -1,6 +1,6 @@ ```hbs template
    - - - {{select.option.label}} - - + + {{combobox.option.label}} + + - - - {{select.option}} - - + + {{combobox.option}} + + - - - {{select.option.label}} - - + + {{combobox.option.label}} + +
    ``` @@ -108,10 +108,6 @@ export default class extends Component { 'Tony', ]; - isEqual(one: unknown, two: unknown) { - return Object.is(one, two); - } - @action onChange(option) { this.selected = option; diff --git a/docs/components/select/index.md b/docs/components/combobox/index.md similarity index 80% rename from docs/components/select/index.md rename to docs/components/combobox/index.md index 989dcb2f..613895bf 100644 --- a/docs/components/select/index.md +++ b/docs/components/combobox/index.md @@ -1,31 +1,31 @@ -# Select +# Combobox -Provides a Toucan-styled select with filtering. -If you are building forms, you may be interested in the SelectField component instead. +Provides a Toucan-styled combobox with filtering. +If you are building forms, you may be interested in the ComboboxField component instead. ## Content Class A CSS class to add to this component's content container. Commonly used to specify a `z-index`. ```hbs - + ``` ## Options -`@options` forms the content of this component. To support a variety of data shapes, `@options` is typed as `unknown[]` and treated as though it were opaque. `@options` is simply iterated over then passed back to you as a block parameter (`select.option`). +`@options` forms the content of this component. To support a variety of data shapes, `@options` is typed as `unknown[]` and treated as though it were opaque. `@options` is simply iterated over then passed back to you as a block parameter (`combobox.option`). ```hbs - - + - {{select.option}} - - + {{combobox.option}} +
    + ``` ## Selected @@ -33,16 +33,16 @@ A CSS class to add to this component's content container. Commonly used to speci The currently selected option. Can be either an object or a string. If `@options` is an array of strings, provide a string. If `@options` is an array of objects, pass the entire object. Works in combination with `@onChange`. ```hbs - - + - {{select.option}} - - + {{combobox.option}} + + ``` ```js @@ -59,16 +59,16 @@ export default class extends Component { Called when the user makes a selection. It is called with the selected option (derived from `@options`) as its only argument. You'll want to update `@selected` with the new value in your on change handler. ```hbs - - - {{select.option}} - - + + {{combobox.option}} + + ``` ```js @@ -100,17 +100,17 @@ The `@optionKey` argument is used when your `@options` take the shape of an arra In the example below, we set `@optionKey='label'`. Our `@options` objects have a `label` key and we want the label of the selected option to be used for the selected value, as well as for filtering as the user types. ```hbs - - - {{select.option}} - - + + {{combobox.option}} + + ``` ```js @@ -164,18 +164,18 @@ export default class extends Component { The function called when a user types into the combobox textbox, typically used to write custom filtering logic. ```hbs - - - {{select.option}} - - + + {{combobox.option}} + + ``` ```js @@ -234,7 +234,7 @@ export default class extends Component { Set the `@isDisabled` argument to disable the input. ```hbs - + ``` ## Read Only State @@ -242,7 +242,7 @@ Set the `@isDisabled` argument to disable the input. Set the `@isReadOnly` argument to put the input in the read only state. ```hbs - + ``` ## Error State @@ -250,5 +250,5 @@ Set the `@isReadOnly` argument to put the input in the read only state. Set the `@hasError` argument to apply an error box shadow to the ``. ```hbs - + ``` diff --git a/docs/toucan-form/changeset-validation/demo/base-demo.md b/docs/toucan-form/changeset-validation/demo/base-demo.md index 1b687db7..5ad958bf 100644 --- a/docs/toucan-form/changeset-validation/demo/base-demo.md +++ b/docs/toucan-form/changeset-validation/demo/base-demo.md @@ -26,19 +26,19 @@ - - - {{select.option.label}} - - + + {{combobox.option.label}} + + { +export default class ToucanFormComboboxOptionControlComponent extends Component { className = className; Check = Check; diff --git a/packages/ember-toucan-core/src/components/form/controls/select.hbs b/packages/ember-toucan-core/src/components/form/controls/combobox.hbs similarity index 100% rename from packages/ember-toucan-core/src/components/form/controls/select.hbs rename to packages/ember-toucan-core/src/components/form/controls/combobox.hbs diff --git a/packages/ember-toucan-core/src/components/form/controls/select.ts b/packages/ember-toucan-core/src/components/form/controls/combobox.ts similarity index 97% rename from packages/ember-toucan-core/src/components/form/controls/select.ts rename to packages/ember-toucan-core/src/components/form/controls/combobox.ts index a0351995..b67ec019 100644 --- a/packages/ember-toucan-core/src/components/form/controls/select.ts +++ b/packages/ember-toucan-core/src/components/form/controls/combobox.ts @@ -10,13 +10,13 @@ import { offset, size } from '@floating-ui/dom'; import OptionComponent, { selector as optionComponentSelector, -} from '../../../-private/components/form/controls/select/option'; +} from '../../../-private/components/form/controls/combobox/option'; import Chevron from '../../../-private/icons/chevron'; import type { Middleware as VelcroMiddleware } from '@floating-ui/dom'; import type { WithBoundArgs } from '@glint/template'; -export interface ToucanFormSelectControlComponentSignature { +export interface ToucanFormComboboxControlComponentSignature { Args: { /** * A CSS class to add to this component's content container. @@ -96,7 +96,7 @@ export interface ToucanFormSelectControlComponentSignature { Element: HTMLInputElement; } -export default class ToucanFormSelectControlComponent extends Component { +export default class ToucanFormComboboxControlComponent extends Component { @tracked activeIndex: number | null = null; @tracked inputValue: string | undefined; @tracked isPopoverOpen = false; @@ -108,7 +108,7 @@ export default class ToucanFormSelectControlComponent extends Component {{/if}} - {{yield select}} - + {{#if @error}} diff --git a/packages/ember-toucan-core/src/components/form/fields/select.ts b/packages/ember-toucan-core/src/components/form/fields/combobox.ts similarity index 79% rename from packages/ember-toucan-core/src/components/form/fields/select.ts rename to packages/ember-toucan-core/src/components/form/fields/combobox.ts index 39820597..195ec85e 100644 --- a/packages/ember-toucan-core/src/components/form/fields/select.ts +++ b/packages/ember-toucan-core/src/components/form/fields/combobox.ts @@ -5,9 +5,9 @@ import LockIcon from '../../../-private/icons/lock'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ErrorMessage } from '../../../-private/types'; -import type { ToucanFormSelectControlComponentSignature } from '../controls/select'; +import type { ToucanFormComboboxControlComponentSignature } from '../controls/combobox'; -export interface ToucanFormSelectFieldComponentSignature { +export interface ToucanFormComboboxFieldComponentSignature { Element: HTMLInputElement; Args: { /** @@ -49,7 +49,7 @@ export interface ToucanFormSelectFieldComponentSignature { /** * The function called when a new selection is made. */ - onChange?: ToucanFormSelectControlComponentSignature['Args']['onChange']; + onChange?: ToucanFormComboboxControlComponentSignature['Args']['onChange']; /** * The function called when a user types into the combobox textbox. @@ -57,7 +57,7 @@ export interface ToucanFormSelectFieldComponentSignature { * Typically used for making a request to the server and populating * `@options` with the results. */ - onFilter?: ToucanFormSelectControlComponentSignature['Args']['onFilter']; + onFilter?: ToucanFormComboboxControlComponentSignature['Args']['onFilter']; /** * When `@options` is an array of objects, `@selected` is also an object. @@ -65,7 +65,7 @@ export interface ToucanFormSelectFieldComponentSignature { * be used for both filtering and displayed the selected value in the * textbox. */ - optionKey?: ToucanFormSelectControlComponentSignature['Args']['optionKey']; + optionKey?: ToucanFormComboboxControlComponentSignature['Args']['optionKey']; /** * `@options` forms the content of this component. @@ -73,7 +73,7 @@ export interface ToucanFormSelectFieldComponentSignature { * To support a variety of data shapes, `@options` is typed as `unknown[]` and treated as though it were opaque. * `@options` is simply iterated over then passed back to you as a block parameter (`select.option`). */ - options?: ToucanFormSelectControlComponentSignature['Args']['options']; + options?: ToucanFormComboboxControlComponentSignature['Args']['options']; /** * A test selector for targeting the root element of the field. @@ -84,16 +84,16 @@ export interface ToucanFormSelectFieldComponentSignature { /** * The currently selected option. If `@options` is an array of strings, provide a string. If `@options` is an array of objects, pass the entire object and use `@optionKey`. */ - selected?: ToucanFormSelectControlComponentSignature['Args']['selected']; + selected?: ToucanFormComboboxControlComponentSignature['Args']['selected']; }; Blocks: { - default: ToucanFormSelectControlComponentSignature['Blocks']['default']; + default: ToucanFormComboboxControlComponentSignature['Blocks']['default']; label: []; hint: []; }; } -export default class ToucanFormInputFieldComponent extends Component { +export default class ToucanFormComboboxFieldComponent extends Component { LockIcon = LockIcon; assertBlockOrArgumentExists = ({ diff --git a/packages/ember-toucan-core/src/template-registry.ts b/packages/ember-toucan-core/src/template-registry.ts index 5b0be21c..467b703f 100644 --- a/packages/ember-toucan-core/src/template-registry.ts +++ b/packages/ember-toucan-core/src/template-registry.ts @@ -1,19 +1,19 @@ import type ButtonComponent from './components/button'; import type FormControlsCharacterCount from './components/form/controls/character-count'; import type CheckboxControlComponent from './components/form/controls/checkbox'; +import type ComboboxControlComponent from './components/form/controls/combobox'; import type FormControlsFileInputComponent from './components/form/controls/file-input'; import type InputControlComponent from './components/form/controls/input'; import type RadioControlComponent from './components/form/controls/radio'; -import type SelectControlComponent from './components/form/controls/select'; import type TextareaControlComponent from './components/form/controls/textarea'; import type FieldComponent from './components/form/field'; import type CheckboxFieldComponent from './components/form/fields/checkbox'; import type CheckboxGroupFieldComponent from './components/form/fields/checkbox-group'; +import type ComboboxFieldComponent from './components/form/fields/combobox'; import type FormFileInputFieldComponent from './components/form/fields/file-input'; import type InputFieldComponent from './components/form/fields/input'; import type RadioFieldComponent from './components/form/fields/radio'; import type RadioGroupFieldComponent from './components/form/fields/radio-group'; -import type SelectFieldComponent from './components/form/fields/select'; import type TextareaFieldComponent from './components/form/fields/textarea'; import type FormFileInputDeleteButtonComponent from './components/form/file-input/delete-button'; import type FormFileInputListComponent from './components/form/file-input/list'; @@ -23,19 +23,19 @@ export default interface Registry { 'Form::Field': typeof FieldComponent; 'Form::Fields::Checkbox': typeof CheckboxFieldComponent; 'Form::Fields::CheckboxGroup': typeof CheckboxGroupFieldComponent; + 'Form::Fields::Combobox': typeof ComboboxFieldComponent; 'Form::Fields::Input': typeof InputFieldComponent; 'Form::Controls::Checkbox': typeof CheckboxControlComponent; + 'Form::Controls::Combobox': typeof ComboboxControlComponent; 'Form::Controls::FileInput': typeof FormControlsFileInputComponent; 'Form::Controls::Input': typeof InputControlComponent; 'Form::Controls::Radio': typeof RadioControlComponent; - 'Form::Controls::Select': typeof SelectControlComponent; 'Form::Controls::Textarea': typeof TextareaControlComponent; 'Form::FileInput::List': typeof FormFileInputListComponent; 'Form::FileInput::DeleteButton': typeof FormFileInputDeleteButtonComponent; 'Form::Fields::FileInput': typeof FormFileInputFieldComponent; 'Form::Fields::Radio': typeof RadioFieldComponent; 'Form::Fields::RadioGroup': typeof RadioGroupFieldComponent; - 'Form::Fields::Select': typeof SelectFieldComponent; 'Form::Fields::Textarea': typeof TextareaFieldComponent; 'Form::Controls::CharacterCount': typeof FormControlsCharacterCount; } diff --git a/packages/ember-toucan-form/src/-private/select-field.hbs b/packages/ember-toucan-form/src/-private/combobox-field.hbs similarity index 92% rename from packages/ember-toucan-form/src/-private/select-field.hbs rename to packages/ember-toucan-form/src/-private/combobox-field.hbs index 0e703f7c..578afb96 100644 --- a/packages/ember-toucan-form/src/-private/select-field.hbs +++ b/packages/ember-toucan-form/src/-private/combobox-field.hbs @@ -1,7 +1,7 @@ {{! Regarding Conditionals - This looks really messy, but Form::Fields::Select exposes named blocks; HOWEVER, + This looks really messy, but Form::Fields::Combobox exposes named blocks; HOWEVER, we cannot conditionally render named blocks due to https://github.com/emberjs/rfcs/issues/735. We *can* conditionally render components though, based on the blocks and argument combinations @@ -18,7 +18,7 @@ }} <@form.Field @name={{@name}} as |field|> {{#if (this.hasOnlyLabelBlock (has-block 'label') (has-block 'hint'))}} - {{yield (hash Option=select.Option option=select.option)}} - + {{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint')) }} - {{yield (hash Option=select.Option option=select.option)}} - + {{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}} - {{yield (hash Option=select.Option option=select.option)}} - + {{else}} {{! Argument-only case }} - {{yield (hash Option=select.Option option=select.option)}} - + {{/if}} \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/select-field.ts b/packages/ember-toucan-form/src/-private/combobox-field.ts similarity index 79% rename from packages/ember-toucan-form/src/-private/select-field.ts rename to packages/ember-toucan-form/src/-private/combobox-field.ts index 3deae470..a7a6dcdc 100644 --- a/packages/ember-toucan-form/src/-private/select-field.ts +++ b/packages/ember-toucan-form/src/-private/combobox-field.ts @@ -3,16 +3,16 @@ import { assert } from '@ember/debug'; import { action } from '@ember/object'; import type { HeadlessFormBlock, UserData } from './types'; -import type { ToucanFormSelectFieldComponentSignature as BaseSelectFieldSignature } from '@crowdstrike/ember-toucan-core/components/form/fields/select'; +import type { ToucanFormComboboxFieldComponentSignature as BaseComboboxFieldSignature } from '@crowdstrike/ember-toucan-core/components/form/fields/combobox'; import type { FormData, FormKey, ValidationError } from 'ember-headless-form'; -export interface ToucanFormSelectFieldComponentSignature< +export interface ToucanFormComboboxFieldComponentSignature< DATA extends UserData, KEY extends FormKey> = FormKey> > { Element: HTMLInputElement; Args: Omit< - BaseSelectFieldSignature['Args'], + BaseComboboxFieldSignature['Args'], 'error' | 'onChange' | 'selected' > & { /** @@ -25,13 +25,13 @@ export interface ToucanFormSelectFieldComponentSignature< */ form: HeadlessFormBlock; }; - Blocks: BaseSelectFieldSignature['Blocks']; + Blocks: BaseComboboxFieldSignature['Blocks']; } -export default class ToucanFormSelectFieldComponent< +export default class ToucanFormComboboxFieldComponent< DATA extends UserData, KEY extends FormKey> = FormKey> -> extends Component> { +> extends Component> { hasOnlyLabelBlock = (hasLabel: boolean, hasHint: boolean) => hasLabel && !hasHint; hasHintAndLabelBlocks = (hasLabel: boolean, hasHint: boolean) => diff --git a/packages/ember-toucan-form/src/components/toucan-form.hbs b/packages/ember-toucan-form/src/components/toucan-form.hbs index f5382f67..0d9b4fd0 100644 --- a/packages/ember-toucan-form/src/components/toucan-form.hbs +++ b/packages/ember-toucan-form/src/components/toucan-form.hbs @@ -17,6 +17,9 @@ CheckboxGroup=(component (ensure-safe-component this.CheckboxGroupComponent) form=form ) + Combobox=(component + (ensure-safe-component this.ComboboxFieldComponent) form=form + ) Field=form.Field FileInput=(component (ensure-safe-component this.FileInputFieldComponent) form=form @@ -27,9 +30,6 @@ RadioGroup=(component (ensure-safe-component this.RadioGroupFieldComponent) form=form ) - Select=(component - (ensure-safe-component this.SelectFieldComponent) form=form - ) Textarea=(component (ensure-safe-component this.TextareaFieldComponent) form=form ) diff --git a/packages/ember-toucan-form/src/components/toucan-form.ts b/packages/ember-toucan-form/src/components/toucan-form.ts index fa5ee025..ad1f5da0 100644 --- a/packages/ember-toucan-form/src/components/toucan-form.ts +++ b/packages/ember-toucan-form/src/components/toucan-form.ts @@ -2,10 +2,10 @@ import Component from '@glimmer/component'; import CheckboxFieldComponent from '../-private/checkbox-field'; import CheckboxGroupFieldComponent from '../-private/checkbox-group-field'; +import ComboboxFieldComponent from '../-private/combobox-field'; import FileInputFieldComponent from '../-private/file-input-field'; import InputFieldComponent from '../-private/input-field'; import RadioGroupFieldComponent from '../-private/radio-group-field'; -import SelectFieldComponent from '../-private/select-field'; import TextareaFieldComponent from '../-private/textarea-field'; import type { HeadlessFormBlock, UserData } from '../-private/types'; @@ -31,6 +31,7 @@ export interface ToucanFormComponentSignature< typeof CheckboxGroupFieldComponent, 'form' >; + Combobox: WithBoundArgs, 'form'>; Field: HeadlessFormBlock['Field']; FileInput: WithBoundArgs, 'form'>; Input: WithBoundArgs, 'form'>; @@ -38,7 +39,6 @@ export interface ToucanFormComponentSignature< typeof RadioGroupFieldComponent, 'form' >; - Select: WithBoundArgs, 'form'>; Textarea: WithBoundArgs, 'form'>; /** @@ -70,7 +70,7 @@ export default class ToucanFormComponent< FileInputFieldComponent = FileInputFieldComponent; InputFieldComponent = InputFieldComponent; RadioGroupFieldComponent = RadioGroupFieldComponent; - SelectFieldComponent = SelectFieldComponent; + ComboboxFieldComponent = ComboboxFieldComponent; TextareaFieldComponent = TextareaFieldComponent; get validateOn() { From 70bbaf98e19f012778ef4856c8c805ea40582fb0 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:58:45 -0400 Subject: [PATCH 49/86] toucan-core: Remove redundant class --- .../src/components/form/controls/combobox.hbs | 2 +- test-app/package.json | 4 +- .../integration/components/combobox-test.gts | 115 ++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 test-app/tests/integration/components/combobox-test.gts diff --git a/packages/ember-toucan-core/src/components/form/controls/combobox.hbs b/packages/ember-toucan-core/src/components/form/controls/combobox.hbs index 24a6a1bc..8c6eae6b 100644 --- a/packages/ember-toucan-core/src/components/form/controls/combobox.hbs +++ b/packages/ember-toucan-core/src/components/form/controls/combobox.hbs @@ -14,7 +14,7 @@ autocapitalize="none" autocomplete="off" autocorrect="off" - class="focus:outline-none bg-overlay-1 w-full rounded-sm border-none py-1 pl-2 pr-6 transition-shadow + class="focus:outline-none w-full rounded-sm border-none py-1 pl-2 pr-6 transition-shadow {{this.styles}}" disabled={{@isDisabled}} readonly={{@isReadOnly}} diff --git a/test-app/package.json b/test-app/package.json index e498ef08..f751d042 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -30,8 +30,8 @@ "devDependencies": { "@babel/core": "^7.0.0", "@babel/eslint-parser": "^7.19.1", - "@crowdstrike/ember-toucan-core": "workspace:*", - "@crowdstrike/ember-toucan-form": "workspace:*", + "@crowdstrike/ember-toucan-core": "^0.2.0", + "@crowdstrike/ember-toucan-form": "^0.2.0", "@crowdstrike/ember-toucan-styles": "^2.0.1", "@ember/optional-features": "^2.0.0", "@ember/string": "^3.0.1", diff --git a/test-app/tests/integration/components/combobox-test.gts b/test-app/tests/integration/components/combobox-test.gts new file mode 100644 index 00000000..8a855432 --- /dev/null +++ b/test-app/tests/integration/components/combobox-test.gts @@ -0,0 +1,115 @@ +/* eslint-disable no-undef -- Until https://github.com/ember-cli/eslint-plugin-ember/issues/1747 is resolved... */ +import { fillIn, render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; + +import ComboboxControl from '@crowdstrike/ember-toucan-core/components/form/controls/combobox'; +import { setupRenderingTest } from 'test-app/tests/helpers'; + +module('Integration | Component | Combobox', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasTagName('input'); + assert.dom('[data-combobox]').hasClass('text-titles-and-attributes'); + assert.dom('[data-combobox]').hasClass('shadow-focusable-outline'); + assert.dom('[data-combobox]').doesNotHaveClass('text-disabled'); + assert.dom('[data-combobox]').doesNotHaveClass('shadow-error-outline'); + assert + .dom('[data-combobox]') + .doesNotHaveClass('focus:shadow-error-focus-outline'); + }); + + test('it disables the combobox using `@isDisabled`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').isDisabled(); + assert.dom('[data-combobox]').hasClass('text-disabled'); + assert + .dom('[data-combobox]') + .doesNotHaveClass('text-titles-and-attributes'); + }); + + test('it sets readonly on the combobox using `@isReadOnly`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasAttribute('readonly'); + + assert.dom('[data-combobox]').hasClass('shadow-read-only-outline'); + assert.dom('[data-combobox]').hasClass('bg-surface-xl'); + assert.dom('[data-combobox]').hasNoClass('bg-overlay-1'); + assert.dom('[data-combobox]').hasNoClass('text-disabled'); + assert.dom('[data-combobox]').hasNoClass('shadow-error-outline'); + assert.dom('[data-combobox]').hasNoClass('shadow-focusable-outline'); + }); + + // test('it spreads attributes to the underlying textarea', async function (assert) { + // await render(); + + // assert + // .dom('[data-combobox]') + // .hasAttribute('placeholder', 'Placeholder text'); + // }); + + // test('it sets the value attribute via `@value`', async function (assert) { + // await render(); + + // assert.dom('[data-combobox]').hasValue('tony'); + // }); + + // test('it calls `@onChange` when input is received', async function (assert) { + // assert.expect(6); + + // let handleChange = (value: string, e: Event | InputEvent) => { + // assert.strictEqual(value, 'test', 'Expected input to match'); + // assert.ok(e, 'Expected `e` to be available as the second argument'); + // assert.ok(e.target, 'Expected direct access to target from `e`'); + // assert.step('handleChange'); + // }; + + // await render(); + + // assert.verifySteps([]); + + // await fillIn('[data-combobox]', 'test'); + + // assert.verifySteps(['handleChange']); + // }); + + // test('it applies the error shadow when `@hasError={{true}}`', async function (assert) { + // await render(); + + // assert.dom('[data-combobox]').hasClass('shadow-error-outline'); + // assert.dom('[data-combobox]').hasClass('focus:shadow-error-focus-outline'); + // assert.dom('[data-combobox]').doesNotHaveClass('shadow-focusable-outline'); + // }); +}); From 41356a361c1bea7741c28e6c660242f811629f9c Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:59:04 -0400 Subject: [PATCH 50/86] Revert "toucan-core: Remove redundant class" This reverts commit 6c88cca124f5f221ad9123ce090009be59e5a09f. --- .../src/components/form/controls/combobox.hbs | 2 +- test-app/package.json | 6 +- .../integration/components/combobox-test.gts | 115 ------------------ 3 files changed, 4 insertions(+), 119 deletions(-) delete mode 100644 test-app/tests/integration/components/combobox-test.gts diff --git a/packages/ember-toucan-core/src/components/form/controls/combobox.hbs b/packages/ember-toucan-core/src/components/form/controls/combobox.hbs index 8c6eae6b..24a6a1bc 100644 --- a/packages/ember-toucan-core/src/components/form/controls/combobox.hbs +++ b/packages/ember-toucan-core/src/components/form/controls/combobox.hbs @@ -14,7 +14,7 @@ autocapitalize="none" autocomplete="off" autocorrect="off" - class="focus:outline-none w-full rounded-sm border-none py-1 pl-2 pr-6 transition-shadow + class="focus:outline-none bg-overlay-1 w-full rounded-sm border-none py-1 pl-2 pr-6 transition-shadow {{this.styles}}" disabled={{@isDisabled}} readonly={{@isReadOnly}} diff --git a/test-app/package.json b/test-app/package.json index f751d042..d2bf15cd 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -30,8 +30,8 @@ "devDependencies": { "@babel/core": "^7.0.0", "@babel/eslint-parser": "^7.19.1", - "@crowdstrike/ember-toucan-core": "^0.2.0", - "@crowdstrike/ember-toucan-form": "^0.2.0", + "@crowdstrike/ember-toucan-core": "workspace:*", + "@crowdstrike/ember-toucan-form": "workspace:*", "@crowdstrike/ember-toucan-styles": "^2.0.1", "@ember/optional-features": "^2.0.0", "@ember/string": "^3.0.1", @@ -90,8 +90,8 @@ "ember-load-initializers": "^2.1.2", "ember-qunit": "^7.0.0", "ember-resolver": "^10.0.0", - "ember-source": "~5.1.0", "ember-source-channel-url": "^3.0.0", + "ember-source": "~5.1.0", "ember-template-imports": "^3.1.2", "ember-template-lint": "^5.8.0", "ember-try": "^2.0.0", diff --git a/test-app/tests/integration/components/combobox-test.gts b/test-app/tests/integration/components/combobox-test.gts deleted file mode 100644 index 8a855432..00000000 --- a/test-app/tests/integration/components/combobox-test.gts +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable no-undef -- Until https://github.com/ember-cli/eslint-plugin-ember/issues/1747 is resolved... */ -import { fillIn, render } from '@ember/test-helpers'; -import { module, test } from 'qunit'; - -import ComboboxControl from '@crowdstrike/ember-toucan-core/components/form/controls/combobox'; -import { setupRenderingTest } from 'test-app/tests/helpers'; - -module('Integration | Component | Combobox', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - await render(); - - assert.dom('[data-combobox]').hasTagName('input'); - assert.dom('[data-combobox]').hasClass('text-titles-and-attributes'); - assert.dom('[data-combobox]').hasClass('shadow-focusable-outline'); - assert.dom('[data-combobox]').doesNotHaveClass('text-disabled'); - assert.dom('[data-combobox]').doesNotHaveClass('shadow-error-outline'); - assert - .dom('[data-combobox]') - .doesNotHaveClass('focus:shadow-error-focus-outline'); - }); - - test('it disables the combobox using `@isDisabled`', async function (assert) { - await render(); - - assert.dom('[data-combobox]').isDisabled(); - assert.dom('[data-combobox]').hasClass('text-disabled'); - assert - .dom('[data-combobox]') - .doesNotHaveClass('text-titles-and-attributes'); - }); - - test('it sets readonly on the combobox using `@isReadOnly`', async function (assert) { - await render(); - - assert.dom('[data-combobox]').hasAttribute('readonly'); - - assert.dom('[data-combobox]').hasClass('shadow-read-only-outline'); - assert.dom('[data-combobox]').hasClass('bg-surface-xl'); - assert.dom('[data-combobox]').hasNoClass('bg-overlay-1'); - assert.dom('[data-combobox]').hasNoClass('text-disabled'); - assert.dom('[data-combobox]').hasNoClass('shadow-error-outline'); - assert.dom('[data-combobox]').hasNoClass('shadow-focusable-outline'); - }); - - // test('it spreads attributes to the underlying textarea', async function (assert) { - // await render(); - - // assert - // .dom('[data-combobox]') - // .hasAttribute('placeholder', 'Placeholder text'); - // }); - - // test('it sets the value attribute via `@value`', async function (assert) { - // await render(); - - // assert.dom('[data-combobox]').hasValue('tony'); - // }); - - // test('it calls `@onChange` when input is received', async function (assert) { - // assert.expect(6); - - // let handleChange = (value: string, e: Event | InputEvent) => { - // assert.strictEqual(value, 'test', 'Expected input to match'); - // assert.ok(e, 'Expected `e` to be available as the second argument'); - // assert.ok(e.target, 'Expected direct access to target from `e`'); - // assert.step('handleChange'); - // }; - - // await render(); - - // assert.verifySteps([]); - - // await fillIn('[data-combobox]', 'test'); - - // assert.verifySteps(['handleChange']); - // }); - - // test('it applies the error shadow when `@hasError={{true}}`', async function (assert) { - // await render(); - - // assert.dom('[data-combobox]').hasClass('shadow-error-outline'); - // assert.dom('[data-combobox]').hasClass('focus:shadow-error-focus-outline'); - // assert.dom('[data-combobox]').doesNotHaveClass('shadow-focusable-outline'); - // }); -}); From 64212326d522983f04fb9927a4b6ff95c4c98bf8 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 13:00:19 -0400 Subject: [PATCH 51/86] toucan-core: Remove redundant class --- .../ember-toucan-core/src/components/form/controls/combobox.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/combobox.hbs b/packages/ember-toucan-core/src/components/form/controls/combobox.hbs index 24a6a1bc..8c6eae6b 100644 --- a/packages/ember-toucan-core/src/components/form/controls/combobox.hbs +++ b/packages/ember-toucan-core/src/components/form/controls/combobox.hbs @@ -14,7 +14,7 @@ autocapitalize="none" autocomplete="off" autocorrect="off" - class="focus:outline-none bg-overlay-1 w-full rounded-sm border-none py-1 pl-2 pr-6 transition-shadow + class="focus:outline-none w-full rounded-sm border-none py-1 pl-2 pr-6 transition-shadow {{this.styles}}" disabled={{@isDisabled}} readonly={{@isReadOnly}} From 228c1350b49348489df90367115273fd2e26803b Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 13:47:20 -0400 Subject: [PATCH 52/86] toucan-core: Move splattributes to li element instead --- .../src/-private/components/form/controls/combobox/option.hbs | 2 +- .../src/-private/components/form/controls/combobox/option.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs b/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs index faf1e51a..da9fd928 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs +++ b/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs @@ -11,6 +11,7 @@ {{! template-lint-disable no-pointer-down-event-binding }} {{on "mousedown" this.onMousedown}} {{on "mouseover" @onMouseover}} + ...attributes >
  • \ No newline at end of file diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.ts b/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.ts index 9d478be2..37355d07 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.ts +++ b/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.ts @@ -18,7 +18,7 @@ interface ToucanFormComboboxOptionControlComponentSignature { Blocks: { default: []; }; - Element: HTMLInputElement; + Element: HTMLLIElement; } const className = 'toucan-form-select-option-control'; From 9adfbf62a86a1ef0725a6e39213569bb5bcdb499 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 13:59:08 -0400 Subject: [PATCH 53/86] tests: Add combobox control tests --- .../form/controls/combobox/option.hbs | 2 + .../src/components/form/controls/combobox.ts | 46 +- .../integration/components/combobox-test.gts | 895 ++++++++++++++++++ 3 files changed, 939 insertions(+), 4 deletions(-) create mode 100644 test-app/tests/integration/components/combobox-test.gts diff --git a/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs b/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs index da9fd928..75d8d750 100644 --- a/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs +++ b/packages/ember-toucan-core/src/-private/components/form/controls/combobox/option.hbs @@ -5,6 +5,7 @@ {{this.styles}} {{this.className}} " + data-active={{if @isActive "true" "false"}} id="{{@popoverId}}-{{@index}}" role="option" {{on "click" this.onClick}} @@ -23,6 +24,7 @@ they need, which means the INPUT is superfluous and is thus noise? }} {{! template-lint-disable no-nested-interactive }} + {{! template-lint-disable require-input-label }} Promise; + onFilter?: (input: string) => unknown[]; /** * `@options` forms the content of this component. @@ -135,6 +135,9 @@ export default class ToucanFormComboboxControlComponent extends Component); + + assert.dom('[data-combobox]').hasTagName('input'); + assert.dom('[data-combobox]').hasClass('text-titles-and-attributes'); + assert.dom('[data-combobox]').hasClass('shadow-focusable-outline'); + assert.dom('[data-combobox]').doesNotHaveClass('text-disabled'); + assert.dom('[data-combobox]').doesNotHaveClass('shadow-error-outline'); + assert + .dom('[data-combobox]') + .doesNotHaveClass('focus:shadow-error-focus-outline'); + + assert.dom('[data-combobox]').hasAttribute('aria-autocomplete', 'list'); + assert.dom('[data-combobox]').hasAttribute('aria-haspopup', 'listbox'); + assert.dom('[data-combobox]').hasAttribute('autocapitalize', 'none'); + assert.dom('[data-combobox]').hasAttribute('autocomplete', 'off'); + assert.dom('[data-combobox]').hasAttribute('autocorrect', 'off'); + assert.dom('[data-combobox]').hasAttribute('role', 'combobox'); + assert.dom('[data-combobox]').hasAttribute('spellcheck', 'false'); + assert.dom('[data-combobox]').hasAttribute('type', 'text'); + }); + + test('it disables the combobox using `@isDisabled`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').isDisabled(); + assert.dom('[data-combobox]').hasClass('text-disabled'); + assert + .dom('[data-combobox]') + .doesNotHaveClass('text-titles-and-attributes'); + }); + + test('it sets readonly on the combobox using `@isReadOnly`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasAttribute('readonly'); + + assert.dom('[data-combobox]').hasClass('shadow-read-only-outline'); + assert.dom('[data-combobox]').hasClass('bg-surface-xl'); + assert.dom('[data-combobox]').hasNoClass('bg-overlay-1'); + assert.dom('[data-combobox]').hasNoClass('text-disabled'); + assert.dom('[data-combobox]').hasNoClass('shadow-error-outline'); + assert.dom('[data-combobox]').hasNoClass('shadow-focusable-outline'); + }); + + test('it spreads attributes to the underlying combobox', async function (assert) { + await render(); + + assert + .dom('[data-combobox]') + .hasAttribute('placeholder', 'Placeholder text'); + }); + + test('it applies the error shadow when `@hasError={{true}}`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasClass('shadow-error-outline'); + assert.dom('[data-combobox]').hasClass('focus:shadow-error-focus-outline'); + assert.dom('[data-combobox]').doesNotHaveClass('shadow-focusable-outline'); + }); + + test('it opens the popover on click', async function (assert) { + await render(); + + assert.dom('[role="listbox"]').doesNotExist(); + + await click('[data-combobox]'); + + assert.dom('[role="listbox"]').exists(); + }); + + test('it opens the popover when the input receives input', async function (assert) { + await render(); + + assert.dom('[role="listbox"]').doesNotExist(); + + await fillIn('[data-combobox]', 'b'); + + assert.dom('[role="listbox"]').exists(); + }); + + test('it sets `aria-expanded` based on the popover state', async function (assert) { + await render(); + + assert.dom('[role="listbox"]').doesNotExist(); + + assert.dom('[data-combobox]').hasNoAttribute('aria-expanded'); + + await click('[data-combobox]'); + + assert.dom('[data-combobox]').hasAttribute('aria-expanded'); + }); + + test('it sets `aria-controls`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasAttribute('aria-controls'); + }); + + test('it applies the provided `@contentClass` to the popover content list', async function (assert) { + await render(); + + await click('[data-combobox]'); + + assert.dom('[role="listbox"]').exists(); + assert.dom('[role="listbox"]').hasClass('test-class'); + }); + + test('it renders the provided options in the popover list', async function (assert) { + await render(); + + await click('[data-combobox]'); + + assert.dom('[role="option"]').exists({ count: 2 }); + assert.dom('[data-option]').exists({ count: 2 }); + }); + + test('it sets the value attribute via `@selected`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasValue('blue'); + }); + + test('it sets the value attribute via `@selected` and `@optionKey` when `@selected` is an object', async function (assert) { + let options = [ + { + label: 'Blue', + value: 'blue', + }, + ]; + + let selected = options[0]; + + await render(); + + assert.dom('[data-combobox]').hasValue('Blue'); + }); + + test('it sets `aria-selected` properly on the list item that is currently selected', async function (assert) { + await render(); + + await click('[data-combobox]'); + + // Since `@selected="blue"`, we expect it to be selected + assert + .dom('[role="option"]:first-child') + .hasAttribute('aria-selected', 'true'); + + // ...but not the "red" one! + assert + .dom('[role="option"]:last-child') + .hasAttribute('aria-selected', 'false'); + }); + + test('it provides default filtering when `@options` is an array of strings', async function (assert) { + await render(); + + await fillIn('[data-combobox]', 'blue'); + + // Filtering works as we expect + assert.dom('[role="option"]').exists({ count: 1 }); + assert.dom('[role="option"]').hasText('blue'); + + // Resetting the filter by clearing the input should + // display all available options + await fillIn('[data-combobox]', ''); + assert.dom('[role="option"]').exists({ count: 2 }); + + // Verify we can filter again after clearing + await fillIn('[data-combobox]', 'red'); + assert.dom('[role="option"]').exists({ count: 1 }); + assert.dom('[role="option"]').hasText('red'); + }); + + test('it provides default filtering when `@options` is an array of objects and is provided with `@optionKey`', async function (assert) { + let options = [ + { + label: 'Blue', + value: 'blue', + }, + { + label: 'Red', + value: 'red', + }, + ]; + + await render(); + + await fillIn('[data-combobox]', 'blue'); + + // Filtering works as we expect + assert.dom('[role="option"]').exists({ count: 1 }); + // NOTE: We should be using option.label (capitalized "Blue" instead of "blue") + assert.dom('[role="option"]').hasText('Blue'); + + // Resetting the filter by clearing the input should + // display all available options + await fillIn('[data-combobox]', ''); + assert.dom('[role="option"]').exists({ count: 2 }); + + // Verify we can filter again after clearing + await fillIn('[data-combobox]', 'red'); + assert.dom('[role="option"]').exists({ count: 1 }); + // NOTE: We should be using option.label (capitalized "Red" instead of "red") + assert.dom('[role="option"]').hasText('Red'); + }); + + test('it uses the provided `@noResultsText` when no results are found with filtering', async function (assert) { + await render(); + + await fillIn('[data-combobox]', 'something-not-in-the-list'); + + // We should not have any list items + assert.dom('[role="option"]').exists({ count: 0 }); + + // ...but we should have our no results item! + assert.dom('[role="status"]').exists(); + assert.dom('[role="status"]').hasTagName('li'); + assert.dom('[role="status"]').hasText('No results'); + assert + .dom('[role="status"]') + .hasAttribute( + 'aria-live', + 'assertive', + 'Expected assertive so it is announced to screenreaders' + ); + }); + + test('it calls `@onChange` when an option is selected via mouse click', async function (assert) { + assert.expect(5); + + let handleChange = (value: unknown) => { + assert.strictEqual(value, 'blue', 'Expected input to match'); + assert.step('handleChange'); + }; + + await render(); + + assert.verifySteps([]); + + await fillIn('[data-combobox]', 'blue'); + + assert.dom('[role="option"]').exists({ count: 1 }); + + await click('[role="option"]'); + + assert.verifySteps(['handleChange']); + }); + + test('it calls `@onChange` when an option is selected via the keyboard with ENTER', async function (assert) { + assert.expect(5); + + let handleChange = (value: unknown) => { + assert.strictEqual(value, 'blue', 'Expected input to match'); + assert.step('handleChange'); + }; + + await render(); + + assert.verifySteps([]); + + await fillIn('[data-combobox]', 'blue'); + + assert.dom('[role="option"]').exists({ count: 1 }); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'Enter'); + + assert.verifySteps(['handleChange']); + }); + + test('it uses the results from `@onFilter` to populate the filtered options', async function (assert) { + assert.expect(5); + + let handleFilter = (value: unknown) => { + assert.strictEqual( + value, + 'y', + 'Expected the input to match what was entered via fillIn' + ); + assert.step('onFilter'); + + return ['yellow']; + }; + + await render(); + + await fillIn('[data-combobox]', 'y'); + + assert.verifySteps(['onFilter']); + + assert.dom('[role="option"]').exists({ count: 1 }); + assert.dom('[role="option"]').hasText('yellow'); + }); + + test('it sets the "active" item to the first one in the list when the combobox gains focus', async function (assert) { + await render(); + + await click('[data-combobox]'); + + assert.dom('[role="option"]').exists({ count: 2 }); + + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'true'); + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'false'); + }); + + test('it sets the "active" item to the next item in the list when using the DOWN arrow', async function (assert) { + await render(); + + await click('[data-combobox]'); + + assert.dom('[role="option"]').exists({ count: 2 }); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'ArrowDown'); + + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'false'); + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'true'); + }); + + test('it sets the "active" item to the previous item in the list when using the UP arrow', async function (assert) { + // NOTE: Setting the selected option to "red" here so that + // the last item in the list will be active so that we can + // move up! + await render(); + + await click('[data-combobox]'); + + assert.dom('[role="option"]').exists({ count: 2 }); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'ArrowUp'); + + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'true'); + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'false'); + }); + + test('it closes an open popover when the ESCAPE key is pressed', async function (assert) { + await render(); + + await click('[data-combobox]'); + + assert.dom('[role="listbox"]').exists(); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'Escape'); + + assert.dom('[role="listbox"]').doesNotExist(); + }); + + test('it closes an open popover when the component is blurred', async function (assert) { + await render(); + + // Open the popover + await click('[data-combobox]'); + + assert.dom('[role="listbox"]').exists(); + + // Now blur the elment + await click('[data-input]'); + + assert.dom('[role="listbox"]').doesNotExist(); + }); + + test('it reopens the popover when any key is pressed if the popover is closed', async function (assert) { + await render(); + + // Open the popover by clicking it + await click('[data-combobox]'); + + // Now close it + await triggerKeyEvent('[data-combobox]', 'keydown', 'Escape'); + // Now reopen it + await triggerKeyEvent('[data-combobox]', 'keydown', 'ArrowDown'); + + assert.dom('[role="listbox"]').exists(); + }); + + test('it makes the first option "active" when the metakey and UP arrow is pressed', async function (assert) { + let options = ['a', 'b', 'c', 'd', 'e', 'f']; + + // NOTE: Our selected option is currently at the bottom of the list! + await render(); + + // Open the popover by clicking it + await click('[data-combobox]'); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'ArrowUp', { + metaKey: true, + }); + + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'true'); + // Verify our last item is no longer "active" + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'false'); + }); + + test('it makes the last option "active" when the metakey and DOWN arrow is pressed', async function (assert) { + let options = ['a', 'b', 'c', 'd', 'e', 'f']; + + // NOTE: Our selected option is currently at the top of the list! + await render(); + + // Open the popover by clicking it + await click('[data-combobox]'); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'ArrowDown', { + metaKey: true, + }); + + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'true'); + + // Verify our first item is no longer "active" + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'false'); + }); + + test('it makes the last option "active" when the PAGEDOWN key is pressed', async function (assert) { + let options = ['a', 'b', 'c', 'd', 'e', 'f']; + + // NOTE: Our selected option is currently at the top of the list! + await render(); + + // Open the popover by clicking it + await click('[data-combobox]'); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'PageDown'); + + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'true'); + + // Verify our first item is no longer "active" + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'false'); + }); + + test('it makes the first option "active" when the PAGEUP key is pressed', async function (assert) { + let options = ['a', 'b', 'c', 'd', 'e', 'f']; + + // NOTE: Our selected option is currently at the bottom of the list! + await render(); + + // Open the popover by clicking it + await click('[data-combobox]'); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'PageUp'); + + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'true'); + // Verify our last item is no longer "active" + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'false'); + }); + + test('it makes the first option "active" when the HOME key is pressed', async function (assert) { + let options = ['a', 'b', 'c', 'd', 'e', 'f']; + + // NOTE: Our selected option is currently at the bottom of the list! + await render(); + + // Open the popover by clicking it + await click('[data-combobox]'); + + await triggerKeyEvent('[data-combobox]', 'keydown', 'Home'); + + assert + .dom('[role="option"]:first-child') + .hasAttribute('data-active', 'true'); + // Verify our last item is no longer "active" + assert + .dom('[role="option"]:last-child') + .hasAttribute('data-active', 'false'); + }); + + /** + * This test covers the case where we have a selected option, + * the user clears the input completely, and then the component's + * input tag is blurred. In that case, we want to clear the input + * and reset their selection to null. + */ + test('it calls `@onChange` with null after a user clears their original input selection', async function (assert) { + assert.expect(4); + + let handleChange = (value: unknown) => { + assert.strictEqual(value, null, 'Expected `value` to be null'); + assert.step('handleChange'); + }; + + // NOTE: We have a selected option + // NOTE: We add an input tag so we have something to blur to (by focusing another element) + await render(); + + await click('[data-combobox]'); + + // Clear the input + await fillIn('[data-combobox]', ''); + + // Now blur the elment + await click('[data-input]'); + + assert.dom('[data-combobox]').hasValue(''); + assert.verifySteps(['handleChange']); + }); + + test('it reverts to the selected option when a user enters garbage after previously having a valid selection (with `@selected` as a string)', async function (assert) { + assert.expect(2); + + let handleChange = () => { + assert.step('do-not-expect-this-to-be-called!'); + }; + + // NOTE: We have a selected option + // NOTE: We add an input tag so we have something to blur to (by focusing another element) + await render(); + + await click('[data-combobox]'); + + // Enter garbage into the input + await fillIn('[data-combobox]', 'some-garbage'); + + // Now blur the elment + await click('[data-input]'); + + // Verify the input is reset to our `@selected` option + assert.dom('[data-combobox]').hasValue('blue'); + + // NOTE: We do not expect the `@onChange` to be called in this + // case as we are only visually resetting to the previously + // selected value + assert.verifySteps([]); + }); + + test('it reverts to the selected option when a user enters garbage after previously having a valid selection (with `@selected` as an object)', async function (assert) { + assert.expect(2); + + let options = [ + { + label: 'Blue', + value: 'blue', + }, + { + label: 'Red', + value: 'red', + }, + ]; + + let selected = options[0]; + + let handleChange = () => { + assert.step('do-not-expect-this-to-be-called!'); + }; + + // NOTE: We have a selected option + // NOTE: We add an input tag so we have something to blur to (by focusing another element) + await render(); + + await click('[data-combobox]'); + + // Enter garbage into the input + await fillIn('[data-combobox]', 'some-garbage'); + + // Now blur the elment + await click('[data-input]'); + + // Verify the input is reset to our `@selected` option + assert.dom('[data-combobox]').hasValue('Blue'); + + // NOTE: We do not expect the `@onChange` to be called in this + // case as we are only visually resetting to the previously + // selected value + assert.verifySteps([]); + }); +}); From e7e1b15332f10ec28bafdbf12c34c2a6b42d5d7e Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Thu, 6 Jul 2023 16:02:27 -0400 Subject: [PATCH 54/86] tests: Add combobox field tests --- .../src/components/form/fields/combobox.hbs | 2 +- .../components/combobox-field-test.gts | 250 ++++++++++++++++++ 2 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 test-app/tests/integration/components/combobox-field-test.gts diff --git a/packages/ember-toucan-core/src/components/form/fields/combobox.hbs b/packages/ember-toucan-core/src/components/form/fields/combobox.hbs index 7dc0f32f..1059d0b1 100644 --- a/packages/ember-toucan-core/src/components/form/fields/combobox.hbs +++ b/packages/ember-toucan-core/src/components/form/fields/combobox.hbs @@ -48,7 +48,7 @@ + + ); + + assert.dom('[data-label]').hasText('Label'); + + assert + .dom('[data-hint]') + .doesNotExist( + 'Expected hint block not to be displayed as a hint was not provided' + ); + + assert.dom('[data-combobox]').hasTagName('input'); + assert.dom('[data-combobox]').hasAttribute('id'); + assert.dom('[data-combobox]').hasClass('text-titles-and-attributes'); + + assert + .dom('[data-error]') + .doesNotExist( + 'Expected hint block not to be displayed as an error was not provided' + ); + + assert.dom('[data-lock-icon]').doesNotExist(); + }); + + test('it renders with a hint', async function (assert) { + await render(); + + let hint = find('[data-hint]'); + + assert.dom(hint).hasText('Hint text'); + assert.dom(hint).hasAttribute('id'); + + let hintId = hint?.getAttribute('id') || ''; + + assert.ok(hintId, 'Expected hintId to be truthy'); + + let describedby = + find('[data-combobox]')?.getAttribute('aria-describedby') || ''; + + assert.ok( + describedby.includes(hintId), + 'Expected hintId to be included in the aria-describedby' + ); + }); + + test('it renders with a hint and label block', async function (assert) { + await render(); + + assert.dom('[data-hint]').hasText('hint block content'); + assert.dom('[data-label]').hasText('label block content'); + }); + + test('it renders with an error', async function (assert) { + await render(); + + let error = find('[data-error]'); + + assert.dom(error).hasText('Error text'); + assert.dom(error).hasAttribute('id'); + + let errorId = error?.getAttribute('id') || ''; + + assert.ok(errorId, 'Expected errorId to be truthy'); + + let describedby = + find('[data-combobox]')?.getAttribute('aria-describedby') || ''; + + assert.ok( + describedby.includes(errorId), + 'Expected errorId to be included in the aria-describedby' + ); + + assert.dom('[data-combobox]').hasAttribute('aria-invalid', 'true'); + + assert.dom('[data-combobox]').hasClass('shadow-error-outline'); + assert.dom('[data-combobox]').hasClass('focus:shadow-error-focus-outline'); + assert.dom('[data-combobox]').doesNotHaveClass('shadow-focusable-outline'); + }); + + test('it sets aria-describedby when both a hint and error are provided using the hint and errorIds', async function (assert) { + await render(); + + let errorId = find('[data-error]')?.getAttribute('id') || ''; + + assert.ok(errorId, 'Expected errorId to be truthy'); + + let hintId = find('[data-hint]')?.getAttribute('id') || ''; + + assert.ok(hintId, 'Expected hintId to be truthy'); + + assert + .dom('[data-combobox]') + .hasAttribute('aria-describedby', `${errorId} ${hintId}`); + }); + + test('it disables the textarea using `@isDisabled` and renders a lock icon', async function (assert) { + await render(); + + assert.dom('[data-combobox]').isDisabled(); + assert.dom('[data-combobox]').hasClass('text-disabled'); + + assert.dom('[data-lock-icon]').exists(); + }); + + test('it sets readonly on the textarea using `@isReadOnly` and renders a lock icon', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasAttribute('readonly'); + + assert.dom('[data-lock-icon]').exists(); + }); + + test('it spreads attributes to the underlying textarea', async function (assert) { + await render(); + + assert + .dom('[data-combobox]') + .hasAttribute('placeholder', 'Placeholder text'); + }); + + test('it sets the value attribute via `@selected`', async function (assert) { + await render(); + + assert.dom('[data-combobox]').hasValue('blue'); + }); + + // NOTE: This functionality is deeply tested in the Control, as it can + // get pretty complex. This test is to ensure `@onChange` is generally + // working. + test('it calls `@onChange` when an option is selected', async function (assert) { + assert.expect(5); + + let options = ['blue', 'red']; + + let handleChange = (value: unknown) => { + assert.strictEqual(value, 'blue', 'Expected input to match'); + assert.step('handleChange'); + }; + + await render(); + + assert.verifySteps([]); + + await fillIn('[data-combobox]', 'blue'); + + assert.dom('[role="option"]').exists({ count: 1 }); + + await click('[role="option"]'); + + assert.verifySteps(['handleChange']); + }); + + test('it applies the provided `@rootTestSelector` to the data-root-field attribute', async function (assert) { + await render(); + + assert.dom('[data-root-field="selector"]').exists(); + }); + + test('it throws an assertion error if no `@label` is provided', async function (assert) { + assert.expect(1); + + setupOnerror((e: Error) => { + assert.ok( + e.message.includes( + 'Assertion Failed: You need either :label or @label' + ), + 'Expected assertion error message' + ); + }); + + await render(); + }); + + test('it throws an assertion error if a `@label` and :label are provided', async function (assert) { + assert.expect(1); + + setupOnerror((e: Error) => { + assert.ok( + e.message.includes( + 'Assertion Failed: You can have :label or @label, but not both' + ), + 'Expected assertion error message' + ); + }); + + await render(); + }); +}); From 5916adcb6a98530376ff7e2c588ee0680fe4c4da Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Fri, 7 Jul 2023 08:38:32 -0400 Subject: [PATCH 55/86] tests: Add ToucanForm Combobox tests --- .../toucan-form/form-combobox-test.gts | 111 ++++++++++++++++++ .../toucan-form/toucan-form-test.gts | 61 +++++++++- 2 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 test-app/tests/integration/components/toucan-form/form-combobox-test.gts diff --git a/test-app/tests/integration/components/toucan-form/form-combobox-test.gts b/test-app/tests/integration/components/toucan-form/form-combobox-test.gts new file mode 100644 index 00000000..0608215e --- /dev/null +++ b/test-app/tests/integration/components/toucan-form/form-combobox-test.gts @@ -0,0 +1,111 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; + +import ToucanForm from '@crowdstrike/ember-toucan-form/components/toucan-form'; +import { setupRenderingTest } from 'test-app/tests/helpers'; + +interface TestData { + selection?: string; +} + +module('Integration | Component | ToucanForm | Combobox', function (hooks) { + setupRenderingTest(hooks); + + test('it renders `@label` and `@hint` component arguments', async function (assert) { + const data: TestData = { + selection: '', + }; + + await render(); + + assert.dom('[data-label]').exists(); + assert.dom('[data-hint]').exists(); + }); + + test('it renders a `:label` named block with a `@hint` argument', async function (assert) { + const data: TestData = { + selection: '', + }; + + await render(); + + assert.dom('[data-label-block]').exists(); + + // NOTE: `data-hint` comes from `@hint`. + assert.dom('[data-hint]').exists(); + assert.dom('[data-hint]').hasText('Hint'); + }); + + test('it renders a `:hint` named block with a `@label` argument', async function (assert) { + const data: TestData = { + selection: '', + }; + + await render(); + + // NOTE: `data-label` comes from `@label`. + assert.dom('[data-label]').exists(); + assert.dom('[data-label]').hasText('Label'); + + assert.dom('[data-hint-block]').exists(); + }); + + test('it renders both a `:label` and `:hint` named block', async function (assert) { + const data: TestData = { + selection: '', + }; + + await render(); + + assert.dom('[data-label-block]').exists(); + assert.dom('[data-hint-block]').exists(); + }); +}); diff --git a/test-app/tests/integration/components/toucan-form/toucan-form-test.gts b/test-app/tests/integration/components/toucan-form/toucan-form-test.gts index dfdd949b..573472c0 100644 --- a/test-app/tests/integration/components/toucan-form/toucan-form-test.gts +++ b/test-app/tests/integration/components/toucan-form/toucan-form-test.gts @@ -1,6 +1,11 @@ /* eslint-disable no-undef -- Until https://github.com/ember-cli/eslint-plugin-ember/issues/1747 is resolved... */ -import { on } from '@ember/modifier'; -import { click, fillIn, render, triggerEvent } from '@ember/test-helpers'; +import { + click, + fillIn, + render, + triggerEvent, + triggerKeyEvent, +} from '@ember/test-helpers'; import { module, test } from 'qunit'; import ToucanForm from '@crowdstrike/ember-toucan-form/components/toucan-form'; @@ -12,8 +17,11 @@ const testFile = new File(['Some sample content'], 'file.txt', { type: 'text/plain', }); +const options = ['blue', 'red', 'yellow']; + interface TestData { checkboxes?: Array; + combobox?: string; comment?: string; firstName?: string; radio?: string; @@ -132,6 +140,7 @@ module('Integration | Component | ToucanForm', function (hooks) { test('it sets the yielded component values based on `@data`', async function (assert) { const data: TestData = { checkboxes: ['option-1', 'option-3'], + combobox: 'blue', comment: 'multi-line text', firstName: 'single line text', radio: 'option-2', @@ -178,6 +187,18 @@ module('Integration | Component | ToucanForm', function (hooks) { @trigger="Select files" @deleteLabel="Delete" /> + + + {{! Need to figure out these types }} + {{! @glint-expect-error }} + {{combobox.option}} + ); @@ -208,10 +229,14 @@ module('Integration | Component | ToucanForm', function (hooks) { // File input assert.dom('[data-files] [data-file-name]').hasText('file.txt'); + + // Combobox + assert.dom('[data-combobox]').hasAttribute('name', 'combobox'); + assert.dom('[data-combobox]').hasValue('blue'); }); test('it triggers validation and shows error messages in the Toucan Core components', async function (assert) { - assert.expect(18); + assert.expect(19); const handleSubmit = ({ files, ...data }: TestData) => { // Checking deep equality on files can get really tricky due @@ -232,6 +257,7 @@ module('Integration | Component | ToucanForm', function (hooks) { data, { checkboxes: ['option-2'], + combobox: 'red', comment: 'A comment.', firstName: 'CrowdStrike', radio: 'option-2', @@ -246,6 +272,7 @@ module('Integration | Component | ToucanForm', function (hooks) { const formValidateCallback = ({ checkboxes, + combobox, comment, firstName, radio, @@ -264,6 +291,16 @@ module('Integration | Component | ToucanForm', function (hooks) { ]; } + if (!combobox) { + errors.combobox = [ + { + type: 'required', + value: combobox, + message: 'One combobox item must be selected', + }, + ]; + } + if (!comment) { errors.comment = [ { @@ -387,6 +424,19 @@ module('Integration | Component | ToucanForm', function (hooks) { data-file-input-field /> + + {{! Need to figure out these types }} + {{! @glint-expect-error }} + {{combobox.option}} + + ); @@ -427,6 +477,9 @@ module('Integration | Component | ToucanForm', function (hooks) { assert .dom('[data-root-field="data-file-input-wrapper"] [data-error]') .hasText('A file must be added'); + assert + .dom('[data-root-field="data-combobox-wrapper"] [data-error]') + .hasText('One combobox item must be selected'); // Satisfy the validation and submit the form await fillIn('[data-textarea]', 'A comment.'); @@ -437,6 +490,8 @@ module('Integration | Component | ToucanForm', function (hooks) { await triggerEvent('[data-file-input-field]', 'change', { files: [testFile], }); + await fillIn('[data-combobox]', 'red'); + await triggerKeyEvent('[data-combobox]', 'keydown', 'Enter'); await click('[data-test-submit]'); From bcf716eb17db129a79b393dd452cf8f5d96cb50e Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Fri, 7 Jul 2023 08:38:43 -0400 Subject: [PATCH 56/86] tests: Remove eslint-disable no-undef --- test-app/tests/integration/components/combobox-field-test.gts | 1 - test-app/tests/integration/components/combobox-test.gts | 1 - 2 files changed, 2 deletions(-) diff --git a/test-app/tests/integration/components/combobox-field-test.gts b/test-app/tests/integration/components/combobox-field-test.gts index 86722068..de8772c1 100644 --- a/test-app/tests/integration/components/combobox-field-test.gts +++ b/test-app/tests/integration/components/combobox-field-test.gts @@ -1,4 +1,3 @@ -/* eslint-disable no-undef -- Until https://github.com/ember-cli/eslint-plugin-ember/issues/1747 is resolved... */ import { click, fillIn, find, render, setupOnerror } from '@ember/test-helpers'; import { module, test } from 'qunit'; diff --git a/test-app/tests/integration/components/combobox-test.gts b/test-app/tests/integration/components/combobox-test.gts index 6ffd2661..23a65e3f 100644 --- a/test-app/tests/integration/components/combobox-test.gts +++ b/test-app/tests/integration/components/combobox-test.gts @@ -1,4 +1,3 @@ -/* eslint-disable no-undef -- Until https://github.com/ember-cli/eslint-plugin-ember/issues/1747 is resolved... */ import { click, fillIn, render, triggerKeyEvent } from '@ember/test-helpers'; import { module, test } from 'qunit'; From 74ab8542df34117cb9c9e056a6d6ca87db7e0462 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Fri, 7 Jul 2023 08:43:04 -0400 Subject: [PATCH 57/86] deps: Add pnpm-lock changes --- pnpm-lock.yaml | 152 +++++++++++++++++++++++++++++++++++++----- test-app/package.json | 1 + 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62819ed7..1909071a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,7 +33,7 @@ importers: version: 1.1.3(@babel/core@7.20.12)(@crowdstrike/tailwind-toucan-base@3.5.0)(@docfy/core@0.6.0)(@docfy/ember@0.6.0)(@glimmer/component@1.1.2)(@glint/environment-ember-loose@1.0.2)(@glint/template@1.0.2)(@tailwindcss/typography@0.5.9)(ember-source@5.1.0)(highlight.js@11.7.0)(highlightjs-glimmer@2.0.1) '@crowdstrike/ember-toucan-core': specifier: workspace:* - version: file:packages/ember-toucan-core(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@3.2.4) + version: file:packages/ember-toucan-core(@babel/core@7.20.12)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@3.2.4) '@crowdstrike/ember-toucan-form': specifier: workspace:* version: file:packages/ember-toucan-form(@crowdstrike/ember-toucan-core@0.2.1)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(ember-headless-form@1.0.0-beta.3)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@3.2.4) @@ -55,6 +55,9 @@ importers: ember-resources: specifier: ^6.0.0 version: 6.0.0(@ember/test-waiters@3.0.2)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.0.2)(ember-source@5.1.0) + ember-velcro: + specifier: ^2.1.0 + version: 2.1.0(@babel/core@7.20.12)(ember-source@5.1.0) highlight.js: specifier: ^11.6.0 version: 11.7.0 @@ -355,6 +358,12 @@ importers: '@embroider/addon-shim': specifier: ^1.0.0 version: 1.8.4 + '@floating-ui/dom': + specifier: ^1.4.2 + version: 1.4.4 + ember-velcro: + specifier: ^2.1.0 + version: 2.1.0(@babel/core@7.20.12)(ember-source@5.1.0) devDependencies: '@babel/core': specifier: ^7.17.0 @@ -368,6 +377,9 @@ importers: '@babel/plugin-proposal-decorators': specifier: ^7.17.0 version: 7.20.13(@babel/core@7.20.12) + '@babel/plugin-proposal-private-methods': + specifier: ^7.18.6 + version: 7.18.6(@babel/core@7.20.12) '@babel/plugin-syntax-decorators': specifier: ^7.17.0 version: 7.19.0(@babel/core@7.20.12) @@ -557,7 +569,7 @@ importers: version: 7.18.6(@babel/core@7.21.4) '@crowdstrike/ember-toucan-core': specifier: workspace:* - version: file:packages/ember-toucan-core(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19) + version: file:packages/ember-toucan-core(@babel/core@7.21.4)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19) '@crowdstrike/ember-toucan-styles': specifier: ^2.0.1 version: 2.0.1(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@2.2.19) @@ -728,7 +740,7 @@ importers: version: 7.19.1(@babel/core@7.20.12)(eslint@8.33.0) '@crowdstrike/ember-toucan-core': specifier: workspace:* - version: file:packages/ember-toucan-core(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19) + version: file:packages/ember-toucan-core(@babel/core@7.20.12)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19) '@crowdstrike/ember-toucan-form': specifier: workspace:* version: file:packages/ember-toucan-form(@crowdstrike/ember-toucan-core@0.2.1)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(ember-headless-form@1.0.0-beta.3)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@2.2.19) @@ -921,6 +933,9 @@ importers: ember-try: specifier: ^2.0.0 version: 2.0.0 + ember-velcro: + specifier: ^2.1.0 + version: 2.1.0(@babel/core@7.20.12)(ember-source@5.1.0) eslint: specifier: ^8.32.0 version: 8.33.0 @@ -4132,6 +4147,14 @@ packages: - supports-color dev: true + /@floating-ui/core@1.3.1: + resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==} + + /@floating-ui/dom@1.4.4: + resolution: {integrity: sha512-21hhDEPOiWkGp0Ys4Wi6Neriah7HweToKra626CIK712B5m9qkdz54OP9gVldUg+URnBTpv/j/bi/skmGdstXQ==} + dependencies: + '@floating-ui/core': 1.3.1 + /@glimmer/compiler@0.84.2: resolution: {integrity: sha512-rU8qpqbqxIPwrEQH82yDDFi1hgv6ud1agYexmnmCXlaLS5uCVATJAqKsVozc7aHOgmmF4Ukd/LoF4NYfGr8X3w==} dependencies: @@ -6842,7 +6865,6 @@ packages: loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - dev: true /babel-loader@8.3.0(@babel/core@7.21.4)(webpack@5.75.0): resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} @@ -8957,7 +8979,6 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 3.1.1 semver: 7.5.1 - dev: true /css-loader@5.2.7(webpack@5.75.0): resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} @@ -9600,7 +9621,6 @@ packages: transitivePeerDependencies: - supports-color - webpack - dev: true /ember-auto-import@2.6.3(webpack@5.75.0): resolution: {integrity: sha512-uLhrRDJYWCRvQ4JQ1e64XlSrqAKSd6PXaJ9ZsZI6Tlms9T4DtQFxNXasqji2ZRJBVrxEoLCRYX3RTldsQ0vNGQ==} @@ -10233,6 +10253,19 @@ packages: - supports-color dev: true + /ember-functions-as-helper-polyfill@2.1.1(ember-source@5.1.0): + resolution: {integrity: sha512-vZ2w9G/foohwtPm99Jos1m6bhlXyyyiJ4vhLbxyjWB4wh7bcpRzXPgCewDRrwefZQ2BwtHg3c9zvVMlI0g+o2Q==} + engines: {node: '>= 14.0.0'} + peerDependencies: + ember-source: ^3.25.0 || ^4.0.0 + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-typescript: 5.2.1 + ember-cli-version-checker: 5.1.2 + ember-source: 5.1.0(@babel/core@7.20.12)(@glimmer/component@1.1.2) + transitivePeerDependencies: + - supports-color + /ember-get-config@1.1.0: resolution: {integrity: sha512-diD+HwwY8QqpEk5DnDYfH7onYwl6NOgr1qv1ENbXih+/iiWYUVS/e0S/PlM7A4gdorD9spn1bnisnTLTf49Wpw==} engines: {node: 12.* || 14.* || >= 16} @@ -10290,6 +10323,33 @@ packages: - supports-color dev: true + /ember-modifier@3.2.7(@babel/core@7.20.12): + resolution: {integrity: sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==} + engines: {node: 12.* || >= 14} + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript: 5.2.1 + ember-compatibility-helpers: 1.2.6(@babel/core@7.20.12) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + /ember-modifier@3.2.7(@babel/core@7.21.4): + resolution: {integrity: sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==} + engines: {node: 12.* || >= 14} + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript: 5.2.1 + ember-compatibility-helpers: 1.2.6(@babel/core@7.21.4) + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true + /ember-modifier@4.1.0(ember-source@5.1.0): resolution: {integrity: sha512-YFCNpEYj6jdyy3EjslRb2ehNiDvaOrXTilR9+ngq+iUqSHYto2zKV0rleiA1XJQ27ELM1q8RihT29U6Lq5EyqQ==} peerDependencies: @@ -10474,7 +10534,6 @@ packages: - rsvp - supports-color - webpack - dev: true /ember-source@5.1.0(@babel/core@7.20.12)(@glimmer/component@1.1.2)(webpack@5.75.0): resolution: {integrity: sha512-atUeliA3TGyL8LB8EYIouvJukLtlbqFdtNT83Lst9TEtKtqnoyGJhr/5C/C+AxOX7r5s5Lo5cBBNBQadJgNFNg==} @@ -10745,6 +10804,33 @@ packages: - supports-color dev: true + /ember-velcro@2.1.0(@babel/core@7.20.12)(ember-source@5.1.0): + resolution: {integrity: sha512-5m1JDAGmTT9J8SrVL1NrBj6qe1L0dgb3T1muWL2b36HO4sCHn8/njT/GMHXorcbYz7C/PawYuxKpbILNaGa50A==} + engines: {node: 14.* || >= 16} + dependencies: + '@embroider/addon-shim': 1.8.4 + '@floating-ui/dom': 1.4.4 + ember-functions-as-helper-polyfill: 2.1.1(ember-source@5.1.0) + ember-modifier: 3.2.7(@babel/core@7.20.12) + transitivePeerDependencies: + - '@babel/core' + - ember-source + - supports-color + + /ember-velcro@2.1.0(@babel/core@7.21.4)(ember-source@5.1.0): + resolution: {integrity: sha512-5m1JDAGmTT9J8SrVL1NrBj6qe1L0dgb3T1muWL2b36HO4sCHn8/njT/GMHXorcbYz7C/PawYuxKpbILNaGa50A==} + engines: {node: 14.* || >= 16} + dependencies: + '@embroider/addon-shim': 1.8.4 + '@floating-ui/dom': 1.4.4 + ember-functions-as-helper-polyfill: 2.1.1(ember-source@5.1.0) + ember-modifier: 3.2.7(@babel/core@7.21.4) + transitivePeerDependencies: + - '@babel/core' + - ember-source + - supports-color + dev: true + /ember-window-mock@0.8.1: resolution: {integrity: sha512-wl9TJuBYFWKsPqDY2gms2jbre1L39AkrPQ9EqbhqHbZI4aEq8u8IZJ0nJaOa7IVr/Jy/kSUXYQGTgvNhz1AzPw==} engines: {node: 12.* || 14.* || >= 16} @@ -14375,7 +14461,6 @@ packages: optional: true dependencies: schema-utils: 4.0.0 - dev: true /mini-css-extract-plugin@2.7.2(webpack@5.75.0): resolution: {integrity: sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==} @@ -17373,7 +17458,6 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.1.1 - dev: true /style-loader@2.0.0(webpack@5.75.0): resolution: {integrity: sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==} @@ -18909,7 +18993,7 @@ packages: /zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} - file:packages/ember-toucan-core(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19): + file:packages/ember-toucan-core(@babel/core@7.20.12)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19): resolution: {directory: packages/ember-toucan-core, type: directory} id: file:packages/ember-toucan-core name: '@crowdstrike/ember-toucan-core' @@ -18926,19 +19010,22 @@ packages: dependencies: '@babel/runtime': 7.21.5 '@crowdstrike/ember-toucan-styles': 2.0.1(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@2.2.19) - '@ember/test-helpers': 3.1.0(ember-source@5.1.0) + '@ember/test-helpers': 3.1.0(ember-source@5.1.0)(webpack@5.75.0) '@embroider/addon-shim': 1.8.4 + '@floating-ui/dom': 1.4.4 '@glimmer/tracking': 1.1.2 autoprefixer: 10.4.13(postcss@8.4.21) - ember-source: 5.1.0(@babel/core@7.21.4)(@glimmer/component@1.1.2) + ember-source: 5.1.0(@babel/core@7.20.12)(@glimmer/component@1.1.2)(webpack@5.75.0) + ember-velcro: 2.1.0(@babel/core@7.20.12)(ember-source@5.1.0) fractal-page-object: 0.4.0 postcss: 8.4.21 tailwindcss: 2.2.19(autoprefixer@10.4.13)(postcss@8.4.21) transitivePeerDependencies: + - '@babel/core' - supports-color dev: true - file:packages/ember-toucan-core(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@3.2.4): + file:packages/ember-toucan-core(@babel/core@7.20.12)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@3.2.4): resolution: {directory: packages/ember-toucan-core, type: directory} id: file:packages/ember-toucan-core name: '@crowdstrike/ember-toucan-core' @@ -18957,16 +19044,51 @@ packages: '@crowdstrike/ember-toucan-styles': 2.0.1(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@3.2.4) '@ember/test-helpers': 3.1.0(ember-source@5.1.0)(webpack@5.75.0) '@embroider/addon-shim': 1.8.4 + '@floating-ui/dom': 1.4.4 '@glimmer/tracking': 1.1.2 autoprefixer: 10.4.13(postcss@8.4.21) ember-source: 5.1.0(@babel/core@7.20.12)(@glimmer/component@1.1.2)(webpack@5.75.0) + ember-velcro: 2.1.0(@babel/core@7.20.12)(ember-source@5.1.0) fractal-page-object: 0.4.0 postcss: 8.4.21 tailwindcss: 3.2.4(postcss@8.4.21) transitivePeerDependencies: + - '@babel/core' - supports-color dev: false + file:packages/ember-toucan-core(@babel/core@7.21.4)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19): + resolution: {directory: packages/ember-toucan-core, type: directory} + id: file:packages/ember-toucan-core + name: '@crowdstrike/ember-toucan-core' + version: 0.2.1 + peerDependencies: + '@crowdstrike/ember-toucan-styles': ^2.0.1 + '@ember/test-helpers': ^2.8.1 || ^3.0.0 + '@glimmer/tracking': ^1.1.2 + autoprefixer: ^10.0.2 + ember-source: '>=4.8.0' + fractal-page-object: ^0.3.0 + postcss: ^8.2.14 + tailwindcss: ^2.2.15 || ^3.0.0 + dependencies: + '@babel/runtime': 7.21.5 + '@crowdstrike/ember-toucan-styles': 2.0.1(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@2.2.19) + '@ember/test-helpers': 3.1.0(ember-source@5.1.0) + '@embroider/addon-shim': 1.8.4 + '@floating-ui/dom': 1.4.4 + '@glimmer/tracking': 1.1.2 + autoprefixer: 10.4.13(postcss@8.4.21) + ember-source: 5.1.0(@babel/core@7.21.4)(@glimmer/component@1.1.2) + ember-velcro: 2.1.0(@babel/core@7.21.4)(ember-source@5.1.0) + fractal-page-object: 0.4.0 + postcss: 8.4.21 + tailwindcss: 2.2.19(autoprefixer@10.4.13)(postcss@8.4.21) + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true + file:packages/ember-toucan-form(@crowdstrike/ember-toucan-core@0.2.1)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(ember-headless-form@1.0.0-beta.3)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@2.2.19): resolution: {directory: packages/ember-toucan-form, type: directory} id: file:packages/ember-toucan-form @@ -18983,7 +19105,7 @@ packages: tailwindcss: ^2.2.15 || ^3.0.0 dependencies: '@babel/runtime': 7.21.5 - '@crowdstrike/ember-toucan-core': file:packages/ember-toucan-core(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19) + '@crowdstrike/ember-toucan-core': file:packages/ember-toucan-core(@babel/core@7.20.12)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@2.2.19) '@crowdstrike/ember-toucan-styles': 2.0.1(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@2.2.19) '@ember/test-helpers': 3.1.0(ember-source@5.1.0)(webpack@5.75.0) '@embroider/addon-shim': 1.8.4 @@ -19013,7 +19135,7 @@ packages: tailwindcss: ^2.2.15 || ^3.0.0 dependencies: '@babel/runtime': 7.21.5 - '@crowdstrike/ember-toucan-core': file:packages/ember-toucan-core(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@3.2.4) + '@crowdstrike/ember-toucan-core': file:packages/ember-toucan-core(@babel/core@7.20.12)(@crowdstrike/ember-toucan-styles@2.0.1)(@ember/test-helpers@3.1.0)(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(fractal-page-object@0.4.0)(postcss@8.4.21)(tailwindcss@3.2.4) '@crowdstrike/ember-toucan-styles': 2.0.1(@glimmer/tracking@1.1.2)(autoprefixer@10.4.13)(ember-source@5.1.0)(postcss@8.4.21)(tailwindcss@3.2.4) '@ember/test-helpers': 3.1.0(ember-source@5.1.0)(webpack@5.75.0) '@embroider/addon-shim': 1.8.4 diff --git a/test-app/package.json b/test-app/package.json index d2bf15cd..d6fe1ec7 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -95,6 +95,7 @@ "ember-template-imports": "^3.1.2", "ember-template-lint": "^5.8.0", "ember-try": "^2.0.0", + "ember-velcro": "^2.1.0", "eslint": "^8.32.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-ember": "^11.8.0", From 30d87e866153e72e2a9807693be6e51630100466 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Fri, 7 Jul 2023 10:00:05 -0400 Subject: [PATCH 58/86] fix: Add explicit on modifier import --- .../integration/components/toucan-form/toucan-form-test.gts | 1 + 1 file changed, 1 insertion(+) diff --git a/test-app/tests/integration/components/toucan-form/toucan-form-test.gts b/test-app/tests/integration/components/toucan-form/toucan-form-test.gts index 573472c0..0e623333 100644 --- a/test-app/tests/integration/components/toucan-form/toucan-form-test.gts +++ b/test-app/tests/integration/components/toucan-form/toucan-form-test.gts @@ -1,4 +1,5 @@ /* eslint-disable no-undef -- Until https://github.com/ember-cli/eslint-plugin-ember/issues/1747 is resolved... */ +import { on } from '@ember/modifier'; import { click, fillIn, From 91204aacd1dbec2b4102df0ed7c2c03556520a4d Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Fri, 7 Jul 2023 10:22:00 -0400 Subject: [PATCH 59/86] Add a changeset entry --- .changeset/twelve-gifts-camp.md | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .changeset/twelve-gifts-camp.md diff --git a/.changeset/twelve-gifts-camp.md b/.changeset/twelve-gifts-camp.md new file mode 100644 index 00000000..8f118acb --- /dev/null +++ b/.changeset/twelve-gifts-camp.md @@ -0,0 +1,61 @@ +--- +'@crowdstrike/ember-toucan-core': patch +'@crowdstrike/ember-toucan-form': patch +--- + +Added a `Combobox` component to both core and form packages. + +If you're using `toucan-core`, the control and field components are exposed: + +```hbs + + + {{combobox.option.label}} + + + + + + {{combobox.option.label}} + + +``` + +If you're using `toucan-form`, the component is exposed via: + +```hbs + + + {{combobox.option}} + + +``` + +For more information on using these components, view [the docs](https://ember-toucan-core.pages.dev/docs/components/combobox). From 818905f6b18f9b732150e5f8b85a381d24308d00 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Fri, 7 Jul 2023 10:23:21 -0400 Subject: [PATCH 60/86] toucan-core:Add isDisabled arg to label+hint --- .../src/components/form/fields/combobox.hbs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/fields/combobox.hbs b/packages/ember-toucan-core/src/components/form/fields/combobox.hbs index 1059d0b1..a1afd74f 100644 --- a/packages/ember-toucan-core/src/components/form/fields/combobox.hbs +++ b/packages/ember-toucan-core/src/components/form/fields/combobox.hbs @@ -13,11 +13,11 @@ ) ) }} - {{!-- TODO: Need to pass @isDisabled={{@isDisabled}} after a rebase --}} {{#if (has-block "label")}} {{yield to="label"}} @@ -36,8 +36,7 @@ (hash blockExists=(has-block "hint") argName="hint" arg=@hint) ) }} - {{!-- TODO: Need to pass @isDisabled={{@isDisabled}} after a rebase --}} - + {{#if (has-block "hint")}} {{yield to="hint"}} {{else}} From b638b7dbb666792498e09b51265d5d576cd86628 Mon Sep 17 00:00:00 2001 From: Tony Ward <8069555+ynotdraw@users.noreply.github.com> Date: Fri, 7 Jul 2023 11:17:40 -0400 Subject: [PATCH 61/86] Adjust glint typing --- .../src/components/form/controls/combobox.ts | 31 ++++++---- .../src/components/form/fields/combobox.ts | 27 ++++++--- .../src/-private/combobox-field.ts | 18 ++++-- .../components/combobox-field-test.gts | 2 - .../integration/components/combobox-test.gts | 56 +------------------ .../toucan-form/form-combobox-test.gts | 27 +++++---- .../toucan-form/toucan-form-test.gts | 4 -- 7 files changed, 68 insertions(+), 97 deletions(-) diff --git a/packages/ember-toucan-core/src/components/form/controls/combobox.ts b/packages/ember-toucan-core/src/components/form/controls/combobox.ts index aac0b206..91a2bf49 100644 --- a/packages/ember-toucan-core/src/components/form/controls/combobox.ts +++ b/packages/ember-toucan-core/src/components/form/controls/combobox.ts @@ -16,7 +16,11 @@ import Chevron from '../../../-private/icons/chevron'; import type { Middleware as VelcroMiddleware } from '@floating-ui/dom'; import type { WithBoundArgs } from '@glint/template'; -export interface ToucanFormComboboxControlComponentSignature { +export type Option = string | Record | undefined; + +export interface ToucanFormComboboxControlComponentSignature< + OPTION extends Option +> { Args: { /** * A CSS class to add to this component's content container. @@ -53,7 +57,7 @@ export interface ToucanFormComboboxControlComponentSignature { /** * The function called when a user types into the combobox textbox, typically used to write custom filtering logic. */ - onFilter?: (input: string) => unknown[]; + onFilter?: (input: string) => OPTION[]; /** * `@options` forms the content of this component. @@ -61,7 +65,7 @@ export interface ToucanFormComboboxControlComponentSignature { * To support a variety of data shapes, `@options` is typed as `unknown[]` and treated as though it were opaque. * `@options` is simply iterated over then passed back to you as a block parameter (`select.option`). */ - options?: unknown[]; + options?: OPTION[]; /** * When `@options` is an array of objects, `@selected` is also an object. @@ -74,12 +78,12 @@ export interface ToucanFormComboboxControlComponentSignature { /** * The currently selected option. If `@options` is an array of strings, provide a string. If `@options` is an array of objects, pass the entire object. */ - selected?: string | Record | undefined; + selected?: OPTION; }; Blocks: { default: [ { - option: unknown; + option: OPTION; Option: WithBoundArgs< typeof OptionComponent, | 'index' @@ -96,11 +100,13 @@ export interface ToucanFormComboboxControlComponentSignature { Element: HTMLInputElement; } -export default class ToucanFormComboboxControlComponent extends Component { +export default class ToucanFormComboboxControlComponent< + OPTION extends Option +> extends Component> { @tracked activeIndex: number | null = null; @tracked inputValue: string | undefined; @tracked isPopoverOpen = false; - @tracked filteredOptions: unknown[] | undefined; + @tracked filteredOptions: OPTION[] | undefined; Chevron = Chevron; Option = OptionComponent; @@ -108,7 +114,7 @@ export default class ToucanFormComboboxControlComponent extends Component['Args'] ) { super(owner, args); @@ -275,7 +281,7 @@ export default class ToucanFormComboboxControlComponent extends Component option.toLowerCase().startsWith(value.toLowerCase()) - ); + (option: string) => + option.toLowerCase().startsWith(value.toLowerCase()) as unknown + ) as OPTION[]; } if (!onFilter && optionKey) { diff --git a/packages/ember-toucan-core/src/components/form/fields/combobox.ts b/packages/ember-toucan-core/src/components/form/fields/combobox.ts index 195ec85e..e3bb1363 100644 --- a/packages/ember-toucan-core/src/components/form/fields/combobox.ts +++ b/packages/ember-toucan-core/src/components/form/fields/combobox.ts @@ -5,9 +5,16 @@ import LockIcon from '../../../-private/icons/lock'; import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists'; import type { ErrorMessage } from '../../../-private/types'; -import type { ToucanFormComboboxControlComponentSignature } from '../controls/combobox'; +import type { + Option as ControlOption, + ToucanFormComboboxControlComponentSignature, +} from '../controls/combobox'; -export interface ToucanFormComboboxFieldComponentSignature { +export type Option = ControlOption; + +export interface ToucanFormComboboxFieldComponentSignature< + OPTION extends ControlOption +> { Element: HTMLInputElement; Args: { /** @@ -49,7 +56,7 @@ export interface ToucanFormComboboxFieldComponentSignature { /** * The function called when a new selection is made. */ - onChange?: ToucanFormComboboxControlComponentSignature['Args']['onChange']; + onChange?: ToucanFormComboboxControlComponentSignature ); diff --git a/test-app/tests/integration/components/combobox-test.gts b/test-app/tests/integration/components/combobox-test.gts index 23a65e3f..6f14613d 100644 --- a/test-app/tests/integration/components/combobox-test.gts +++ b/test-app/tests/integration/components/combobox-test.gts @@ -105,8 +105,6 @@ module('Integration | Component | Combobox', function (hooks) { test('it sets `aria-expanded` based on the popover state', async function (assert) { await render(); @@ -123,8 +121,6 @@ module('Integration | Component | Combobox', function (hooks) { test('it sets `aria-controls`', async function (assert) { await render(); @@ -155,8 +151,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -175,8 +169,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -202,9 +194,7 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} - {{combobox.option}} + {{combobox.option.label}} ); @@ -219,8 +209,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -241,8 +229,6 @@ module('Integration | Component | Combobox', function (hooks) { test('it provides default filtering when `@options` is an array of strings', async function (assert) { await render(); @@ -283,8 +269,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option.label}} ); @@ -316,8 +300,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -355,8 +337,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -387,8 +367,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -426,8 +404,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -448,8 +424,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -474,8 +448,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -505,8 +477,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -528,8 +498,6 @@ module('Integration | Component | Combobox', function (hooks) { test('it closes an open popover when the ESCAPE key is pressed', async function (assert) { await render(); @@ -549,8 +517,6 @@ module('Integration | Component | Combobox', function (hooks) { - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -569,8 +535,6 @@ module('Integration | Component | Combobox', function (hooks) { test('it reopens the popover when any key is pressed if the popover is closed', async function (assert) { await render(); @@ -597,8 +561,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -630,8 +592,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -664,8 +624,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -696,8 +654,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -727,8 +683,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} ); @@ -771,8 +725,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} @@ -809,8 +761,6 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} @@ -866,9 +816,7 @@ module('Integration | Component | Combobox', function (hooks) { data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} - {{combobox.option}} + {{combobox.option.label}} {{! template-lint-disable require-input-label }} diff --git a/test-app/tests/integration/components/toucan-form/form-combobox-test.gts b/test-app/tests/integration/components/toucan-form/form-combobox-test.gts index 0608215e..23522417 100644 --- a/test-app/tests/integration/components/toucan-form/form-combobox-test.gts +++ b/test-app/tests/integration/components/toucan-form/form-combobox-test.gts @@ -4,6 +4,8 @@ import { module, test } from 'qunit'; import ToucanForm from '@crowdstrike/ember-toucan-form/components/toucan-form'; import { setupRenderingTest } from 'test-app/tests/helpers'; +const options = ['blue', 'red', 'yellow']; + interface TestData { selection?: string; } @@ -22,11 +24,10 @@ module('Integration | Component | ToucanForm | Combobox', function (hooks) { @label="Label" @hint="Hint" @name="selection" + @options={{options}} data-combobox as |combobox| > - {{! Need to figure out these types }} - {{! @glint-expect-error }} {{combobox.option}} @@ -43,11 +44,14 @@ module('Integration | Component | ToucanForm | Combobox', function (hooks) { await render(