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 autoExpandHorizontally and autoExpandVertically properties (#6515) (CP: 23.4) #6729

Merged
merged 4 commits into from
Nov 16, 2023
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
3 changes: 2 additions & 1 deletion dev/multi-select-combo-box.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
required
error-message="Select at least one"
allow-custom-value
style="width: 300px"
auto-expand-horizontally
auto-expand-vertically
>
<vaadin-tooltip slot="tooltip" text="Vaadin multi-select-combo-box tooltip text"></vaadin-tooltip>
</vaadin-multi-select-combo-box>
Expand Down
26 changes: 16 additions & 10 deletions packages/combo-box/src/vaadin-combo-box-overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,24 @@ export class ComboBoxOverlay extends PositionMixin(Overlay) {
}
}

_setOverlayWidth(positionTarget, opened) {
if (positionTarget && opened) {
const propPrefix = this.localName;
this.style.setProperty(`--_${propPrefix}-default-width`, `${positionTarget.clientWidth}px`);
/** @protected */
_updateOverlayWidth() {
const propPrefix = this.localName;
this.style.setProperty(`--_${propPrefix}-default-width`, `${this.positionTarget.clientWidth}px`);

const customWidth = getComputedStyle(this._comboBox).getPropertyValue(`--${propPrefix}-width`);
const customWidth = getComputedStyle(this._comboBox).getPropertyValue(`--${propPrefix}-width`);

if (customWidth === '') {
this.style.removeProperty(`--${propPrefix}-width`);
} else {
this.style.setProperty(`--${propPrefix}-width`, customWidth);
}
if (customWidth === '') {
this.style.removeProperty(`--${propPrefix}-width`);
} else {
this.style.setProperty(`--${propPrefix}-width`, customWidth);
}
}

/** @private */
_setOverlayWidth(positionTarget, opened) {
if (positionTarget && opened) {
this._updateOverlayWidth();

this._updatePosition();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ registerStyles(
display: flex;
width: 100%;
}

:host([auto-expand-vertically]) #wrapper {
flex-wrap: wrap;
}
`,
{
moduleId: 'vaadin-multi-select-combo-box-container-styles',
Expand Down Expand Up @@ -47,6 +51,21 @@ class MultiSelectComboBoxContainer extends InputContainer {
}
return memoizedTemplate;
}

static get properties() {
return {
/**
* Set to true to not collapse selected items chips into the overflow
* chip and instead always expand vertically, causing input field to
* wrap into multiple lines when width is limited.
* @attr {boolean} auto-expand-vertically
*/
autoExpandVertically: {
type: Boolean,
reflectToAttribute: true,
},
};
}
}

customElements.define(MultiSelectComboBoxContainer.is, MultiSelectComboBoxContainer);
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,21 @@ export interface MultiSelectComboBoxEventMap<TItem> extends HTMLElementEventMap
* @fires {CustomEvent} validated - Fired whenever the field is validated.
*/
declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLElement {
/**
* Set to true to auto expand horizontally, causing input field to
* grow until max width is reached.
* @attr {boolean} auto-expand-horizontally
*/
autoExpandHorizontally: boolean;

/**
* Set to true to not collapse selected items chips into the overflow
* chip and instead always expand vertically, causing input field to
* wrap into multiple lines when width is limited.
* @attr {boolean} auto-expand-vertically
*/
autoExpandVertically: boolean;

/**
* When true, the user can input a value that is not present in the items list.
* @attr {boolean} allow-custom-value
Expand Down
104 changes: 101 additions & 3 deletions packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ const multiSelectComboBox = css`
flex-basis: 0;
padding: 0;
}

:host([auto-expand-vertically]) #chips {
display: contents;
}

:host([auto-expand-horizontally]) [class$='container'] {
width: auto;
}
`;

registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectComboBox], {
Expand Down Expand Up @@ -179,6 +187,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
>
<vaadin-multi-select-combo-box-container
part="input-field"
auto-expand-vertically="[[autoExpandVertically]]"
readonly="[[readonly]]"
disabled="[[disabled]]"
invalid="[[invalid]]"
Expand Down Expand Up @@ -223,6 +232,31 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El

static get properties() {
return {
/**
* Set to true to auto expand horizontally, causing input field to
* grow until max width is reached.
* @attr {boolean} auto-expand-horizontally
*/
autoExpandHorizontally: {
type: Boolean,
value: false,
reflectToAttribute: true,
observer: '_autoExpandHorizontallyChanged',
},

/**
* Set to true to not collapse selected items chips into the overflow
* chip and instead always expand vertically, causing input field to
* wrap into multiple lines when width is limited.
* @attr {boolean} auto-expand-vertically
*/
autoExpandVertically: {
type: Boolean,
value: false,
reflectToAttribute: true,
observer: '_autoExpandVerticallyChanged',
},

/**
* Set true to prevent the overlay from opening automatically.
* @attr {boolean} auto-open-disabled
Expand Down Expand Up @@ -650,6 +684,20 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
super._delegateAttribute(name, value);
}

/** @private */
_autoExpandHorizontallyChanged(autoExpand, oldAutoExpand) {
if (autoExpand || oldAutoExpand) {
this.__updateChips();
}
}

/** @private */
_autoExpandVerticallyChanged(autoExpand, oldAutoExpand) {
if (autoExpand || oldAutoExpand) {
this.__updateChips();
}
}

/**
* Setting clear button visible reduces total space available
* for rendering chips, and making it hidden increases it.
Expand Down Expand Up @@ -728,6 +776,10 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El

// Update selected for dropdown items
this.requestContentUpdate();

if (this.opened) {
this.$.comboBox.$.overlay._updateOverlayWidth();
}
}

/** @private */
Expand Down Expand Up @@ -916,14 +968,60 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
remainingWidth -= this.__getOverflowWidth();
}

const chipMinWidth = parseInt(getComputedStyle(this).getPropertyValue('--_chip-min-width'));

if (this.autoExpandHorizontally) {
const chips = [];

// First, add all chips to make the field fully expand
for (let i = items.length - 1, refNode = null; i >= 0; i--) {
const chip = this.__createChip(items[i]);
this.$.chips.insertBefore(chip, refNode);
refNode = chip;
chips.unshift(chip);
}

const overflowItems = [];
const availableWidth = this._inputField.$.wrapper.clientWidth - this.$.chips.clientWidth;

// When auto expanding vertically, no need to measure width
if (!this.autoExpandVertically && availableWidth < inputWidth) {
// Always show at least last item as a chip
while (chips.length > 1) {
const lastChip = chips.pop();
lastChip.remove();
overflowItems.unshift(items.pop());

// Remove chips until there is enough width for the input element to fit
const neededWidth = overflowItems.length > 0 ? inputWidth + this.__getOverflowWidth() : inputWidth;
if (this._inputField.$.wrapper.clientWidth - this.$.chips.clientWidth >= neededWidth) {
break;
}
}

if (chips.length === 1) {
chips[0].style.maxWidth = `${Math.max(chipMinWidth, remainingWidth)}px`;
}
}

this._overflowItems = overflowItems;
return;
}

// Add chips until remaining width is exceeded
for (let i = items.length - 1, refNode = null; i >= 0; i--) {
const chip = this.__createChip(items[i]);
this.$.chips.insertBefore(chip, refNode);

if (this.$.chips.clientWidth > remainingWidth) {
chip.remove();
break;
// When auto expanding vertically, no need to measure remaining width
if (!this.autoExpandVertically && this.$.chips.clientWidth > remainingWidth) {
// Always show at least last selected item as a chip
if (refNode === null) {
chip.style.maxWidth = `${Math.max(chipMinWidth, remainingWidth)}px`;
} else {
chip.remove();
break;
}
}

items.pop();
Expand Down
117 changes: 117 additions & 0 deletions packages/multi-select-combo-box/test/chips.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,4 +418,121 @@ describe('chips', () => {
});
});
});

describe('autoExpandVertically', () => {
let overflow;

beforeEach(async () => {
comboBox.style.width = '250px';
await nextResize(comboBox);
overflow = getChips(comboBox)[0];
});

it('should not show overflow chip when autoExpandVertically is set to true', async () => {
comboBox.autoExpandVertically = true;
comboBox.selectedItems = ['apple', 'banana'];
await nextRender();
expect(getChips(comboBox).length).to.equal(3);
expect(overflow.hasAttribute('hidden')).to.be.true;
});

it('should show overflow chip when autoExpandVertically is set to false', async () => {
comboBox.autoExpandVertically = true;
comboBox.selectedItems = ['apple', 'banana'];
await nextRender();

comboBox.autoExpandVertically = false;
await nextRender();
expect(getChips(comboBox).length).to.equal(2);
expect(overflow.hasAttribute('hidden')).to.be.false;
});

it('should update chips when autoExpandVertically is set after selectedItems', async () => {
comboBox.selectedItems = ['apple', 'banana'];
await nextRender();
expect(getChips(comboBox).length).to.equal(2);
expect(overflow.hasAttribute('hidden')).to.be.false;

comboBox.autoExpandVertically = true;
await nextRender();
expect(getChips(comboBox).length).to.equal(3);
expect(overflow.hasAttribute('hidden')).to.be.true;
});

it('should wrap chips and increase input field height if chips do not fit', async () => {
const inputField = comboBox.shadowRoot.querySelector('[part="input-field"]');
const height = inputField.clientHeight;
comboBox.autoExpandVertically = true;
comboBox.selectedItems = ['apple', 'banana', 'lemon', 'orange'];
await nextRender();
expect(inputField.clientHeight).to.be.greaterThan(height);
});
});

describe('autoExpandHorizontally', () => {
let overflow;

beforeEach(async () => {
comboBox.autoExpandHorizontally = true;
await nextResize(comboBox);
comboBox.selectedItems = ['apple', 'banana', 'lemon', 'orange'];
overflow = getChips(comboBox)[0];
await nextRender();
});

it('should show all chips when there is enough space by default', () => {
expect(getChips(comboBox).length).to.equal(5);
expect(overflow.hasAttribute('hidden')).to.be.true;
});

it('should collapse chips to overflow when max-width is set on the host', async () => {
comboBox.style.maxWidth = '300px';
await nextResize(comboBox);
expect(getChips(comboBox).length).to.equal(3);
expect(overflow.hasAttribute('hidden')).to.be.false;
});

it('should collapse chips to overflow when width is set on the host', async () => {
comboBox.style.width = '300px';
await nextResize(comboBox);
expect(getChips(comboBox).length).to.equal(3);
expect(overflow.hasAttribute('hidden')).to.be.false;
});

it('should collapse chips to overflow when max-width is set on the parent', async () => {
comboBox.parentElement.style.maxWidth = '300px';
await nextResize(comboBox);
expect(getChips(comboBox).length).to.equal(3);
expect(overflow.hasAttribute('hidden')).to.be.false;
});

it('should set max-width on the chip when the host width does not allow to fit', async () => {
comboBox.style.maxWidth = '180px';
await nextResize(comboBox);
const chips = getChips(comboBox);
expect(chips.length).to.equal(2);
expect(getComputedStyle(chips[1]).maxWidth).to.be.ok;
});

it('should collapse chips when autoExpandHorizontally is set to false', async () => {
comboBox.autoExpandHorizontally = false;
await nextRender();
expect(getChips(comboBox).length).to.equal(2);
expect(overflow.hasAttribute('hidden')).to.be.false;
});

it('should adapt overlay width to the input field width while opened', async () => {
comboBox.opened = true;

const overlay = document.querySelector('vaadin-multi-select-combo-box-overlay');
const overlayPart = overlay.$.overlay;
const width = overlayPart.clientWidth;
expect(width).to.equal(comboBox.clientWidth);

comboBox.selectedItems = ['apple', 'banana'];
await nextRender();
expect(overlayPart.clientWidth).to.be.lessThan(width);
expect(overlayPart.clientWidth).to.be.equal(comboBox.clientWidth);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ assertType<string | null | undefined>(narrowedComboBox.label);
assertType<boolean>(narrowedComboBox.required);
assertType<string | null | undefined>(narrowedComboBox.theme);
assertType<boolean>(narrowedComboBox.selectedItemsOnTop);
assertType<boolean>(narrowedComboBox.autoExpandVertically);

// Mixins
assertType<ControllerMixinClass>(narrowedComboBox);
Expand Down
Loading