Xây dựng thành phần nút phân tách

Thông tin tổng quan cơ bản về cách tạo thành phần nút phân tách dễ tiếp cận.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo một nút phân tách. Dùng thử bản minh hoạ.

Bản minh hoạ

Nếu bạn thích xem video, hãy xem phiên bản video của bài đăng này trên YouTube:

Tổng quan

Nút phân tách là các nút che giấu một nút chính và danh sách các nút bổ sung. Chúng rất hữu ích khi hiển thị một thao tác phổ biến trong khi lồng các thao tác phụ, ít được sử dụng hơn cho đến khi cần. Nút phân tách có thể đóng vai trò quan trọng trong việc giúp giảm thiểu cảm giác thiết kế bận rộn. Nút phân tách nâng cao thậm chí có thể ghi nhớ hành động gần đây nhất của người dùng và quảng bá thao tác đó ở vị trí chính.

Bạn có thể tìm thấy nút tách phổ biến trong ứng dụng email. Hành động chính là gửi, nhưng có thể bạn có thể gửi sau hoặc lưu bản nháp:

Ví dụ về nút phân tách như trong ứng dụng email.

Khu vực thao tác chung rất hữu ích vì người dùng không cần phải nhìn xung quanh. Họ biết rằng các thao tác email thiết yếu nằm trong nút phân tách.

Phụ tùng

Hãy phân tích các phần thiết yếu của nút phân tách trước khi thảo luận về cách điều phối tổng thể và trải nghiệm người dùng cuối cùng. Công cụ kiểm tra khả năng hỗ trợ tiếp cận của VisBug được dùng để hiển thị chế độ xem macro của thành phần, hiển thị các khía cạnh HTML, kiểu và hỗ trợ tiếp cận cho từng phần chính.

Các phần tử HTML tạo nên nút phân tách.

Vùng chứa nút phân tách cấp cao nhất

Thành phần cấp cao nhất là một hộp flex nội tuyến, với một lớp gui-split-button, chứa thao tác chính.gui-popup-button.

Lớp gui-split-button được kiểm tra và hiển thị các thuộc tính CSS được sử dụng trong lớp này.

Nút hành động chính

<button> ban đầu hiển thị và có thể lấy tiêu điểm vừa với vùng chứa có hai hình dạng góc phù hợp cho các hoạt động tương tác tiêu điểm, di chuộtđang hoạt động xuất hiện trong .gui-split-button.

Trình kiểm tra hiển thị các quy tắc CSS cho phần tử nút.

Nút bật/tắt cửa sổ bật lên

Phần tử hỗ trợ "nút bật lên" dùng để kích hoạt và gợi nhắc đến danh sách các nút phụ. Lưu ý rằng đây không phải là <button> và không thể lấy tiêu điểm. Tuy nhiên, đây là neo định vị cho .gui-popup và máy chủ lưu trữ cho :focus-within dùng để hiển thị cửa sổ bật lên.

Trình kiểm tra hiển thị các quy tắc CSS cho lớp gui-popup-button.

Thẻ bật lên

Đây là một thẻ con nổi đối với phần neo .gui-popup-button, được định vị tuyệt đối và bao bọc danh sách nút theo ngữ nghĩa.

Trình kiểm tra hiển thị các quy tắc CSS cho lớp gui-popup

(Các) hành động phụ

<button> có thể lấy tiêu điểm với cỡ chữ hơi nhỏ hơn nút hành động chính, có biểu tượng và kiểu bổ sung cho nút chính.

Trình kiểm tra hiển thị các quy tắc CSS cho phần tử nút.

Thuộc tính tuỳ chỉnh

Các biến sau đây hỗ trợ tạo ra sự hài hoà màu sắc và là trung tâm để sửa đổi các giá trị dùng trong toàn bộ thành phần.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);

.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);

  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;

  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}

Bố cục và màu sắc

Markup (note: đây là tên ứng dụng)

Phần tử bắt đầu là <div> với tên lớp tuỳ chỉnh.

<div class="gui-split-button"></div>

Thêm nút chính và các phần tử .gui-popup-button.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>

Lưu ý các thuộc tính aria aria-haspopuparia-expanded. Những tín hiệu này rất quan trọng để trình đọc màn hình nhận biết được chức năng và trạng thái của trải nghiệm nút phân tách. Thuộc tính title hữu ích cho mọi người.

Thêm biểu tượng <svg> và phần tử vùng chứa .gui-popup.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>

Để đặt cửa sổ bật lên một cách đơn giản, .gui-popup là phần tử con của nút mở rộng cửa sổ bật lên. Điểm hạn chế duy nhất của chiến lược này là vùng chứa .gui-split-button không thể sử dụng overflow: hidden, vì vùng chứa này sẽ cắt bớt cửa sổ bật lên khỏi chế độ hiển thị.

<ul> chứa nội dung <li><button> sẽ tự thông báo là "danh sách nút" cho trình đọc màn hình, chính là giao diện đang hiển thị.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>

