feat(accordion): add accordion and accordion-group components (#22865)

resolves #17094
This commit is contained in:
Liam DeBeasi
2021-03-24 09:17:54 -04:00
committed by GitHub
parent 2c53363901
commit 073883a098
40 changed files with 4977 additions and 49 deletions

View File

@ -12,6 +12,7 @@
- [Accessibility](#accessibility)
* [Checkbox](#checkbox)
* [Switch](#switch)
* [Accordion](#accordion)
- [Rendering Anchor or Button](#rendering-anchor-or-button)
* [Example Components](#example-components-1)
* [Component Structure](#component-structure-1)
@ -623,6 +624,19 @@ You are currently on a switch. To select or deselect this checkbox, press Contro
There is a WebKit bug open for this: https://bugs.webkit.org/show_bug.cgi?id=196354
### Accordion
#### Example Components
- [ion-accordion](https://github.com/ionic-team/ionic/tree/master/core/src/components/accordion)
- [ion-accordion-group](https://github.com/ionic-team/ionic/tree/master/core/src/components/accordion-group)
#### NVDA
In order to use the arrow keys to navigate the accordions, users must be in "Focus Mode". Typically, NVDA automatically switches between Browse and Focus modes when inside of a form, but not every accordion needs a form.
You can either wrap your `ion-accordion-group` in a form, or manually toggle Focus Mode using NVDA's keyboard shortcut.
## Rendering Anchor or Button

View File

@ -2,6 +2,8 @@
import type * as d from './proxies';
export const DIRECTIVES = [
d.IonAccordion,
d.IonAccordionGroup,
d.IonApp,
d.IonAvatar,
d.IonBackButton,

View File

@ -4,6 +4,30 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, NgZone } from "@angular/core";
import { ProxyCmp, proxyOutputs } from "./proxies-utils";
import { Components } from "@ionic/core";
export declare interface IonAccordion extends Components.IonAccordion {
}
@ProxyCmp({ inputs: ["disabled", "mode", "readonly", "toggleIcon", "toggleIconSlot", "value"] })
@Component({ selector: "ion-accordion", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["disabled", "mode", "readonly", "toggleIcon", "toggleIconSlot", "value"] })
export class IonAccordion {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
export declare interface IonAccordionGroup extends Components.IonAccordionGroup {
}
@ProxyCmp({ inputs: ["disabled", "expand", "mode", "multiple", "readonly", "value"] })
@Component({ selector: "ion-accordion-group", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["disabled", "expand", "mode", "multiple", "readonly", "value"] })
export class IonAccordionGroup {
ionChange!: EventEmitter<CustomEvent>;
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
proxyOutputs(this, this.el, ["ionChange"]);
}
}
export declare interface IonApp extends Components.IonApp {
}
@Component({ selector: "ion-app", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>" })

View File

@ -13,7 +13,7 @@ import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
import { IonTabs } from './directives/navigation/ion-tabs';
import { NavDelegate } from './directives/navigation/nav-delegate';
import { RouterLinkDelegate } from './directives/navigation/router-link-delegate';
import { IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies';
import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies';
import { VirtualFooter } from './directives/virtual-scroll/virtual-footer';
import { VirtualHeader } from './directives/virtual-scroll/virtual-header';
import { VirtualItem } from './directives/virtual-scroll/virtual-item';
@ -25,6 +25,8 @@ import { PopoverController } from './providers/popover-controller';
const DECLARATIONS = [
// proxies
IonAccordion,
IonAccordionGroup,
IonApp,
IonAvatar,
IonBackButton,

View File

@ -1,4 +1,24 @@
ion-accordion,shadow
ion-accordion,prop,disabled,boolean,false,false,false
ion-accordion,prop,mode,"ios" | "md",undefined,false,false
ion-accordion,prop,readonly,boolean,false,false,false
ion-accordion,prop,toggleIcon,string,'chevron-down',false,false
ion-accordion,prop,toggleIconSlot,"end" | "start",'end',false,false
ion-accordion,prop,value,string,`ion-accordion-${accordionIds++}`,false,false
ion-accordion,part,content
ion-accordion,part,expanded
ion-accordion,part,header
ion-accordion-group,shadow
ion-accordion-group,prop,disabled,boolean,false,false,false
ion-accordion-group,prop,expand,"compact" | "inset",'compact',false,false
ion-accordion-group,prop,mode,"ios" | "md",undefined,false,false
ion-accordion-group,prop,multiple,boolean | undefined,undefined,false,false
ion-accordion-group,prop,readonly,boolean,false,false,false
ion-accordion-group,prop,value,null | string | string[] | undefined,undefined,false,false
ion-accordion-group,event,ionChange,AccordionGroupChangeEventDetail<any>,true
ion-action-sheet,scoped
ion-action-sheet,prop,animated,boolean,true,false,false
ion-action-sheet,prop,backdropDismiss,boolean,true,false,false

40
core/package-lock.json generated
View File

@ -14,6 +14,7 @@
"tslib": "^1.10.0"
},
"devDependencies": {
"@axe-core/puppeteer": "^4.1.1",
"@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
@ -43,6 +44,21 @@
"typescript": "^4.0.5"
}
},
"node_modules/@axe-core/puppeteer": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@axe-core/puppeteer/-/puppeteer-4.1.1.tgz",
"integrity": "sha512-Ao9N7HL//s26hdasx3Ba18tlJgxpoO+1SmIN6eSx5vC50dqYhiRU0xp6wBKWqzo10u1jpzl/s4RFsOAuolFMBA==",
"dev": true,
"dependencies": {
"axe-core": "^4.1.1"
},
"engines": {
"node": ">=6.4.0"
},
"peerDependencies": {
"puppeteer": ">=1.10.0 < 6"
}
},
"node_modules/@babel/code-frame": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -2029,6 +2045,15 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"dev": true
},
"node_modules/axe-core": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz",
"integrity": "sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/babel-plugin-istanbul": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",
@ -13778,6 +13803,15 @@
}
},
"dependencies": {
"@axe-core/puppeteer": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@axe-core/puppeteer/-/puppeteer-4.1.1.tgz",
"integrity": "sha512-Ao9N7HL//s26hdasx3Ba18tlJgxpoO+1SmIN6eSx5vC50dqYhiRU0xp6wBKWqzo10u1jpzl/s4RFsOAuolFMBA==",
"dev": true,
"requires": {
"axe-core": "^4.1.1"
}
},
"@babel/code-frame": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -15457,6 +15491,12 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"dev": true
},
"axe-core": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz",
"integrity": "sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ==",
"dev": true
},
"babel-plugin-istanbul": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",

View File

@ -36,6 +36,7 @@
"tslib": "^1.10.0"
},
"devDependencies": {
"@axe-core/puppeteer": "^4.1.1",
"@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",

View File

@ -5,11 +5,65 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimeOptions, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, MenuChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, ViewController } from "./interface";
import { AccordionGroupChangeEventDetail, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimeOptions, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, MenuChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, ViewController } from "./interface";
import { IonicSafeString } from "./utils/sanitization";
import { NavigationHookCallback } from "./components/route/route-interface";
import { SelectCompareFn } from "./components/select/select-interface";
export namespace Components {
interface IonAccordion {
/**
* If `true`, the accordion cannot be interacted with.
*/
"disabled": boolean;
/**
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* If `true`, the accordion cannot be interacted with, but does not alter the opacity.
*/
"readonly": boolean;
/**
* The toggle icon to use. This icon will be rotated when the accordion is expanded or collapsed.
*/
"toggleIcon": string;
/**
* The slot inside of `ion-item` to place the toggle icon. Defaults to `'end'`.
*/
"toggleIconSlot": 'start' | 'end';
/**
* The value of the accordion. Defaults to an autogenerated value.
*/
"value": string;
}
interface IonAccordionGroup {
/**
* If `true`, the accordion group cannot be interacted with.
*/
"disabled": boolean;
/**
* Describes the expansion behavior for each accordion. Possible values are `"compact"` and `"inset"`. Defaults to `"compact"`.
*/
"expand": 'compact' | 'inset';
"getAccordions": () => Promise<HTMLIonAccordionElement[]>;
/**
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* If `true`, the accordion group can have multiple accordion components expanded at the same time.
*/
"multiple"?: boolean;
/**
* If `true`, the accordion group cannot be interacted with, but does not alter the opacity.
*/
"readonly": boolean;
"requestAccordionToggle": (accordionValue: string | undefined, accordionExpand: boolean) => Promise<void>;
/**
* The value of the accordion group.
*/
"value"?: string | string[] | null;
}
interface IonActionSheet {
/**
* If `true`, the action sheet will animate.
@ -2708,6 +2762,18 @@ export namespace Components {
}
}
declare global {
interface HTMLIonAccordionElement extends Components.IonAccordion, HTMLStencilElement {
}
var HTMLIonAccordionElement: {
prototype: HTMLIonAccordionElement;
new (): HTMLIonAccordionElement;
};
interface HTMLIonAccordionGroupElement extends Components.IonAccordionGroup, HTMLStencilElement {
}
var HTMLIonAccordionGroupElement: {
prototype: HTMLIonAccordionGroupElement;
new (): HTMLIonAccordionGroupElement;
};
interface HTMLIonActionSheetElement extends Components.IonActionSheet, HTMLStencilElement {
}
var HTMLIonActionSheetElement: {
@ -3231,6 +3297,8 @@ declare global {
new (): HTMLIonVirtualScrollElement;
};
interface HTMLElementTagNameMap {
"ion-accordion": HTMLIonAccordionElement;
"ion-accordion-group": HTMLIonAccordionGroupElement;
"ion-action-sheet": HTMLIonActionSheetElement;
"ion-alert": HTMLIonAlertElement;
"ion-app": HTMLIonAppElement;
@ -3321,6 +3389,62 @@ declare global {
}
}
declare namespace LocalJSX {
interface IonAccordion {
/**
* If `true`, the accordion cannot be interacted with.
*/
"disabled"?: boolean;
/**
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* If `true`, the accordion cannot be interacted with, but does not alter the opacity.
*/
"readonly"?: boolean;
/**
* The toggle icon to use. This icon will be rotated when the accordion is expanded or collapsed.
*/
"toggleIcon"?: string;
/**
* The slot inside of `ion-item` to place the toggle icon. Defaults to `'end'`.
*/
"toggleIconSlot"?: 'start' | 'end';
/**
* The value of the accordion. Defaults to an autogenerated value.
*/
"value"?: string;
}
interface IonAccordionGroup {
/**
* If `true`, the accordion group cannot be interacted with.
*/
"disabled"?: boolean;
/**
* Describes the expansion behavior for each accordion. Possible values are `"compact"` and `"inset"`. Defaults to `"compact"`.
*/
"expand"?: 'compact' | 'inset';
/**
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* If `true`, the accordion group can have multiple accordion components expanded at the same time.
*/
"multiple"?: boolean;
/**
* Emitted when the value property has changed.
*/
"onIonChange"?: (event: CustomEvent<AccordionGroupChangeEventDetail>) => void;
/**
* If `true`, the accordion group cannot be interacted with, but does not alter the opacity.
*/
"readonly"?: boolean;
/**
* The value of the accordion group.
*/
"value"?: string | string[] | null;
}
interface IonActionSheet {
/**
* If `true`, the action sheet will animate.
@ -6044,6 +6168,8 @@ declare namespace LocalJSX {
"renderItem"?: (item: any, index: number) => any;
}
interface IntrinsicElements {
"ion-accordion": IonAccordion;
"ion-accordion-group": IonAccordionGroup;
"ion-action-sheet": IonActionSheet;
"ion-alert": IonAlert;
"ion-app": IonApp;
@ -6137,6 +6263,8 @@ export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"ion-accordion": LocalJSX.IonAccordion & JSXBase.HTMLAttributes<HTMLIonAccordionElement>;
"ion-accordion-group": LocalJSX.IonAccordionGroup & JSXBase.HTMLAttributes<HTMLIonAccordionGroupElement>;
"ion-action-sheet": LocalJSX.IonActionSheet & JSXBase.HTMLAttributes<HTMLIonActionSheetElement>;
"ion-alert": LocalJSX.IonAlert & JSXBase.HTMLAttributes<HTMLIonAlertElement>;
"ion-app": LocalJSX.IonApp & JSXBase.HTMLAttributes<HTMLIonAppElement>;

View File

@ -0,0 +1,8 @@
export interface AccordionGroupChangeEventDetail<T = any> {
value: T;
}
export interface AccordionGroupChangeEvent extends CustomEvent {
detail: AccordionGroupChangeEventDetail;
target: HTMLIonAccordionGroupElement;
}

View File

@ -0,0 +1,9 @@
@import "./accordion-group";
// iOS Accordion Group
// --------------------------------------------------
:host(.accordion-group-expand-inset) ::slotted(ion-accordion.accordion-expanding),
:host(.accordion-group-expand-inset) ::slotted(ion-accordion.accordion-expanded) {
border-bottom: none;
}

View File

@ -0,0 +1,27 @@
@import "./accordion-group";
@import "../accordion/accordion.md.vars";
// Material Design Accordion Group
// --------------------------------------------------
:host(.accordion-group-expand-inset) ::slotted(ion-accordion) {
box-shadow: $accordion-md-box-shadow;
}
:host(.accordion-group-expand-inset) ::slotted(ion-accordion.accordion-expanding),
:host(.accordion-group-expand-inset) ::slotted(ion-accordion.accordion-expanded) {
@include margin($accordion-md-expanded-margin, 0, $accordion-md-expanded-margin, 0);
@include border-radius($accordion-md-border-radius, $accordion-md-border-radius, $accordion-md-border-radius, $accordion-md-border-radius);
}
:host(.accordion-group-expand-inset) ::slotted(ion-accordion.accordion-previous) {
@include border-radius(null, null, $accordion-md-border-radius, $accordion-md-border-radius);
}
:host(.accordion-group-expand-inset) ::slotted(ion-accordion.accordion-next) {
@include border-radius($accordion-md-border-radius, $accordion-md-border-radius, null, null);
}
:host(.accordion-group-expand-inset) ::slotted(ion-accordion):first-of-type,
:host(.accordion-group-expand-inset) ::slotted(ion-accordion):first-of-type {
@include margin(0, 0, 0, 0);
}

View File

@ -0,0 +1,13 @@
@import "../../themes/ionic.globals";
@import "../accordion/accordion.vars";
// Accordion Group
// --------------------------------------------------
:host {
display: block;
}
:host(.accordion-group-expand-inset) {
@include margin($accordion-inset-margin, $accordion-inset-margin, $accordion-inset-margin, $accordion-inset-margin);
}

View File

@ -0,0 +1,214 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Method, Prop, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { AccordionGroupChangeEventDetail } from '../../interface';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*/
@Component({
tag: 'ion-accordion-group',
styleUrls: {
ios: 'accordion-group.ios.scss',
md: 'accordion-group.md.scss'
},
shadow: true
})
export class AccordionGroup implements ComponentInterface {
@Element() el!: HTMLIonAccordionGroupElement;
/**
* If `true`, the accordion group can have multiple
* accordion components expanded at the same time.
*/
@Prop() multiple?: boolean;
/**
* The value of the accordion group.
*/
@Prop({ mutable: true }) value?: string | string[] | null;
/**
* If `true`, the accordion group cannot be interacted with.
*/
@Prop() disabled = false;
/**
* If `true`, the accordion group cannot be interacted with,
* but does not alter the opacity.
*/
@Prop() readonly = false;
/**
* Describes the expansion behavior for each accordion.
* Possible values are `"compact"` and `"inset"`.
* Defaults to `"compact"`.
*/
@Prop() expand: 'compact' | 'inset' = 'compact';
/**
* Emitted when the value property has changed.
*/
@Event() ionChange!: EventEmitter<AccordionGroupChangeEventDetail>;
@Watch('value')
valueChanged() {
const { value, multiple } = this;
/**
* If accordion group does not
* let multiple accordions be open
* at once, but user passes an array
* just grab the first value.
*/
if (!multiple && Array.isArray(value)) {
this.value = value[0];
} else {
this.ionChange.emit({ value: this.value });
}
}
@Watch('disabled')
async disabledChanged() {
const { disabled } = this;
const accordions = await this.getAccordions();
for (const accordion of accordions) {
accordion.disabled = disabled;
}
}
@Watch('readonly')
async readonlyChanged() {
const { readonly } = this;
const accordions = await this.getAccordions();
for (const accordion of accordions) {
accordion.readonly = readonly;
}
}
@Listen('keydown')
async onKeydown(ev: KeyboardEvent) {
const activeElement = document.activeElement;
if (!activeElement) { return; }
const accordionEl = (activeElement.tagName === 'ION-ACCORDION') ? activeElement : activeElement.closest('ion-accordion');
if (!accordionEl) { return; }
const closestGroup = accordionEl.closest('ion-accordion-group');
if (closestGroup !== this.el) { return; }
// If the active accordion is not in the current array of accordions, do not do anything
const accordions = await this.getAccordions();
const startingIndex = accordions.findIndex(a => a === accordionEl);
if (startingIndex === -1) { return; }
let accordion: HTMLIonAccordionElement | undefined;
if (ev.key === 'ArrowDown') {
accordion = this.findNextAccordion(accordions, startingIndex);
} else if (ev.key === 'ArrowUp') {
accordion = this.findPreviousAccordion(accordions, startingIndex);
} else if (ev.key === 'Home') {
accordion = accordions[0];
} else if (ev.key === 'End') {
accordion = accordions[accordions.length - 1];
}
if (accordion !== undefined && accordion !== activeElement) {
accordion.focus();
}
}
async componentDidLoad() {
if (this.disabled) {
this.disabledChanged();
}
if (this.readonly) {
this.readonlyChanged();
}
}
/**
* @internal
*/
@Method()
async requestAccordionToggle(accordionValue: string | undefined, accordionExpand: boolean) {
const { multiple, value, readonly, disabled } = this;
if (readonly || disabled) { return; }
if (accordionExpand) {
/**
* If group accepts multiple values
* check to see if value is already in
* in values array. If not, add it
* to the array.
*/
if (multiple) {
const groupValue = (value || []) as string[];
const valueExists = groupValue.find(v => v === accordionValue);
if (valueExists === undefined && accordionValue !== undefined) {
this.value = [...groupValue, accordionValue];
}
} else {
this.value = accordionValue;
}
} else {
/**
* If collapsing accordion, either filter the value
* out of the values array or unset the value.
*/
if (multiple) {
const groupValue = (value || []) as string[];
this.value = groupValue.filter(v => v !== accordionValue);
} else {
this.value = undefined;
}
}
}
private findNextAccordion(accordions: HTMLIonAccordionElement[], startingIndex: number) {
const nextAccordion = accordions[startingIndex + 1];
// tslint:disable-next-line:strict-type-predicates
if (nextAccordion === undefined) {
return accordions[0];
}
return nextAccordion;
}
private findPreviousAccordion(accordions: HTMLIonAccordionElement[], startingIndex: number) {
const prevAccordion = accordions[startingIndex - 1];
// tslint:disable-next-line:strict-type-predicates
if (prevAccordion === undefined) {
return accordions[accordions.length - 1];
}
return prevAccordion;
}
/**
* @internal
*/
@Method()
async getAccordions() {
return Array.from(this.el.querySelectorAll('ion-accordion'));
}
render() {
const { disabled, readonly, expand } = this;
const mode = getIonMode(this);
return (
<Host
class={{
[mode]: true,
'accordion-group-disabled': disabled,
'accordion-group-readonly': readonly,
[`accordion-group-expand-${expand}`]: true
}}
role="presentation"
>
<slot></slot>
</Host>
);
}
}

View File

@ -0,0 +1,31 @@
# ion-accordion-group
Accordion group is a container for accordion instances. It manages the state of the accordions and provides keyboard navigation.
For more information as well as usage, see the [Accordion Documentation](../accordion)
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ----------- |
| `disabled` | `disabled` | If `true`, the accordion group cannot be interacted with. | `boolean` | `false` |
| `expand` | `expand` | Describes the expansion behavior for each accordion. Possible values are `"compact"` and `"inset"`. Defaults to `"compact"`. | `"compact" \| "inset"` | `'compact'` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `multiple` | `multiple` | If `true`, the accordion group can have multiple accordion components expanded at the same time. | `boolean \| undefined` | `undefined` |
| `readonly` | `readonly` | If `true`, the accordion group cannot be interacted with, but does not alter the opacity. | `boolean` | `false` |
| `value` | `value` | The value of the accordion group. | `null \| string \| string[] \| undefined` | `undefined` |
## Events
| Event | Description | Type |
| ----------- | -------------------------------------------- | --------------------------------------------------- |
| `ionChange` | Emitted when the value property has changed. | `CustomEvent<AccordionGroupChangeEventDetail<any>>` |
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@ -0,0 +1,9 @@
@import "./accordion.scss";
@import "../item/item.ios.vars";
// iOS Accordion
// --------------------------------------------------
:host(.accordion-next) ::slotted(ion-item[slot="header"]) {
--border-width: #{$item-ios-border-bottom-width 0px $item-ios-border-bottom-width 0px};
}

View File

@ -0,0 +1,4 @@
@import "./accordion.scss";
// Material Design Accordion
// --------------------------------------------------

View File

@ -0,0 +1,13 @@
@import "../../themes/ionic.globals.md";
// Accordion
// --------------------------------------------------
/// @prop - Border radius applied to the accordion
$accordion-md-border-radius: 6px !default;
/// @prop - Box shadow of the accordion
$accordion-md-box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12) !default;
/// @prop - Margin of the expanded accordion
$accordion-md-expanded-margin: 16px !default;

View File

@ -0,0 +1,81 @@
@import "./accordion.vars.scss";
// Accordion
// --------------------------------------------------
:host {
display: block;
position: relative;
width: 100%;
background-color: $accordion-background-color;
overflow: hidden;
/**
* This is required to force WebKit
* to create a new stacking context
* otherwise the border radius is
* temporarily lost when hovering over
* the ion-item or expanding/collapsing
* the accordion.
*/
z-index: 0;
}
:host(.accordion-expanding) ::slotted(ion-item[slot="header"]),
:host(.accordion-expanded) ::slotted(ion-item[slot="header"]) {
--border-width: 0px;
}
:host(.accordion-animated) {
transition: all $accordion-transition-duration $accordion-transition-easing;
}
:host(.accordion-animated) #content {
transition: max-height $accordion-transition-duration $accordion-transition-easing;
}
#content {
overflow: hidden;
will-change: max-height;
}
:host(.accordion-collapsing) #content {
/* stylelint-disable-next-line declaration-no-important */
max-height: 0 !important;
}
:host(.accordion-collapsed) #content {
display: none;
}
:host(.accordion-expanding) #content {
max-height: 0;
}
:host(.accordion-disabled) #header,
:host(.accordion-readonly) #header {
pointer-events: none;
}
/**
* We do not set the opacity on the
* host otherwise you would see the
* box-shadow behind it.
*/
:host(.accordion-disabled) #header,
:host(.accordion-disabled) #content {
opacity: $accordion-disabled-opacity;
}
@media (prefers-reduced-motion: reduce) {
:host,
#content {
/* stylelint-disable declaration-no-important */
transition: none !important;
}
}

