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 hỗ trợ tiếp cận.

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

Bản minh hoạ

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

Tổng quan

Nút phân tách là các nút ẩn một nút chính và danh sách các nút bổ sung. Các thao tác này rất hữu ích trong việc 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 bận rộn trong thiết kế. 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à đưa hành động đó vào vị trí chính.

Bạn có thể tìm thấy một nút phân tách phổ biến trong ứng dụng email của mình. 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 một ứng dụng email.

Khu vực hành động được chia sẻ rất đẹp vì người dùng không cần phải quan sát xung quanh. Họ biết rằng các thao tác thiết yếu đối với email đều có 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ề việc tổ chức tổng thể và trải nghiệm người dùng cuối cùng. Công cụ kiểm tra hỗ trợ tiếp cận của VisBug được dùng ở đây để hiển thị khung hiển thị macro của thành phần, hiển thị các khía cạnh về HTML, kiểu và khả năng tiếp cận cho từng phần chính.

Các phần tử HTML tạo nên nút 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 linh hoạt cùng dòng, có một lớp gui-split-button, chứa thao tác chính.gui-popup-button.

Lớp nút phân tách đượ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> có thể hiển thị và có thể làm tâm điểm ban đầu nằm vừa trong vùng chứa có hai hình góc giống nhau dành cho các hoạt động tương tác lấy nét, hoverđ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à ám chỉ danh sách các nút phụ. Hãy lưu ý rằng đây không phải là <button> và không thể làm tâm điểm. Tuy nhiên, đây là neo định vị cho .gui-popup và máy chủ lưu trữ cho :focus-within được dùng để hiển thị cửa sổ bật lên.

Trình kiểm tra cho thấy các quy tắc CSS cho lớp gui-pop-button.

Thẻ bật lên

Đây là một thẻ nổi con đối với điểm neo .gui-popup-button của nó, được định vị tuyệt đối và gói 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-pop

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

<button> có thể làm tâm điểm với cỡ chữ nhỏ hơn một chút so với nút hành động chính có một biểu tượng và kiểu tuỳ chỉnh 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 hỗ trợ tạo ra sự hài hoà màu sắc và là vị trí trung tâm để sửa đổi các giá trị được sử 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 dưới dạng <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>

Hãy lưu ý thuộc tính aria aria-haspopuparia-expanded. Những tín hiệu này đóng vai trò quan trọng giúp trình đọc màn hình nhận biết được tính năng và trạng thái của trải nghiệm nút phân tách. Thuộc tính title rất 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>

Đối với vị trí cửa sổ bật lên đơn giản, .gui-popup là phần tử con của nút mở rộng vị trí đó. 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ì chiến lược này sẽ cắt bớt cửa sổ bật lên để không hiển thị trực quan.

<ul> chứa nội dung <li><button> sẽ tự thông báo là "danh sách nút" đối với trình đọc màn hình, chính là giao diện đang được 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ăng sự tinh tế và tận hưởng màu sắc thú vị, tôi đã thêm các biểu tượng vào các nút phụ trong https://heroicons.com. Bạn không bắt buộc phải sử dụng các 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

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

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

Loại màn hình inline-flex hoạt động hiệu quả cho thành phần gói này vì loại màn hình này phải phù hợp với các nút, thao tác 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 chia tách.

Kiểu <button>

Các nút rất hiệu quả trong việc ẩn 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 cũng cần thực thi một số kiểu kế thừa, thêm trạng thái tương tác và điều chỉnh cho phù hợp với các lựa chọn ưu tiên của người dùng và kiểu dữ liệu đầu vào. Kiểu nút sẽ tăng lên nhanh chóng.

Các nút này khác với các nút thông thường vì chúng có 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. Tuy nhiên, những chủ đề này chia sẻ ý tưởng và chỉ áp dụng nền tảng của riêng chúng vào 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 trạng thái tương tác bằng một số 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 vài 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, để tinh tế hơn, nút và biểu tượng giao diện sáng sẽ có đổ 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));
    }
  }
}

Nút tuyệt vời đã chú ý đến các 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à điểm quan trọng để tạo một giao diện người dùng dễ tiếp cận, nhưng nó có một điểm hạn chế: không thông minh về việc người dùng có cần xem hay không, giao diện này sẽ áp dụng cho bất kỳ tiêu điểm nào.

Video dưới đây cố gắng phân tích 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.

Tạo kiểu cho nút bật lên

