React Bootstrap

React 0.13.1 - React Bootstrap 0.19.1

Méthodologie

Pour rendre accessibles les différents composants fournis par React Bootstrap, nous avons choisi de les encapsuler dans de nouveaux composants, afin de leur ajouter des fonctionnalités en présentant la même API. Cela s'inscrit également dans la philosophie de React, qui favorise la composition plutôt que l'héritage.

La technique de base utilisée pour encapsuler les composants se présente comme suit.

On dispose d'un composant simple qui place ses enfants dans une balise <div />.

								
var Original = React.createClass({

	render: function() {
		return (
			<div {...this.props}>
				{this.props.children}
			</div>
		);
	}
});

// utilisation
<Original id="test">
	<p>I am a component</p>
</Original>

// rendu
<div id="test">
	<p>I am a component</p>
</div>
								
							

Pour encapsuler ce composant, on le rend simplement en lui passant toutes les propriétés et les enfants du wrapper. Pour l'exemple, on ajoute ici du texte supplémentaire dans la balise <div />.

								
var Wrapper = React.createClass({

	render: function() {
		return (
			<Original {...this.props}>
				<p>Hello</p>
				{this.props.children}
			</Original>
		);
	}
});

// utilisation
<Wrapper id="test">
	<p>I am a component</p>
</Wrapper>

// rendu
<div id="test">
	<p>Hello</p>
	<p>I am a component</p>
</div>
								
							

Ces deux composants sont donc interchangeables, et on évite de modifier le code de la bibliothèque.

Accordion

Démonstration

Corrections

