Skip to content

Commit

Permalink
fix(core/select): prevent list items to expand beyond screen width an…
Browse files Browse the repository at this point in the history
…d add dropdown width properties (#1669)
  • Loading branch information
AndreasBerliner authored Jan 30, 2025
1 parent 7e4a748 commit 6ce2929
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .changeset/orange-geckos-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@siemens/ix-angular': minor
'@siemens/ix': minor
'@siemens/ix-vue': minor
---

Prevent `ix-select` list items to expand beyond screen width and add properties: dropdown-width + dropdown-max-width
4 changes: 2 additions & 2 deletions packages/angular/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2186,15 +2186,15 @@ export declare interface IxRow extends Components.IxRow {}


@ProxyCmp({
inputs: ['allowClear', 'disabled', 'editable', 'helperText', 'hideListHeader', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'selectedIndices', 'showTextAsTooltip', 'validText', 'value', 'warningText'],
inputs: ['allowClear', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'selectedIndices', 'showTextAsTooltip', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'focusInput']
})
@Component({
selector: 'ix-select',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['allowClear', 'disabled', 'editable', 'helperText', 'hideListHeader', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'selectedIndices', 'showTextAsTooltip', 'validText', 'value', 'warningText'],
inputs: ['allowClear', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'selectedIndices', 'showTextAsTooltip', 'validText', 'value', 'warningText'],
})
export class IxSelect {
protected el: HTMLElement;
Expand Down
58 changes: 58 additions & 0 deletions packages/core/component-doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -16946,6 +16946,64 @@
"optional": false,
"required": false
},
{
"name": "dropdownMaxWidth",
"type": "string | undefined",
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"mutable": false,
"attr": "dropdown-max-width",
"reflectToAttr": false,
"docs": "The maximum width of the dropdown element with value and unit (e.g. \"200px\" or \"12.5rem\").\nBy default the maximum width of the dropdown element is set to 100%.",
"docsTags": [
{
"name": "since",
"text": "2.7.0"
}
],
"values": [
{
"type": "string"
},
{
"type": "undefined"
}
],
"optional": true,
"required": false
},
{
"name": "dropdownWidth",
"type": "string | undefined",
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"mutable": false,
"attr": "dropdown-width",
"reflectToAttr": false,
"docs": "The width of the dropdown element with value and unit (e.g. \"200px\" or \"12.5rem\").",
"docsTags": [
{
"name": "since",
"text": "2.7.0"
}
],
"values": [
{
"type": "string"
},
{
"type": "undefined"
}
],
"optional": true,
"required": false
},
{
"name": "editable",
"type": "boolean",
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2461,6 +2461,16 @@ export namespace Components {
* If true the select will be in disabled state
*/
"disabled": boolean;
/**
* The maximum width of the dropdown element with value and unit (e.g. "200px" or "12.5rem"). By default the maximum width of the dropdown element is set to 100%.
* @since 2.7.0
*/
"dropdownMaxWidth"?: string;
/**
* The width of the dropdown element with value and unit (e.g. "200px" or "12.5rem").
* @since 2.7.0
*/
"dropdownWidth"?: string;
/**
* Select is extendable
*/
Expand Down Expand Up @@ -7685,6 +7695,16 @@ declare namespace LocalJSX {
* If true the select will be in disabled state
*/
"disabled"?: boolean;
/**
* The maximum width of the dropdown element with value and unit (e.g. "200px" or "12.5rem"). By default the maximum width of the dropdown element is set to 100%.
* @since 2.7.0
*/
"dropdownMaxWidth"?: string;
/**
* The width of the dropdown element with value and unit (e.g. "200px" or "12.5rem").
* @since 2.7.0
*/
"dropdownWidth"?: string;
/**
* Select is extendable
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/components/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@
}
}

ix-dropdown {
max-width: 100%;
}

.dropdown-visible {
--ix-icon-button-color: var(--theme-color-dynamic--hover);
}
Expand Down
34 changes: 31 additions & 3 deletions packages/core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,22 @@ export class Select implements IxInputFieldComponent<string | string[]> {
*/
@Prop() hideListHeader = false;

/**
* The width of the dropdown element with value and unit (e.g. "200px" or "12.5rem").
*
* @since 2.7.0
*/
@Prop() dropdownWidth?: string;

/**
* The maximum width of the dropdown element with value and unit (e.g. "200px" or "12.5rem").
* By default the maximum width of the dropdown element is set to 100%.
*
* @since 2.7.0
*
*/
@Prop() dropdownMaxWidth?: string;

/**
* Value changed
* @since 2.0.0
Expand Down Expand Up @@ -953,13 +969,25 @@ export class Select implements IxInputFieldComponent<string | string[]> {
onShowChanged={(e) => this.dropdownVisibilityChanged(e)}
placement="bottom-start"
overwriteDropdownStyle={async () => {
const styleOverwrites: Partial<CSSStyleDeclaration> = {};

const minWidth = this.hostElement.shadowRoot
?.querySelector('.select')
?.getBoundingClientRect().width;

return {
minWidth: `${minWidth}px`,
};
if (minWidth) {
styleOverwrites.minWidth = `${minWidth}px`;
}

if (this.dropdownWidth) {
styleOverwrites.width = `min(${this.dropdownWidth}, 100%)`;
}

if (this.dropdownMaxWidth) {
styleOverwrites.maxWidth = `min(${this.dropdownMaxWidth}, 100%)`;
}

return styleOverwrites;
}}
>
<div
Expand Down
120 changes: 120 additions & 0 deletions packages/core/src/components/select/test/select.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,3 +551,123 @@ test.describe('Enter selection with non-existing and existing items', () => {
await expect(input).toHaveValue('Item 1');
});
});

test.describe('Dropdown width', () => {
test('should be 25rem when dropdown-width is set to 35rem and dropdown-max-width to 25rem', async ({
mount,
page,
}) => {
await mount(`<ix-select value="1" dropdown-width="35rem" dropdown-max-width="25rem">
<ix-select-item label="this is an example for a very long selection option. this is an example for a very long selection option." value="1"></ix-select-item>
</ix-select>`);

const select = page.locator('ix-select');

await page.locator('[data-select-dropdown]').click();

const dropdown = select.locator('ix-dropdown');
await expect(dropdown).toBeVisible();

const box = await dropdown.boundingBox();
expect(box?.width).toBe(16 * 25);
});

test('should be 25rem when dropdown-width is set to 25 and dropdown-max-width to 35rem', async ({
mount,
page,
}) => {
await mount(`<ix-select value="1" dropdown-width="25rem" dropdown-max-width="35rem">
<ix-select-item label="this is an example for a very long selection option. this is an example for a very long selection option." value="1"></ix-select-item>
</ix-select>`);

const select = page.locator('ix-select');

await page.locator('[data-select-dropdown]').click();

const dropdown = select.locator('ix-dropdown');
await expect(dropdown).toBeVisible();

const box = await dropdown.boundingBox();
expect(box?.width).toBe(16 * 25);
});

test('should be 25rem when dropdown-width is set to 25 and dropdown-max-width is not set', async ({
mount,
page,
}) => {
await mount(`<ix-select value="1" dropdown-width="25rem">
<ix-select-item label="this is an example for a very long selection option. this is an example for a very long selection option." value="1"></ix-select-item>
</ix-select>`);

const select = page.locator('ix-select');

await page.locator('[data-select-dropdown]').click();

const dropdown = select.locator('ix-dropdown');
await expect(dropdown).toBeVisible();

const box = await dropdown.boundingBox();
expect(box?.width).toBe(16 * 25);
});

test('should be 35rem when dropdown-width is not set and dropdown-max-width is set to 35rem', async ({
mount,
page,
}) => {
await mount(`<ix-select value="1" dropdown-max-width="35rem">
<ix-select-item label="this is an example for a very long selection option. this is an example for a very long selection option." value="1"></ix-select-item>
</ix-select>`);

const select = page.locator('ix-select');

await page.locator('[data-select-dropdown]').click();

const dropdown = select.locator('ix-dropdown');
await expect(dropdown).toBeVisible();

const box = await dropdown.boundingBox();
expect(box?.width).toBe(16 * 35);
});

test('should be 100% when dropdown-width and dropdown-max-width are not set', async ({
mount,
page,
}) => {
await mount(`<ix-select value="1">
<ix-select-item label="this is an example for a very long selection option. this is an example for a very long selection option. this is an example for a very long selection option. this is an example for a very long selection option. this is an example for a very long selection option." value="1"></ix-select-item>
</ix-select>`);

const select = page.locator('ix-select');

await page.locator('[data-select-dropdown]').click();

const dropdown = select.locator('ix-dropdown');
await expect(dropdown).toBeVisible();

const box = await dropdown.boundingBox();
const pageWidth = page.viewportSize()?.width;

expect(box?.width).toBe(pageWidth);
});
});

test('should be 100% when dropdown-max-width is greater than the viewport width', async ({
mount,
page,
}) => {
await mount(`<ix-select value="1" dropdown-max-width="10000rem">
<ix-select-item label="this is an example for a very long selection option. this is an example for a very long selection option. this is an example for a very long selection option. this is an example for a very long selection option. this is an example for a very long selection option." value="1"></ix-select-item>
</ix-select>`);

const select = page.locator('ix-select');

await page.locator('[data-select-dropdown]').click();

const dropdown = select.locator('ix-dropdown');
await expect(dropdown).toBeVisible();

const box = await dropdown.boundingBox();
const pageWidth = page.viewportSize()?.width;

expect(box?.width).toBe(pageWidth);
});
2 changes: 1 addition & 1 deletion packages/documentation/docs/controls/_select_styleguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ A select component allows users to choose from a list of options. It supports si
- **Overflow:**
- The text in an input field is truncated with the length of the container.
- On the multiselect, the selected items break into a second line and then show a scrollbar if it extends beyond two lines.
- The dropdown list is scrollable when the list exceeds the container height. Its width is defined by the longest item.
- The dropdown list is scrollable when the list exceeds the container height. Its width is defined by the longest item. The maximum width of the dropdown list is set to 100% by default. Use the properties `dropdownWidth` and `dropdownMaxWidth` to customize the dimensions.
- **Alignment:** Selects are always aligned to the left, while right alignment is reserved exclusively for [number inputs](input-number.mdx).

### States
Expand Down
25 changes: 25 additions & 0 deletions packages/storybook-docs/src/stories/select.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,28 @@ export const editableSelect: Story = {
disabled: false,
},
};

export const editable_with_dropdown_width: Story = {
render: ({ editable, disabled, dropdownWidth, dropdownMaxWidth }) => {
return html` <ix-select
editable=${editable}
disabled=${disabled}
dropdown-width=${dropdownWidth}
dropdown-max-width=${dropdownMaxWidth}
>
<ix-select-item
label="this is an example for a very long selection option. this is an example for a very long selection option."
value="1"
></ix-select-item>
<ix-select-item label="Item 2" value="2"></ix-select-item>
<ix-select-item label="Item 3" value="3"></ix-select-item>
<ix-select-item label="Item 4" value="4"></ix-select-item>
</ix-select>`;
},
args: {
editable: true,
disabled: false,
dropdownWidth: '35rem',
dropdownMaxWidth: '25rem',
},
};
2 changes: 2 additions & 0 deletions packages/vue/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,8 @@ export const IxSelect = /*@__PURE__*/ defineContainer<JSX.IxSelect, JSX.IxSelect
'i18nSelectListHeader',
'i18nNoMatches',
'hideListHeader',
'dropdownWidth',
'dropdownMaxWidth',
'valueChange',
'itemSelectionChange',
'inputChange',
Expand Down

0 comments on commit 6ce2929

Please sign in to comment.