View File

@ -0,0 +1,427 @@
import { Component, ComponentInterface, Element, Host, Prop, State, h } from '@stencil/core';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { addEventListener, getElementRoot, raf, removeEventListener, transitionEndAsync } from '../../utils/helpers';
const enum AccordionState {
Collapsed = 1 << 0,
Collapsing = 1 << 1,
Expanded = 1 << 2,
Expanding = 1 << 3
}
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot header - Content is placed at the top and is used to
* expand or collapse the accordion item.
* @slot content - Content is placed below the header and is
* shown or hidden based on expanded state.
*
* @part header - The wrapper element for the header slot.
* @part content - The wrapper element for the content slot.
* @part expanded - The expanded element. Can be used in combination
* with the `header` and `content` parts (i.e. `::part(header expanded)`).
*/
@Component({
tag: 'ion-accordion',
styleUrls: {
ios: 'accordion.ios.scss',
md: 'accordion.md.scss'
},
shadow: {
delegatesFocus: true
}
})
export class Accordion implements ComponentInterface {
private accordionGroupEl?: HTMLIonAccordionGroupElement | null;
private updateListener = () => this.updateState(false);
private contentEl: HTMLDivElement | undefined;
private contentElWrapper: HTMLDivElement | undefined;
private headerEl: HTMLDivElement | undefined;
private currentRaf: number | undefined;
@Element() el?: HTMLElement;
@State() state: AccordionState = AccordionState.Collapsed;
@State() isNext = false;
@State() isPrevious = false;
/**
* The value of the accordion. Defaults to an autogenerated
* value.
*/
@Prop() value = `ion-accordion-${accordionIds++}`;
/**
* If `true`, the accordion cannot be interacted with.
*/
@Prop() disabled = false;
/**
* If `true`, the accordion cannot be interacted with,
* but does not alter the opacity.
*/
@Prop() readonly = false;
/**
* The toggle icon to use. This icon will be
* rotated when the accordion is expanded
* or collapsed.
*/
@Prop() toggleIcon = 'chevron-down';
/**
* The slot inside of `ion-item` to
* place the toggle icon. Defaults to `'end'`.
*/
@Prop() toggleIconSlot: 'start' | 'end' = 'end';
connectedCallback() {
const accordionGroupEl = this.accordionGroupEl = this.el && this.el.closest('ion-accordion-group');
if (accordionGroupEl) {
this.updateState(true);
addEventListener(accordionGroupEl, 'ionChange', this.updateListener);
}
}
disconnectedCallback() {
const accordionGroupEl = this.accordionGroupEl;
if (accordionGroupEl) {
removeEventListener(accordionGroupEl, 'ionChange', this.updateListener);
}
}
componentDidLoad() {
this.setItemDefaults();
this.slotToggleIcon();
/**
* We need to wait a tick because we
* just set ionItem.button = true and
* the button has not have been rendered yet.
*/
raf(() => {
/**
* Set aria label on button inside of ion-item
* once the inner content has been rendered.
*/
const expanded = this.state === AccordionState.Expanded || this.state === AccordionState.Expanding;
this.setAria(expanded);
});
}
private setItemDefaults = () => {
const ionItem = this.getSlottedHeaderIonItem();
if (!ionItem) { return; }
/**
* For a11y purposes, we make
* the ion-item a button so users
* can tab to it and use keyboard
* navigation to get around.
*/
ionItem.button = true;
ionItem.detail = false;
/**
* By default, the lines in an
* item should be full here, but
* only do that if a user has
* not explicitly overridden them
*/
if (ionItem.lines === undefined) {
ionItem.lines = 'full';
}
}
private getSlottedHeaderIonItem = () => {
const { headerEl } = this;
if (!headerEl) { return; }
/**
* Get the first ion-item
* slotted in the header slot
*/
const slot = headerEl.querySelector('slot');
if (!slot) { return; }
// This is not defined in unit tests
const ionItem = slot.assignedElements && (slot.assignedElements().find(el => el.tagName === 'ION-ITEM') as HTMLIonItemElement | undefined);
return ionItem;
}
private setAria = (expanded = false) => {
const ionItem = this.getSlottedHeaderIonItem();
if (!ionItem) { return; }
/**
* Get the native <button> element inside of
* ion-item because that is what will be focused
*/
const root = getElementRoot(ionItem);
const button = root.querySelector('button');
if (!button) { return; }
button.setAttribute('aria-expanded', `${expanded}`);
}
private slotToggleIcon = () => {
const ionItem = this.getSlottedHeaderIonItem();
if (!ionItem) { return; }
const { toggleIconSlot, toggleIcon } = this;
/**
* Check if there already is a toggle icon.
* If so, do not add another one.
*/
const existingToggleIcon = ionItem.querySelector('.ion-accordion-toggle-icon');
if (existingToggleIcon) { return; }
const iconEl = document.createElement('ion-icon');
iconEl.slot = toggleIconSlot;
iconEl.lazy = false;
iconEl.classList.add('ion-accordion-toggle-icon');
iconEl.icon = toggleIcon;
iconEl.ariaHidden = 'true';
ionItem.appendChild(iconEl);
}
private expandAccordion = (initialUpdate = false) => {
if (initialUpdate) {
this.state = AccordionState.Expanded;
return;
}
if (this.state === AccordionState.Expanded) { return; }
const { contentEl, contentElWrapper } = this;
if (contentEl === undefined || contentElWrapper === undefined) { return; }
if (this.currentRaf !== undefined) {
cancelAnimationFrame(this.currentRaf);
}
if (this.shouldAnimate()) {
this.state = AccordionState.Expanding;
this.currentRaf = raf(async () => {
const contentHeight = contentElWrapper.offsetHeight;
const waitForTransition = transitionEndAsync(contentEl, 2000);
contentEl.style.setProperty('max-height', `${contentHeight}px`);
/**
* Force a repaint. We can't use an raf
* here as it could cause the collapse animation
* to get out of sync with the other
* accordion's expand animation.
*/
// tslint:disable-next-line
void contentEl.offsetHeight;
await waitForTransition;
this.state = AccordionState.Expanded;
contentEl.style.removeProperty('max-height');
});
} else {
this.state = AccordionState.Expanded;
}
}
private collapseAccordion = (initialUpdate = false) => {
if (initialUpdate) {
this.state = AccordionState.Collapsed;
return;
}
if (this.state === AccordionState.Collapsed) { return; }
const { contentEl } = this;
if (contentEl === undefined) { return; }
if (this.currentRaf !== undefined) {
cancelAnimationFrame(this.currentRaf);
}
if (this.shouldAnimate()) {
this.currentRaf = raf(async () => {
const contentHeight = contentEl.offsetHeight;
contentEl.style.setProperty('max-height', `${contentHeight}px`);
/**
* Force a repaint. We can't use an raf
* here as it could cause the collapse animation
* to get out of sync with the other
* accordion's expand animation.
*/
// tslint:disable-next-line
void contentEl.offsetHeight;
const waitForTransition = transitionEndAsync(contentEl, 2000);
this.state = AccordionState.Collapsing;
await waitForTransition;
this.state = AccordionState.Collapsed;
contentEl.style.removeProperty('max-height');
});
} else {
this.state = AccordionState.Collapsed;
}
}
/**
* Helper function to determine if
* something should animate.
* If prefers-reduced-motion is set
* then we should not animate, regardless
* of what is set in the config.
*/
private shouldAnimate = () => {
if (typeof (window as any) === 'undefined') { return false; }
const prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) { return false; }
const animated = config.get('animated', true);
if (!animated) { return false; }
return true;
}
private updateState = async (initialUpdate = false) => {
const accordionGroup = this.accordionGroupEl;
const accordionValue = this.value;
if (!accordionGroup) { return; }
const value = accordionGroup.value;
const shouldExpand = (Array.isArray(value)) ? value.includes(accordionValue) : value === accordionValue;
if (shouldExpand) {
this.expandAccordion(initialUpdate);
this.isNext = this.isPrevious = false;
} else {
this.collapseAccordion(initialUpdate);
/**
* When using popout or inset,
* the collapsed accordion items
* may need additional border radius
* applied. Check to see if the
* next or previous accordion is selected.
*/
const nextAccordion = this.getNextSibling();
const nextAccordionValue = nextAccordion && nextAccordion.value;
if (nextAccordionValue !== undefined) {
this.isPrevious = (Array.isArray(value)) ? value.includes(nextAccordionValue) : value === nextAccordionValue;
}
const previousAccordion = this.getPreviousSibling();
const previousAccordionValue = previousAccordion && previousAccordion.value;
if (previousAccordionValue !== undefined) {
this.isNext = (Array.isArray(value)) ? value.includes(previousAccordionValue) : value === previousAccordionValue;
}
}
}
private getNextSibling = () => {
if (!this.el) { return; }
const nextSibling = this.el.nextElementSibling;
if (nextSibling?.tagName !== 'ION-ACCORDION') { return; }
return nextSibling as HTMLIonAccordionElement;
}
private getPreviousSibling = () => {
if (!this.el) { return; }
const previousSibling = this.el.previousElementSibling;
if (previousSibling?.tagName !== 'ION-ACCORDION') { return; }
return previousSibling as HTMLIonAccordionElement;
}
private toggleExpanded() {
const { accordionGroupEl, value, state } = this;
if (accordionGroupEl) {
/**
* Because the accordion group may or may
* not allow multiple accordions open, we
* need to request the toggling of this
* accordion and the accordion group will
* make the decision on whether or not
* to allow it.
*/
const expand = state === AccordionState.Collapsed || state === AccordionState.Collapsing;
accordionGroupEl.requestAccordionToggle(value, expand);
}
}
render() {
const { disabled, readonly } = this;
const mode = getIonMode(this);
const expanded = this.state === AccordionState.Expanded || this.state === AccordionState.Expanding;
const headerPart = expanded ? 'header expanded' : 'header';
const contentPart = expanded ? 'content expanded' : 'content';
this.setAria(expanded);
return (
<Host
class={{
[mode]: true,
'accordion-expanding': this.state === AccordionState.Expanding,
'accordion-expanded': this.state === AccordionState.Expanded,
'accordion-collapsing': this.state === AccordionState.Collapsing,
'accordion-collapsed': this.state === AccordionState.Collapsed,
'accordion-next': this.isNext,
'accordion-previous': this.isPrevious,
'accordion-disabled': disabled,
'accordion-readonly': readonly,
'accordion-animated': config.getBoolean('animated', true)
}}
>
<div
onClick={() => this.toggleExpanded()}
id="header"
part={headerPart}
aria-controls="content"
ref={headerEl => this.headerEl = headerEl}
>
<slot name="header"></slot>
</div>
<div
id="content"
part={contentPart}
role="region"
aria-labelledby="header"
ref={contentEl => this.contentEl = contentEl}
>
<div id="content-wrapper" ref={contentElWrapper => this.contentElWrapper = contentElWrapper}>
<slot name="content"></slot>
</div>
</div>
</Host>
);
}
}
let accordionIds = 0;

