Skip to content

Commit

Permalink
Merge pull request #288 from josemarluedke/feat/input-clear-button
Browse files Browse the repository at this point in the history
feat: add option to clear content of input & (start/end)ContentPointerEvents arg
  • Loading branch information
josemarluedke committed Apr 5, 2024
2 parents 5df5c63 + 7a45c00 commit 5855a4d
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 67 deletions.
12 changes: 9 additions & 3 deletions packages/buttons/src/components/close-button.gts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import { VisuallyHidden } from '@frontile/utilities';
import { useStyles } from '@frontile/theme';
import { useStyles, type CloseButtonVariants } from '@frontile/theme';

interface CloseButtonSignature {
Args: {
Expand All @@ -18,7 +18,12 @@ interface CloseButtonSignature {
*
* @defaultValue 'lg'
*/
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
size?: CloseButtonVariants['size'];

/**
* @defaultValue 'transparent'
*/
variant?: CloseButtonVariants['variant'];

/**
* The function to call when button is clicked
Expand All @@ -41,7 +46,8 @@ class CloseButton extends Component<CloseButtonSignature> {
const { closeButton } = useStyles();

let { base, icon } = closeButton({
size: this.args.size || 'md'
size: this.args.size || 'md',
variant: this.args.variant || 'transparent'
});

return {
Expand Down
1 change: 1 addition & 0 deletions packages/forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"@ember/test-waiters": "^3.0.2",
"@embroider/addon-shim": "^1.8.7",
"@frontile/buttons": "workspace:0.17.0-alpha.15",
"@frontile/collections": "workspace:0.17.0-alpha.15",
"@frontile/overlays": "workspace:0.17.0-alpha.15",
"@frontile/theme": "workspace:0.17.0-alpha.15",
Expand Down
2 changes: 0 additions & 2 deletions packages/forms/src/components/form-control.gts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import Description from './form-description';
import Label from './label';
import type { WithBoundArgs } from '@glint/template';

// TODO allowClear or isClearable

interface FormControlSharedArgs {
label?: string;
isRequired?: boolean;
Expand Down
138 changes: 119 additions & 19 deletions packages/forms/src/components/input.gts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import {
Expand All @@ -8,6 +9,9 @@ import {
type SlotsToClasses
} from '@frontile/theme';
import { FormControl, type FormControlSharedArgs } from './form-control';
import { triggerFormInputEvent } from '../utils';
import { ref } from '@frontile/utilities';
import { CloseButton } from '@frontile/buttons';

interface Args extends FormControlSharedArgs {
type?: string;
Expand All @@ -16,11 +20,36 @@ interface Args extends FormControlSharedArgs {
size?: InputVariants['size'];
classes?: SlotsToClasses<InputSlots>;

// Callback when oninput is triggered
onInput?: (value: string, event: InputEvent) => void;
/**
* Whether to include a clear button
*/
isClearable?: boolean;

// Callback when onchange is triggered
onChange?: (value: string, event: InputEvent) => void;
/**
* Controls pointer-events property of startContent.
* If you want to pass the click event to the input, set it to `none`.
*
* @defaultValue 'auto'
*/
startContentPointerEvents?: 'none' | 'auto';

/**
* Controls pointer-events property of endContent.
* If you want to pass the click event to the input, set it to `none`.
*
* @defaultValue 'auto'
*/
endContentPointerEvents?: 'none' | 'auto';

/**
* Callback when oninput is triggered
*/
onInput?: (value: string, event?: InputEvent) => void;

/**
* Callback when onchange is triggered
*/
onChange?: (value: string, event?: InputEvent) => void;
}

interface InputSignature {
Expand All @@ -32,7 +61,26 @@ interface InputSignature {
Element: HTMLInputElement;
}

function or(arg1: unknown, arg2: unknown): boolean {
return !!(arg1 || arg2);
}

class Input extends Component<InputSignature> {
@tracked uncontrolledValue: string = '';

inputRef = ref<HTMLInputElement>();

get isControlled() {
return (
typeof this.args.onChange === 'function' ||
typeof this.args.onInput === 'function'
);
}

get value(): string | undefined {
return this.isControlled ? this.args.value : this.uncontrolledValue;
}

get type(): string {
if (typeof this.args.type === 'string') {
return this.args.type;
Expand All @@ -41,21 +89,46 @@ class Input extends Component<InputSignature> {
}

@action handleOnInput(event: Event): void {
if (typeof this.args.onInput === 'function') {
this.args.onInput(
(event.target as HTMLInputElement).value,
event as InputEvent
);
const value = (event.target as HTMLInputElement).value;

if (this.isControlled) {
this.args.onInput?.(value, event as InputEvent);
} else {
this.uncontrolledValue = value;
}
}

@action handleOnChange(event: Event): void {
if (typeof this.args.onChange === 'function') {
this.args.onChange(
(event.target as HTMLInputElement).value,
event as InputEvent
);
const value = (event.target as HTMLInputElement).value;

if (this.isControlled) {
this.args.onChange?.(value, event as InputEvent);
} else {
this.uncontrolledValue = value;
}
}

@action clearValue(): void {
if (this.isControlled) {
this.args.onChange?.('');
this.args.onInput?.('');
} else {
this.uncontrolledValue = '';
}

this.inputRef.element?.focus();
triggerFormInputEvent(this.inputRef.element);
}

get isClearable(): boolean {
if (
this.args.isClearable === true &&
this.value !== '' &&
typeof this.value !== 'undefined'
) {
return true;
}
return false;
}

get classes() {
Expand All @@ -78,30 +151,57 @@ class Input extends Component<InputSignature> {
>
<div class={{this.classes.innerContainer class=@classes.innerContainer}}>
{{#if (has-block "startContent")}}
<div class={{this.classes.startContent class=@classes.startContent}}>
<div
data-test-id="input-start-content"
class={{this.classes.startContent
class=@classes.startContent
startContentPointerEvents=(if
@startContentPointerEvents @startContentPointerEvents "auto"
)
}}
>
{{yield to="startContent"}}
</div>
{{/if}}
<input
{{this.inputRef.setup}}
{{on "input" this.handleOnInput}}
{{on "change" this.handleOnChange}}
id={{c.id}}
name={{@name}}
value={{@value}}
value={{this.value}}
type={{this.type}}
class={{this.classes.input
class=@classes.input
hasStartContent=(has-block "startContent")
hasEndContent=(has-block "endContent")
hasEndContent=(or (has-block "endContent") this.isClearable)
}}
data-component="input"
aria-invalid={{if c.isInvalid "true"}}
aria-describedby={{c.describedBy @description c.isInvalid}}
...attributes
/>
{{#if (has-block "endContent")}}
<div class={{this.classes.endContent class=@classes.endContent}}>
{{#if (or (has-block "endContent") this.isClearable)}}
<div
data-test-id="input-end-content"
class={{this.classes.endContent
class=@classes.endContent
endContentPointerEvents=(if
@endContentPointerEvents @endContentPointerEvents "auto"
)
}}
>
{{yield to="endContent"}}

{{#if this.isClearable}}
<CloseButton
@title="Clear"
@variant="subtle"
@size="xs"
data-test-id="input-clear-button"
@onClick={{this.clearValue}}
/>
{{/if}}
</div>
{{/if}}
</div>
Expand Down
26 changes: 2 additions & 24 deletions packages/forms/src/components/select.gts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Component from '@glimmer/component';
import type { TOC } from '@ember/component/template-only';
import { tracked } from '@glimmer/tracking';
import { modifier } from 'ember-modifier';
import { buildWaiter } from '@ember/test-waiters';
import { NativeSelect, type ListItem } from './native-select';
import { Listbox, type ListboxSignature } from '@frontile/collections';
import {
Expand All @@ -18,23 +17,7 @@ import {
type ContentSignature
} from '@frontile/overlays';
import { FormControl, type FormControlSharedArgs } from './form-control';

function triggerFormInputEvent(element: HTMLElement | null): void {
if (!element) return;

let parent = element.parentElement;
while (parent) {
if (parent.tagName === 'FORM') {
(parent as HTMLFormElement).dispatchEvent(
new Event('input', { bubbles: true })
);
break;
}
parent = parent.parentElement;
}
}

const waiter = buildWaiter('@frontile/forms:select');
import { triggerFormInputEvent } from '../utils';

interface SelectArgs<T>
extends Pick<
Expand Down Expand Up @@ -131,18 +114,13 @@ class Select<T = unknown> extends Component<SelectSignature<T>> {
}

onSelectionChange = (keys: string[]) => {
const waiterToken = waiter.beginAsync();

if (typeof this.args.onSelectionChange === 'function') {
this.args.onSelectionChange(keys);
} else {
this._selectedKeys = keys;
}

requestAnimationFrame(() => {
triggerFormInputEvent(this.el);
waiter.endAsync(waiterToken);
});
triggerFormInputEvent(this.el);
};

onOpenChange = (isOpen: boolean) => {
Expand Down
24 changes: 24 additions & 0 deletions packages/forms/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { buildWaiter } from '@ember/test-waiters';
const waiter = buildWaiter('@frontile/forms:triggerFormInputEvent)');

function triggerFormInputEvent(element?: HTMLElement | null): void {
if (!element) return;
const waiterToken = waiter.beginAsync();

requestAnimationFrame(() => {
let parent = element.parentElement;
while (parent) {
if (parent.tagName === 'FORM') {
(parent as HTMLFormElement).dispatchEvent(
new Event('input', { bubbles: true })
);
break;
}
parent = parent.parentElement;
}

waiter.endAsync(waiterToken);
});
}

export { triggerFormInputEvent };
29 changes: 21 additions & 8 deletions packages/theme/src/components/close-button.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
import { tv } from '../tw';
import { tv, type VariantProps } from '../tw';

const closeButton = tv({
slots: {
base: 'rounded-full hover:bg-default-100 transition transition-200 focus-visable:ring text-inherit',
base: 'rounded-full transition transition-200 focus-visable:ring text-inherit',
icon: 'size-[1em]'
},

variants: {
size: {
xs: 'text-sm p-1',
sm: 'text-base p-2',
md: 'text-xl p-2',
lg: 'text-2xl p-3',
xl: 'text-4xl p-3'
xs: { base: 'text-sm p-1' },
sm: { base: 'text-base p-2' },
md: { base: 'text-xl p-2' },
lg: { base: 'text-2xl p-3' },
xl: { base: 'text-4xl p-3' }
},
variant: {
transparent: { base: ['bg-transparent', 'hover:bg-default-100'] },
subtle: {
base: [
'bg-default-100',
'text-default-foreground dark:text-default-background',
'dark:bg-default-200',
'hover:bg-default-200/60 dark:hover:bg-default-800/60'
]
}
}
},
defaultVariants: {
size: 'md'
size: 'md',
variant: 'transparent'
}
});

export type CloseButtonVariants = VariantProps<typeof closeButton>;
export { closeButton };

0 comments on commit 5855a4d

Please sign in to comment.