Plusieurs améliorations sont nécessaires pour rendre ce composant accessible :

  • L'accordéon doit posséder un attribut role="tablist" afin d'indiquer sa fonction
  • Chaque titre doit posséder un attribut role="tab"
  • Chaque titre d'un panneau ouvert doit comporter un attribut aria-selected="true" (ou "false" si le panneau est fermé) pour préciser son état.
  • Chaque panneau doit posséder un attribut role="tabpanel"
  • Chaque panneau doit posséder un attribut aria-labelledby="id-du-titre" pour le lier à son titre.
  • Chaque panneau ouvert doit posséder un attribut aria-hidden="false" (ou "true" s'il est fermé) pour préciser son état.
  • Depuis un titre, les touches et doivent permettre d'atteindre le titre précédent.
  • Depuis un titre, les touches et doivent permettre d'atteindre le titre suivant.
  • Depuis un titre, la touche Espace doit permettre d'ouvrir ou de fermer le titre correspondant.

La méthode render() retourne un Accordion configuré avec la clé du panneau actif et un gestionnaire d'événement qui sera appelé à chaque sélection de panneau.

								
var AccessibleAccordion = React.createClass({

	render: function() {
		return (
			<Accordion
				activeKey={this.state.activeKey}
				onSelect={this.handleSelect}
			>
				{this.props.children}
			</Accordion>
		);
	}
});
								
							

Il faut donc initialiser la variable d'état activeKey. Comme dans le composant original, sa valeur est donnée par la propriété defaultActiveKey.

								
	getInitialState: function() {
		return {
			activeKey: this.props.defaultActiveKey || null
		};
	}
								
							

À l'initialisation du composant, on récupère les onglets et les panneaux composant l'accordéon. On ajoute aussi des gestionnaires d'événements pour gérer la navigation au clavier et le focus. On appelle finalement différentes méthodes d'initialisation pour ne pas surcharger la méthode componentDidMount().

								
	componentDidMount: function() {
		this.node = React.findDOMNode(this);
		this.tabs = this.node.querySelectorAll('.panel-heading a');
		this.panes = this.node.getElementsByClassName('panel-collapse');

		this.node.addEventListener('keydown', this.handleKeyDown);
		document.addEventListener('focus', this.handleFocus, true);

		this.setupAttributes();
		this.setupPanesAttributes();
		this.updatePanesAttributes();
	}
								
							

Au démontage du composant, on détache les gestionnaires d'événements.

								
	componentWillUnmount: function() {
		this.node.removeEventListener('keydown', this.handleKeyDown);
		document.removeEventListener('focus', this.handleFocus, true);
	}
								
							

La méthode setupAttributes() initialise les attributs permettant de préciser la fonction du composant.

								
	setupAttributes: function() {
		this.node.setAttribute('role', 'tablist');
		this.node.setAttribute('aria-multiselectable', 'false');
	}
								
							

La méthode setupPanesAttributes() initialise les attributs permettant de lier les titres et les panneaux, et de préciser leur état.

								
	setupPanesAttributes: function() {
		for (var i = 0, l = this.tabs.length; i < l; i++) {
			var tab = this.tabs[i];
			var pane = this.panes[i];
			var id = tab.getAttribute('id');

			// si l'onglet n'a pas d'id, on lui en assigne un
			// suivant son index
			if (!id) {
				id = 'tab-' + i;
				tab.setAttribute('id', id);
			}

			tab.setAttribute('role', 'tab');
			pane.setAttribute('role', 'tabpanel');
			pane.setAttribute('aria-labelledby', id);
		}
	}
								
							

La méthode updatePanesAttributes() met à jour les attributs précisant l'état des titres et des panneaux. On se base ici sur l'attribut aria-expanded, déjà défini sur les panneaux par la bibliothèque originale, pour en connaître l'état.

								
	updatePanesAttributes: function() {
		for (var i = 0, l = this.panes.length; i < l; i++) {
			var tab = this.tabs[i];
			var pane = this.panes[i];
			var isActive = (pane.getAttribute('aria-expanded') === 'true');

			pane.setAttribute('aria-hidden', isActive ? 'false' : 'true');
			tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
		}
	}
								
							

La méthode handleSelect() permet d'ouvrir ou de refermer un panneau de l'accordéon. Une fois l'état modifié, le focus est donné au panneau ouvert. Finalement, on transmet l'événement si nécessaire.

								
	handleSelect: function(key) {
		// si le panneau demandé est déjà ouvert, on le referme
		if (key === this.state.activeKey) {
			key = null;
		}

		this.setState({
			activeKey: key
		}, function() {
			this.updatePanesAttributes();

			if (this.state.activeKey !== null) {
				this.focusActiveTab();
			}
		});

		if (this.props.onSelect) {
			this.props.onSelect(key);
		}
	}
								
							

Dans la méthode précédente, on trouve un appel à focusActiveTab(), qui n'est pas encore définie. Cette méthode permet simplement de transporter le focus sur le panneau actuellement ouvert. Elle utilise activeTabIndex(), qui retourne l'index du panneau ouvert en cherchant celui qui possède un attribut aria-selected="true".

								
	focusActiveTab: function() {
		var index = this.activeTabIndex();
		this.tabs[index].focus();
	}

	activeTabIndex: function() {
		for (var i = 0, l = this.tabs.length; i < l; i++) {
			if (this.tabs[i].getAttribute('aria-selected') === 'true') {
				return i;
			}
		}

		return 0;
	}
								
							

La méthode handleFocus() est appelée lorsqu'un événement focus se produit dans la page. Si le focus vient de l'extérieur du composant vers l'intérieur, on le transporte sur le titre actif :

								
	handleFocus: function(event) {
		if (!this.node.contains(this.focused) && this.node.contains(event.target)) {
			this.focusActiveTab();
		}

		this.focused = event.target;
	}
								
							

Il reste à traiter la navigation au clavier dans la méthode handleKeyDown(). Les flèches et transportent le focus sur le titre précédent, les flèches et sur le titre suivant. La touche Espace ouvre ou ferme le panneau courant.

								
	handleKeyDown: function(event) {
		// on ne traite l'événement que si on est sur un titre
		if (event.target.getAttribute('role') !== 'tab') {
			return;
		}

		switch (event.keyCode) {
			case 37: // gauche
			case 38: // haut
				this.focusSiblingTab(-1);
				break;

			case 39: // droite
			case 40: // bas
				this.focusSiblingTab(1);
				break;

			case 32: // espace
				event.target.click();
				break;

			default:
				return;
		}

		event.preventDefault();
	}
								
							

On utilise au-dessus la méthode focusSiblingTab() qui permet de donner le focus à un titre voisin du titre courant. Elle prend en paramètre une direction : 1 pour sélectionner le suivant, -1 pour le précédent. Cette méthode utilise focusedTabIndex(), qui retourne le titre ayant actuellement le focus.

								
	focusSiblingTab: function(direction) {
		var index = this.focusedTabIndex() + direction;

		if (index < 0) {
			index = this.tabs.length - 1;
		}

		if (index > this.tabs.length - 1) {
			index = 0;
		}

		this.tabs[index].focus();
	}

	focusedTabIndex: function() {
		for (var i = 0, l = this.tabs.length; i < l; i++) {
			if (this.tabs[i] === document.activeElement) {
				return i;
			}
		}

		return 0;
	}
								
							

Modal

Démonstration

Corrections

Plusieurs améliorations sont nécessaires pour rendre ce composant accessible :

  • Le composant doit comporter un attribut aria-labelledby ou aria-label afin que le titre de la modale soit vocalisé lors de la prise de focus.
  • À l'ouverture, le focus doit être déplacé sur le premier élément focusable de la modale.
  • Une fois ouverte, la tabulation doit être restreinte aux éléments focusables de la modale pour éviter que l'utilisateur n'en sorte.
  • Lors de la fermeture de la modale, le focus doit être revenir sur l'élément qui a déclenché l'ouverture de la modale, afin que l'utilisateur poursuive la navigation.

Pour rendre accessible ce composant, nous allons créer deux nouveaux composants : l'un chargé de gérer le focus à l'intérieur d'un élément, et l'autre de gérer une fenêtre modale.

FocusTrap

Ce composant est chargé de "piéger" le focus dans un composant donné.

La méthode render() encapsule les enfants donnés entre deux bornes, c'est-à-dire deux éléments tabulables qui déclenchent un événement lorsqu'ils recoivent le focus.

De cette manière, si le premier élément reçoit le focus, il le transportera au dernier, et inversement.

								
var FocusTrap = React.createClass({

	render: function() {
		return (
			<div>
				<div onFocus={this.handleFocus} tabIndex="0" />

				<div ref="children">
					{this.props.children}
				</div>

				<div onFocus={this.handleFocus} tabIndex="0" />
			</div>
		);
	}
});
								
							

À l'initialisation du composant, on enregistre son état initial et on prépare la gestion du focus.

								
	componentDidMount: function() {
		var children = React.findDOMNode(this.refs.children);

		// on stocke si la touche Shift est enfoncée, ce qui sera utile pour
		// déterminer le sens de déplacement lors d'une tabulation
		this.shiftPressed = false;

		// on enregistre un gestionnaire d'événements pour surveiller l'état
		// de la touche Shift
		document.addEventListener('keydown', this.handleKeyEvent);
		document.addEventListener('keyup', this.handleKeyEvent);

		// on enregistre l'élément qui a le focus au moment de l'affichage
		this.previouslyFocused = document.activeElement;

		// on stocke tous les éléments focusables à l'intérieur du composant
		this.focusable = this.focusableElements(children);

		// on donne le focus au premier élément focusable
		this.focusable[0].focus();
	}
								
							

Lors de l'appui sur une touche, la méthode handleKeyEvent() stocke l'état de la touche Shift :

								
	handleKeyEvent: function(event) {
		this.shiftPressed = event.shiftKey;
	}
								
							

Lorsque le gestionnaire de focus est appelé (donc lorsque l'on tabule sur une des "bornes" définies plus haut), suivant l'état de la touche Shift, on donne le focus au premier ou au dernier élément tabulable.

								
	handleFocus: function(event) {
		// si la touche Shift est enfoncée, cela signifie qu'on tabule en arrière
		// on va donc donner le focus au dernier élément.
		// sinon, on le donne au premier.
		var index = this.shiftPressed
			? this.focusable.length - 1
			: 0;

		this.focusable[index].focus();
	}
								
							

Lors du démontage du composant, on détache les gestionnaires d'événements et on redonne le focus à l'élément qui a déclenché l'ouverture de la modale.

								
	componentWillUnmount: function() {
		document.removeEventListener('keydown', this.handleKeyEvent);
		document.removeEventListener('keyup', this.handleKeyEvent);

		if (this.previouslyFocused) {
			this.previouslyFocused.focus();
		}
	}
								
							

AccessibleModal

Ce composant wrappe le composant Modal proposé par la bibliothèque afin de le rendre accessible.

La méthode render() passe les propriétés et les enfants au composant original, en le wrappant dans un FocusTrap afin que l'utilisateur ne puisse pas tabuler hors de la fenêtre.

								
var AccessibleModal = React.createClass({

	render: function() {
		return (
			<FocusTrap>
				<Modal {...this.props}>
					{this.props.children}
				</Modal>
			</FocusTrap>
		);
	}
});
								
							

À l'initialisation du composant, on lie la modale à son titre par un attribut aria-labelledby. De cette manière, le titre sera vocalisé lors du focus sur la modale.

								
	componentDidMount: function() {
		var node = React.findDOMNode(this);
		var dialog = node.getElementsByClassName('modal-dialog')[0];
		var title = dialog.getElementsByClassName('modal-title')[0];

		dialog.setAttribute('aria-labelledby', 'modal-title');
		title.setAttribute('id', 'modal-title');
	}
								
							

Progress bar

Démonstration

Région mise à jour.

Corrections

Plusieurs améliorations sont nécessaires pour rendre ce composant accessible :

  • La progress bar doit posséder un attribut title ou aria-labelledby faisant office de nom
  • Un attribut aria-valuetext doit indiquer la valeur courante sous une forme lisible

Si la progress bar met à jour une zone de l'interface :

  • Cette zone doit comporter un attribut aria-describedby="id-de-la-progress-bar" qui la lie à la progress bar.
  • Lorsqu'une mise à jour est en cours, la zone doit comporter un attribut aria-busy="true", indiquant que son contenu est en cours de mise à jour.

La méthode render() se contente de retourner une ProgressBar, en lui passant les propriétés nécéssaires :

								
var LabelledProgressBar = React.createClass({

	render: function() {
		return <ProgressBar {...this.props} ref="progress" />;
	}
});
								
							

Pour mettre à jour la propriété aria-valuetext, le plus simple est de récupérer la valeur déjà calculée par le composant original. Ce texte n'étant pas accessible par JavaScript, on le récupère directement dans le DOM :

								
	updateTexts: function() {
		// on récupère les enfants de la progress bar
		var node = React.findDOMNode(this.refs.progress);
		var children = node.childNodes;

		// on met à jour le texte de chacun des enfants
		for (var i = 0, l = children.length; i < l; i++) {
			this.updateText(children[i]);
		}
	},

	updateText: function(node) {
		node.setAttribute('aria-valuetext', node.textContent);
	}
								
							

Finalement, on s'assure que le texte est mis à jour lors de chaque rendu du composant :

								
	componentDidMount: function() {
		this.updateTexts();
	},

	componentDidUpdate: function() {
		this.updateTexts();
	}
								
							

Dans le cas ou la progress bar met à jour une zone de l'interface, il est nécessaire de renseigner plusieurs informations pour les lier ensemble.

Si une zone à mettre à jour (un élément du DOM) est passée en propriété, on lui ajoute un attribut aria-describedby référencant la progress bar. On effectue cet ajout à l'initialisation du composant.

								
	setupTarget: function() {
		if (this.props.target) {
			this.props.target.setAttribute(
				'aria-describedby',
				this.props.id
			);
		}
	},

	componentDidMount: function() {
		// ...
		this.setupTarget();
	}
								
							

Lorsque la progress bar est en cours de mise à jour, on ajoute à la zone un attribut aria-busy="true" indiquant qu'une mise à jour du contenu est en cours. On met à jour cet attribut à l'initialisation ainsi qu'à chaque rendu du composant.

								
	updateTarget: function() {
		if (!this.props.target) {
			return;
		}

		var min = this.props.min || 0;
		var max = this.props.max || 100;

		// on considère qu'une mise à jour est en cours si la
		// valeur courant est entre le minimum et le maximum
		var busy = (this.props.now > min) && (this.props.now < max);

		this.props.target.setAttribute('aria-busy', busy);
	},

	componentDidMount: function() {
		// ...
		this.updateTarget();
	},

	componentDidUpdate: function() {
		// ...
		this.updateTarget();
	}
								
							

Tabs

Démonstration

Il se peut qu'en naviguant rapidement au clavier entre les onglets, les panneaux finissent par disparaître. C'est un bug de la bibliothèque originale qui n'est pas lié aux corrections apportées. Un ticket a été ouvert à ce sujet sur le dépôt officiel du projet.

Corrections

Plusieurs améliorations sont nécessaires pour rendre ce composant accessible :

  • La liste d'onglets doit posséder un attribut role="tablist"
  • Chaque onglet doit posséder un attribut role="tab"
  • Chaque onglet actif doit posséder un attribut aria-selected="true" (ou "false" s'il est inactif) pour préciser son état.
  • Chaque onglet doit posséder un attribut aria-controls="id-du-panneau" qui le lie au panneau qu'il contrôle.
  • Chaque panneau doit posséder un attribut role="tabpanel"
  • Chaque panneau doit posséder un attribut aria-labelledby="id-de-l-onglet" qui le lie à l'onglet qui le contrôle.
  • Depuis un onglet, les touches et doivent permettre d'atteindre l'onglet précédent.
  • Depuis un onglet, les touches et doivent permettre d'atteindre l'onglet suivant.

La méthode render() retourne un TabbedArea configuré avec la clé du panneau actif et un gestionnaire d'événement qui sera appelé à chaque sélection de panneau. On le référence également sous le nom "area", afin de pouvoir y accéder dans les autres méthodes par this.refs.area.

								
var AccessibleTabbedArea = React.createClass({

	render: function() {
		return (
			<TabbedArea
				ref="area"
				activeKey={this.state.activeKey}
				onSelect={this.handleSelect}
			>
				{this.props.children}
			</TabbedArea>
		);
	}
});
								
							

Il faut donc initialiser la variable d'état activeKey. Comme dans le composant original, sa valeur est donnée par la propriété defaultActiveKey.

								
	getInitialState: function() {
		return {
			activeKey: this.props.defaultActiveKey || null
		};
	}
								
							

À l'initialisation du composant, on récupère les onglets et les panneaux. On ajoute aussi un gestionnaire d'événements pour gérer la navigation au clavier. On appelle finalement la méthode setup(), chargée de mettre en place les attributs requis, puis on met à jour les attributs de l'onglet actif.

								
	componentDidMount: function() {
		this.tabList = React.findDOMNode(this.refs.area.refs.tabs);
		this.tabs = this.tabList.getElementsByTagName('a');

		this.paneList = React.findDOMNode(this.refs.area.refs.panes);
		this.panes = this.paneList.children;

		this.tabList.addEventListener('keydown', this.handleKeyDown);

		this.setupAttributes();
		this.updateAttributes();
	},

	componentWillUnmount: function() {
		this.tabList.removeEventListener('keydown', this.handleKeyDown);
	}
								
							

La méthode setup() initialise les attributs permettant de lier les titres et les panneaux, et de préciser leur état.

								
	setupAttributes: function() {
		this.tabList.setAttribute('role', 'tablist');

		for (var i = 0, l = this.panes.length; i < l; i++) {
			var tab = this.tabs[i];
			var pane = this.panes[i];
			var id = pane.getAttribute('id');

			if (!id) {
				id = 'pane-' + i;
				pane.setAttribute('id', id);
			}

			tab.setAttribute('aria-controls', id);
			tab.setAttribute('role', 'tab');
			pane.setAttribute('role', 'tabpanel');
		}
	}
								
							

La méthode updateAttributes() met à jour les attributs de tous les onglets, mais active seulement celui demandé. Seul cet onglet sera tabulable, et son attribut aria-selected aura pour valeur"true".

								
	updateAttributes: function() {
		var ref = 'tab' + this.state.activeKey;
		var active = React.findDOMNode(this.refs.area.refs[ref].refs.anchor);

		for (var i = 0, l = this.tabs.length; i < l; i++) {
			var tab = this.tabs[i];
			var isActive = (tab === active);

			tab.setAttribute('tabindex', isActive ? 0 : -1)
			tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
		}
	}
								
							

La méthode handleSelect() permet d'ouvrir ou de refermer un panneau. Une fois l'état modifié, les attributs sont mis à jour et le focus est donné au panneau ouvert. Finalement, on transmet l'événement si nécessaire.

								
	handleSelect: function(key) {
		this.setState({
			activeKey: key
		}, function() {
			this.updateAttributes();
			this.focusActiveTab();
		});

		if (this.props.onSelect) {
			this.props.onSelect(key);
		}
	}
								
							

Dans la méthode précédente, on trouve un appel à la méthode focusActiveTab(), qui n'est pas encore définie. Cette méthode permet simplement de transporter le focus sur le panneau actuellement ouvert.

								
	focusActiveTab: function() {
		var ref = 'tab' + this.state.activeKey;
		var active = React.findDOMNode(this.refs.area.refs[ref].refs.anchor);

		active.focus();
	}
								
							

Il reste à traiter la navigation au clavier dans la méthode handleKeyDown(). Les flèches et activent l'onglet précédent, les flèches et l'onglet suivant.

								
	handleKeyDown: function(event) {
		var ref = 'tab' + this.refs.area.getActiveKey();
		var node = React.findDOMNode(this.refs.area.refs[ref]);
		var next;

		switch (event.keyCode) {
			case 37: // gauche
			case 38: // haut
				next = node.previousElementSibling || node.parentElement.lastChild;
				break;

			case 39: // droite
			case 40: // bas
				next = node.nextElementSibling || node.parentElement.firstChild;
				break;

			default:
				return;
		}

		event.preventDefault();
		next.firstElementChild.click();
	}
								
							

Tooltip

Démonstration

Corrections

Plusieurs améliorations sont nécessaires pour rendre ce composant accessible :

  • Le tooltip doit posséder un attribut role="tooltip"
  • Le texte doit être lié au tooltip par un attribut aria-describedby
  • Le tooltip doit pouvoir être caché lors de l'appui sur Echap

Pour ajouter ces différentes fonctionnalités, la méthode retenue consiste à encapsuler le composant OverlayTrigger. Cela permet d'utiliser le wrapper de la même manière que OverlayTrigger, tout en profitant des améliorations d'accessibilité.

Nous pouvons d'abord implémenter la méthode render(), chargée de rendre un OverlayTrigger :

								
var TooltipOverlayTrigger = React.createClass({

	render: function() {
		// on génère un id pour lier le contenu au tooltip
		// (l'implémentation de cette fonction n'est pas importante pour l'exemple)
		var id = generateUniqueId();

		// on rend les différents composants
		var child = this.renderChild(id);
		var tooltip = this.renderTooltip(id);

		// on rend le composant wrappé en lui passant les propriétés,
		// le contenu et le tooltip
		return (
			<OverlayTrigger {...this.props} ref="trigger" overlay={tooltip}>
				{child}
			</OverlayTrigger>
		);
	}
});
								
							

Nous pouvons maintenant compléter les méthodes de rendu des composants, la plus simple étant renderTooltip(), qui retourne le tooltip passé en propriété en lui ajoutant un id et un role adapté :

								
	renderTooltip: function(id) {
		return React.cloneElement(this.props.overlay, {
			id: id,
			role: 'tooltip'
		});
	}
								
							

La méthode renderChild() est très similaire. Elle retourne le tooltip passé en propriété en lui ajoutant un attribut aria-describedby qui le lie au contenu, et un gestionnaire d'événements pour gérer la touche Echap :

								
	renderChild: function(tooltipId) {
		var child = React.Children.only(this.props.children);

		return React.cloneElement(child, {
			'aria-describedby': tooltipId,
			'onKeyDown': this.handleKeyDown
		});
	}
								
							

Finalement, il reste à implémenter le gestionnaire d'événements attaché au composant enfant. Si l'utilisateur appuie sur Echap, on clôt la modale. Pour ne pas casser la chaîne des événements, on appelle le callback onKeyDown s'il est défini.

								
	handleKeyDown: function(event) {
		if (event.keyCode === 27) {
			this.refs.trigger.handleDelayedHide();
		}

		if (this.props.onKeyDown) {
			this.props.onKeyDown(event);
		}
	}
								
							

Avis du développeur

Les composants React sont très "fermés", du fait qu'ils ne permettent leur configuration que par des propriétés. Il peut devenir compliqué d'enrichir un composant sans le réécrire si son auteur ne prévoit pas de moyen de configurer certains comportements, ou trop peu d'événements auquels souscrire lors de changements d'état.

Il reste possible de modifier tout type de composant, mais cela peut s'avérer complexe, et relativement verbeux par rapport à un code pensé pour React depuis le début. Avant de choisir un composant, il est donc judicieux d'évaluer les options de configuration possibles (modification des attributs DOM, des classes CSS), pour faciliter son intégration dans un contexte particulier.

Télécharger la correction des composants React Bootstrap au format JavaScript (16,1Ko)