No notes defined.
<div class="sds-chatbot -chatbotHasValue" data-js-chatbot>
<label class="sr-only" for="chatbot-question">Posez votre question</label>
<div class="sds-chatbot__bar">
<input id="chatbot-question" class="sds-chatbot__input form-control" data-chatbot-input type="text" name="question" value="Est-ce que ma fille de 12 ans peut ouvrir un compte à la Spuerkeess ? Et je voudrais aussi pouvoir gérer mes opérations quotidiennes." placeholder="Posez-moi votre question" autocomplete="off" />
<button type="button" class="sds-btn -iconBtn -btnSecondary -ghost sds-chatbot__clear" data-chatbot-clear="true">
<span aria-hidden="true" class="sds-icon sds-icon-cross"></span>
<span class="sr-only">Effacer la question</span>
</button>
<button type="button" class="sds-btn -btnPrimary sds-chatbot__submit" data-chatbot-submit="true" disabled>
<span class="sds-btn__text">
Envoyer
</span>
</button>
<div class="sds-circularProgress -circularProgressTokenUsage sds-chatbot__tokenUsage -circularProgressTokenUsageDanger" role="meter" aria-label="Utilisation des crédits IA" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" aria-valuetext="Limite de crédits IA atteinte" data-toggle="tooltip" data-placement="bottom" title="Vous avez atteint la limite de longueur de l'input">
<svg class="sds-circularProgress__svg" viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<circle class="sds-circularProgress__bgShape" cx="10" cy="10" r="9" fill="none" />
<circle class="sds-circularProgress__shape" cx="10" cy="10" r="9" fill="none" stroke-dasharray="56.548667 56.548667" transform="rotate(-90 10 10)" />
</svg>
</div>
</div>
</div>
<div class="{{ namespace }}chatbot{% for mod in modifiers %} {{ mod }}{% endfor %}{% for componentClass in classes %} {{ componentClass }}{% endfor %}"
data-js-chatbot
{% for attrKey, attr in attrs %} {{ attrKey }}="{{ attr }}"{% endfor %}
{% if isLoading %} aria-busy="true"{% endif %}>
<label class="sr-only" for="{{ id }}">{{ label }}</label>
<div class="{{ namespace }}chatbot__bar">
<input
id="{{ id }}"
class="{{ namespace }}chatbot__input form-control"
data-chatbot-input
type="text"
name="{{ inputName }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
autocomplete="off"
{% if isLoading %} readonly{% endif %}
{% if suggestions.length and not value and not isLoading %} aria-describedby="{{ suggestionsHintId }}"{% endif %}
/>
{% render "@icon-btn-secondary--ghost", {
classes: [namespace + "chatbot__clear"],
icon: "icon-cross",
action: clearActionLabel,
attrs: {
"data-chatbot-clear": "true"
}
}, true %}
{% render "@btn-primary", {
classes: [namespace + "chatbot__submit"],
text: submitLabel,
disabled: submitDisabled,
attrs: {
"data-chatbot-submit": "true"
}
}, true %}
{% if tokenUsage %}
{% render "@circular-progress--token-usage", {
classes: [namespace + "chatbot__tokenUsage"],
attrs: tokenUsage.attrs,
progress: tokenUsage.progress,
tokenUsageDanger: tokenUsage.danger,
a11yLabel: tokenUsage.a11yLabel,
a11yValueText: tokenUsage.a11yValueText
}, true %}
{% endif %}
</div>
{% if suggestions.length and not value and not isLoading %}
<p class="sr-only" id="{{ suggestionsHintId }}">{{ suggestionsHint }}</p>
<div class="{{ namespace }}chatbot__suggestions list-unstyled" data-chatbot-suggestions>
<ul class="list-unstyled">
{% for suggestion in suggestions %}
<li class="{{ namespace }}chatbot__suggestionItem">
<button class="{{ namespace }}chatbot__suggestion" type="button" data-chatbot-suggestion value="{{ suggestion }}">
<span class="{{ namespace }}icon {{ namespace }}icon-search" aria-hidden="true"></span>
<span>{{ suggestion }}</span>
</button>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
export default class Chatbot {
// Enhances the server-rendered component with input state and suggestion navigation.
constructor() {
this.chatbots = document.querySelectorAll("[data-js-chatbot]");
this.chatbots.forEach((chatbot) => this.init(chatbot));
}
init(chatbot) {
const input = chatbot.querySelector("[data-chatbot-input]");
if (!input) return;
const submitButton = chatbot.querySelector("[data-chatbot-submit]");
const clearButton = chatbot.querySelector("[data-chatbot-clear]");
const suggestions = Array.from(chatbot.querySelectorAll("[data-chatbot-suggestion]"));
const suggestionsContainer = chatbot.querySelector("[data-chatbot-suggestions]");
const suggestionsHintId = input.getAttribute("aria-describedby");
let isPointerOverSuggestions = false;
const isSuggestion = (element) => suggestions.some((suggestion) => suggestion.contains(element));
const setFocusedState = (isFocused) => chatbot.classList.toggle("-chatbotFocused", isFocused);
chatbot.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
setFocusedState(false);
}
});
input.addEventListener("focus", () => setFocusedState(input.value.length < 1));
input.addEventListener("blur", (event) => {
if (!isPointerOverSuggestions && !isSuggestion(event.relatedTarget)) {
setFocusedState(false);
}
});
suggestionsContainer?.addEventListener("pointerenter", () => {
isPointerOverSuggestions = true;
});
suggestionsContainer?.addEventListener("pointerleave", () => {
isPointerOverSuggestions = false;
if (document.activeElement !== input && !isSuggestion(document.activeElement)) {
setFocusedState(false);
}
});
// Keep the Figma variant's initial button state, then derive it from user input.
const updateValueState = ({ preserveSubmitState = false } = {}) => {
const hasValue = input.value.trim() !== "";
chatbot.classList.toggle("-chatbotHasValue", hasValue);
if (clearButton) {
clearButton.hidden = !hasValue;
}
if (submitButton && !preserveSubmitState && !chatbot.hasAttribute("aria-busy")) {
submitButton.disabled = !hasValue;
}
if (suggestionsHintId) {
input.toggleAttribute("aria-describedby", !hasValue);
if (!hasValue) input.setAttribute("aria-describedby", suggestionsHintId);
}
};
input.addEventListener("input", () => {
updateValueState();
setFocusedState(input.value.length < 1);
});
input.addEventListener("keydown", (event) => {
// Suggestions are regular buttons, so move real focus into the list.
if (event.key === "ArrowDown" && suggestions.length && input.value.trim() === "") {
event.preventDefault();
suggestions[0].focus();
}
});
clearButton?.addEventListener("click", () => {
input.value = "";
updateValueState();
input.focus();
});
suggestions.forEach((suggestion, index) => {
suggestion.addEventListener("focus", () => setFocusedState(true));
suggestion.addEventListener("blur", (event) => {
if (!isPointerOverSuggestions && event.relatedTarget !== input && !isSuggestion(event.relatedTarget)) {
setFocusedState(false);
}
});
suggestion.addEventListener("keydown", (event) => {
// Keep keyboard focus inside the suggestion list, with input as its first item.
if (event.key === "ArrowDown" && index < suggestions.length - 1) {
event.preventDefault();
suggestions[index + 1].focus();
}
if (event.key === "ArrowUp") {
event.preventDefault();
(index === 0 ? input : suggestions[index - 1]).focus();
}
if (event.key === "Escape") {
event.preventDefault();
input.focus();
}
});
suggestion.addEventListener("click", () => {
input.value = suggestion.value;
updateValueState();
input.focus();
setFocusedState(false);
});
});
// Do not overwrite the disabled state supplied by the current Fractal/backend variant.
updateValueState({ preserveSubmitState: true });
}
}
.#{$namespace}chatbot {
$self: &;
position: relative;
box-shadow: map-deep-get($token-shadow-map, "active");
border-radius: var(--sys-border-radius-1000);
&__bar {
position: relative;
z-index: z("low");
display: flex;
align-items: center;
gap: var(--ui-space-inline-static-1000);
padding: var(--ui-space-inset-vertical-static-500) var(--ui-space-inset-horizontal-static-1000);
@include custom-prop-fallback("background-color", "comp-box-background-color");
border-radius: inherit;
}
&__input {
flex: 1 1 auto;
padding-inline: var(--comp-input-inset-h);
border: 0;
background-color: transparent !important;
@include custom-prop-fallback("color", "comp-search-filled-text-color");
&::placeholder {
@include custom-prop-fallback("color", "comp-search-enabled-text-color");
}
}
&__clear,
&__submit {
flex: 0 0 auto;
}
&__clear {
display: none;
}
&__suggestions {
padding: var(--ui-space-inset-vertical-static-500) var(--ui-space-inset-horizontal-static-1000);
position: absolute;
top: 100%;
left: 0;
right: 0;
@include custom-prop-fallback("background-color", "comp-box-background-color");
border-radius: inherit;
border-start-start-radius: 0;
border-start-end-radius: 0;
box-shadow: map-deep-get($token-shadow-map, "active");
display: none;
}
&__suggestion {
display: flex;
align-items: flex-start;
gap: var(--ui-space-inline-static-500);
width: 100%;
padding: var(--ui-space-inset-vertical-static-375) var(--ui-space-inset-horizontal-static-250);
@include custom-prop-fallback("color", "comp-input-text-color");
text-align: left;
&:hover {
@media (hover: hover) {
@include custom-prop-fallback("background-color", "comp-box-sunken-background-color");
}
}
}
&__suggestion {
&:focus-visible {
@include box-outline();
@include custom-prop-fallback("outline-color","comp-input-active-border-color","true","true")
}
}
&.-chatbotHasValue {
#{$self}__clear {
display: inline-flex;
}
#{$self}__suggestions {
display: none;
}
}
&.-chatbotFocused {
#{$self}__suggestions {
display: block;
}
#{$self}__bar {
border-end-start-radius: 0;
border-end-end-radius: 0;
&:after {
content: "";
position: absolute;
bottom: 0;
left: var(--ui-space-inset-horizontal-static-1000);
right: var(--ui-space-inset-horizontal-static-1000);
border-top: var(--sys-border-width-thin) solid var(--comp-radio-border-color);
}
}
}
&:has(INPUT:focus-visible) {
#{$self}__bar {
@include box-outline("comp-search-focused-border-color");
@include custom-prop-fallback("outline-color","comp-input-active-border-color","true","true");
&::after {
content: none;
}
}
INPUT {
outline: none !important;
}
}
}