Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eui-Accordion #103

Merged
merged 5 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
191 changes: 106 additions & 85 deletions packages/core/addon/components/eui-accordion/index.hbs
Original file line number Diff line number Diff line change
@@ -1,96 +1,117 @@
<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"
}}
...attributes
>
<div class={{class-names "euiAccordion__triggerWrapper" @triggerClassName}}>
{{#if (eq this._arrowDisplay "left")}}
<EuiButtonIcon
@color="text"
@iconClasses={{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}}
<div
class={{class-names
componentName="EuiAccordion"
paddingSize=this.paddingSize
}}
>
{{yield to="content"}}
</div>
{{/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