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

Make CW Dropdown searchable #2226

Closed
wants to merge 6 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,6 @@ export class ComponentShowcase implements m.ClassComponent {
view() {
return (
<div class="ComponentShowcase">
<h1>Dropdown</h1>
<div class="form-gallery">
<CWDropdown
inputOptions={[
{ label: 'Dropdown Option 1' },
{ label: 'Dropdown Option 2' },
{ label: 'Dropdown Option 3' },
]}
onSelect={(optionLabel) =>
console.log('Selected option: ', optionLabel)
}
/>
</div>
<h1>Spinner</h1>
<div class="basic-gallery">
<CWSpinner />
Expand Down Expand Up @@ -123,7 +110,7 @@ export class ComponentShowcase implements m.ClassComponent {
label: 'Report',
iconLeft: 'cautionCircle',
isSecondary: true,
onclick: () => console.log('clicked'),
onclick: () => notifySuccess('clicked'),
},
]}
/>
Expand Down Expand Up @@ -394,6 +381,35 @@ export class ComponentShowcase implements m.ClassComponent {
label="Text area"
placeholder="Type here"
/>
<CWDropdown
defaultMenuItems={[
{ label: 'Dropdown Option 1' },
{ label: 'Dropdown Option 2' },
{ label: 'Dropdown Option 3' },
]}
onSelect={(optionLabel: string) =>
notifySuccess(`Selected option: ${optionLabel}`)
}
/>
<CWDropdown
defaultMenuItems={[
{ label: 'Dropdown Option 1' },
{ label: 'Dropdown Option 2' },
{ label: 'Dropdown Option 3' },
]}
inputValidationFn={(val: string): [ValidationStatus, string] => {
if (val.match(/[^A-Za-z]/)) {
return ['failure', 'Must enter characters A-Z'];
} else {
return ['success', 'Input validated'];
}
}}
label="This input accepts chars A-Z"
onSelect={(optionLabel) =>
notifySuccess(`Selected option: ${optionLabel}`)
}
searchable={true}
/>
<CWCoverImageUploader
uploadCompleteCallback={(url: string) => {
notifySuccess(`Image uploaded to ${url.slice(0, 18)}...`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,129 @@
/* @jsx m */

import m from 'mithril';
import m, { VnodeDOM } from 'mithril';

import 'components/component_kit/cw_dropdown.scss';

import { CWTextInput } from './cw_text_input';
import { CWTextInput, TextInputAttrs } from './cw_text_input';
import { CWPopoverMenuItem } from './cw_popover/cw_popover_menu';
import { MenuItem } from './types';
import { DefaultMenuItem } from './types';
import { ValidationStatus } from './cw_validation_text';

export type DropdownInputAttrs = {
inputOptions: Array<MenuItem>;
customFilter: (item: DefaultMenuItem, query: string) => DefaultMenuItem[];
defaultActiveIndex?: number;
defaultMenuItems: DefaultMenuItem[];
inputValidationFn?: (value: string) => [ValidationStatus, string];
label: string;
placeholder?: string;
onSelect?: (optionLabel: string, index?: number) => void;
initialValue?: string;
searchable?: boolean;
textInputAttrs?: TextInputAttrs;
};

export class CWDropdown implements m.ClassComponent<DropdownInputAttrs> {
private showDropdown: boolean;
private selectedValue: string;
private activeMenuItems: DefaultMenuItem[];
private value: string;

oninit(vnode) {
filterMenuItems(
items: DefaultMenuItem[],
query: string,
customFilter?: (item: DefaultMenuItem, query: string) => DefaultMenuItem[]
) {
const defaultFilter = (item: DefaultMenuItem) => {
return item.label.toLowerCase().includes(query.toLowerCase());
};
const filterFn = customFilter
? (item: DefaultMenuItem) => customFilter(item, query)
: defaultFilter;

return items.filter(filterFn);
}

oninit(vnode: VnodeDOM<DropdownInputAttrs, this>) {
this.showDropdown = false;
this.selectedValue =
vnode.attrs.initialValue ?? vnode.attrs.inputOptions[0].label;
this.activeMenuItems = vnode.attrs.defaultMenuItems;

document.body.addEventListener('click', (event) => {
const $dropdown = document.querySelector('.dropdown-wrapper');
if (!$dropdown) return;
if (!$dropdown.contains(event.target as Node)) {
this.showDropdown = false;
m.redraw();
}
});
}

view(vnode) {
const { inputOptions, onSelect } = vnode.attrs;
view(vnode: VnodeDOM<DropdownInputAttrs, this>) {
const {
customFilter,
defaultActiveIndex,
defaultMenuItems,
inputValidationFn,
label,
onSelect,
placeholder,
searchable,
} = vnode.attrs;

// Input value must be passed as spread to CWTextInput, or it will
// always overwrite the defaultValue prop

if (!this.activeMenuItems.length) this.showDropdown = false;
const { activeMenuItems, showDropdown, ...thisParams } = this;

return (
<div class="dropdown-wrapper">
<CWTextInput
iconRight="chevronDown"
placeholder={this.selectedValue}
displayOnly
iconRightonclick={() => {
// Only here because it makes TextInput display correctly
defaultValue={defaultMenuItems[defaultActiveIndex ?? 0].label}
displayOnly={!searchable}
iconRightonclick={(e: MouseEvent) => {
this.showDropdown = !showDropdown;
e.stopPropagation();
}}
inputValidationFn={(val: string) => {
if (defaultMenuItems.find((i) => i.label === val)) {
return ['success', 'Input validated'];
} else {
return inputValidationFn(val);
}
}}
onclick={() => {
this.showDropdown = !this.showDropdown;
if (searchable) {
delete this.value;
}
this.showDropdown = !showDropdown;
}}
oninput={(e) => {
this.showDropdown = true;
if (e.target.value?.length > 0) {
const inputText = e.target.value;
this.activeMenuItems = this.filterMenuItems(
defaultMenuItems,
inputText,
customFilter
);
} else {
this.activeMenuItems = defaultMenuItems;
m.redraw();
}
}}
label={label}
placeholder={placeholder}
{...thisParams}
/>
{this.showDropdown && (
{showDropdown && (
<div class="dropdown-options-display">
{inputOptions.map((item, idx) => {
{activeMenuItems.map((item, idx) => {
return (
<CWPopoverMenuItem
{...item}
type="default"
onclick={() => {
this.showDropdown = false;
this.selectedValue = item.label;
this.value = item.label;
if (onSelect) onSelect(item.label, idx);
}}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* @jsx m */

import m from 'mithril';
import m, { VnodeDOM } from 'mithril';

import 'components/component_kit/cw_text_input.scss';

import _ from 'lodash';
import { ComponentType } from './types';
import { getClasses } from './helpers';
import { CWLabel } from './cw_label';
Expand All @@ -14,11 +15,20 @@ import { CWIconButton } from './cw_icon_button';

type TextInputSize = 'small' | 'large';

export type InputStyleAttrs = {
inputClassName?: string;
darkMode?: boolean;
disabled?: boolean;
size: TextInputSize;
validationStatus?: ValidationStatus;
displayOnly?: boolean;
};

export type TextInputAttrs = {
autocomplete?: string;
autofocus?: boolean;
containerClassName?: string;
value?: string;
defaultValue;
iconRight?: string;
iconRightonclick?: () => void;
inputValidationFn?: (value: string) => [ValidationStatus, string];
Expand All @@ -29,33 +39,26 @@ export type TextInputAttrs = {
onenterkey?: (e) => void;
onclick?: (e) => void;
placeholder?: string;
required?: boolean;
tabindex?: number;
};

type InputStyleAttrs = {
inputClassName?: string;
darkMode?: boolean;
disabled?: boolean;
size: TextInputSize;
validationStatus?: ValidationStatus;
displayOnly?: boolean;
};
value?: string;
} & InputStyleAttrs;

type InputInternalStyleAttrs = {
export type InputInternalStyleAttrs = {
hasRightIcon?: boolean;
isTyping: boolean;
};

type MessageRowAttrs = {
export type MessageRowAttrs = {
hasFeedback?: boolean;
label: string;
statusMessage?: string;
validationStatus?: ValidationStatus;
};

export class MessageRow implements m.ClassComponent<MessageRowAttrs> {
view(vnode) {
const { hasFeedback, label, statusMessage, validationStatus, displayOnly } =
view(vnode: VnodeDOM<MessageRowAttrs, this>) {
const { hasFeedback, label, statusMessage, validationStatus } =
vnode.attrs;

return (
Expand Down Expand Up @@ -88,13 +91,14 @@ export class CWTextInput implements m.ClassComponent<TextInputAttrs> {
private statusMessage?: string = '';
private validationStatus?: ValidationStatus = undefined;

view(vnode) {
view(vnode: VnodeDOM<TextInputAttrs, this>) {
const {
autocomplete = 'off',
autofocus,
containerClassName,
darkMode,
value,
displayOnly,
defaultValue,
disabled,
iconRight,
iconRightonclick,
Expand All @@ -107,9 +111,10 @@ export class CWTextInput implements m.ClassComponent<TextInputAttrs> {
onenterkey,
onclick,
placeholder,
required,
size = 'large',
tabindex,
displayOnly,
value,
} = vnode.attrs;

return (
Expand Down Expand Up @@ -148,13 +153,17 @@ export class CWTextInput implements m.ClassComponent<TextInputAttrs> {
darkMode,
inputClassName,
})}
defaultValue={defaultValue}
disabled={disabled || displayOnly}
tabindex={tabindex}
maxlength={maxlength}
name={name}
placeholder={placeholder}
required={required}
oninput={(e) => {
if (oninput) oninput(e);
_.debounce(() => {
if (oninput) oninput(e);
}, 250)();

if (e.target.value?.length === 0) {
this.isTyping = false;
Expand All @@ -168,7 +177,7 @@ export class CWTextInput implements m.ClassComponent<TextInputAttrs> {
const timeout = e.target.value?.length > 3 ? 250 : 1000;
this.inputTimeout = setTimeout(() => {
this.isTyping = false;
if (inputValidationFn && e.target.value?.length > 3) {
if (inputValidationFn && e.target.value?.length > 0) {
[this.validationStatus, this.statusMessage] =
inputValidationFn(e.target.value);
m.redraw();
Expand Down
Loading