Skip to content

Commit

Permalink
feat(core/dropdown): allow trigger to toggle dropdown (#872)
Browse files Browse the repository at this point in the history
  • Loading branch information
nuke-ellington authored and danielleroux committed Dec 13, 2023
1 parent c3dd1fd commit 132c435
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 14 deletions.
4 changes: 2 additions & 2 deletions packages/angular/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,14 +649,14 @@ export declare interface IxDropdown extends Components.IxDropdown {


@ProxyCmp({
inputs: ['disabled', 'ghost', 'icon', 'label', 'outline', 'placement', 'variant']
inputs: ['closeBehavior', 'disabled', 'ghost', 'icon', 'label', 'outline', 'placement', 'variant']
})
@Component({
selector: 'ix-dropdown-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['disabled', 'ghost', 'icon', 'label', 'outline', 'placement', 'variant'],
inputs: ['closeBehavior', 'disabled', 'ghost', 'icon', 'label', 'outline', 'placement', 'variant'],
})
export class IxDropdownButton {
protected el: HTMLElement;
Expand Down
34 changes: 34 additions & 0 deletions packages/core/component-doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3865,6 +3865,40 @@
]
},
"props": [
{
"name": "closeBehavior",
"type": "\"both\" | \"inside\" | \"outside\" | boolean",
"mutable": false,
"attr": "close-behavior",
"reflectToAttr": false,
"docs": "Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown.",
"docsTags": [
{
"name": "since",
"text": "2.1.0"
}
],
"default": "'both'",
"values": [
{
"value": "both",
"type": "string"
},
{
"value": "inside",
"type": "string"
},
{
"value": "outside",
"type": "string"
},
{
"type": "boolean"
}
],
"optional": false,
"required": false
},
{
"name": "disabled",
"type": "boolean",
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,11 @@ export namespace Components {
* @since 1.3.0
*/
interface IxDropdownButton {
/**
* Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown.
* @since 2.1.0
*/
"closeBehavior": 'inside' | 'outside' | 'both' | boolean;
/**
* Disable button
*/
Expand Down Expand Up @@ -4346,6 +4351,11 @@ declare namespace LocalJSX {
* @since 1.3.0
*/
interface IxDropdownButton {
/**
* Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown.
* @since 2.1.0
*/
"closeBehavior"?: 'inside' | 'outside' | 'both' | boolean;
/**
* Disable button
*/
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/components/category-filter/category-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { LogicalFilterOperator } from './logical-filter-operator';
export class CategoryFilter {
private readonly ID_CUSTOM_FILTER_VALUE = 'CW_CUSTOM_FILTER_VALUE';

@State() showDropdown: boolean;
@State() private textInput?: HTMLInputElement;
private formElement?: HTMLFormElement;
private isScrollStateDirty: boolean;
Expand Down Expand Up @@ -241,6 +242,7 @@ export class CategoryFilter {
break;

case 'ArrowDown':
this.showDropdown = true;
this.focusNextItem();
e.preventDefault();
break;
Expand Down Expand Up @@ -389,7 +391,8 @@ export class CategoryFilter {
this.categoryChanged.emit(category);
}

private resetFilter() {
private resetFilter(e: Event) {
e.stopPropagation();
this.closeDropdown();
this.filterTokens = [];
this.emitFilterEvent();
Expand Down Expand Up @@ -621,7 +624,7 @@ export class CategoryFilter {
private getResetButton() {
return (
<ix-icon-button
onClick={() => this.resetFilter()}
onClick={(e) => this.resetFilter(e)}
class={{
'reset-button': true,
'hide-reset-button':
Expand Down Expand Up @@ -680,6 +683,7 @@ export class CategoryFilter {
<ix-filter-chip
disabled={this.disabled}
readonly={this.readonly}
onClick={(e) => e.stopPropagation()}
onCloseClick={() => this.removeToken(index)}
>
{this.getFilterChipLabel(value)}
Expand All @@ -706,6 +710,7 @@ export class CategoryFilter {
this.disabled ||
this.category !== undefined,
}}
name="category-filter-input"
disabled={this.disabled}
readonly={this.readonly}
ref={(el) => (this.textInput = el)}
Expand All @@ -722,10 +727,11 @@ export class CategoryFilter {
''
) : (
<ix-dropdown
show={this.showDropdown}
closeBehavior="outside"
offset={{ mainAxis: 2 }}
trigger={this.textInput}
triggerEvent={['click', 'focus']}
anchor={this.textInput}
trigger={this.hostElement}
header={this.getDropdownHeader()}
>
{this.renderDropdownContent()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export class DropdownButton {
*/
@Prop() icon: string;

/**
* Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown.
* @since 2.1.0
*/
@Prop() closeBehavior: 'inside' | 'outside' | 'both' | boolean = 'both';

/**
* Placement of the dropdown
*
Expand Down Expand Up @@ -130,6 +136,7 @@ export class DropdownButton {
class="dropdown"
trigger={this.dropdownAnchor}
placement={this.placement}
closeBehavior={this.closeBehavior}
>
<slot></slot>
</ix-dropdown>
Expand Down
41 changes: 35 additions & 6 deletions packages/core/src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,19 @@ export class Dropdown {

private toggleBind: any;
private openBind: any;
private focusInBind: any;
private focusOutBind: any;

private localUId = `dropdown-${sequenceId++}-${new Date().valueOf()}`;

constructor() {
this.toggleBind = this.toggle.bind(this);
this.openBind = this.open.bind(this);
this.focusInBind = this.focusIn.bind(this);
this.focusOutBind = this.focusOut.bind(this);

if (dropdownDisposer.has(this.localUId)) {
console.warn('Dropdown with duplicated id detected');
console.warn('Dropdown with duplicated ID detected');
}

dropdownDisposer.set(this.localUId, {
Expand Down Expand Up @@ -192,10 +196,23 @@ export class Dropdown {
return this.hostElement.shadowRoot.querySelector('slot');
}

private hasFocusTrigger() {
return (
Array.isArray(this.triggerEvent) &&
this.triggerEvent.indexOf('focus') != -1
);
}

private addEventListenersFor(triggerEvent: DropdownTriggerEvent) {
switch (triggerEvent) {
case 'click':
this.triggerElement.addEventListener('click', this.openBind);
if (this.hasFocusTrigger()) {
// Delay mouse handler registration to prevent events from immediately closing dropdown again
this.triggerElement.addEventListener('focusin', this.focusInBind);
this.triggerElement.addEventListener('focusout', this.focusOutBind);
} else {
this.triggerElement.addEventListener('click', this.toggleBind);
}
break;

case 'hover':
Expand All @@ -214,8 +231,12 @@ export class Dropdown {
) {
switch (triggerEvent) {
case 'click':
if (this.closeBehavior === 'outside') {
triggerElement.removeEventListener('click', this.openBind);
if (this.hasFocusTrigger()) {
this.triggerElement.removeEventListener('focusin', this.focusInBind);
this.triggerElement.removeEventListener(
'focusout',
this.focusOutBind
);
} else {
triggerElement.removeEventListener('click', this.toggleBind);
}
Expand Down Expand Up @@ -361,7 +382,7 @@ export class Dropdown {

@OnListener<Dropdown>('keydown', (self) => self.show)
keydown(event: KeyboardEvent) {
if (this.show === true && event.code === 'Escape') {
if (event.code === 'Escape') {
this.close();
}
}
Expand All @@ -386,7 +407,7 @@ export class Dropdown {
event.stopPropagation();
}

const { defaultPrevented } = this.showChanged.emit(this.show);
const { defaultPrevented } = this.showChanged.emit(!this.show);

if (!defaultPrevented) {
this.show = !this.show;
Expand Down Expand Up @@ -415,6 +436,14 @@ export class Dropdown {
}
}

private focusIn() {
this.triggerElement.addEventListener('mousedown', this.toggleBind);
}

private focusOut() {
this.triggerElement.removeEventListener('mousedown', this.toggleBind);
}

private async applyDropdownPosition() {
if (!this.anchorElement) {
return;
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/components/dropdown/test/dropdown.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,22 @@ test.describe('nested dropdown tests', () => {
await expect(nestedDropdownItem).toHaveClass(/hydrated/);
});
});

test('trigger toggles', async ({ mount, page }) => {
await mount(`<ix-button id="trigger">Open</ix-button>
<ix-dropdown trigger="trigger" trigger-toggles="true">
<ix-dropdown-item label="Item 1"></ix-dropdown-item>
<ix-dropdown-item label="Item 2"></ix-dropdown-item>
</ix-dropdown>
`);

await page.locator('ix-button').click();
const dropdown = page.locator('.dropdown-menu');
await expect(dropdown).toHaveClass(/show/);
await expect(dropdown).toBeVisible();

await page.locator('ix-button').click();
const after = page.locator('.dropdown-menu');
await expect(after).not.toHaveClass(/show/);
await expect(dropdown).not.toBeVisible();
});
2 changes: 0 additions & 2 deletions packages/core/src/components/select/test/select.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,7 @@ test('multiple selection', async ({ mount, page }) => {
const item1 = element.locator('ix-select-item').nth(0);
const item3 = element.locator('ix-select-item').nth(2);
await item1.click();
await page.locator('[data-select-dropdown]').click();
await item3.click();
await page.locator('[data-select-dropdown]').click();

await expect(item1.locator('ix-icon')).toBeVisible();
await expect(item3.locator('ix-icon')).toBeVisible();
Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export const IxDropdownButton = /*@__PURE__*/ defineContainer<JSX.IxDropdownButt
'disabled',
'label',
'icon',
'closeBehavior',
'placement'
]);

Expand Down

0 comments on commit 132c435

Please sign in to comment.