mirror of
				https://github.com/ggml-org/llama.cpp.git
				synced 2025-11-04 09:32:00 +00:00 
			
		
		
		
	webui : improve accessibility for visually impaired people (#13551)
* webui : improve accessibility for visually impaired people * add a11y for extra contents * fix some labels being read twice * add skip to main content
This commit is contained in:
		@@ -28,13 +28,13 @@ function AppLayout() {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Sidebar />
 | 
			
		||||
      <div
 | 
			
		||||
      <main
 | 
			
		||||
        className="drawer-content grow flex flex-col h-screen w-screen mx-auto px-4 overflow-auto bg-base-100"
 | 
			
		||||
        id="main-scroll"
 | 
			
		||||
      >
 | 
			
		||||
        <Header />
 | 
			
		||||
        <Outlet />
 | 
			
		||||
      </div>
 | 
			
		||||
      </main>
 | 
			
		||||
      {
 | 
			
		||||
        <SettingDialog
 | 
			
		||||
          show={showSettings}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,16 +18,26 @@ export default function ChatInputExtraContextItem({
 | 
			
		||||
  if (!items) return null;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1">
 | 
			
		||||
    <div
 | 
			
		||||
      className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1"
 | 
			
		||||
      role="group"
 | 
			
		||||
      aria-description="Selected files"
 | 
			
		||||
    >
 | 
			
		||||
      {items.map((item, i) => (
 | 
			
		||||
        <div
 | 
			
		||||
          className="indicator"
 | 
			
		||||
          key={i}
 | 
			
		||||
          onClick={() => clickToShow && setShow(i)}
 | 
			
		||||
          tabIndex={0}
 | 
			
		||||
          aria-description={
 | 
			
		||||
            clickToShow ? `Click to show: ${item.name}` : undefined
 | 
			
		||||
          }
 | 
			
		||||
          role={clickToShow ? 'button' : 'menuitem'}
 | 
			
		||||
        >
 | 
			
		||||
          {removeItem && (
 | 
			
		||||
            <div className="indicator-item indicator-top">
 | 
			
		||||
              <button
 | 
			
		||||
                aria-label="Remove file"
 | 
			
		||||
                className="btn btn-neutral btn-sm w-4 h-4 p-0 rounded-full"
 | 
			
		||||
                onClick={() => removeItem(i)}
 | 
			
		||||
              >
 | 
			
		||||
@@ -46,13 +56,16 @@ export default function ChatInputExtraContextItem({
 | 
			
		||||
              <>
 | 
			
		||||
                <img
 | 
			
		||||
                  src={item.base64Url}
 | 
			
		||||
                  alt={item.name}
 | 
			
		||||
                  alt={`Preview image for ${item.name}`}
 | 
			
		||||
                  className="w-14 h-14 object-cover rounded-md"
 | 
			
		||||
                />
 | 
			
		||||
              </>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <div className="w-14 h-14 flex items-center justify-center">
 | 
			
		||||
                <div
 | 
			
		||||
                  className="w-14 h-14 flex items-center justify-center"
 | 
			
		||||
                  aria-description="Document icon"
 | 
			
		||||
                >
 | 
			
		||||
                  <DocumentTextIcon className="h-8 w-14 text-base-content/50" />
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
@@ -66,16 +79,25 @@ export default function ChatInputExtraContextItem({
 | 
			
		||||
      ))}
 | 
			
		||||
 | 
			
		||||
      {showingItem && (
 | 
			
		||||
        <dialog className="modal modal-open">
 | 
			
		||||
        <dialog
 | 
			
		||||
          className="modal modal-open"
 | 
			
		||||
          aria-description={`Preview ${showingItem.name}`}
 | 
			
		||||
        >
 | 
			
		||||
          <div className="modal-box">
 | 
			
		||||
            <div className="flex justify-between items-center mb-4">
 | 
			
		||||
              <b>{showingItem.name ?? 'Extra content'}</b>
 | 
			
		||||
              <button className="btn btn-ghost btn-sm">
 | 
			
		||||
              <button
 | 
			
		||||
                className="btn btn-ghost btn-sm"
 | 
			
		||||
                aria-label="Close preview dialog"
 | 
			
		||||
              >
 | 
			
		||||
                <XMarkIcon className="h-5 w-5" onClick={() => setShow(-1)} />
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            {showingItem.type === 'imageFile' ? (
 | 
			
		||||
              <img src={showingItem.base64Url} alt={showingItem.name} />
 | 
			
		||||
              <img
 | 
			
		||||
                src={showingItem.base64Url}
 | 
			
		||||
                alt={`Preview image for ${showingItem.name}`}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <div className="overflow-x-auto">
 | 
			
		||||
                <pre className="whitespace-pre-wrap break-words text-sm">
 | 
			
		||||
 
 | 
			
		||||
@@ -83,13 +83,20 @@ export default function ChatMessage({
 | 
			
		||||
 | 
			
		||||
  if (!viewingChat) return null;
 | 
			
		||||
 | 
			
		||||
  const isUser = msg.role === 'user';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="group" id={id}>
 | 
			
		||||
    <div
 | 
			
		||||
      className="group"
 | 
			
		||||
      id={id}
 | 
			
		||||
      role="group"
 | 
			
		||||
      aria-description={`Message from ${msg.role}`}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames({
 | 
			
		||||
          chat: true,
 | 
			
		||||
          'chat-start': msg.role !== 'user',
 | 
			
		||||
          'chat-end': msg.role === 'user',
 | 
			
		||||
          'chat-start': !isUser,
 | 
			
		||||
          'chat-end': isUser,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        {msg.extra && msg.extra.length > 0 && (
 | 
			
		||||
@@ -99,7 +106,7 @@ export default function ChatMessage({
 | 
			
		||||
        <div
 | 
			
		||||
          className={classNames({
 | 
			
		||||
            'chat-bubble markdown': true,
 | 
			
		||||
            'chat-bubble bg-transparent': msg.role !== 'user',
 | 
			
		||||
            'chat-bubble bg-transparent': !isUser,
 | 
			
		||||
          })}
 | 
			
		||||
        >
 | 
			
		||||
          {/* textarea for editing message */}
 | 
			
		||||
@@ -142,7 +149,7 @@ export default function ChatMessage({
 | 
			
		||||
              ) : (
 | 
			
		||||
                <>
 | 
			
		||||
                  {/* render message as markdown */}
 | 
			
		||||
                  <div dir="auto">
 | 
			
		||||
                  <div dir="auto" tabIndex={0}>
 | 
			
		||||
                    {thought && (
 | 
			
		||||
                      <ThoughtProcess
 | 
			
		||||
                        isThinking={!!isThinking && !!isPending}
 | 
			
		||||
@@ -196,13 +203,18 @@ export default function ChatMessage({
 | 
			
		||||
          })}
 | 
			
		||||
        >
 | 
			
		||||
          {siblingLeafNodeIds && siblingLeafNodeIds.length > 1 && (
 | 
			
		||||
            <div className="flex gap-1 items-center opacity-60 text-sm">
 | 
			
		||||
            <div
 | 
			
		||||
              className="flex gap-1 items-center opacity-60 text-sm"
 | 
			
		||||
              role="navigation"
 | 
			
		||||
              aria-description={`Message version ${siblingCurrIdx + 1} of ${siblingLeafNodeIds.length}`}
 | 
			
		||||
            >
 | 
			
		||||
              <button
 | 
			
		||||
                className={classNames({
 | 
			
		||||
                  'btn btn-sm btn-ghost p-1': true,
 | 
			
		||||
                  'opacity-20': !prevSibling,
 | 
			
		||||
                })}
 | 
			
		||||
                onClick={() => prevSibling && onChangeSibling(prevSibling)}
 | 
			
		||||
                aria-label="Previous message version"
 | 
			
		||||
              >
 | 
			
		||||
                <ChevronLeftIcon className="h-4 w-4" />
 | 
			
		||||
              </button>
 | 
			
		||||
@@ -215,6 +227,7 @@ export default function ChatMessage({
 | 
			
		||||
                  'opacity-20': !nextSibling,
 | 
			
		||||
                })}
 | 
			
		||||
                onClick={() => nextSibling && onChangeSibling(nextSibling)}
 | 
			
		||||
                aria-label="Next message version"
 | 
			
		||||
              >
 | 
			
		||||
                <ChevronRightIcon className="h-4 w-4" />
 | 
			
		||||
              </button>
 | 
			
		||||
@@ -223,7 +236,7 @@ export default function ChatMessage({
 | 
			
		||||
          {/* user message */}
 | 
			
		||||
          {msg.role === 'user' && (
 | 
			
		||||
            <BtnWithTooltips
 | 
			
		||||
              className="btn-mini show-on-hover w-8 h-8"
 | 
			
		||||
              className="btn-mini w-8 h-8"
 | 
			
		||||
              onClick={() => setEditingContent(msg.content)}
 | 
			
		||||
              disabled={msg.content === null}
 | 
			
		||||
              tooltipsContent="Edit message"
 | 
			
		||||
@@ -236,7 +249,7 @@ export default function ChatMessage({
 | 
			
		||||
            <>
 | 
			
		||||
              {!isPending && (
 | 
			
		||||
                <BtnWithTooltips
 | 
			
		||||
                  className="btn-mini show-on-hover w-8 h-8"
 | 
			
		||||
                  className="btn-mini w-8 h-8"
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    if (msg.content !== null) {
 | 
			
		||||
                      onRegenerateMessage(msg as Message);
 | 
			
		||||
@@ -250,10 +263,7 @@ export default function ChatMessage({
 | 
			
		||||
              )}
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
          <CopyButton
 | 
			
		||||
            className="btn-mini show-on-hover w-8 h-8"
 | 
			
		||||
            content={msg.content}
 | 
			
		||||
          />
 | 
			
		||||
          <CopyButton className="btn-mini w-8 h-8" content={msg.content} />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -271,6 +281,8 @@ function ThoughtProcess({
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      role="button"
 | 
			
		||||
      aria-label="Toggle thought process display"
 | 
			
		||||
      tabIndex={0}
 | 
			
		||||
      className={classNames({
 | 
			
		||||
        'collapse bg-none': true,
 | 
			
		||||
@@ -292,7 +304,11 @@ function ThoughtProcess({
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="collapse-content text-base-content/70 text-sm p-1">
 | 
			
		||||
      <div
 | 
			
		||||
        className="collapse-content text-base-content/70 text-sm p-1"
 | 
			
		||||
        tabIndex={0}
 | 
			
		||||
        aria-description="Thought process content"
 | 
			
		||||
      >
 | 
			
		||||
        <div className="border-l-2 border-base-content/20 pl-4 mb-4">
 | 
			
		||||
          <MarkdownDisplay content={content} />
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -279,7 +279,11 @@ export default function ChatScreen() {
 | 
			
		||||
function ServerInfo() {
 | 
			
		||||
  const { serverProps } = useAppContext();
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="card card-sm shadow-sm border-1 border-base-content/20 text-base-content/70 mb-6">
 | 
			
		||||
    <div
 | 
			
		||||
      className="card card-sm shadow-sm border-1 border-base-content/20 text-base-content/70 mb-6"
 | 
			
		||||
      tabIndex={0}
 | 
			
		||||
      aria-description="Server information"
 | 
			
		||||
    >
 | 
			
		||||
      <div className="card-body">
 | 
			
		||||
        <b>Server Info</b>
 | 
			
		||||
        <p>
 | 
			
		||||
@@ -311,6 +315,8 @@ function ChatInput({
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      role="group"
 | 
			
		||||
      aria-label="Chat input"
 | 
			
		||||
      className={classNames({
 | 
			
		||||
        'flex items-end pt-8 pb-6 sticky bottom-0 bg-base-100': true,
 | 
			
		||||
        'opacity-50': isDrag, // simply visual feedback to inform user that the file will be accepted
 | 
			
		||||
@@ -400,13 +406,15 @@ function ChatInput({
 | 
			
		||||
                    'btn w-8 h-8 p-0 rounded-full': true,
 | 
			
		||||
                    'btn-disabled': isGenerating,
 | 
			
		||||
                  })}
 | 
			
		||||
                  aria-label="Upload file"
 | 
			
		||||
                  tabIndex={0}
 | 
			
		||||
                  role="button"
 | 
			
		||||
                >
 | 
			
		||||
                  <PaperClipIcon className="h-5 w-5" />
 | 
			
		||||
                </label>
 | 
			
		||||
                <input
 | 
			
		||||
                  id="file-upload"
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  className="hidden"
 | 
			
		||||
                  disabled={isGenerating}
 | 
			
		||||
                  {...getInputProps()}
 | 
			
		||||
                  hidden
 | 
			
		||||
@@ -422,6 +430,7 @@ function ChatInput({
 | 
			
		||||
                  <button
 | 
			
		||||
                    className="btn btn-primary w-8 h-8 p-0 rounded-full"
 | 
			
		||||
                    onClick={onSend}
 | 
			
		||||
                    aria-label="Send message"
 | 
			
		||||
                  >
 | 
			
		||||
                    <ArrowUpIcon className="h-5 w-5" />
 | 
			
		||||
                  </button>
 | 
			
		||||
 
 | 
			
		||||
@@ -38,8 +38,12 @@ export default function Header() {
 | 
			
		||||
 | 
			
		||||
      {/* action buttons (top right) */}
 | 
			
		||||
      <div className="flex items-center">
 | 
			
		||||
        <div className="tooltip tooltip-bottom" data-tip="Settings">
 | 
			
		||||
          <button className="btn" onClick={() => setShowSettings(true)}>
 | 
			
		||||
        <div
 | 
			
		||||
          className="tooltip tooltip-bottom"
 | 
			
		||||
          data-tip="Settings"
 | 
			
		||||
          onClick={() => setShowSettings(true)}
 | 
			
		||||
        >
 | 
			
		||||
          <button className="btn" aria-hidden={true}>
 | 
			
		||||
            {/* settings button */}
 | 
			
		||||
            <Cog8ToothIcon className="w-5 h-5" />
 | 
			
		||||
          </button>
 | 
			
		||||
 
 | 
			
		||||
@@ -335,14 +335,22 @@ export default function SettingDialog({
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <dialog className={classNames({ modal: true, 'modal-open': show })}>
 | 
			
		||||
    <dialog
 | 
			
		||||
      className={classNames({ modal: true, 'modal-open': show })}
 | 
			
		||||
      aria-label="Settings dialog"
 | 
			
		||||
    >
 | 
			
		||||
      <div className="modal-box w-11/12 max-w-3xl">
 | 
			
		||||
        <h3 className="text-lg font-bold mb-6">Settings</h3>
 | 
			
		||||
        <div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
 | 
			
		||||
          {/* Left panel, showing sections - Desktop version */}
 | 
			
		||||
          <div className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200">
 | 
			
		||||
          <div
 | 
			
		||||
            className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200"
 | 
			
		||||
            role="complementary"
 | 
			
		||||
            aria-description="Settings sections"
 | 
			
		||||
            tabIndex={0}
 | 
			
		||||
          >
 | 
			
		||||
            {SETTING_SECTIONS.map((section, idx) => (
 | 
			
		||||
              <div
 | 
			
		||||
              <button
 | 
			
		||||
                key={idx}
 | 
			
		||||
                className={classNames({
 | 
			
		||||
                  'btn btn-ghost justify-start font-normal w-44 mb-1': true,
 | 
			
		||||
@@ -352,12 +360,16 @@ export default function SettingDialog({
 | 
			
		||||
                dir="auto"
 | 
			
		||||
              >
 | 
			
		||||
                {section.title}
 | 
			
		||||
              </div>
 | 
			
		||||
              </button>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {/* Left panel, showing sections - Mobile version */}
 | 
			
		||||
          <div className="md:hidden flex flex-row gap-2 mb-4">
 | 
			
		||||
          {/* This menu is skipped on a11y, otherwise it's repeated the desktop version */}
 | 
			
		||||
          <div
 | 
			
		||||
            className="md:hidden flex flex-row gap-2 mb-4"
 | 
			
		||||
            aria-disabled={true}
 | 
			
		||||
          >
 | 
			
		||||
            <details className="dropdown">
 | 
			
		||||
              <summary className="btn bt-sm w-full m-1">
 | 
			
		||||
                {SETTING_SECTIONS[sectionIdx].title}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,44 +50,72 @@ export default function Sidebar() {
 | 
			
		||||
        id="toggle-drawer"
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
        className="drawer-toggle"
 | 
			
		||||
        aria-label="Toggle sidebar"
 | 
			
		||||
        defaultChecked
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div className="drawer-side h-screen lg:h-screen z-50 lg:max-w-64">
 | 
			
		||||
      <div
 | 
			
		||||
        className="drawer-side h-screen lg:h-screen z-50 lg:max-w-64"
 | 
			
		||||
        role="complementary"
 | 
			
		||||
        aria-label="Sidebar"
 | 
			
		||||
        tabIndex={0}
 | 
			
		||||
      >
 | 
			
		||||
        <label
 | 
			
		||||
          htmlFor="toggle-drawer"
 | 
			
		||||
          aria-label="close sidebar"
 | 
			
		||||
          aria-label="Close sidebar"
 | 
			
		||||
          className="drawer-overlay"
 | 
			
		||||
        ></label>
 | 
			
		||||
 | 
			
		||||
        <a
 | 
			
		||||
          href="#main-scroll"
 | 
			
		||||
          className="absolute -left-80 top-0 w-1 h-1 overflow-hidden"
 | 
			
		||||
        >
 | 
			
		||||
          Skip to main content
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <div className="flex flex-col bg-base-200 min-h-full max-w-64 py-4 px-4">
 | 
			
		||||
          <div className="flex flex-row items-center justify-between mb-4 mt-4">
 | 
			
		||||
            <h2 className="font-bold ml-4">Conversations</h2>
 | 
			
		||||
            <h2 className="font-bold ml-4" role="heading">
 | 
			
		||||
              Conversations
 | 
			
		||||
            </h2>
 | 
			
		||||
 | 
			
		||||
            {/* close sidebar button */}
 | 
			
		||||
            <label htmlFor="toggle-drawer" className="btn btn-ghost lg:hidden">
 | 
			
		||||
            <label
 | 
			
		||||
              htmlFor="toggle-drawer"
 | 
			
		||||
              className="btn btn-ghost lg:hidden"
 | 
			
		||||
              aria-label="Close sidebar"
 | 
			
		||||
              role="button"
 | 
			
		||||
              tabIndex={0}
 | 
			
		||||
            >
 | 
			
		||||
              <XMarkIcon className="w-5 h-5" />
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {/* new conversation button */}
 | 
			
		||||
          <div
 | 
			
		||||
          <button
 | 
			
		||||
            className={classNames({
 | 
			
		||||
              'btn btn-ghost justify-start px-2': true,
 | 
			
		||||
              'btn-soft': !currConv,
 | 
			
		||||
            })}
 | 
			
		||||
            onClick={() => navigate('/')}
 | 
			
		||||
            aria-label="New conversation"
 | 
			
		||||
          >
 | 
			
		||||
            <PencilSquareIcon className="w-5 h-5" />
 | 
			
		||||
            New conversation
 | 
			
		||||
          </div>
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          {/* list of conversations */}
 | 
			
		||||
          {groupedConv.map((group, i) => (
 | 
			
		||||
            <div key={i}>
 | 
			
		||||
            <div key={i} role="group">
 | 
			
		||||
              {/* group name (by date) */}
 | 
			
		||||
              {group.title ? (
 | 
			
		||||
                // we use btn class here to make sure that the padding/margin are aligned with the other items
 | 
			
		||||
                <b className="btn btn-ghost btn-xs bg-none btn-disabled block text-xs text-base-content text-start px-2 mb-0 mt-6 font-bold">
 | 
			
		||||
                <b
 | 
			
		||||
                  className="btn btn-ghost btn-xs bg-none btn-disabled block text-xs text-base-content text-start px-2 mb-0 mt-6 font-bold"
 | 
			
		||||
                  role="note"
 | 
			
		||||
                  aria-description={group.title}
 | 
			
		||||
                  tabIndex={0}
 | 
			
		||||
                >
 | 
			
		||||
                  {group.title}
 | 
			
		||||
                </b>
 | 
			
		||||
              ) : (
 | 
			
		||||
@@ -184,20 +212,23 @@ function ConversationItem({
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      role="menuitem"
 | 
			
		||||
      tabIndex={0}
 | 
			
		||||
      aria-label={conv.name}
 | 
			
		||||
      className={classNames({
 | 
			
		||||
        'group flex flex-row btn btn-ghost justify-start items-center font-normal px-2 h-9':
 | 
			
		||||
          true,
 | 
			
		||||
        'btn-soft': isCurrConv,
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
      <button
 | 
			
		||||
        key={conv.id}
 | 
			
		||||
        className="w-full overflow-hidden truncate text-start"
 | 
			
		||||
        onClick={onSelect}
 | 
			
		||||
        dir="auto"
 | 
			
		||||
      >
 | 
			
		||||
        {conv.name}
 | 
			
		||||
      </div>
 | 
			
		||||
      </button>
 | 
			
		||||
      <div className="dropdown dropdown-end h-5">
 | 
			
		||||
        <BtnWithTooltips
 | 
			
		||||
          // on mobile, we always show the ellipsis icon
 | 
			
		||||
@@ -211,22 +242,23 @@ function ConversationItem({
 | 
			
		||||
        </BtnWithTooltips>
 | 
			
		||||
        {/* dropdown menu */}
 | 
			
		||||
        <ul
 | 
			
		||||
          aria-label="More options"
 | 
			
		||||
          tabIndex={0}
 | 
			
		||||
          className="dropdown-content menu bg-base-100 rounded-box z-[1] p-2 shadow"
 | 
			
		||||
        >
 | 
			
		||||
          <li onClick={onRename}>
 | 
			
		||||
          <li onClick={onRename} tabIndex={0}>
 | 
			
		||||
            <a>
 | 
			
		||||
              <PencilIcon className="w-4 h-4" />
 | 
			
		||||
              Rename
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li onClick={onDownload}>
 | 
			
		||||
          <li onClick={onDownload} tabIndex={0}>
 | 
			
		||||
            <a>
 | 
			
		||||
              <ArrowDownTrayIcon className="w-4 h-4" />
 | 
			
		||||
              Download
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li className="text-error" onClick={onDelete}>
 | 
			
		||||
          <li className="text-error" onClick={onDelete} tabIndex={0}>
 | 
			
		||||
            <a>
 | 
			
		||||
              <TrashIcon className="w-4 h-4" />
 | 
			
		||||
              Delete
 | 
			
		||||
 
 | 
			
		||||
@@ -34,9 +34,6 @@ html {
 | 
			
		||||
  /* TODO: fix markdown table */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.show-on-hover {
 | 
			
		||||
  @apply md:opacity-0 md:group-hover:opacity-100;
 | 
			
		||||
}
 | 
			
		||||
.btn-mini {
 | 
			
		||||
  @apply cursor-pointer;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,13 +52,20 @@ export function BtnWithTooltips({
 | 
			
		||||
  tooltipsContent: string;
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  // the onClick handler is on the container, so screen readers can safely ignore the inner button
 | 
			
		||||
  // this prevents the label from being read twice
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="tooltip tooltip-bottom" data-tip={tooltipsContent}>
 | 
			
		||||
    <div
 | 
			
		||||
      className="tooltip tooltip-bottom"
 | 
			
		||||
      data-tip={tooltipsContent}
 | 
			
		||||
      role="button"
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
    >
 | 
			
		||||
      <button
 | 
			
		||||
        className={`${className ?? ''} flex items-center justify-center`}
 | 
			
		||||
        onClick={onClick}
 | 
			
		||||
        disabled={disabled}
 | 
			
		||||
        onMouseLeave={onMouseLeave}
 | 
			
		||||
        aria-hidden={true}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </button>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user