View File

@ -0,0 +1,19 @@
@import "../../themes/ionic.globals";
// Accordion
// --------------------------------------------------
/// @prop - Background color of the accordion
$accordion-background-color: var(--ion-background-color, #ffffff) !default;
/// @prop - Duration of the accordion transition
$accordion-transition-duration: 300ms !default;
/// @prop - Timing function of the accordion transition
$accordion-transition-easing: cubic-bezier(0.25, 0.8, 0.5, 1) !default;
/// @prop - Opacity of the disabled accordion
$accordion-disabled-opacity: 0.4 !default;
/// @prop - Margin of the inset accordion
$accordion-inset-margin: 16px !default;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Accordion - a11y</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-content>
<h1>Accordion Group - a11y</h1>
<ion-accordion-group expand="inset">
<ion-accordion value="personal-information">
<ion-item slot="header">
<ion-label>Personal Information</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Name</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Email</ion-label>
<ion-input type="email"></ion-input>
</ion-item>
<ion-item>
<ion-label>Phone</ion-label>
<ion-input type="tel"></ion-input>
</ion-item>
<ion-item>
<ion-label>Extension</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Country</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>City/Province</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="billing-address">
<ion-item slot="header">
<ion-label>Billing Address</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Address 1</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Address 2</ion-label>
<ion-input type="email"></ion-input>
</ion-item>
<ion-item>
<ion-label>City</ion-label>
<ion-input type="tel"></ion-input>
</ion-item>
<ion-item>
<ion-label>State</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Zip Code</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shipping-address">
<ion-item slot="header">
<ion-label>Shipping Address</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Address 1</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Address 2</ion-label>
<ion-input type="email"></ion-input>
</ion-item>
<ion-item>
<ion-label>City</ion-label>
<ion-input type="tel"></ion-input>
</ion-item>
<ion-item>
<ion-label>State</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Zip Code</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,21 @@
"native" refers to this sample: https://www.w3.org/TR/wai-aria-practices/examples/accordion/accordion.html
### Selecting Accordion
  | native | Ionic
-- | -- | --
VoiceOver macOS - Chrome | Personal information, collapsed, button | Personal information, collapsed, button
VoiceOver macOS - Safari | Personal information, collapsed, button | Personal information, collapsed, button
VoiceOver iOS | Personal information, collapsed | Personal information, button, main, landmark, collapsed
Android TalkBack | Collapsed, personal information, button | Collapsed, personal information, button
Windows NVDA | Personal information, button, unavailable, collapsed | Clickable Personal Information button collapsed
### Toggling Accordion
  | native | Ionic
-- | -- | --
VoiceOver macOS - Chrome | Personal information, dimmed expanded, button | Personal information, expanded, button
VoiceOver macOS - Safari | Personal information, dimmed expanded, button | Personal information, expanded, button
VoiceOver iOS | Personal information, dimmed, expanded | Personal information, main, landmark, expanded
Android TalkBack | Expanded | Expanded
Windows NVDA | Unavailable, expanded | Expanded

View File

@ -0,0 +1,259 @@
import { newSpecPage } from '@stencil/core/testing';
import { AccordionGroup } from '../../accordion-group/accordion-group.tsx';
import { Accordion } from '../accordion.tsx';
import { Item } from '../../item/item.tsx';
it('should properly set readonly on child accordions', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group>
<ion-accordion>
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions.length).toEqual(1);
accordions.forEach(accordion => {
expect(accordion.readonly).toEqual(false);
});
accordionGroup.readonly = true;
await page.waitForChanges();
accordions.forEach(accordion => {
expect(accordion.readonly).toEqual(true);
});
});
it('should properly set disabled on child accordions', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group>
<ion-accordion>
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions.length).toEqual(1);
accordions.forEach(accordion => {
expect(accordion.disabled).toEqual(false);
});
accordionGroup.disabled = true;
await page.waitForChanges();
accordions.forEach(accordion => {
expect(accordion.disabled).toEqual(true);
});
});
it('should open correct accordions', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group>
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="second">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="third">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach(accordion => {
expect(accordion.classList.contains('accordion-collapsed')).toEqual(true);
});
accordionGroup.value = 'second';
await page.waitForChanges();
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(true);
expect(accordions[1].classList.contains('accordion-collapsed')).toEqual(false);
expect(accordions[2].classList.contains('accordion-collapsed')).toEqual(true);
});
it('should not open more than one accordion when multiple="false"', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group>
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="second">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="third">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach(accordion => {
expect(accordion.classList.contains('accordion-collapsed')).toEqual(true);
});
accordionGroup.value = ['first', 'second'];
await page.waitForChanges();
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
expect(accordions[1].classList.contains('accordion-collapsed')).toEqual(true);
expect(accordions[2].classList.contains('accordion-collapsed')).toEqual(true);
});
it('should open more than one accordion when multiple="true"', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group multiple="true">
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="second">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="third">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach(accordion => {
expect(accordion.classList.contains('accordion-collapsed')).toEqual(true);
});
accordionGroup.value = ['first', 'second'];
await page.waitForChanges();
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
expect(accordions[1].classList.contains('accordion-collapsed')).toEqual(false);
expect(accordions[2].classList.contains('accordion-collapsed')).toEqual(true);
});
it('should render with accordion open', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group value="first">
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="second">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="third">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
expect(accordions[1].classList.contains('accordion-collapsed')).toEqual(true);
expect(accordions[2].classList.contains('accordion-collapsed')).toEqual(true);
});
it('should accept a string when multiple="true"', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group multiple="true" value="first">
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="second">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="third">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
expect(accordions[1].classList.contains('accordion-collapsed')).toEqual(true);
expect(accordions[2].classList.contains('accordion-collapsed')).toEqual(true);
});
it('should set default values if not provided', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group>
<ion-accordion>
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordion = accordionGroup.querySelector('ion-accordion');
/**
* ID is determined via an auto incrementing counter
* so do not hard code ion-accordion-0 as it might
* change depending on how many accordions
* are used in these tests.
*/
expect(accordion.value).toContain('ion-accordion-');
accordionGroup.value = accordion.value;
await page.waitForChanges();
expect(accordion.classList.contains('accordion-collapsed')).toEqual(false);
});

