mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 02:31:34 +08:00
feat(select): add props to customize toggle icons (#27648)
Issue number: resolves #17248 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> While the `icon` shadow part allows customization of the existing toggle icon, developers do not have a way to specify a different icon to use entirely. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> New props `toggleIcon` and `expandedIcon` added. (Design docs are [here](https://github.com/ionic-team/ionic-framework-design-documents/blob/main/projects/ionic-framework/components/select/0002-custom-icons.md) and [here](https://github.com/ionic-team/ionic-framework-design-documents/blob/main/projects/ionic-framework/components/select/0003-custom-icon-on-open.md) respectively.) ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Docs PR: https://github.com/ionic-team/ionic-docs/pull/2996 Dev build: `7.0.15-dev.11687278023.161b97d8` --------- Co-authored-by: ionitron <hi@ionicframework.com>
This commit is contained in:
@ -1954,7 +1954,7 @@ export declare interface IonSegmentButton extends Components.IonSegmentButton {}
|
|||||||
|
|
||||||
|
|
||||||
@ProxyCmp({
|
@ProxyCmp({
|
||||||
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'value'],
|
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
|
||||||
methods: ['open']
|
methods: ['open']
|
||||||
})
|
})
|
||||||
@Component({
|
@Component({
|
||||||
@ -1962,7 +1962,7 @@ export declare interface IonSegmentButton extends Components.IonSegmentButton {}
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: '<ng-content></ng-content>',
|
template: '<ng-content></ng-content>',
|
||||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||||
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'value'],
|
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
|
||||||
})
|
})
|
||||||
export class IonSelect {
|
export class IonSelect {
|
||||||
protected el: HTMLElement;
|
protected el: HTMLElement;
|
||||||
|
@ -1238,6 +1238,7 @@ ion-select,prop,cancelText,string,'Cancel',false,false
|
|||||||
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
|
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
|
||||||
ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
|
ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
|
||||||
ion-select,prop,disabled,boolean,false,false,false
|
ion-select,prop,disabled,boolean,false,false,false
|
||||||
|
ion-select,prop,expandedIcon,string | undefined,undefined,false,false
|
||||||
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
|
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
|
||||||
ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false
|
ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false
|
||||||
ion-select,prop,interfaceOptions,any,{},false,false
|
ion-select,prop,interfaceOptions,any,{},false,false
|
||||||
@ -1252,6 +1253,7 @@ ion-select,prop,okText,string,'OK',false,false
|
|||||||
ion-select,prop,placeholder,string | undefined,undefined,false,false
|
ion-select,prop,placeholder,string | undefined,undefined,false,false
|
||||||
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
|
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
|
||||||
ion-select,prop,shape,"round" | undefined,undefined,false,false
|
ion-select,prop,shape,"round" | undefined,undefined,false,false
|
||||||
|
ion-select,prop,toggleIcon,string | undefined,undefined,false,false
|
||||||
ion-select,prop,value,any,undefined,false,false
|
ion-select,prop,value,any,undefined,false,false
|
||||||
ion-select,method,open,open(event?: UIEvent) => Promise<any>
|
ion-select,method,open,open(event?: UIEvent) => Promise<any>
|
||||||
ion-select,event,ionBlur,void,true
|
ion-select,event,ionBlur,void,true
|
||||||
|
16
core/src/components.d.ts
vendored
16
core/src/components.d.ts
vendored
@ -2681,6 +2681,10 @@ export namespace Components {
|
|||||||
* If `true`, the user cannot interact with the select.
|
* If `true`, the user cannot interact with the select.
|
||||||
*/
|
*/
|
||||||
"disabled": boolean;
|
"disabled": boolean;
|
||||||
|
/**
|
||||||
|
* The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed.
|
||||||
|
*/
|
||||||
|
"expandedIcon"?: string;
|
||||||
/**
|
/**
|
||||||
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
|
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
|
||||||
*/
|
*/
|
||||||
@ -2742,6 +2746,10 @@ export namespace Components {
|
|||||||
* The shape of the select. If "round" it will have an increased border radius.
|
* The shape of the select. If "round" it will have an increased border radius.
|
||||||
*/
|
*/
|
||||||
"shape"?: 'round';
|
"shape"?: 'round';
|
||||||
|
/**
|
||||||
|
* The toggle icon to use. Defaults to `chevronExpand` for `ios` mode, or `caretDownSharp` for `md` mode.
|
||||||
|
*/
|
||||||
|
"toggleIcon"?: string;
|
||||||
/**
|
/**
|
||||||
* The value of the select.
|
* The value of the select.
|
||||||
*/
|
*/
|
||||||
@ -6755,6 +6763,10 @@ declare namespace LocalJSX {
|
|||||||
* If `true`, the user cannot interact with the select.
|
* If `true`, the user cannot interact with the select.
|
||||||
*/
|
*/
|
||||||
"disabled"?: boolean;
|
"disabled"?: boolean;
|
||||||
|
/**
|
||||||
|
* The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed.
|
||||||
|
*/
|
||||||
|
"expandedIcon"?: string;
|
||||||
/**
|
/**
|
||||||
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
|
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
|
||||||
*/
|
*/
|
||||||
@ -6835,6 +6847,10 @@ declare namespace LocalJSX {
|
|||||||
* The shape of the select. If "round" it will have an increased border radius.
|
* The shape of the select. If "round" it will have an increased border radius.
|
||||||
*/
|
*/
|
||||||
"shape"?: 'round';
|
"shape"?: 'round';
|
||||||
|
/**
|
||||||
|
* The toggle icon to use. Defaults to `chevronExpand` for `ios` mode, or `caretDownSharp` for `md` mode.
|
||||||
|
*/
|
||||||
|
"toggleIcon"?: string;
|
||||||
/**
|
/**
|
||||||
* The value of the select.
|
* The value of the select.
|
||||||
*/
|
*/
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
* when the select is activated.
|
* when the select is activated.
|
||||||
* This should only happen on MD.
|
* This should only happen on MD.
|
||||||
*/
|
*/
|
||||||
:host(.select-expanded:not(.legacy-select)) .select-icon {
|
:host(.select-expanded:not(.legacy-select):not(.has-expanded-icon)) .select-icon {
|
||||||
@include transform(rotate(180deg));
|
@include transform(rotate(180deg));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +123,7 @@
|
|||||||
@include transform(translate3d(0, -9px, 0));
|
@include transform(translate3d(0, -9px, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.item-has-focus) .select-icon {
|
:host-context(.item-has-focus):host(:not(.has-expanded-icon)) .select-icon {
|
||||||
@include transform(rotate(180deg));
|
@include transform(rotate(180deg));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,8 +131,8 @@
|
|||||||
* Ensure that the translation we did
|
* Ensure that the translation we did
|
||||||
* above is preserved when we rotate the select icon.
|
* above is preserved when we rotate the select icon.
|
||||||
*/
|
*/
|
||||||
:host-context(.item-has-focus.item-label-stacked) .select-icon,
|
:host-context(.item-has-focus.item-label-stacked):host(:not(.has-expanded-icon)) .select-icon,
|
||||||
:host-context(.item-has-focus.item-label-floating:not(.item-fill-outline)) .select-icon {
|
:host-context(.item-has-focus.item-label-floating:not(.item-fill-outline)):host(:not(.has-expanded-icon)) .select-icon {
|
||||||
@include transform(rotate(180deg), translate3d(0, -9px, 0));
|
@include transform(rotate(180deg), translate3d(0, -9px, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +183,19 @@ export class Select implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Prop() selectedText?: string | null;
|
@Prop() selectedText?: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The toggle icon to use. Defaults to `chevronExpand` for `ios` mode,
|
||||||
|
* or `caretDownSharp` for `md` mode.
|
||||||
|
*/
|
||||||
|
@Prop() toggleIcon?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The toggle icon to show when the select is open. If defined, the icon
|
||||||
|
* rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon`
|
||||||
|
* will be used for when the select is both open and closed.
|
||||||
|
*/
|
||||||
|
@Prop() expandedIcon?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The shape of the select. If "round" it will have an increased border radius.
|
* The shape of the select. If "round" it will have an increased border radius.
|
||||||
*/
|
*/
|
||||||
@ -820,7 +833,8 @@ export class Select implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderSelect() {
|
private renderSelect() {
|
||||||
const { disabled, el, isExpanded, labelPlacement, justify, placeholder, fill, shape, name, value } = this;
|
const { disabled, el, isExpanded, expandedIcon, labelPlacement, justify, placeholder, fill, shape, name, value } =
|
||||||
|
this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const hasFloatingOrStackedLabel = labelPlacement === 'floating' || labelPlacement === 'stacked';
|
const hasFloatingOrStackedLabel = labelPlacement === 'floating' || labelPlacement === 'stacked';
|
||||||
const justifyEnabled = !hasFloatingOrStackedLabel;
|
const justifyEnabled = !hasFloatingOrStackedLabel;
|
||||||
@ -839,6 +853,7 @@ export class Select implements ComponentInterface {
|
|||||||
'in-item-color': hostContext('ion-item.ion-color', el),
|
'in-item-color': hostContext('ion-item.ion-color', el),
|
||||||
'select-disabled': disabled,
|
'select-disabled': disabled,
|
||||||
'select-expanded': isExpanded,
|
'select-expanded': isExpanded,
|
||||||
|
'has-expanded-icon': expandedIcon !== undefined,
|
||||||
'has-value': this.hasValue(),
|
'has-value': this.hasValue(),
|
||||||
'has-placeholder': placeholder !== undefined,
|
'has-placeholder': placeholder !== undefined,
|
||||||
'ion-focusable': true,
|
'ion-focusable': true,
|
||||||
@ -893,7 +908,7 @@ Developers can use the "legacy" property to continue using the legacy form marku
|
|||||||
this.hasLoggedDeprecationWarning = true;
|
this.hasLoggedDeprecationWarning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { disabled, el, inputId, isExpanded, name, placeholder, value } = this;
|
const { disabled, el, inputId, isExpanded, expandedIcon, name, placeholder, value } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const { labelText, labelId } = getAriaLabel(el, inputId);
|
const { labelText, labelId } = getAriaLabel(el, inputId);
|
||||||
|
|
||||||
@ -926,6 +941,7 @@ Developers can use the "legacy" property to continue using the legacy form marku
|
|||||||
'in-item-color': hostContext('ion-item.ion-color', el),
|
'in-item-color': hostContext('ion-item.ion-color', el),
|
||||||
'select-disabled': disabled,
|
'select-disabled': disabled,
|
||||||
'select-expanded': isExpanded,
|
'select-expanded': isExpanded,
|
||||||
|
'has-expanded-icon': expandedIcon !== undefined,
|
||||||
'legacy-select': true,
|
'legacy-select': true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -974,7 +990,16 @@ Developers can use the "legacy" property to continue using the legacy form marku
|
|||||||
*/
|
*/
|
||||||
private renderSelectIcon() {
|
private renderSelectIcon() {
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const icon = mode === 'ios' ? chevronExpand : caretDownSharp;
|
const { isExpanded, toggleIcon, expandedIcon } = this;
|
||||||
|
let icon: string;
|
||||||
|
|
||||||
|
if (isExpanded && expandedIcon !== undefined) {
|
||||||
|
icon = expandedIcon;
|
||||||
|
} else {
|
||||||
|
const defaultIcon = mode === 'ios' ? chevronExpand : caretDownSharp;
|
||||||
|
icon = toggleIcon ?? defaultIcon;
|
||||||
|
}
|
||||||
|
|
||||||
return <ion-icon class="select-icon" part="icon" aria-hidden="true" icon={icon}></ion-icon>;
|
return <ion-icon class="select-icon" part="icon" aria-hidden="true" icon={icon}></ion-icon>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
core/src/components/select/test/toggle-icon/index.html
Normal file
58
core/src/components/select/test/toggle-icon/index.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Select - toggleIcon</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Select - toggleIcon</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content class="test-content">
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-select label="toggleIcon" toggle-icon="arrow-down" placeholder="Select one" interface="popover">
|
||||||
|
<ion-select-option value="apples">Apples</ion-select-option>
|
||||||
|
<ion-select-option value="oranges">Oranges</ion-select-option>
|
||||||
|
<ion-select-option value="pears">Pears</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-select label="expandedIcon" expanded-icon="arrow-up" placeholder="Select one" interface="popover">
|
||||||
|
<ion-select-option value="apples">Apples</ion-select-option>
|
||||||
|
<ion-select-option value="oranges">Oranges</ion-select-option>
|
||||||
|
<ion-select-option value="pears">Pears</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-select
|
||||||
|
label="Both"
|
||||||
|
toggle-icon="arrow-down"
|
||||||
|
expanded-icon="pizza"
|
||||||
|
placeholder="Select one"
|
||||||
|
interface="popover"
|
||||||
|
>
|
||||||
|
<ion-select-option value="apples">Apples</ion-select-option>
|
||||||
|
<ion-select-option value="oranges">Oranges</ion-select-option>
|
||||||
|
<ion-select-option value="pears">Pears</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
core/src/components/select/test/toggle-icon/select.e2e.ts
Normal file
39
core/src/components/select/test/toggle-icon/select.e2e.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { configs, test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||||
|
test.describe(title('select: toggleIcon'), () => {
|
||||||
|
test('should render a custom toggleIcon', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-select toggle-icon="pizza" label="Select" value="a">
|
||||||
|
<ion-select-option value="a">Apple</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const select = page.locator('ion-select');
|
||||||
|
await expect(select).toHaveScreenshot(screenshot(`select-toggle-icon`));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render a custom expandedIcon', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-select expanded-icon="pizza" interface="popover" label="Select" value="a">
|
||||||
|
<ion-select-option value="a">Apple</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const select = page.locator('ion-select');
|
||||||
|
const popoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||||
|
|
||||||
|
await select.click();
|
||||||
|
await popoverDidPresent.next();
|
||||||
|
|
||||||
|
await expect(select).toHaveScreenshot(screenshot(`select-expanded-icon`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
@ -739,6 +739,8 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
|
|||||||
'okText',
|
'okText',
|
||||||
'placeholder',
|
'placeholder',
|
||||||
'selectedText',
|
'selectedText',
|
||||||
|
'toggleIcon',
|
||||||
|
'expandedIcon',
|
||||||
'shape',
|
'shape',
|
||||||
'value',
|
'value',
|
||||||
'ionChange',
|
'ionChange',
|
||||||
|
Reference in New Issue
Block a user