Hộp linh hoạt 4ch để căn giữa một biểu tượng và cố định vào danh sách nút bật lên. Giống như nút chính, nút này trong suốt cho đến khi được di chuột hoặc tương tác và được 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);
}

Tạo 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 CSS Nesting 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à yếu tố thu hút chính để hiện 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;
    }
  }
}

Khi đã hoàn tất các kiểu vào và ra, phần cuối cùng là biến đổi chuyển đổi có điều kiện tuỳ thuộc vào lựa chọn chuyển động ưu tiên 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 tâm kỹ đến mã này, bạn sẽ nhận thấy độ mờ vẫn đang được chuyển đổi đối với những người dùng muốn giảm chuyển động.

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

Phần tử .gui-popup là một 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 sao cho nhỏ hơn một cách tinh tế, khớp tương tác với nút chính và trên thương hiệu có sử dụng màu sắc. Lưu ý các biểu tượng có độ tương phản thấp hơn, mỏng hơn và bóng có chút màu xanh thương hiệu. Giống như với các nút, trải nghiệm người dùng và trải nghiệm người dùng mạnh mẽ là nhờ những chi tiết nhỏ được xếp chồng lên nhau.

Một thành phần 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 thương hiệu để tạo kiểu đẹp mắt trong mỗi thẻ theo chủ đề tối và sáng:

Các đường liên kết và biểu tượng để thanh toán, Thanh toán nhanh và Lưu để dùng 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ủa giao diện tối có thêm hiệu ứng đổ bóng văn bản và biểu tượng, cùng với hiệu ứng bóng đổ hộp mạnh mẽ hơn một chút:

Cửa sổ bật lên trong 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 dùng trong đó bằng cách sử dụng đơn vị ch làm inline-size. Mỗi kiểu cũng được cung cấp một số kiểu để giúp vẽ biểu tượng một cách 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 mọi công việc phức tạp. Dưới đây là danh sách các thuộc tính logic được 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ì cách viết tắt padding, tận hưở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 theo hướng tài liệu. – inline-size thay vì width đảm bảo kích thước không liên quan đến kích thước thực. – border-inline-start thêm đường viền vào phần đầu, có thể nằm ở 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ư tất cả JavaScript sau đây là để tăng cường khả năng tiếp cận. Tôi dùng 2 thư viện trình trợ giúp để thực hiện các nhiệm vụ dễ dàng hơn một chút. BlingBlingJS được dùng cho các 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 trên bàn phím và tay điều khiển trò chơi dễ tiếp cận 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')

Sau khi đã nhập các thư viện ở trên cũng như các phần tử được chọn và lưu vào các biến, việc nâng cấp trải nghiệm sẽ chỉ còn vài chức năng nữa là hoàn thành.

Chỉ số lưu động

Khi trình đọc màn hình hoặc bàn phím lấy tiêu điểm .gui-popup-button, chúng ta muốn chuyển tiếp tiêu điểm đến nút đầu tiên (hoặc nút được lấy làm tâm điểm gần đây nhất) trong .gui-popup. Thư viện 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ẽ truyền tiêu điểm đến phần tử con <button> mục tiêu và cho phép điều hướng bằng phím mũi tên chuẩn để duyệt qua các lựa chọn.

Bật/tắt aria-expanded

Mặc dù có thể thấy rõ ràng là cửa sổ bật lên đang xuất hiện và ẩn, nhưng trình đọc màn hình không chỉ cần đưa ra tín hiệu bằng hình ảnh. JavaScript được dùng ở đây để bổ trợ cho hoạt động tương tác :focus-within do CSS mang lại bằng cách bật/tắt một thuộc tính thích hợp của 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 phím Escape

Tiêu điểm của người dùng đã bị cố ý chuyển đến một 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. Để thực hiện việc nà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 phần tử con sẽ hiện lên bong bóng cho phần tử 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ỳ thao tác nhấn phím Escape nào, thì nút này sẽ xoá tiêu điểm khỏi chính nó bằng blur().

Số lần nhấp vào nút 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 bong bóng sự kiện được sử dụng lại ở đây, nhưng lần này trên vùng chứa .gui-split-button để thu thập các lượt nhấp vào nút từ cửa sổ bật lên con hoặc hành động chính.

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

Kết luận

Giờ bạn đã biết tôi làm được như thế nào, bạn sẽ làm thế nào 🙂

Hãy đa dạng hoá phương pháp tiếp cận của chúng ta và tìm hiểu tất cả các cách xây dựng trên web. Hãy tạo một bản minh hoạ, đường liên kết tweet me và tôi sẽ thêm bản phối lại đó vào phần bả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