Để tạo điểm nhấn và vui chơi với màu sắc, tôi đã thêm các biểu tượng vào nút phụ từ https://heroicons.com. Bạn không bắt buộc phải sử dụng biểu tượng cho cả nút chính và nút phụ.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>

Kiểu

Khi đã có HTML và nội dung, các kiểu sẽ sẵn sàng để cung cấp màu sắc và bố cục.

Tạo kiểu cho vùng chứa nút phân tách

Loại màn hình inline-flex hoạt động tốt cho thành phần gói này vì nó phải phù hợp với cùng dòng với các nút, hành động hoặc phần tử phân tách khác.

.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);

  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

Nút phân tách.

Kiểu <button>

Nút rất hữu ích trong việc che giấu lượng mã cần thiết. Bạn có thể cần huỷ hoặc thay thế các kiểu mặc định của trình duyệt, nhưng bạn cũng cần thực thi một số tính năng kế thừa, thêm trạng thái tương tác và điều chỉnh cho phù hợp với nhiều lựa chọn ưu tiên của người dùng và loại dữ liệu đầu vào. Các kiểu nút sẽ nhanh chóng được thêm vào.

Các nút này khác với nút thông thường vì dùng chung nền với một phần tử mẹ. Thông thường, nút có màu nền và màu văn bản riêng. Tuy nhiên, những chủ đề này chia sẻ và chỉ áp dụng kiến thức nền tảng của riêng họ về hoạt động tương tác.

.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;

  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;

  font-family: inherit;
  font-size: inherit;
  font-weight: 500;

  padding-block: 1.25ch;
  padding-inline: 2.5ch;

  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}

Thêm các trạng thái tương tác bằng một vài lớp giả CSS và sử dụng các thuộc tính tuỳ chỉnh phù hợp cho trạng thái:

.gui-split-button button {
  

  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);

    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }

  &:active {
    background: var(--theme-active);
  }
}

Nút chính cần một số kiểu đặc biệt để hoàn tất hiệu ứng thiết kế:

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

Cuối cùng, để tạo thêm điểm nhấn, nút và biểu tượng giao diện sáng sẽ có một bóng:

.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}

Một nút tuyệt vời đã chú ý đến các hoạt động tương tác vi mô và các chi tiết nhỏ.

Ghi chú về :focus-visible

Hãy lưu ý cách các kiểu nút sử dụng :focus-visible thay vì :focus. :focus là một điểm nhấn quan trọng để tạo ra một giao diện người dùng dễ truy cập nhưng nó có một nhược điểm: nó không thông minh về việc người dùng có cần xem giao diện hay không, nó có áp dụng cho bất kỳ tiêu điểm nào hay không.

Video bên dưới cố gắng phân tích chi tiết về hoạt động tương tác vi mô này để cho thấy :focus-visible là một giải pháp thay thế thông minh như thế nào.

Tạo kiểu cho nút cửa sổ bật lên

Hộp linh hoạt 4ch dùng để căn giữa biểu tượng và cố định danh sách nút bật lên. Giống như nút chính, nút này sẽ trong suốt cho đến khi bạn di chuột qua hoặc tương tác với nút này và kéo giãn để lấp đầy.

Phần mũi tên của nút phân tách dùng để kích hoạt cửa sổ bật lên.

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

Lớp trong trạng thái di chuột, tiêu điểm và trạng thái đang hoạt động bằng tính năng Lồng ghép CSS và bộ chọn chức năng :is():

.gui-popup-button {
  

  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }

  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }

  &:active {
    background: var(--theme-active);
  }
}

Các kiểu này là phần phụ thuộc chính để hiển thị và ẩn cửa sổ bật lên. Khi .gui-popup-buttonfocus trên bất kỳ phần tử con nào, hãy đặt opacity, vị trí và pointer-events trên biểu tượng và cửa sổ bật lên.

.gui-popup-button {
  

  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}

Sau khi hoàn tất các kiểu vào và ra, phần cuối cùng là các biến đổi chuyển đổi có điều kiện tuỳ thuộc vào lựa chọn ưu tiên về chuyển động của người dùng:

.gui-popup-button {
  

  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);

      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}

Nếu quan sát kỹ mã, bạn sẽ thấy độ mờ vẫn được chuyển đổi cho những người dùng thích giảm chuyển động.

Định kiểu cho cửa sổ bật lên

Phần tử .gui-popup là danh sách nút thẻ nổi sử dụng các thuộc tính tuỳ chỉnh và đơn vị tương đối để có kích thước nhỏ hơn một chút, tương tác với nút chính và phù hợp với thương hiệu bằng cách sử dụng màu sắc. Lưu ý rằng các biểu tượng có độ tương phản thấp hơn, mỏng hơn và có một chút màu xanh dương của thương hiệu. Giống như các nút, giao diện người dùng và trải nghiệm người dùng mạnh mẽ là kết quả của những chi tiết nhỏ này.

Phần tử thẻ nổi.

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;

  opacity: 0;
  pointer-events: none;

  position: absolute;
  bottom: 80%;
  left: -1.5ch;

  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;

  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}

