Skip to content

Commit

Permalink
feat(collapse): add accessibility elements of the collapse (#982)
Browse files Browse the repository at this point in the history
* feat(collapse): add accessibility elements of the collapse

* fix: small fixes

---------

Co-authored-by: Quentin Deroubaix <[email protected]>
  • Loading branch information
ExFlo and quentinderoubaix authored Oct 29, 2024
1 parent fd3cda6 commit 648c8c6
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export class CollapseDirective extends BaseWidgetDirective<CollapseWidget> {
*/
@Input({alias: 'auVisible', transform: auBooleanAttribute}) visible: boolean | undefined;

/**
* id of the collapse
*
* @defaultValue `''`
*/
@Input('auId') id: string | undefined;

/**
* Callback called when the collapse visibility changed.
*
Expand Down Expand Up @@ -86,7 +93,7 @@ export class CollapseDirective extends BaseWidgetDirective<CollapseWidget> {
onHidden: () => this.hidden.emit(),
},
afterInit: (widget) => {
useDirectiveForHost(widget.directives.transitionDirective);
useDirectiveForHost(widget.directives.collapseDirective);
},
}),
);
Expand Down
26 changes: 16 additions & 10 deletions angular/demo/bootstrap/src/app/samples/collapse/default.route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import {AgnosUIAngularModule} from '@agnos-ui/angular-bootstrap';
import {Component} from '@angular/core';
import {CollapseDirective} from '@agnos-ui/angular-bootstrap';
import {Component, signal} from '@angular/core';

@Component({
standalone: true,
imports: [AgnosUIAngularModule],
imports: [CollapseDirective],
template: `
<p class="d-inline-flex gap-1">
<button class="btn btn-primary" type="button" (click)="collapse.api.open()">Open collapse</button>
<button class="btn btn-primary" type="button" (click)="collapse.api.close()">Close collapse</button>
<button class="btn btn-primary" type="button" (click)="collapse.api.toggle()">Toggle collapse</button>
</p>
<div auCollapse #collapse="auCollapse">
<button class="btn btn-primary mb-2" type="button" aria-controls="auId-0" [attr.aria-expanded]="expanded()" (click)="toggle()">
Toggle collapse
</button>
<div auCollapse auId="auId-0" [auVisible]="expanded()" (auHidden)="onHidden()">
<div class="card card-body">Visible content</div>
</div>
`,
})
export default class DefaultCollapseComponent {}
export default class DefaultCollapseComponent {
readonly expanded = signal(true);
toggle() {
this.expanded.update((expanded) => !expanded);
}
onHidden() {
console.log('Hidden');
}
}
25 changes: 20 additions & 5 deletions core-bootstrap/src/components/collapse/collapse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createTransition} from '@agnos-ui/core/services/transitions/baseTransitions';
import type {ConfigValidator, Directive, PropsConfig, Widget} from '@agnos-ui/core/types';
import {stateStores, writablesForProps} from '@agnos-ui/core/utils/stores';
import {bindDirectiveNoArg} from '@agnos-ui/core/utils/directive';
import {bindDirectiveNoArg, createAttributesDirective, mergeDirectives} from '@agnos-ui/core/utils/directive';
import {typeBoolean, typeFunction, typeString} from '@agnos-ui/core/utils/writables';
import {collapseHorizontalTransition, collapseVerticalTransition} from '../../services/transitions/collapse';
import {asWritable, computed} from '@amadeus-it-group/tansu';
Expand Down Expand Up @@ -77,6 +77,12 @@ export interface CollapseProps extends CollapseCommonPropsAndState {
* @defaultValue `true`
*/
animated: boolean;
/**
* id of the collapse
*
* @defaultValue `''`
*/
id: string;
}

export interface CollapseApi {
Expand All @@ -98,9 +104,9 @@ export interface CollapseApi {

export interface CollapseDirectives {
/**
* the transition directive, piloting what is the visual effect of going from hidden to visible
* Directive to apply the collapse.
*/
transitionDirective: Directive;
collapseDirective: Directive;
}

export type CollapseWidget = Widget<CollapseProps, CollapseState, CollapseApi, CollapseDirectives>;
Expand All @@ -114,6 +120,7 @@ const defaultCollapseConfig: CollapseProps = {
animated: true,
animatedOnInit: false,
className: '',
id: '',
};

/**
Expand All @@ -133,6 +140,7 @@ const commonCollapseConfigValidator: ConfigValidator<CollapseProps> = {
animated: typeBoolean,
className: typeString,
visible: typeBoolean,
id: typeString,
};

/**
Expand All @@ -141,7 +149,7 @@ const commonCollapseConfigValidator: ConfigValidator<CollapseProps> = {
* @returns an CollapseWidget
*/
export function createCollapse(config?: PropsConfig<CollapseProps>): CollapseWidget {
const [{animatedOnInit$, animated$, visible$: requestedVisible$, onVisibleChange$, onHidden$, onShown$, horizontal$, ...stateProps}, patch] =
const [{animatedOnInit$, animated$, visible$: requestedVisible$, onVisibleChange$, onHidden$, onShown$, horizontal$, id$, ...stateProps}, patch] =
writablesForProps(defaultCollapseConfig, config, commonCollapseConfigValidator);

const currentTransitionFn$ = asWritable(computed(() => (horizontal$() ? collapseHorizontalTransition : collapseVerticalTransition)));
Expand Down Expand Up @@ -169,7 +177,14 @@ export function createCollapse(config?: PropsConfig<CollapseProps>): CollapseWid
toggle: transition.api.toggle,
},
directives: {
transitionDirective: bindDirectiveNoArg(transition.directives.directive),
collapseDirective: mergeDirectives(
bindDirectiveNoArg(transition.directives.directive),
createAttributesDirective(() => ({
attributes: {
id: computed(() => id$() || undefined),
},
})),
),
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@
<Section label="Default" level={2}>
<Sample title="Default example" sample={sampleDefault} height={395} />
</Section>

<Section label="Accessibility" level={2}>
<p>
The collapse component for being accessible oblige the usage of <a
href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls"
target="_blank">ARIA attribute aria-controls</a
>
as well as the
<a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role" target="_blank">ARIA attribute aria-expanded</a> as shown
in the examples.
</p>
</Section>
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,17 @@
class="container p-3"
id="root"
>
<p
class="d-inline-flex gap-1"
<button
aria-controls="rewritten-id-1"
aria-expanded="true"
class="btn btn-primary mb-2"
type="button"
>
<button
class="btn btn-primary"
type="button"
>
"Open collapse"
</button>
<button
class="btn btn-primary"
type="button"
>
"Close collapse"
</button>
<button
class="btn btn-primary"
type="button"
>
"Toggle collapse"
</button>
</p>
"Toggle collapse"
</button>
<div
class="collapse show"
id="rewritten-id-1"
>
<div
class="card card-body"
Expand Down
4 changes: 2 additions & 2 deletions react/bootstrap/src/components/collapse/collapse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {useDirectives} from '@agnos-ui/react-headless/utils/directive';

export const Collapse: ForwardRefExoticComponent<PropsWithChildren<Partial<CollapseProps>> & RefAttributes<CollapseApi>> = forwardRef(
function Collapse(props: PropsWithChildren<Partial<CollapseProps>>, ref: ForwardedRef<CollapseApi>) {
const {api, directives} = useWidgetWithConfig(createCollapse, props, 'collapse', {});
const {api, directives} = useWidgetWithConfig(createCollapse, props, 'collapse');
useImperativeHandle(ref, () => api, []);

return <div {...useDirectives(directives.transitionDirective)}>{props.children}</div>;
return <div {...useDirectives(directives.collapseDirective)}>{props.children}</div>;
},
);
23 changes: 7 additions & 16 deletions react/demo/src/bootstrap/samples/collapse/Default.route.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import type {CollapseApi} from '@agnos-ui/react-bootstrap/components/collapse';
import {Collapse} from '@agnos-ui/react-bootstrap/components/collapse';
import {useRef} from 'react';
import {useState} from 'react';

const CollapseDemo = () => {
const refCollapse = useRef<CollapseApi>(null);

const [visible, setVisible] = useState(true);
const id = 'auId-0';
return (
<>
<p className="d-inline-flex gap-1">
<button className="btn btn-primary" type="button" onClick={() => refCollapse?.current?.open()}>
Open collapse
</button>
<button className="btn btn-primary" type="button" onClick={() => refCollapse?.current?.close()}>
Close collapse
</button>
<button className="btn btn-primary" type="button" onClick={() => refCollapse?.current?.toggle()}>
Toggle collapse
</button>
</p>
<Collapse ref={refCollapse}>
<button className="btn btn-primary mb-2" type="button" aria-expanded={visible} aria-controls={id} onClick={() => setVisible(!visible)}>
Toggle collapse
</button>
<Collapse id={id} visible={visible} onHidden={() => console.log('Hidden')}>
<div className="card card-body">Visible content</div>
</Collapse>
</>
Expand Down
15 changes: 11 additions & 4 deletions svelte/bootstrap/src/components/collapse/Collapse.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@
import {callWidgetFactory} from '../../config';
import type {Snippet} from 'svelte';
let {children, ...props}: Partial<CollapseProps> & {children: Snippet} = $props();
let {children, visible = $bindable(), ...props}: Partial<CollapseProps> & {children: Snippet} = $props();
const {
directives: {transitionDirective},
directives: {collapseDirective},
api: collapseApi,
} = callWidgetFactory({
factory: createCollapse,
widgetName: 'collapse',
props,
get props() {
return {...props, visible};
},
events: {
onVisibleChange: (event) => {
visible = event;
},
},
enablePatchChanged: true,
});
export const api: CollapseApi = collapseApi;
</script>

<div use:transitionDirective>
<div use:collapseDirective>
{@render children()}
</div>
12 changes: 5 additions & 7 deletions svelte/demo/src/bootstrap/samples/collapse/Default.route.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
<script lang="ts">
import {Collapse} from '@agnos-ui/svelte-bootstrap/components/collapse';
let collapse: Collapse;
let visible = $state(true);
</script>

<p class="d-inline-flex gap-1">
<button class="btn btn-primary" type="button" onclick={() => collapse.api.open()}>Open collapse</button>
<button class="btn btn-primary" type="button" onclick={() => collapse.api.close()}>Close collapse</button>
<button class="btn btn-primary" type="button" onclick={() => collapse.api.toggle()}>Toggle collapse</button>
</p>
<Collapse bind:this={collapse} onHidden={() => console.log('Hidden')}><div class="card card-body">Visible content</div></Collapse>
<button class="btn btn-primary mb-2" type="button" aria-controls="auId-0" aria-expanded={visible} onclick={() => (visible = !visible)}
>Toggle collapse</button
>
<Collapse {visible} onHidden={() => console.log('Hidden')} id="auId-0"><div class="card card-body">Visible content</div></Collapse>

0 comments on commit 648c8c6

Please sign in to comment.