From 3749a920074207b83f3a7dec0bbcef5ef011243c Mon Sep 17 00:00:00 2001 From: Josemar Luedke Date: Sun, 18 Feb 2024 17:11:17 -0800 Subject: [PATCH] feat: implement controlled popover, add styles for select; move select from forms to collections --- .../changeset-form/fields/select.gts | 20 +-- packages/collections/package.json | 3 +- .../collections/src/components/dropdown.gts | 4 +- .../src/components}/select.gts | 94 ++++++++--- packages/collections/src/components/select.md | 156 ++++++++++++++++++ packages/collections/src/index.ts | 1 + packages/forms/package.json | 1 - packages/overlays/src/components/popover.gts | 36 +++- .../theme/src/components/forms/form-field.ts | 15 +- test-app/app/components/forms/select.gts | 6 +- .../components/buttons/listbox-test.ts | 18 +- 11 files changed, 294 insertions(+), 60 deletions(-) rename packages/{forms/src/components/form-field => collections/src/components}/select.gts (72%) create mode 100644 packages/collections/src/components/select.md diff --git a/packages/changeset-form/src/components/changeset-form/fields/select.gts b/packages/changeset-form/src/components/changeset-form/fields/select.gts index 2b84c18f..089a4a6c 100644 --- a/packages/changeset-form/src/components/changeset-form/fields/select.gts +++ b/packages/changeset-form/src/components/changeset-form/fields/select.gts @@ -1,23 +1,19 @@ import Base, { type BaseArgs, type BaseSignature } from './base'; import { action } from '@ember/object'; -// import FormSelect, { -import { - type FormSelectArgs, - type Select -} from '@frontile/forms/components/form-select'; +import { type FormSelectArgs } from '@frontile/forms/components/form-select'; export interface ChangesetFormFieldsSelectArgs extends BaseArgs, FormSelectArgs { - onChange: (selection: unknown, select: Select, event?: Event) => void; - onFocusOut?: (select: Select, event: FocusEvent) => void; - onClose?: (select: Select, e: Event) => boolean | undefined; + onChange: (selection: unknown, select: unknown, event?: Event) => void; + onFocusOut?: (select: unknown, event: FocusEvent) => void; + onClose?: (select: unknown, e: Event) => boolean | undefined; } export interface ChangesetFormFieldsSelectSignature extends BaseSignature { Args: ChangesetFormFieldsSelectArgs; Blocks: { - default: [option: unknown, select: Select]; + default: [option: unknown, select: unknown]; }; Element: HTMLDivElement; } @@ -26,7 +22,7 @@ export default class ChangesetFormFieldsSelect extends Base { this.args.changeset.set(this.args.fieldName, selection); @@ -38,7 +34,7 @@ export default class ChangesetFormFieldsSelect extends Base { + async handleFocusOut(select: unknown, event: FocusEvent): Promise { await this.validate(); if (typeof this.args.onFocusOut === 'function') { @@ -47,7 +43,7 @@ export default class ChangesetFormFieldsSelect extends Base { + async handleClose(select: unknown, event: Event): Promise { await this.validate(); if (typeof this.args.onClose === 'function') { diff --git a/packages/collections/package.json b/packages/collections/package.json index 7af42e42..2e5b13eb 100644 --- a/packages/collections/package.json +++ b/packages/collections/package.json @@ -88,7 +88,8 @@ "app-js": { "./components/dropdown.js": "./dist/_app_/components/dropdown.js", "./components/listbox.js": "./dist/_app_/components/listbox.js", - "./components/native-select.js": "./dist/_app_/components/native-select.js" + "./components/native-select.js": "./dist/_app_/components/native-select.js", + "./components/select.js": "./dist/_app_/components/select.js" } }, "exports": { diff --git a/packages/collections/src/components/dropdown.gts b/packages/collections/src/components/dropdown.gts index 980b9d7c..cd9f9457 100644 --- a/packages/collections/src/components/dropdown.gts +++ b/packages/collections/src/components/dropdown.gts @@ -23,7 +23,7 @@ interface DropdownArgs | 'shiftOptions' | 'offsetOptions' | 'strategy' - | 'onClose' + | 'didClose' > { /** * Whether the dropdown should close upon selecting an item. @@ -58,7 +58,7 @@ class Dropdown extends Component { @shiftOptions={{@shiftOptions}} @offsetOptions={{@offsetOptions}} @strategy={{@strategy}} - @onClose={{@onClose}} + @didClose={{@didClose}} as |p| > {{yield diff --git a/packages/forms/src/components/form-field/select.gts b/packages/collections/src/components/select.gts similarity index 72% rename from packages/forms/src/components/form-field/select.gts rename to packages/collections/src/components/select.gts index 3b90d18e..75e0b028 100644 --- a/packages/forms/src/components/form-field/select.gts +++ b/packages/collections/src/components/select.gts @@ -1,12 +1,8 @@ import Component from '@glimmer/component'; +import type { TOC } from '@ember/component/template-only'; import { tracked } from '@glimmer/tracking'; -import { on } from '@ember/modifier'; -import { - NativeSelect, - Listbox, - type ListboxSignature, - type ListItemNode -} from '@frontile/collections'; +import { NativeSelect } from './native-select'; +import { Listbox, type ListboxSignature, type ListItemNode } from './listbox'; import { useStyles } from '@frontile/theme'; import { VisuallyHidden } from '@frontile/utilities'; import { @@ -14,12 +10,8 @@ import { type PopoverSignature, type ContentSignature } from '@frontile/overlays'; -import { assert } from '@ember/debug'; -import { hash } from '@ember/helper'; -import type { ModifierLike } from '@glint/template'; -import type { WithBoundArgs } from '@glint/template'; -interface FormFieldSelectArgs +interface SelectArgs extends Pick< PopoverSignature['Args'], | 'placement' @@ -28,7 +20,7 @@ interface FormFieldSelectArgs | 'shiftOptions' | 'offsetOptions' | 'strategy' - | 'onClose' + | 'didClose' >, Pick< ListboxSignature['Args'], @@ -40,6 +32,7 @@ interface FormFieldSelectArgs | 'allowEmpty' | 'onSelectionChange' | 'items' + | 'onAction' >, Pick< ContentSignature['Args'], @@ -73,14 +66,19 @@ interface FormFieldSelectArgs disableFocusTrap?: boolean; } -interface FormFieldSelectSignature { - Args: FormFieldSelectArgs; +interface SelectSignature { + Args: SelectArgs; Element: HTMLUListElement | HTMLSelectElement; Blocks: ListboxSignature['Blocks']; } -class FormFieldSelect extends Component { +class Select extends Component { @tracked nodes: ListItemNode[] = []; + @tracked isOpen = false; + + onOpenChange = (isOpen: boolean) => { + this.isOpen = isOpen; + }; get blockScroll() { if (this.args.blockScroll === false) { @@ -97,7 +95,16 @@ class FormFieldSelect extends Component { } onAction = (key: string) => { - // todo + if (typeof this.args.onAction === 'function') { + this.args.onAction(key); + } + + if ( + this.args.closeOnItemSelect !== false && + this.args.selectionMode !== 'multiple' + ) { + this.isOpen = false; + } }; get selectedText() { @@ -125,6 +132,15 @@ class FormFieldSelect extends Component { return this.args.backdrop; } + get classNames() { + const { select } = useStyles(); + const { base, icon } = select(); + return { + base: base({ class: this.args.class }), + icon: icon() + }; + } + } -export { FormFieldSelect, type FormFieldSelectSignature }; -export default FormFieldSelect; +const Icon: TOC<{ + Element: SVGElement; +}> = ; + +export { Select, type SelectSignature }; +export default Select; diff --git a/packages/collections/src/components/select.md b/packages/collections/src/components/select.md new file mode 100644 index 00000000..80e82466 --- /dev/null +++ b/packages/collections/src/components/select.md @@ -0,0 +1,156 @@ +--- +label: New +--- +# Select + + +## Import + +```js +import { Select } from '@frontile/collections'; +``` + +## Usage + +```gts preview +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { array } from '@ember/helper'; +import { Select } from '@frontile/collections'; +import { Divider } from '@frontile/utilities'; + +const animals = [ + 'cheetah', + 'crocodile', + 'elephant', + 'giraffe', + 'kangaroo', + 'koala', + 'lemming', + 'lemur', + 'lion', + 'lobster', + 'panda', + 'penguin', + 'tiger', + 'zebra' +]; + +const animalsAsOject = [ + { key: 'cheetah', label: 'Cheetah' }, + { key: 'crocodile', label: 'Crocodile' }, + { key: 'elephant', label: 'Elephant' }, + { key: 'giraffe', label: 'Giraffe' }, + { key: 'kangaroo', label: 'Kangaroo' }, + { key: 'koala', label: 'Koala' }, + { key: 'lemming', label: 'Lemming' }, + { key: 'lemur', label: 'Lemur' }, + { key: 'lion', label: 'Lion' }, + { key: 'lobster', label: 'Lobster' }, + { key: 'panda', label: 'Panda' }, + { key: 'penguin', label: 'Penguin' }, + { key: 'tiger', label: 'Tiger' }, + { key: 'zebra', label: 'Zebra' } +]; + +export default class Example extends Component { + @tracked selectedKeys: string[] = []; + @tracked selectedKeys2: string[] = ['elephant']; + @tracked selectedKeys3: string[] = []; + + @action + onChange(value: boolean): void { + this.isSelected = value; + } + + @action + onAction(key: string) { + // eslint-disable-next-line + console.log('Click on key', key); + } + + @action + onSelectionChange(keys: string[]) { + this.selectedKeys = keys; + } + + @action + onSelectionChange2(keys: string[]) { + this.selectedKeys2 = keys; + } + + @action + onSelectionChange3(keys: string[]) { + this.selectedKeys3 = keys; + } + + +} +``` + diff --git a/packages/collections/src/index.ts b/packages/collections/src/index.ts index 46db15d6..e4af263e 100644 --- a/packages/collections/src/index.ts +++ b/packages/collections/src/index.ts @@ -1,3 +1,4 @@ export * from './components/listbox'; export * from './components/dropdown'; export * from './components/native-select'; +export * from './components/select'; diff --git a/packages/forms/package.json b/packages/forms/package.json index 206919e1..19368673 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -92,7 +92,6 @@ "./components/form-field/input.js": "./dist/_app_/components/form-field/input.js", "./components/form-field/label.js": "./dist/_app_/components/form-field/label.js", "./components/form-field/radio.js": "./dist/_app_/components/form-field/radio.js", - "./components/form-field/select.js": "./dist/_app_/components/form-field/select.js", "./components/form-field/textarea.js": "./dist/_app_/components/form-field/textarea.js", "./components/form-input.js": "./dist/_app_/components/form-input.js", "./components/form-radio-group.js": "./dist/_app_/components/form-radio-group.js", diff --git a/packages/overlays/src/components/popover.gts b/packages/overlays/src/components/popover.gts index 6c9016e2..adce4cde 100644 --- a/packages/overlays/src/components/popover.gts +++ b/packages/overlays/src/components/popover.gts @@ -46,9 +46,9 @@ interface PopoverSignature { */ strategy?: VelcroSignature['Args']['Named']['strategy']; - // isOpen?: boolean; - // onOpenChange?: () => void; - onClose?: () => void; + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; + didClose?: () => void; }; Element: HTMLUListElement; Blocks: { @@ -75,7 +75,18 @@ interface PopoverSignature { class Popover extends Component { triggerEl?: HTMLElement; menuId = guidFor(this); - @tracked isOpen = false; + @tracked _isOpen = false; + + get isOpen(): boolean { + if ( + typeof this.args.isOpen !== 'undefined' && + typeof this.args.onOpenChange === 'function' + ) { + return this.args.isOpen; + } + + return this._isOpen; + } toggle = () => { if (this.isOpen) { @@ -86,13 +97,22 @@ class Popover extends Component { }; open = () => { - this.isOpen = true; + if (typeof this.args.onOpenChange === 'function') { + this.args.onOpenChange(true); + } else { + this._isOpen = true; + } }; close = () => { - this.isOpen = false; - if (this.isOpen === false && typeof this.args.onClose === 'function') { - this.args.onClose(); + if (typeof this.args.onOpenChange === 'function') { + this.args.onOpenChange(false); + } else { + this._isOpen = false; + } + + if (typeof this.args.didClose === 'function') { + this.args.didClose(); } }; diff --git a/packages/theme/src/components/forms/form-field.ts b/packages/theme/src/components/forms/form-field.ts index d8653048..ebc2623a 100644 --- a/packages/theme/src/components/forms/form-field.ts +++ b/packages/theme/src/components/forms/form-field.ts @@ -138,4 +138,17 @@ const radio = tv({ } }); -export { label, hint, feedback, input, textarea, checkbox, radio }; +const select = tv({ + slots: { + base: [ + input(), + 'flex items-center justify-between', + 'disabled:cursor-not-allowed disabled:opacity-50' + ], + icon: 'w-5 h-5' + } +}); + +// flex h-9 items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 w-[180px] + +export { label, hint, feedback, input, textarea, checkbox, radio, select }; diff --git a/test-app/app/components/forms/select.gts b/test-app/app/components/forms/select.gts index 4b4d24b5..7955569e 100644 --- a/test-app/app/components/forms/select.gts +++ b/test-app/app/components/forms/select.gts @@ -2,8 +2,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { array } from '@ember/helper'; -import Select from '@frontile/forms/components/form-field/select'; -// import { Select } from '@frontile/collections'; +import { Select } from '@frontile/collections'; import { Divider } from '@frontile/utilities'; const animals = [ @@ -42,7 +41,7 @@ const animalsAsOject = [ export default class Example extends Component { @tracked selectedKeys: string[] = []; - @tracked selectedKeys2: string[] = []; + @tracked selectedKeys2: string[] = ['elephant']; @tracked selectedKeys3: string[] = []; @action @@ -102,6 +101,7 @@ export default class Example extends Component { {{this.selectedKeys2}}