Skip to content

Commit

Permalink
Merge branch 'opt-group_fix'
Browse files Browse the repository at this point in the history
  • Loading branch information
Xon committed Sep 5, 2024
2 parents 3839cec + 59f7d67 commit d0a7e70
Show file tree
Hide file tree
Showing 23 changed files with 119 additions and 85 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* Fix regression "no choices to choose from"/"no results found" notice did not reliably trigger. [#1185](https://github.com/Choices-js/Choices/issues/1185) [#1191](https://github.com/Choices-js/Choices/issues/1191)
* Fix regression of `UnhighlightItem` event not firing [#1173](https://github.com/Choices-js/Choices/issues/1173)
* Fix `clearChoices()` would remove items, and clear the search flag.
* Fixes for opt-group handling/rendering
* Fix `removeChoice()` did not properly remove a choice which was part of a group

### Chore
* Add e2e tests for "no choices" behavior to match v10
Expand Down
4 changes: 3 additions & 1 deletion public/test/select-multiple/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,9 @@ <h2>Select multiple inputs</h2>
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<optgroup label="test">
<option value="Choice 2">Choice 2</option>
</optgroup>
<option value="Choice 3">Choice 3</option>
</select>
<button class="destroy">Destroy</button>
Expand Down
8 changes: 5 additions & 3 deletions public/test/select-one/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,9 @@ <h2>Select one inputs</h2>
id="choices-new-destroy-init"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<optgroup label="test">
<option value="Choice 2">Choice 2</option>
</optgroup>
<option value="Choice 3">Choice 3</option>
</select>
<button class="destroy">Destroy</button>
Expand Down Expand Up @@ -926,7 +928,7 @@ <h2>Select one inputs</h2>
</script>
</div>

<div data-test-hook="autocomplete">
<div data-test-hook="autocomplete">
<label for="choices-autocomplete">Autocomplete example</label>
<select
class="form-control"
Expand Down Expand Up @@ -966,7 +968,7 @@ <h2>Select one inputs</h2>
</script>
</div>


</div>
</div>
</body>
</html>
51 changes: 27 additions & 24 deletions src/scripts/choices.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { activateChoices, addChoice, removeChoice, filterChoices } from './actions/choices';
import { addGroup } from './actions/groups';
import { addItem, highlightItem, removeItem } from './actions/items';
Expand Down Expand Up @@ -28,7 +27,7 @@ import Store from './store/store';
import { coerceBool, mapInputToChoice } from './lib/choice-input';
import { ChoiceFull } from './interfaces/choice-full';
import { GroupFull } from './interfaces/group-full';
import { EventType, KeyCodeMap, PassedElementType, PassedElementTypes } from './interfaces';
import { EventChoiceValueType, EventType, KeyCodeMap, PassedElementType, PassedElementTypes } from './interfaces';
import { EventChoice } from './interfaces/event-choice';
import { NoticeType, NoticeTypes, Templates } from './interfaces/templates';
import { isHtmlInputElement, isHtmlSelectElement } from './lib/html-guard-statements';
Expand Down Expand Up @@ -539,13 +538,10 @@ class Choices {
return this;
}

getValue(valueOnly = false): string[] | EventChoice[] | EventChoice | string {
const values = this._store.items.reduce<any[]>((selectedItems, item) => {
const itemValue = valueOnly ? item.value : this._getChoiceForOutput(item);
selectedItems.push(itemValue);

return selectedItems;
}, []);
getValue<B extends boolean = false>(valueOnly?: B): EventChoiceValueType<B> | EventChoiceValueType<B>[] {
const values = this._store.items.map((item) => {
return (valueOnly ? item.value : this._getChoiceForOutput(item)) as EventChoiceValueType<B>;
});

return this._isSelectOneElement || this.config.singleModeForMultiSelect ? values[0] : values;
}
Expand Down Expand Up @@ -788,16 +784,21 @@ class Choices {

this.clearStore(false);

choicesFromOptions.forEach((groupOrChoice) => {
if ('choices' in groupOrChoice) {
return;
}
const choice = groupOrChoice;
const updateChoice = (choice: ChoiceFull): void => {
if (deselectAll) {
this._store.dispatch(removeItem(choice));
} else if (existingItems[choice.value]) {
choice.selected = true;
}
};

choicesFromOptions.forEach((groupOrChoice) => {
if ('choices' in groupOrChoice) {
groupOrChoice.choices.forEach(updateChoice);

return;
}
updateChoice(groupOrChoice);
});

/* @todo only generate add events for the added options instead of all
Expand Down Expand Up @@ -984,7 +985,7 @@ class Choices {
if (!this._hasNonChoicePlaceholder && !isSearching && this._isSelectOneElement) {
// If we have a placeholder choice along with groups
renderChoices(
activeChoices.filter((choice) => choice.placeholder && !choice.groupId),
activeChoices.filter((choice) => choice.placeholder && !choice.group),
false,
undefined,
);
Expand All @@ -995,6 +996,13 @@ class Choices {
if (config.shouldSort) {
activeGroups.sort(config.sorter);
}
// render Choices without group first, regardless of sort, otherwise they won't be distinguishable
// from the last group
renderChoices(
activeChoices.filter((choice) => !choice.placeholder && !choice.group),
false,
undefined,
);

activeGroups.forEach((group) => {
const groupChoices = renderableChoices(group.choices);
Expand Down Expand Up @@ -1160,13 +1168,8 @@ class Choices {
}
}

_getChoiceForOutput(choice?: ChoiceFull, keyCode?: number): EventChoice | undefined {
if (!choice) {
return undefined;
}

const group = choice.groupId ? this._store.getGroupById(choice.groupId) : null;

// eslint-disable-next-line class-methods-use-this
_getChoiceForOutput(choice: ChoiceFull, keyCode?: number): EventChoice {
return {
id: choice.id,
highlighted: choice.highlighted,
Expand All @@ -1178,7 +1181,7 @@ class Choices {
label: choice.label,
placeholder: choice.placeholder,
value: choice.value,
groupValue: group && group.label ? group.label : undefined,
groupValue: choice.group ? choice.group.label : undefined,
element: choice.element,
keyCode,
};
Expand Down Expand Up @@ -2131,7 +2134,7 @@ class Choices {
group.id = this._lastAddedGroupId;

group.choices.forEach((item: ChoiceFull) => {
item.groupId = group.id;
item.group = group;
if (group.disabled) {
item.disabled = true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/components/wrapped-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default class WrappedSelect extends WrappedElement<HTMLSelectElement> {

return {
id: 0,
groupId: 0,
group: null,
score: 0,
rank: 0,
value: option.value,
Expand Down
10 changes: 5 additions & 5 deletions src/scripts/interfaces/choice-full.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { StringUntrusted } from './string-untrusted';

export type CustomProperties = Record<string, any> | string;
import { Types } from './types';
// eslint-disable-next-line import/no-cycle
import { GroupFull } from './group-full';

/*
A disabled choice appears in the choice dropdown but cannot be selected
Expand All @@ -16,11 +16,11 @@ export interface ChoiceFull {
choiceEl?: HTMLElement;
labelClass?: Array<string>;
labelDescription?: string;
customProperties?: CustomProperties;
customProperties?: Types.CustomProperties;
disabled: boolean;
active: boolean;
elementId?: string;
groupId: number;
group: GroupFull | null;
label: StringUntrusted | string;
placeholder: boolean;
selected: boolean;
Expand Down
2 changes: 2 additions & 0 deletions src/scripts/interfaces/event-choice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { InputChoice } from './input-choice';

export type EventChoiceValueType<B extends boolean> = B extends true ? string : EventChoice;

export interface EventChoice extends InputChoice {
element?: HTMLOptionElement | HTMLOptGroupElement;
groupValue?: string;
Expand Down
3 changes: 1 addition & 2 deletions src/scripts/interfaces/group-full.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

// eslint-disable-next-line import/no-cycle
import { ChoiceFull } from './choice-full';

export interface GroupFull {
Expand Down
6 changes: 3 additions & 3 deletions src/scripts/interfaces/input-choice.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { StringUntrusted } from './string-untrusted';
import { Types } from './types';

export interface InputChoice {
id?: number;
highlighted?: boolean;
labelClass?: string | Array<string>;
labelDescription?: string;
customProperties?: Record<string, any> | string;
customProperties?: Types.CustomProperties;
disabled?: boolean;
active?: boolean;
label: StringUntrusted | string;
placeholder?: boolean;
selected?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
}
2 changes: 0 additions & 2 deletions src/scripts/interfaces/input-group.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { InputChoice } from './input-choice';
import { StringUntrusted } from './string-untrusted';

Expand Down
2 changes: 2 additions & 0 deletions src/scripts/interfaces/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export namespace Types {
label?: StringUntrusted | string;
}
export type ValueOf<T extends object> = T[keyof T];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CustomProperties = Record<string, any> | string;
}
2 changes: 1 addition & 1 deletion src/scripts/lib/choice-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const mapInputToChoice = <T extends string | InputChoice | InputGroup>(

const result: ChoiceFull = {
id: 0, // actual ID will be assigned during _addChoice
groupId: 0, // actual ID will be assigned during _addGroup but before _addChoice
group: null, // actual group will be assigned during _addGroup but before _addChoice
score: 0, // used in search
rank: 0, // used in search, stable sort order
value: choice.value,
Expand Down
3 changes: 1 addition & 2 deletions src/scripts/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { EventTypes } from '../interfaces/event-type';
import { StringUntrusted } from '../interfaces/string-untrusted';
import { StringPreEscaped } from '../interfaces/string-pre-escaped';
Expand Down Expand Up @@ -179,6 +177,7 @@ export const cloneObject = <T>(obj: T): T => (obj !== undefined ? JSON.parse(JSO
/**
* Returns an array of keys present on the first but missing on the second object
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const diff = (a: Record<string, any>, b: Record<string, any>): string[] => {
const aKeys = Object.keys(a).sort();
const bKeys = Object.keys(b).sort();
Expand Down
3 changes: 3 additions & 0 deletions src/scripts/reducers/choices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export default function choices(s: StateType, action: ActionTypes, context?: Opt
case ActionType.REMOVE_CHOICE: {
action.choice.choiceEl = undefined;

if (action.choice.group) {
action.choice.group.choices = action.choice.group.choices.filter((obj) => obj.id !== action.choice.id);
}
state = state.filter((obj) => obj.id !== action.choice.id);
break;
}
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/reducers/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export default function items(s: StateType, action: ActionTypes, context?: Optio
}

case ActionType.REMOVE_CHOICE: {
state = state.filter((item) => item.id !== action.choice.id);
removeItem(action.choice);
state = state.filter((item) => item.id !== action.choice.id);
break;
}

Expand Down
6 changes: 4 additions & 2 deletions src/scripts/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,6 @@ const templates: TemplatesInterface = {
label = escapeForTemplate(allowHTML, label);
label += ` (${groupName})`;
label = { trusted: label };
div.dataset.groupId = `${choice.groupId}`;
}

let describedBy: HTMLElement = div;
Expand Down Expand Up @@ -300,14 +299,17 @@ const templates: TemplatesInterface = {
addClassesToElement(div, placeholder);
}

div.setAttribute('role', choice.groupId ? 'treeitem' : 'option');
div.setAttribute('role', choice.group ? 'treeitem' : 'option');

div.dataset.choice = '';
div.dataset.id = choice.id as unknown as string;
div.dataset.value = rawValue;
if (selectText) {
div.dataset.selectText = selectText;
}
if (choice.group) {
div.dataset.groupId = `${choice.group.id}`;
}

assignCustomProperties(div, choice, false);

Expand Down
10 changes: 6 additions & 4 deletions test-e2e/tests/select-multiple.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,9 +683,10 @@ describe(`Choices - select multiple`, () => {
await suite.startWithClick();

await expect(suite.dropdown.locator('.choices__group[data-group]')).toHaveCount(1);
expect(
await suite.selectableChoices.filter({ hasNot: page.locator('[data-group-id]') }).count(),
).toBeGreaterThan(0);
expect(await suite.dropdown.locator('.choices__item--choice[data-group-id]').count()).toEqual(1);
expect(await suite.dropdown.locator('.choices__item--choice:not([data-group-id])').count()).toBeGreaterThan(
1,
);
});
});
});
Expand Down Expand Up @@ -960,7 +961,8 @@ describe(`Choices - select multiple`, () => {
await suite.group.locator('.destroy').click({ force: true });
await suite.advanceClock();

await expect(suite.group.locator('select > option')).toHaveCount(3);
await expect(suite.group.locator('select > optgroup > option')).toHaveCount(1);
await expect(suite.group.locator('select > option')).toHaveCount(2);

await suite.group.locator('.init').click({ force: true });
await suite.advanceClock();
Expand Down
10 changes: 6 additions & 4 deletions test-e2e/tests/select-one.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,9 +528,10 @@ describe(`Choices - select one`, () => {
await suite.startWithClick();

await expect(suite.dropdown.locator('.choices__group[data-group]')).toHaveCount(1);
expect(
await suite.selectableChoices.filter({ hasNot: page.locator('[data-group-id]') }).count(),
).toBeGreaterThan(0);
expect(await suite.dropdown.locator('.choices__item--choice[data-group-id]').count()).toEqual(1);
expect(await suite.dropdown.locator('.choices__item--choice:not([data-group-id])').count()).toBeGreaterThan(
1,
);
});
});
});
Expand Down Expand Up @@ -834,7 +835,8 @@ describe(`Choices - select one`, () => {
await suite.group.locator('.destroy').click({ force: true });
await suite.advanceClock();

await expect(suite.group.locator('select > option')).toHaveCount(3);
await expect(suite.group.locator('select > optgroup > option')).toHaveCount(1);
await expect(suite.group.locator('select > option')).toHaveCount(2);

await suite.group.locator('.init').click({ force: true });
await suite.advanceClock();
Expand Down
Loading

0 comments on commit d0a7e70

Please sign in to comment.