Các biểu tượng và nút được cung cấp màu sắc thương hiệu để tạo kiểu đẹp mắt trong mỗi thẻ theo chủ đề tối và sáng:

Đường liên kết và biểu tượng để thanh toán, Thanh toán nhanh và Lưu để sau.

.gui-popup {
  

  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);

    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }

  & button {
    color: var(--theme-text);
    width: 100%;
  }
}

Cửa sổ bật lên có giao diện tối bổ sung bóng văn bản và bóng biểu tượng, cùng với bóng hộp mạnh hơn một chút:

Cửa sổ bật lên ở giao diện tối.

.gui-popup {
  

  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;

    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }

    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}

Kiểu biểu tượng <svg> chung

Tất cả biểu tượng đều có kích thước tương đối so với nút font-size mà chúng được sử dụng trong đó bằng cách sử dụng đơn vị ch làm inline-size. Mỗi biểu tượng cũng được cung cấp một số kiểu để giúp đường viền biểu tượng mềm mại và mượt mà.

.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}

Bố cục từ phải sang trái

Thuộc tính logic thực hiện tất cả các công việc phức tạp. Dưới đây là danh sách các thuộc tính logic được sử dụng: – display: inline-flex tạo một phần tử linh hoạt cùng dòng. – padding-blockpadding-inline dưới dạng một cặp, thay vì viết tắt padding, tận dụng lợi ích của khoảng đệm các cạnh logic. – border-end-start-radiusbạn bè sẽ bo tròn các góc dựa trên hướng của tài liệu. – inline-size thay vì width đảm bảo kích thước không gắn liền với kích thước thực. – border-inline-start thêm đường viền vào phần bắt đầu, có thể ở bên phải hoặc bên trái tuỳ thuộc vào hướng tập lệnh.

JavaScript

Hầu như toàn bộ JavaScript sau đây đều dùng để tăng cường khả năng hỗ trợ tiếp cận. Hai trong số các thư viện trình trợ giúp của tôi được dùng để giúp các tác vụ trở nên dễ dàng hơn một chút. BlingBlingJS được dùng để truy vấn DOM ngắn gọn và thiết lập trình nghe sự kiện dễ dàng, trong khi roving-ux giúp hỗ trợ các hoạt động tương tác bằng bàn phím và tay điều khiển có thể truy cập được cho cửa sổ bật lên.

import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'

const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')

Với các thư viện trên đã được nhập và các phần tử được chọn và lưu vào biến, bạn chỉ cần thêm một vài hàm nữa là có thể hoàn tất việc nâng cấp trải nghiệm.

Chỉ mục di chuyển

Khi bàn phím hoặc trình đọc màn hình đặt tiêu điểm vào .gui-popup-button, chúng ta muốn chuyển tiêu điểm vào nút đầu tiên (hoặc nút được đặt tiêu điểm gần đây nhất) trong .gui-popup. Thư viện này giúp chúng ta thực hiện việc này bằng các tham số elementtarget.

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

Giờ đây, phần tử này sẽ chuyển tiêu điểm đến các phần tử con <button> mục tiêu và cho phép di chuyển bằng phím mũi tên tiêu chuẩn để duyệt qua các tuỳ chọn.

Bật/tắt aria-expanded

Mặc dù rõ ràng là cửa sổ bật lên đang hiển thị và ẩn, nhưng trình đọc màn hình cần nhiều hơn là các tín hiệu hình ảnh. JavaScript được dùng ở đây để bổ sung cho hoạt động tương tác :focus-within do CSS điều khiển bằng cách bật/tắt một thuộc tính thích hợp cho trình đọc màn hình.

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

Bật khoá Escape

Sự tập trung của người dùng được chủ ý đưa vào một cái bẫy, nghĩa là chúng ta cần phải đưa ra cách rời đi. Cách phổ biến nhất là cho phép sử dụng khoá Escape. Để làm như vậy, hãy theo dõi các thao tác nhấn phím trên nút bật lên, vì mọi sự kiện bàn phím trên các thành phần con sẽ chuyển lên thành phần mẹ này.

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

Nếu nút bật lên thấy bất kỳ lần nhấn phím Escape nào, nút này sẽ tự xoá tiêu điểm khỏi chính nút đó bằng blur().

Số lượt nhấp vào nút phân tách

Cuối cùng, nếu người dùng nhấp, nhấn hoặc bàn phím tương tác với các nút, thì ứng dụng cần thực hiện hành động thích hợp. Tính năng nổi sự kiện được sử dụng lại ở đây, nhưng lần này là trên vùng chứa .gui-split-button, để phát hiện các lượt nhấp vào nút từ cửa sổ bật lên con hoặc thao tác chính.

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

Kết luận

Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao‽ 🙂

Hãy đa dạng hoá các phương pháp và tìm hiểu tất cả các cách xây dựng trên web. Hãy tạo bản minh hoạ, gửi đường liên kết cho tôi trên Twitter và tôi sẽ thêm bản minh hoạ đó vào phần phối lại của cộng đồng ở bên dưới!

Bản phối lại của cộng đồng