Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option to clear content of input & (start/end)ContentPointerEvents arg #288

Merged
merged 2 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };
Loading
Loading