View File

@ -0,0 +1,739 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Accordion - Basic</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Accordion - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true" color="light">
<ion-header collapse="condense">
<ion-toolbar color="light">
<ion-title size="large">Accordion - Basic</ion-title>
</ion-toolbar>
</ion-header>
<div class="grid ion-padding">
<div class="grid-item">
<h2>Inset, Color - iOS</h2>
<ion-accordion-group mode="ios" expand="inset">
<ion-accordion value="attractions">
<ion-item color="primary" slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item color="warning" slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item color="danger" slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item color="success" slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Inset - iOS</h2>
<ion-accordion-group mode="ios" expand="inset">
<ion-accordion value="attractions">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Inset, Color - MD</h2>
<ion-accordion-group mode="md" expand="inset">
<ion-accordion value="attractions">
<ion-item color="primary" slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item color="warning" slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item color="danger" slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item color="success" slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Inset - MD</h2>
<ion-accordion-group mode="md" expand="inset">
<ion-accordion value="attractions">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
</div>
<div class="grid ion-padding">
<div class="grid-item">
<h2>Compact, Color - iOS</h2>
<ion-accordion-group mode="ios">
<ion-accordion value="attractions">
<ion-item color="primary" slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item color="warning" slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item color="danger" slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item color="success" slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Compact - iOS</h2>
<ion-accordion-group mode="ios">
<ion-accordion value="attractions">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Compact, Color - MD</h2>
<ion-accordion-group mode="md">
<ion-accordion value="attractions">
<ion-item color="primary" slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item color="warning" slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item color="danger" slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item color="success" slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Compact - MD</h2>
<ion-accordion-group mode="md">
<ion-accordion value="attractions">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
</div>
<div class="grid ion-padding">
<div class="grid-item">
<h2>Multiple</h2>
<ion-accordion-group expand="inset" mode="md" multiple="true">
<ion-accordion value="attractions">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="film-outline" md="film"></ion-icon>
<ion-label> Attractions</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Movie Theaters</ion-label>
</ion-item>
<ion-item>
<ion-label>Amusement Parks</ion-label>
</ion-item>
<ion-item>
<ion-label>Mini Golf</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="dining">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" name="pizza"></ion-icon>
<ion-label>Dining</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Breakfast & Brunch</ion-label>
</ion-item>
<ion-item>
<ion-label>New American</ion-label>
</ion-item>
<ion-item>
<ion-label>Sushi Bars</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="games">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="game-controller-outline" md="game-controller"></ion-icon>
<ion-label>Games</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Xbox</ion-label>
</ion-item>
<ion-item>
<ion-label>Playstation</ion-label>
</ion-item>
<ion-item>
<ion-label>Switch</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="exercise">
<ion-item slot="header" button detail="false">
<ion-icon slot="start" ios="bicycle-outline" md="bicycle"></ion-icon>
<ion-label>Exercise</ion-label>
</ion-item>
<ion-list lines="none" slot="content">
<ion-item>
<ion-label>Jog</ion-label>
</ion-item>
<ion-item>
<ion-label>Swim</ion-label>
</ion-item>
<ion-item>
<ion-label>Nap</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</div>
</div>
</ion-content>
</ion-app>
<script>
const multipleAccordion = document.querySelector('ion-accordion-group[multiple="true"]');
multipleAccordion.value = ['dining', 'exercise'];
</script>
</body>
</html>

