Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/addon/components/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type OneOf<T, K extends keyof T> = Omit<T, K> &
{ [k in K]: Pick<Required<T>, k> & { [k1 in Exclude<K, k>]?: never } }[K];

export interface CommonArgs {
className?: string;
'aria-label'?: string;
'data-test-subj'?: string;
}
Expand Down
185 changes: 100 additions & 85 deletions packages/core/addon/components/eui-accordion/index.hbs
Original file line number Diff line number Diff line change
@@ -1,96 +1,111 @@
<div
class={{class-names
(if this.isOpen "euiAccordion-isOpen")
componentName="EuiAccordion"
paddingSize=this.paddingSize
}}
...attributes
>
<div class={{class-names "euiAccordion__triggerWrapper" @triggerClassName}}>
<button
id={{this.buttonId}}
aria-controls={{@id}}
aria-expanded={{this.isOpen}}
class={{this.buttonClasses}}
type="button"
{{on "click" this.onToggle}}
>
{{#if this.hasArrowDisplay}}
<span
class={{concat
"euiAccordion__iconWrapper "
(if this.hasIconButton "euiAccordion__iconButton ")
{{#let
(element (arg-or-default @element "div"))
(element this.buttonElement)
as |Element ButtonElement|
}}
<Element
class={{class-names
(if this.isOpen "euiAccordion-isOpen")
componentName="EuiAccordion"
paddingSize=this.paddingSize
}}
...attributes
>
<div class={{class-names "euiAccordion__triggerWrapper" @triggerClassName}}>
{{#if (eq this._arrowDisplay "left")}}
<EuiButtonIcon
@color="text"
class={{class-names
"euiAccordion__iconButton"
(if this.isOpen "euiAccordion__iconButton-isOpen")
(if
(eq this._arrowDisplay "right") "euiAccordion__iconButton--right"
)
(if @arrowProps.className @arrowProps.className)
}}
>
<EuiIcon
@iconClasses={{class-names
"euiAccordion__icon"
(if this.isOpen "euiAccordion__icon-isOpen")
}}
@type="arrowRight"
@size="m"
/>
</span>
@iconType="arrowRight"
{{on "click" this.onToggle}}
aria-controls={{@id}}
aria-expanded={{this.isOpen}}
aria-labelledby={{this.buttonId}}
tabindex={{if this.buttonElementIsFocusable "-1" "0"}}
/>
{{/if}}
<span class={{concat "euiIEFlexWrapFix " @buttonContentClassName}}>
{{yield to="buttonContent"}}
</span>
</button>
{{#if (and @extraAction (not this.isLoading))}}
<div class="euiAccordion__optionalAction">
{{@extraAction}}
</div>
{{else if this.isLoading}}
<div class="euiAccordion__optionalAction">
<EuiLoadingSpinner />
</div>
{{/if}}
{{#if this.hasIconButton}}
<button
<ButtonElement
type="button"
id={{arg-or-default this.buttonProps.id (unique-id)}}
class={{this.buttonClasses}}
aria-controls={{@id}}
aria-expanded={{this.isOpen}}
aria-labelledby={{this.buttonId}}
tabIndex={{-1}}
class={{concat
"euiAccordion__iconWrapper "
(if this.hasIconButton "euiAccordion__iconButton ")
}}
type="button"
aria-labelledby={{arg-or-default this.buttonProps.id (unique-id)}}
{{on "click" this.onToggle}}
>
<EuiIcon
@iconClasses={{class-names
"euiAccordion__icon"
(if this.isOpen "euiAccordion__icon-isOpen")
<span class={{this.buttonContentClasses}}>
{{yield to="buttonContent"}}
</span>
</ButtonElement>
{{#if (and @extraAction (not this.isLoading))}}
<div class="euiAccordion__optionalAction">
{{yield to="extraAction"}}
</div>
{{else if this.isLoading}}
<div class="euiAccordion__optionalAction">
<EuiLoadingSpinner />
</div>
{{/if}}
{{#if (eq this._arrowDisplay "right")}}
<EuiButtonIcon
@color="text"
class={{class-names
"euiAccordion__iconButton"
(if this.isOpen "euiAccordion__iconButton-isOpen")
(if
(eq this._arrowDisplay "right") "euiAccordion__iconButton--right"
)
(if @arrowProps.className @arrowProps.className)
}}
@type="arrowRight"
@size="m"
@iconType="arrowRight"
{{on "click" this.onToggle}}
aria-controls={{@id}}
aria-expanded={{this.isOpen}}
aria-labelledby={{this.buttonId}}
tabindex={{if this.buttonElementIsFocusable "-1" "0"}}
/>
</button>
{{/if}}
</div>
<div class="euiAccordion__childWrapper" style={{this.childContentStyle}} id={{@id}}>
{{/if}}
</div>
<div
class={{class-names
(if this.isLoading " euiAccordion__children-isLoading")
@childClassName
paddingSize=this.paddingSize
componentName="EuiAccordion"
}}
class="euiAccordion__childWrapper"
style={{this.childContentStyle}}
id={{@id}}
{{did-insert (set this "childWrapper")}}
tabindex="-1"
role="region"
aria-labelledby={{this.buttonId}}
>
{{#if (and this.isLoading this.isLoadingMessage)}}
<EuiLoadingSpinner class="euiAccordion__spinner" />
<span>
{{#if this.hasLoadingMessage}}
{{this.isLoadingMessage}}
{{else}}
{{! <EuiI18n @token="euiAccordion.isLoading" @default="Loading" /> }}
Loading...
{{/if}}
</span>
{{else}}
{{yield to="content"}}
{{/if}}
<div
class={{class-names
(if this.isLoading "euiAccordion__children-isLoading")
@childClassName
}}
{{did-insert
(queue (set this "childContent") this.setChildContentHeight)
}}
{{resize-observer onResize=this.setChildContentHeight}}
>
{{#if (and this.isLoading this.isLoadingMessage)}}
<EuiLoadingSpinner class="euiAccordion__spinner" />
<span>
{{#if this.hasLoadingMessage}}
{{this.isLoadingMessage}}
{{else}}
{{! <EuiI18n @token="euiAccordion.isLoading" @default="Loading" /> }}
Loading...
{{/if}}
</span>
{{else}}
{{yield to="content"}}
{{/if}}
</div>
</div>
</div>
</div>
</Element>
{{/let}}
77 changes: 58 additions & 19 deletions packages/core/addon/components/eui-accordion/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { uniqueId } from '../../helpers/unique-id';
import { argOrDefaultDecorator as argOrDefault } from '../../helpers/arg-or-default';
import { paddingMapping } from '../../utils/css-mappings/eui-accordion';
import { htmlSafe } from '@ember/template';
import { CommonArgs } from '../common';

type EuiAccordionPaddingSize = keyof typeof paddingMapping;

type AccordionArgs = {
id: string;

element?: 'div' | 'fieldset';
/**
* Class that will apply to the trigger for the accordion.
*/
buttonClassName?: string;

buttonProps?: CommonArgs;

/**
* Applied to the main button receiving the `onToggle` event.
* Anything other than the default `button` does not support removing the arrow display (for accessibility of focus).
*/
buttonElement?: 'div' | 'legend' | 'button';
/**
* Extra props to pass to the EuiButtonIcon containing the arrow.
*/
arrowProps?: 'iconType' | 'onClick' | 'aria-labelledby';
/**
* Class that will apply to the trigger content for the accordion.
*/
Expand Down Expand Up @@ -54,6 +68,8 @@ type AccordionArgs = {
* Choose whether the loading message replaces the content. Customize the message by passing a node
*/
isLoadingMessage?: boolean | Component;

isOpen?: boolean;
};

export default class EuiAccordionAccordionComponent extends Component<AccordionArgs> {
Expand All @@ -65,8 +81,8 @@ export default class EuiAccordionAccordionComponent extends Component<AccordionA
@argOrDefault('left') arrowDisplay!: AccordionArgs['arrowDisplay'];

@tracked _opened;

buttonId: string = uniqueId();
@tracked childWrapper: HTMLDivElement | null = null;
@tracked childContent: HTMLDivElement | null = null;

constructor(owner: unknown, args: AccordionArgs) {
super(owner, args);
Expand All @@ -76,22 +92,24 @@ export default class EuiAccordionAccordionComponent extends Component<AccordionA
: this.args.initialIsOpen;
}

get isOpen(): boolean {
return this.args.forceState
? this.args.forceState === 'open'
: this._opened;
get buttonElement() {
return this.args.element === 'fieldset' ? 'legend' : 'button';
}

get hasIconButton(): boolean | undefined {
return this.args.extraAction && this.arrowDisplay === 'right';
get buttonElementIsFocusable() {
return this.buttonElement === 'button';
}

get hasArrowDisplay(): boolean {
return this.arrowDisplay !== 'none';
get _arrowDisplay() {
return this.arrowDisplay === 'none' && !this.buttonElementIsFocusable
? 'left'
: this.arrowDisplay;
}

get buttonReverse(): boolean {
return !this.args.extraAction && this.arrowDisplay === 'right';
get isOpen(): boolean {
return this.args.forceState
? this.args.forceState === 'open'
: this._opened;
}

get hasLoadingMessage(): boolean {
Expand All @@ -101,23 +119,44 @@ export default class EuiAccordionAccordionComponent extends Component<AccordionA
get buttonClasses(): string {
return [
'euiAccordion__button',
this.buttonReverse ? 'euiAccordion__buttonReverse' : '',
this.args.buttonClassName
this.args.buttonClassName,
this.args.buttonProps?.className
].join(' ');
}

get buttonContentClasses(): string {
return [
'euiAccordion__buttonContent',
this.args.buttonContentClassName
].join(' ');
}

get childContentStyle(): string | ReturnType<typeof htmlSafe> {
return this.isOpen ? '' : htmlSafe(`height: 0px;`);
return this._opened ? '' : htmlSafe(`height: 0px;`);
}

setChildContentHeight = () => {
const { forceState } = this.args;
requestAnimationFrame(() => {
const height =
this.childContent && (forceState ? forceState === 'open' : this._opened)
? this.childContent.clientHeight
: 0;
this.childWrapper &&
this.childWrapper.setAttribute('style', `height: ${height}px`);
});
};

@action
onToggle(): void {
if (this.args.forceState) {
this.args.onToggle &&
this.args.onToggle(this.args.forceState === 'open' ? false : true);
this.args.onToggle?.(this.args.forceState === 'open' ? false : true);
} else {
this._opened = !this._opened;
this.args.onToggle && this.args.onToggle(this._opened);
if (this._opened && this.childWrapper) {
this.childWrapper.focus();
}
this.args.onToggle?.(this._opened);
}
}
}
Loading