Skip to content

Commit

Permalink
fix(select): slot change options
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Esteves <[email protected]>
  • Loading branch information
aesteves60 authored and dpellier committed Oct 30, 2024
1 parent 53b7a84 commit f523900
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { OdsSelect } from '@ovhcloud/ods-components/react';
import { useQuery } from '@tanstack/react-query';
import React, { type ReactElement } from 'react';
import React, { type ReactElement, useState } from 'react';

function TestSelect(): ReactElement {
// const [isLoading, setIsLoading] = useState(true);
const [isPending, setIsPending] = useState(true)
const [data, setData] = useState<{ id: string, title: string }[]>([])

const { isPending, data } = useQuery({
const query = useQuery({
queryFn: () => fetch('https://jsonplaceholder.typicode.com/posts').then((res) =>
res.json(),
),
queryKey: ['repoData'],
});

console.log('isPending', isPending, data);
// setTimeout(() => setIsLoading(false), 5000);
setTimeout(() => {
setIsPending(query.isPending);
setData(query.data);
}, 100000);

return (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ export class OdsSelect {
}));
}

@Watch('customRenderer')
onCustomRendererChange(): void {
if (this.selectElement) {
this.select?.destroy();
this.createTomSelect(this.selectElement);
}
}

@Watch('value')
onValueChange(value: string | string[] | null, previousValue?: string | string[] | null): void {
// Value change can be triggered from either value attribute change or select change
Expand Down Expand Up @@ -146,8 +154,7 @@ export class OdsSelect {
this.observer = new MutationObserver((mutations) => {
// We only care about mutations on child element (attributes or content changes)
// as mutations on root element is managed by the onSlotChange
const childrenMutations = mutations.filter((mutation) =>
mutation.target !== this.selectElement && mutation.type !== 'childList');
const childrenMutations = mutations.filter((mutation) => mutation.target !== this.selectElement && mutation.type !== 'childList');

if (childrenMutations.length) {
const currentValue = this.select?.getValue() || '';
Expand All @@ -158,6 +165,10 @@ export class OdsSelect {
}
});

if (this.selectElement) {
this.createTomSelect(this.selectElement);
}

this.observer.observe(this.selectElement!, {
attributes: true,
characterData: true,
Expand All @@ -179,7 +190,7 @@ export class OdsSelect {
private bindSelectControl(): void {
// By setting the lib "openOnFocus" to false, the dropdown doesn't open anymore on click
// So we need to manually add our own open handler
this.select?.control.addEventListener('click', () => {
this.select?.control?.addEventListener('click', () => {
if (this.isDisabled) {
return;
}
Expand All @@ -195,21 +206,91 @@ export class OdsSelect {
}
});

this.select?.control.addEventListener('keydown', (event: KeyboardEvent) => {
this.select?.control?.addEventListener('keydown', (event: KeyboardEvent) => {
// This prevents Space key to scroll the window down
if (event.key === ' ') {
event.preventDefault();
}
});

this.select?.control.addEventListener('keyup', (event: KeyboardEvent) => {
this.select?.control?.addEventListener('keyup', (event: KeyboardEvent) => {
if (!this.isDisabled && event.key === ' ') {
this.select?.open();
}
});
}

private onSlotChange(event: Event): void {
private createTomSelect(selectElement: HTMLSelectElement): void {
const { plugin, template } = getSelectConfig(this.allowMultiple, this.multipleSelectionLabel, this.customRenderer);

this.select?.destroy();
this.select = new TomSelect(selectElement, {
allowEmptyOption: true,
closeAfterSelect: !this.allowMultiple,
controlInput: undefined,
create: false,
maxOptions: undefined,
onBlur: (): void => {
this.odsBlur.emit();
},
onChange: (value: string | string[]): void => {
if (!this.isValueSync) {
this.isSelectSync = true;
this.updateValue(value);
}
this.isValueSync = false;
},
onDropdownClose: (dropdown: HTMLDivElement): void => {
dropdown.classList.remove('ods-select__dropdown--bottom', 'ods-select__dropdown--top');

this.select!.control.style.removeProperty('border-top-right-radius');
this.select!.control.style.removeProperty('border-top-left-radius');
this.select!.control.style.removeProperty('border-bottom-right-radius');
this.select!.control.style.removeProperty('border-bottom-left-radius');
},
onDropdownOpen: async(dropdown: HTMLDivElement): Promise<void> => {
// Delay the position computing at the end of the stack to ensure floating element has its final height
setTimeout(async() => {
const { placement, y } = await getElementPosition('bottom', {
popper: dropdown,
trigger: this.select?.control,
}, {
offset: -1, // offset the border-width size as we want it merged with the trigger.
});

Object.assign(dropdown.style, {
left: '0',
top: `${y}px`,
});

dropdown.classList.add(`ods-select__dropdown--${placement}`);

if (placement === 'top') {
this.select!.control.style.borderTopRightRadius = '0';
this.select!.control.style.borderTopLeftRadius = '0';
} else {
this.select!.control.style.borderBottomRightRadius = '0';
this.select!.control.style.borderBottomLeftRadius = '0';
}
}, 0);
},
onFocus: (): void => {
this.odsFocus.emit();
},
openOnFocus: false,
placeholder: this.placeholder,
plugins: plugin,
render: template,
selectOnTab: true,
});

this.bindSelectControl();
this.onIsDisabledChange(this.isDisabled);
this.onIsReadonlyChange(this.isReadonly);
setSelectValue(this.select, this.value, this.defaultValue, true);
}

private async onSlotChange(event: Event): Promise<void> {
// The initial slot nodes move will trigger this callback again
// but we want to avoid a second select initialisation
if (this.hasMovedNodes) {
Expand All @@ -218,76 +299,14 @@ export class OdsSelect {
}

if (this.selectElement) {
this.select?.clear(); // reset the current selection
this.select?.clearOptions(); // reset the tom-select options

moveSlottedElements(this.selectElement, (event.currentTarget as HTMLSlotElement).assignedElements());
this.hasMovedNodes = true;

const { plugin, template } = getSelectConfig(this.allowMultiple, this.multipleSelectionLabel, this.customRenderer);

this.select?.destroy();
this.select = new TomSelect(this.selectElement, {
allowEmptyOption: true,
closeAfterSelect: !this.allowMultiple,
controlInput: undefined,
create: false,
maxOptions: undefined,
onBlur: (): void => {
this.odsBlur.emit();
},
onChange: (value: string | string[]): void => {
if (!this.isValueSync) {
this.isSelectSync = true;
this.updateValue(value);
}
this.isValueSync = false;
},
onDropdownClose: (dropdown: HTMLDivElement): void => {
dropdown.classList.remove('ods-select__dropdown--bottom', 'ods-select__dropdown--top');

this.select!.control.style.removeProperty('border-top-right-radius');
this.select!.control.style.removeProperty('border-top-left-radius');
this.select!.control.style.removeProperty('border-bottom-right-radius');
this.select!.control.style.removeProperty('border-bottom-left-radius');
},
onDropdownOpen: async(dropdown: HTMLDivElement): Promise<void> => {
// Delay the position computing at the end of the stack to ensure floating element has its final height
setTimeout(async() => {
const { placement, y } = await getElementPosition('bottom', {
popper: dropdown,
trigger: this.select?.control,
}, {
offset: -1, // offset the border-width size as we want it merged with the trigger.
});

Object.assign(dropdown.style, {
left: '0',
top: `${y}px`,
});

dropdown.classList.add(`ods-select__dropdown--${placement}`);

if (placement === 'top') {
this.select!.control.style.borderTopRightRadius = '0';
this.select!.control.style.borderTopLeftRadius = '0';
} else {
this.select!.control.style.borderBottomRightRadius = '0';
this.select!.control.style.borderBottomLeftRadius = '0';
}
}, 0);
},
onFocus: (): void => {
this.odsFocus.emit();
},
openOnFocus: false,
placeholder: this.placeholder,
plugins: plugin,
render: template,
selectOnTab: true,
});

this.bindSelectControl();
this.onIsDisabledChange(this.isDisabled);
this.onIsReadonlyChange(this.isReadonly);
setSelectValue(this.select, this.value, this.defaultValue, true);
this.select?.sync(); // get updated options
this.select?.setValue(this.value || ''); // set the value back
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function setSelectValue(select?: TomSelect, value?: string | string[] | null, de
}

export {
type SelectConfig,
getSelectConfig,
inlineValue,
moveSlottedElements,
Expand Down
25 changes: 24 additions & 1 deletion packages/ods/src/components/select/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

<body>
<p>Default</p>
<ods-select>
<ods-select value="dog">
<option value="dog">Dog</option>
<option value="cat">Cat</option>
<option value="hamster">Hamster</option>
Expand All @@ -27,6 +27,29 @@
<option value="goldfish">Goldfish</option>
</ods-select>

<br>
<br>

<ods-select id="select-default">
</ods-select>

<script>
const selectDefault = document.querySelector('#select-default');
setTimeout(() => {
selectDefault.innerHTML = `<option value="dog">Dog</option>
<option value="cat">Cat</option>
<option value="hamster">Hamster</option>
<option value="parrot">Parrot</option>
<option value="spider">Spider</option>
<option value="goldfish">Goldfish</option>`
}, 1000)

setTimeout(() => {
selectDefault.innerHTML = `<option value="dog">Dog</option>
<option value="cat">Cat</option>`
}, 5000)
</script>

<p>Disabled</p>
<ods-select is-disabled value="parrot">
<option value="dog">Dog</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
jest.mock('../../src/controller/ods-select');

import type { SpecPage } from '@stencil/core/testing';
jest.mock('tom-select');
jest.mock('../../src/controller/ods-select', () => ({
getSelectConfig: (): SelectConfig => {
return { plugin: {}, template: {} } ;
},
inlineValue: jest.fn(),
moveSlottedElements: jest.fn(),
setFormValue: jest.fn(),
setSelectValue: jest.fn(),
}));

import { type SpecPage } from '@stencil/core/testing';
import { newSpecPage } from '@stencil/core/testing';
import { OdsSelect } from '../../src';
import { type SelectConfig } from '../../src/controller/ods-select';

describe('ods-select behaviour', () => {
let page: SpecPage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ describe('ods-select rendering', () => {
expect(selectComponent.classList.contains('disabled')).toBe(true);
});

it('should render the web component without options', async() => {
await setup('<ods-select></ods-select>');

expect(innerSelect).toHaveClass('tomselected');
expect(selectComponent).toBeDefined();
});

describe('watchers', () => {
describe('isDisabled', () => {
it('should disable the select component', async() => {
Expand All @@ -132,5 +139,28 @@ describe('ods-select rendering', () => {
})).toBe(true);
});
});

describe('onSlotChange', () => {
it('should render 2 options after slot change', async() => {
await setup('<ods-select><option value="1">Value 1</option></ods-select>');

el.innerHTML = '<option value="1">Value 1</option><option value="2">Value 2</option>';
await page.waitForChanges();

const optionsNumber = [...innerSelect.children].filter((child) => child.tagName === 'OPTION').length;
expect(optionsNumber).toBe(2);
});

it('should change options but not the selected one', async() => {
const value = '1';
await setup(`<ods-select value="${value}"><option value="${value}">Value 1</option></ods-select>`);

el.innerHTML = `<option value="${value}">Value 1</option><option value="2">Value 2</option>`;
await page.waitForChanges();

const selectedOption = selectComponent.querySelector('.ts-control')?.querySelector(`[data-value="${value}"]`);
expect(selectedOption).toBeDefined();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
jest.mock('../../src/controller/ods-select');
jest.mock('tom-select');
jest.mock('../../src/controller/ods-select', () => ({
getSelectConfig: (): SelectConfig => {
return { plugin: {}, template: {} } ;
},
inlineValue: jest.fn(),
moveSlottedElements: jest.fn(),
setFormValue: jest.fn(),
setSelectValue: jest.fn(),
}));

import { type SpecPage, newSpecPage } from '@stencil/core/testing';
import { OdsSelect } from '../../src';
import { type SelectConfig } from '../../src/controller/ods-select';

describe('ods-select rendering', () => {
let page: SpecPage;
Expand Down

0 comments on commit f523900

Please sign in to comment.