View File

@ -0,0 +1,64 @@
import { newE2EPage } from '@stencil/core/testing';
import { AxePuppeteer } from '@axe-core/puppeteer';
const getActiveElementText = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.innerText, activeElement);
}
test('accordion: a11y', async () => {
const page = await newE2EPage({
url: '/src/components/accordion/test/a11y?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});
test('accordion: basic', async () => {
const page = await newE2EPage({
url: '/src/components/accordion/test/basic?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});
test('accordion:rtl: a11y', async () => {
const page = await newE2EPage({
url: '/src/components/accordion/test/a11y?ionic:_testing=true&rtl=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});
test('accordion: keyboard navigation', async () => {
const page = await newE2EPage({
url: '/src/components/accordion/test/a11y?ionic:_testing=true'
});
await page.keyboard.press('Tab');
expect(await getActiveElementText(page)).toEqual('Personal Information');
await page.keyboard.press('ArrowDown');
expect(await getActiveElementText(page)).toEqual('Billing Address');
await page.keyboard.press('ArrowDown');
expect(await getActiveElementText(page)).toEqual('Shipping Address');
await page.keyboard.press('ArrowDown');
expect(await getActiveElementText(page)).toEqual('Personal Information');
await page.keyboard.press('ArrowUp');
expect(await getActiveElementText(page)).toEqual('Shipping Address');
});
test('accordion: axe', async () => {
const page = await newE2EPage({
url: '/src/components/accordion/test/standalone?ionic:_testing=true'
});
const results = await new AxePuppeteer(page).analyze();
expect(results.violations.length).toEqual(0);
});

View File

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Accordion - Basic</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<main>
<h1 class="ion-padding">Accordion</h1>
<div class="grid ion-padding">
<div class="grid-item">
<h2>Default Accordion</h2>
<ion-accordion-group>
<ion-accordion>
<button class="custom-accordion-button" slot="header">
Attractions
</button>
<div class="custom-accordion-content" slot="content">
Some content
</div>
</ion-accordion>
<ion-accordion>
<button class="custom-accordion-button" slot="header">
Second one
</button>
<div class="custom-accordion-content" slot="content">
Some content
</div>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Readonly Accordion (1st)</h2>
<ion-accordion-group value="expanded">
<ion-accordion readonly value="expanded">
<button class="custom-accordion-button" slot="header">
Readonly - expanded
</button>
<div class="custom-accordion-content" slot="content">
<input id="checkme-1" type="checkbox"></input>
<label for="checkme-1">Check me!</label>
</div>
</ion-accordion>
<ion-accordion value="collapsed">
<button class="custom-accordion-button" slot="header">
Collapsed
</button>
<div class="custom-accordion-content" slot="content">
<input id="checkme-2" type="checkbox"></input>
<label for="checkme-2">Check me!</label>
</div>
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Custom Accordion Colors</h2>
<ion-accordion-group expand="inset" class="custom-colors">
<ion-accordion>
<button class="custom-accordion-button" slot="header">
Danger
</button>
<div class="custom-accordion-content" slot="content">
Some content
</div>
</ion-accordion>
<ion-accordion>
<button class="custom-accordion-button" slot="header">
Primary
</button>
<div class="custom-accordion-content" slot="content">
Some content
</div>
</ion-accordion>
</ion-accordion-group>
</div>
</div>
</main>
</body>
<style>
body {
background: #f6f6f6;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
.custom-accordion-button {
width: 100%;
background: transparent;
outline: none;
text-align: left;
padding: 20px 16px;
}
.custom-accordion-content {
padding: 10px 16px;
}
ion-accordion {
border-bottom: 1px solid #ddd;
}
ion-accordion:last-child {
border-bottom: none;
}
.custom-colors ion-accordion:first-child {
background: rgb(245, 116, 116);
border-bottom-color: red;
}
.custom-colors ion-accordion:last-child {
background: rgb(160, 160, 250);
}
</style>
</html>

View File

@ -0,0 +1,221 @@
```html
<!-- Basic -->
<ion-accordion-group>
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Custom Icon -->
<ion-accordion-group>
<ion-accordion value="colors" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Open Accordion -->
<ion-accordion-group value="colors">
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Multiple Accordions -->
<ion-accordion-group [multiple]="true" [value]="['colors', 'numbers']">
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
```

View File

@ -0,0 +1,226 @@
```html
<!-- Basic -->
<ion-accordion-group>
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Custom Icon -->
<ion-accordion-group>
<ion-accordion value="colors" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Open Accordion -->
<ion-accordion-group value="colors">
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Multiple Accordions -->
<ion-accordion-group multiple="true">
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<script>
let accordionGroup = document.querySelector('ion-accordion-group');
accordionGroup.value = ['colors', 'numbers'];
</script>
```

View File

@ -0,0 +1,228 @@
```tsx
import React from 'react';
import { IonContent, IonAccordionGroup, IonAccordion, IonItem, IonLabel } from '@ionic/react';
import { arrowDownCircle } from 'ionicons/icons';
export const AccordionExample: React.FC = () => (
{/*-- Basic --*/}
<IonAccordionGroup>
<IonAccordion value="colors">
<IonItem slot="header">
<IonLabel>Colors</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Red</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Green</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Blue</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="shapes">
<IonItem slot="header">
<IonLabel>Shapes</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Circle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Triangle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Square</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="numbers">
<IonItem slot="header">
<IonLabel>Numbers</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>1</IonLabel>
</IonItem>
<IonItem>
<IonLabel>2</IonLabel>
</IonItem>
<IonItem>
<IonLabel>3</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
</IonAccordionGroup>
{/*-- Custom Icon --*/}
<IonAccordionGroup>
<IonAccordion value="colors" toggleIcon={arrowDownCircle}>
<IonItem slot="header">
<IonLabel>Colors</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Red</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Green</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Blue</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="shapes" toggleIcon={arrowDownCircle}>
<IonItem slot="header">
<IonLabel>Shapes</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Circle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Triangle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Square</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="numbers" toggleIcon={arrowDownCircle}>
<IonItem slot="header">
<IonLabel>Numbers</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>1</IonLabel>
</IonItem>
<IonItem>
<IonLabel>2</IonLabel>
</IonItem>
<IonItem>
<IonLabel>3</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
</IonAccordionGroup>
{/*-- Open Accordion --*/}
<IonAccordionGroup value="colors">
<IonAccordion value="colors">
<IonItem slot="header">
<IonLabel>Colors</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Red</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Green</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Blue</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="shapes">
<IonItem slot="header">
<IonLabel>Shapes</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Circle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Triangle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Square</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="numbers">
<IonItem slot="header">
<IonLabel>Numbers</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>1</IonLabel>
</IonItem>
<IonItem>
<IonLabel>2</IonLabel>
</IonItem>
<IonItem>
<IonLabel>3</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
</IonAccordionGroup>
{/*-- Multiple Accordions --*/}
<IonAccordionGroup multiple={true} value={['colors', 'numbers']}>
<IonAccordion value="colors">
<IonItem slot="header">
<IonLabel>Colors</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Red</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Green</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Blue</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="shapes">
<IonItem slot="header">
<IonLabel>Shapes</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>Circle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Triangle</IonLabel>
</IonItem>
<IonItem>
<IonLabel>Square</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
<IonAccordion value="numbers">
<IonItem slot="header">
<IonLabel>Numbers</IonLabel>
</IonItem>
<ion-list slot="content">
<IonItem>
<IonLabel>1</IonLabel>
</IonItem>
<IonItem>
<IonLabel>2</IonLabel>
</IonItem>
<IonItem>
<IonLabel>3</IonLabel>
</IonItem>
</ion-list>
</IonAccordion>
</IonAccordionGroup>
);
```

View File

@ -0,0 +1,233 @@
```tsx
import { Component, h } from '@stencil/core';
@Component({
tag: 'accordion-example',
styleUrl: 'accordion-example.css'
})
export const AccordionExample {
render() {
return [
// Basic
<ion-accordion-group>
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
// Custom Icon
<ion-accordion-group>
<ion-accordion value="colors" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers" toggle-icon="arrow-down-circle">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
// Open Accordion
<ion-accordion-group value="colors">
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
// Multiple Accordions
<ion-accordion-group multiple={true} value={['colors', 'numbers']}>
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
];
}
);
```

View File

@ -0,0 +1,236 @@
```html
<template>
<!-- Basic -->
<ion-accordion-group>
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Custom Icon -->
<ion-accordion-group>
<ion-accordion value="colors" :toggle-icon="arrowDownCircle">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes" :toggle-icon="arrowDownCircle">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers" :toggle-icon="arrowDownCircle">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Open Accordion -->
<ion-accordion-group value="colors">
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
<!-- Multiple Accordions -->
<ion-accordion-group :multiple="true" :value="['colors', 'numbers']">
<ion-accordion value="colors">
<ion-item slot="header">
<ion-label>Colors</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Red</ion-label>
</ion-item>
<ion-item>
<ion-label>Green</ion-label>
</ion-item>
<ion-item>
<ion-label>Blue</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="shapes">
<ion-item slot="header">
<ion-label>Shapes</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>Circle</ion-label>
</ion-item>
<ion-item>
<ion-label>Triangle</ion-label>
</ion-item>
<ion-item>
<ion-label>Square</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
<ion-accordion value="numbers">
<ion-item slot="header">
<ion-label>Numbers</ion-label>
</ion-item>
<ion-list slot="content">
<ion-item>
<ion-label>1</ion-label>
</ion-item>
<ion-item>
<ion-label>2</ion-label>
</ion-item>
<ion-item>
<ion-label>3</ion-label>
</ion-item>
</ion-list>
</ion-accordion>
</ion-accordion-group>
</template>
<script>
import { IonAccordion, IonAccordionGroup, IonItem, IonLabel } from '@ionic/vue';
import { defineComponent } from 'vue';
import { arrowDownCircle } from 'ionicons/icons';
export default defineComponent({
components: { IonAccordion, IonAccordionGroup, IonItem, IonLabel },
setup() {
return { arrowDownCircle }
}
});
</script>
```

View File

@ -3,7 +3,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth
import { getIonMode } from '../../global/ionic-global';
import { Animation, Gesture, GestureDetail, RefresherEventDetail } from '../../interface';
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
import { clamp, componentOnReady, getElementRoot, raf } from '../../utils/helpers';
import { clamp, componentOnReady, getElementRoot, raf, transitionEndAsync } from '../../utils/helpers';
import { hapticImpact } from '../../utils/native/haptic';
import {
@ -14,7 +14,6 @@ import {
handleScrollWhileRefreshing,
setSpinnerOpacity,
shouldUseNativeRefresher,
transitionEndAsync,
translateElement
} from './refresher.utils';

View File

@ -1,7 +1,7 @@
import { writeTask } from '@stencil/core';
import { createAnimation } from '../../utils/animation/animation';
import { clamp, componentOnReady } from '../../utils/helpers';
import { clamp, componentOnReady, transitionEndAsync } from '../../utils/helpers';
import { isPlatform } from '../../utils/platform';
// MD Native Refresher
@ -198,46 +198,3 @@ export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElem
);
};
export const transitionEndAsync = (el: HTMLElement | null, expectedDuration = 0) => {
return new Promise(resolve => {
transitionEnd(el, expectedDuration, resolve);
});
};
const transitionEnd = (el: HTMLElement | null, expectedDuration = 0, callback: (ev?: TransitionEvent) => void) => {
let unRegTrans: (() => void) | undefined;
let animationTimeout: any;
const opts: any = { passive: true };
const ANIMATION_FALLBACK_TIMEOUT = 500;
const unregister = () => {
if (unRegTrans) {
unRegTrans();
}
};
const onTransitionEnd = (ev?: Event) => {
if (ev === undefined || el === ev.target) {
unregister();
callback(ev as TransitionEvent);
}
};
if (el) {
el.addEventListener('webkitTransitionEnd', onTransitionEnd, opts);
el.addEventListener('transitionend', onTransitionEnd, opts);
animationTimeout = setTimeout(onTransitionEnd, expectedDuration + ANIMATION_FALLBACK_TIMEOUT);
unRegTrans = () => {
if (animationTimeout) {
clearTimeout(animationTimeout);
animationTimeout = undefined;
}
el.removeEventListener('webkitTransitionEnd', onTransitionEnd, opts);
el.removeEventListener('transitionend', onTransitionEnd, opts);
};
}
return unregister;
};

View File

@ -201,3 +201,41 @@ ion-card-header.ion-color .ion-inherit-color {
.md .menu-content-push {
box-shadow: $menu-md-box-shadow;
}
// Accordion Styles
ion-accordion-group.accordion-group-expand-inset ion-accordion:first-of-type {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
ion-accordion-group.accordion-group-expand-inset ion-accordion:last-of-type {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
ion-accordion-group ion-accordion:last-of-type ion-item {
--border-width: 0px;
}
ion-accordion.accordion-animated .ion-accordion-toggle-icon {
transition: 300ms transform cubic-bezier(0.25, 0.8, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
ion-accordion .ion-accordion-toggle-icon {
/* stylelint-disable declaration-no-important */
transition: none !important;
}
}
ion-accordion.accordion-expanding .ion-accordion-toggle-icon,
ion-accordion.accordion-expanded .ion-accordion-toggle-icon {
transform: rotate(180deg);
}
ion-accordion-group.accordion-group-expand-inset.md ion-accordion.accordion-previous ion-item[slot="header"] {
--border-width: 0px;
--inner-border-width: 0px;
}
ion-accordion-group.accordion-group-expand-inset.md ion-accordion.accordion-expanding:first-of-type,
ion-accordion-group.accordion-group-expand-inset.md ion-accordion.accordion-expanded:first-of-type {
margin-top: 0;
}

View File

@ -2,6 +2,7 @@
import { Components as IoniconsComponents, JSX as IoniconsJSX } from 'ionicons';
export * from './components';
export * from './index';
export * from './components/accordion-group/accordion-group-interface';
export * from './components/alert/alert-interface';
export * from './components/action-sheet/action-sheet-interface';
export * from './components/content/content-interface';

View File

@ -1,7 +1,7 @@
const ION_FOCUSED = 'ion-focused';
const ION_FOCUSABLE = 'ion-focusable';
const FOCUS_KEYS = ['Tab', 'ArrowDown', 'Space', 'Escape', ' ', 'Shift', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp'];
const FOCUS_KEYS = ['Tab', 'ArrowDown', 'Space', 'Escape', ' ', 'Shift', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Home', 'End'];
export const startFocusVisible = () => {

View File

@ -5,6 +5,64 @@ import { Side } from '../interface';
declare const __zone_symbol__requestAnimationFrame: any;
declare const requestAnimationFrame: any;
export const transitionEndAsync = (el: HTMLElement | null, expectedDuration = 0) => {
return new Promise(resolve => {
transitionEnd(el, expectedDuration, resolve);
});
};
/**
* Allows developer to wait for a transition
* to finish and fallback to a timer if the
* transition is cancelled or otherwise
* never finishes. Also see transitionEndAsync
* which is an await-able version of this.
*/
const transitionEnd = (el: HTMLElement | null, expectedDuration = 0, callback: (ev?: TransitionEvent) => void) => {
let unRegTrans: (() => void) | undefined;
let animationTimeout: any;
const opts: any = { passive: true };
const ANIMATION_FALLBACK_TIMEOUT = 500;
const unregister = () => {
if (unRegTrans) {
unRegTrans();
}
};
const onTransitionEnd = (ev?: Event) => {
if (ev === undefined || el === ev.target) {
unregister();
callback(ev as TransitionEvent);
}
};
if (el) {
el.addEventListener('webkitTransitionEnd', onTransitionEnd, opts);
el.addEventListener('transitionend', onTransitionEnd, opts);
animationTimeout = setTimeout(onTransitionEnd, expectedDuration + ANIMATION_FALLBACK_TIMEOUT);
unRegTrans = () => {
if (animationTimeout) {
clearTimeout(animationTimeout);
animationTimeout = undefined;
}
el.removeEventListener('webkitTransitionEnd', onTransitionEnd, opts);
el.removeEventListener('transitionend', onTransitionEnd, opts);
};
}
return unregister;
};
/**
* Utility function to wait for
* componentOnReady on Stencil
* components if not using a
* custom elements build or
* quickly resolve if using
* a custom elements build.
*/
export const componentOnReady = (el: any, callback: any) => {
if (el.componentOnReady) {
el.componentOnReady().then(callback);

View File

@ -52,6 +52,7 @@ export const config: Config = {
{ components: ['ion-toast'] },
{ components: ['ion-toggle'] },
{ components: ['ion-virtual-scroll'] },
{ components: ['ion-accordion-group', 'ion-accordion'] },
],
plugins: [
sass({

View File

@ -8,6 +8,25 @@ import type { JSX } from '@ionic/core';
export const IonAccordion = /*@__PURE__*/ defineContainer<JSX.IonAccordion>('ion-accordion', [
'value',
'disabled',
'readonly',
'toggleIcon',
'toggleIconSlot'
]);
export const IonAccordionGroup = /*@__PURE__*/ defineContainer<JSX.IonAccordionGroup>('ion-accordion-group', [
'multiple',
'value',
'disabled',
'readonly',
'expand',
'ionChange'
]);
export const IonAvatar = /*@__PURE__*/ defineContainer<JSX.IonAvatar>('ion-avatar');