Skip to content

Conversation

vanessatran-ddi
Copy link
Collaborator

@vanessatran-ddi vanessatran-ddi commented Aug 28, 2025

Before (the change)

After (the change)

Make sure that you've checked the boxes below before you submit the PR

  • I have read and followed the setup steps
  • I have created necessary unit tests
  • I have tested the functionality in both React and Angular.

Steps needed to test

React code:

Simplest test:

<GoabDataGrid keyboardNav={"layout"}>
        <GoabBlock data-grid="row">
          <GoabLink data-grid="cell">
            <a href="https://www.w3.org/TR/wai-aria-1.1/">ARIA 1.1 Specification</a>
          </GoabLink>
          <GoabLink data-grid="cell">
            <a href="https://www.w3.org/TR/core-aam-1.1/">Core Accessibility API Mappings 1.1</a>
          </GoabLink>
          <GoabLink data-grid="cell">
            <a href="https://www.w3.org/WAI/intro/aria.php">WAI-ARIA Overview</a>
          </GoabLink>
          <GoabLink data-grid="cell">
            <a href="https://www.w3.org/WAI/intro/wcag">WCAG Overview</a>
          </GoabLink>
          <GoabLink data-grid="cell">
            <a href="https://html.spec.whatwg.org/">HTML Specification</a>
          </GoabLink>
          <GoabLink data-grid="cell">
            <a href="https://www.w3.org/TR/SVG2/">SVG 2 Specification</a>
          </GoabLink>
        </GoabBlock>
      </GoabDataGrid>
data-grid-link.mov

For keyboardNav="layout", when users press Arrow Right at the end of a row, the focus wraps to the first cell of the next row. When users press Arrow Left at the beginning of a row, the focus wraps to the last cellof the previous row.

type User = {
  idNumber: string;
  nameOfChild: string;
  dataStarted: string;
  dateSubmitted: string;
  status: string;
  updated: string;
  email: string;
  program: string;
  programId: string;
  serviceAccess: string;
  approver: string;
};

 const [users, setUsers] = useState<User[]>([
    {
      idNumber: "1",
      nameOfChild: "Mike Zwei",
      dataStarted: "Feb 21, 2023",
      dateSubmitted: "Feb 25, 2023",
      status: "Removed",
      updated: "Jun 30, 2022 at 2:30 PM",
      email: "mike.zwei@gmail.com",
      program: "Wee Wild Ones Curry",
      programId: "74528567",
      serviceAccess: "Claims Adjustments",
      approver: "Sarah Ellis",
    },
    {
      idNumber: "2",
      nameOfChild: "Emma Stroman",
      dataStarted: "Feb 21, 2023",
      dateSubmitted: "Feb 25, 2023",
      status: "To be removed",
      updated: "Nov 28, 2021 at 1:30 PM",
      email: "emma.stroman@gmail.com",
      program: "Fort McMurray",
      programId: "74522643",
      serviceAccess: "Claims Adjustments",
      approver: "Sarah Ellis",
    },
  ]);

  const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
  const [isSelectedAll, setIsSelectedAll] = useState(false);

  const getStatusBadgeType = (status: string): "success" | "emergency" | "information" | "important" => {
    switch (status) {
      case "Removed":
        return "success";
      case "To be removed":
        return "emergency";
      case "Submitted":
        return "information";
      case "In review":
        return "information";
      case "Awaiting documentation":
        return "important";
      case "Denied":
        return "emergency";
      case "Approved":
        return "success";
      case "Closed":
        return "information";
      default:
        return "information";
    }
  };

  const selectAll = (event: GoabCheckboxOnChangeDetail) => {
    setIsSelectedAll(event.checked);
    if (event.checked) {
      setSelectedUsers(users.map((u) => u.idNumber));
    } else {
      setSelectedUsers([]);
    }
  };

  const isSelected = (userId: string): boolean => {
    return selectedUsers.includes(userId);
  };

  const toggleSelection = (userId: string, event: GoabCheckboxOnChangeDetail) => {
    if (event.checked) {
      setSelectedUsers([...selectedUsers, userId]);
    } else {
      setSelectedUsers(selectedUsers.filter((id) => id !== userId));
    }
    setIsSelectedAll(selectedUsers.length + (event.checked ? 1 : -1) === users.length);
  };

  const handleSort = (event: GoabTableOnSortDetail) => {
    const { sortBy, sortDir } = event;
    const sortedUsers = [...users].sort((a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir);
    setUsers(sortedUsers);
  };

  const onDelete = (userId: string) => {
    if (window.confirm(`Are you sure you want to delete user ${userId}?`)) {
      // Remove the user from the users array
      const updatedUsers = users.filter(user => user.idNumber !== userId);
      setUsers(updatedUsers);

      // Remove from selected users if it was selected
      if (selectedUsers.includes(userId)) {
        const updatedSelected = selectedUsers.filter(id => id !== userId);
        setSelectedUsers(updatedSelected);

        // Update "select all" state
        setIsSelectedAll(updatedSelected.length === updatedUsers.length && updatedUsers.length > 0);
      }
    }
  };

  const onOpen = (userId: string) => {
    alert("We are going to open a profile of this user " + userId);
  };

  const onApproverChange = (userId: string, event: any) => {
    const user = users.find((u) => u.idNumber === userId);
    if (user) {
      user.approver = event.value;
      setUsers([...users]);
    }
  };

Table:

 <GoabDataGrid keyboardNav="table">
        <GoabTable width="100%" mb="xl" onSort={handleSort}>
          <thead>
            <tr data-grid="row">
              <th style={{ paddingBottom: 0 }} data-grid="cell">
                <GoabCheckbox testId="selectAll"  name="selectAll" mt="2" onChange={selectAll} checked={isSelectedAll} />
              </th>
              <th data-grid="cell">
                <GoabTableSortHeader name="idNumber">ID Number</GoabTableSortHeader>
              </th>
              <th data-grid="cell">
                <GoabTableSortHeader name="dataStarted">Date Started</GoabTableSortHeader>
              </th>
              <th data-grid="cell">
                <GoabTableSortHeader name="dateSubmitted">Date Submitted</GoabTableSortHeader>
              </th>
              <th data-grid="cell">
                <GoabTableSortHeader name="status">Status</GoabTableSortHeader>
              </th>
              <th data-grid="cell">Actions</th>
            </tr>
          </thead>
          <tbody>
            {users.map((user) => (
              <tr key={user.idNumber} data-grid="row">
                <td data-grid="cell" data-testid={`cell-${user.idNumber}-select`}>
                  <GoabCheckbox
                    testId={`checkbox-${user.idNumber}`}
                    name={"user"+user.idNumber}
                    checked={isSelected(user.idNumber)}
                    onChange={(event) => toggleSelection(user.idNumber, event)}
                  />
                </td>
                <td data-grid="cell" data-testid={`cell-${user.idNumber}-idNumber`}>{user.idNumber}</td>
                <td data-grid="cell" data-testid={`cell-${user.idNumber}-dateStarted`}>{user.dataStarted}</td>
                <td data-grid="cell" data-testid={`cell-${user.idNumber}-dateSubmitted`}>{user.dateSubmitted}</td>
                <td data-grid="cell" data-testid={`cell-${user.idNumber}-status`}>
                  <GoabBadge type={getStatusBadgeType(user.status)} content={user.status} />
                </td>
                <td data-grid="cell">
                  <GoabButton testId={`delete-${user.idNumber}`} type="tertiary" onClick={() => onDelete(user.idNumber)}>
                    Delete
                  </GoabButton>
                  <GoabButton  testId={`open-${user.idNumber}`} type="tertiary" onClick={() => onOpen(user.idNumber)}>
                    Open
                  </GoabButton>
                </td>
              </tr>
            ))}
          </tbody>
        </GoabTable>
      </GoabDataGrid>
data-grid-table.mov

For keyboardNav="table", when users press Arrow Right at the end of a row, the focus stays there. When users press Arrow Left at the beginning of a row, the focus stays there.

Container:

  <h3>Containers</h3>
      <GoabDataGrid keyboardNav="layout">
        {users.map((user) => (
          <GoabContainer key={user.idNumber} mt="l" data-grid="row">
            <GoabBlock direction="row" gap="m" alignment="start">
              <GoabCheckbox
                name={"container-"+user.idNumber}
                data-grid="cell-0"
                checked={isSelected(user.idNumber)}
                onChange={(event) => toggleSelection(user.idNumber, event)}
              />

              <GoabBlock direction="column" gap="s" alignment="start">
                <GoabBlock direction="row" gap="s" alignment="center">
                  <strong data-grid="cell-1">{user.nameOfChild}</strong>
                  <GoabBlock data-grid="cell-2">
                    <GoabBadge type={getStatusBadgeType(user.status)} content={user.status} />
                  </GoabBlock>
                </GoabBlock>

                <GoabBlock direction="row" gap="xl" alignment="start">
                  <GoabBlock direction="column" gap="s" alignment="start">
                    <GoabBlock direction="column" gap="xs" data-grid="cell-4">
                      <strong>Updated</strong>
                      <span>{user.updated}</span>
                    </GoabBlock>
                    <GoabBlock direction="column" gap="xs" data-grid="cell-7">
                      <strong>Program ID</strong>
                      <span>{user.programId}</span>
                    </GoabBlock>
                  </GoabBlock>

                  <GoabBlock direction="column" gap="s" alignment="start">
                    <GoabBlock direction="column" gap="xs" data-grid="cell-5">
                      <strong>Email</strong>
                      <span>{user.email}</span>
                    </GoabBlock>
                    <GoabBlock direction="column" gap="xs" data-grid="cell-8">
                      <strong>Service access</strong>
                      <span>{user.serviceAccess}</span>
                    </GoabBlock>
                  </GoabBlock>

                  <GoabBlock direction="column" gap="s" alignment="start">
                    <GoabBlock direction="column" gap="xs" data-grid="cell-6">
                      <strong>Program</strong>
                      <span>{user.program}</span>
                    </GoabBlock>
                    <GoabBlock direction="column" gap="xs" data-grid="cell-9">
                      <strong>Approver</strong>
                      <GoabDropdown
                        testId={`approver-${user.idNumber}`}
                        value={user.approver}
                        onChange={(event) => onApproverChange(user.idNumber, event)}
                      >
                        <GoabDropdownItem value="Sarah Ellis" ></GoabDropdownItem>
                        <GoabDropdownItem value="John Doe" label={"John Doe"}></GoabDropdownItem>
                        <GoabDropdownItem value="Jane Smith"></GoabDropdownItem>
                      </GoabDropdown>
                    </GoabBlock>
                  </GoabBlock>
                </GoabBlock>
              </GoabBlock>

              <GoabButton type="tertiary" data-grid="cell-3" onClick={() => onOpen(user.idNumber)}>
                Open
              </GoabButton>
            </GoabBlock>
          </GoabContainer>
        ))}
      </GoabDataGrid>

For this example, the cell order is assigned, arrow left and right follow the cell order.

container-data-grid.mov

@vanessatran-ddi vanessatran-ddi marked this pull request as draft August 28, 2025 15:56
@vanessatran-ddi vanessatran-ddi self-assigned this Aug 28, 2025
@vanessatran-ddi vanessatran-ddi force-pushed the vanessa/2609-data-table branch 5 times, most recently from 2c8e4df to 7fce4c9 Compare September 10, 2025 19:24
@vanessatran-ddi vanessatran-ddi changed the title feat(#2609): add data-table header rows feat(#2609): add data-grid component Sep 10, 2025
@vanessatran-ddi vanessatran-ddi force-pushed the vanessa/2609-data-table branch 3 times, most recently from f6acf37 to 29dbfa2 Compare September 16, 2025 00:02
@vanessatran-ddi vanessatran-ddi linked an issue Sep 16, 2025 that may be closed by this pull request
mr={mr}
mb={mb}
ml={ml}
{...dataGridProps}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for code reviewer and @chrisolsen
Without adding data-xx attributes to react component (angular works very well), for example:

 <GoabContainer key={user.idNumber} mt="l" data-gridrow>

The react wrapper will render as the below, with goa-container without custom attributes.
image

Even I assign a value ""
image

As a result, I end up adding dataGridProps, which allows data-grid prefix only, and then assign to react components. I only add for components that makes sense, except some such as Progress spinner or layout column

@vanessatran-ddi vanessatran-ddi force-pushed the vanessa/2609-data-table branch 3 times, most recently from 44eb001 to b445d31 Compare September 16, 2025 16:12
}
return focusableNode;
return shouldFocus(node);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to code reviewer:
I moved the logic to shouldFocus atutils since DataGrid needs it as well.

Copy link
Collaborator

@twjeffery twjeffery left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vanessatran-ddi I reviewed the data grid through the demo you shared here: https://wonderful-chaja-1b63f2.netlify.app/data-grid-examples

Awesome work on this so far. Below are some minor issues I found:


  1. There is something odd happening for the focus when tabbing to the container for the first time. It looks like there is a second blue focus in addition to the standard yellow border.
    https://jam.dev/c/988b51b9-580e-435a-9e30-45260d534443

  1. It might be an issue with my original implementation of this menu on the icon button in the table, but it's not accessible via keyboard. You can open it, and then focus does not move to it.
    https://jam.dev/c/5922bf6e-0370-4c2f-a65d-9a95376abfbb

  1. Again, might be an issue with how I implemented it originally, but the Link to the detail page from the Name is not able to be selected through the focus moving through the data grid
    https://jam.dev/c/6f25727d-8c23-49ac-93ff-f96c313b3e94

Also, when I tab when inside the table, it should go between the interactive elements. It does not, it skips everything in the table other than the sortable headers.
https://jam.dev/c/88e6ba53-603b-4bd7-a212-b35415afad07


  1. It's hard to tell exactly because the dropdown is the last item in the container, but in the data grid when you press down-arrow it moves to the next card. But when you press down on the dropdown it opens up the dropdown menu and goes to the option. Should this open the menu or move past it?
    https://jam.dev/c/7acce053-243e-4430-b579-63b9c41ac700

  1. Regarding the keyboard navigation on the data-grid when using a table: When the user gets to the edge of the table and clicks to the edge again, it should not move.

Data-grid:

  • When focus is on the last cell (on an edge) focus does not move.
  • Ours currently moves to the next row, should not move.
image

Layout-grid:

  • When focus is on the last cell (on an edge) focus moves to the first cell in the next row in that direction.
  • Our works correctly, moving to the first cell in that direction.
image

@vanessatran-ddi vanessatran-ddi marked this pull request as draft September 17, 2025 16:30
@vanessatran-ddi vanessatran-ddi force-pushed the vanessa/2609-data-table branch 3 times, most recently from 3f0c995 to d9cf9a7 Compare September 22, 2025 23:08
@vanessatran-ddi
Copy link
Collaborator Author

vanessatran-ddi commented Sep 23, 2025

Hi @twjeffery I uploaded the react version on https://stately-puffpuff-b4791f.netlify.app/(I used the simpler version to test here instead of the workspace demo, after this is on alpha and we can integrate vs the code easier using React, I tried my best to solve some logic but it was taking me time, so I stopped)

  1. There is something odd happening for the focus when tabbing to the container for the first time. It looks like there is a second blue focus in addition to the standard yellow border.
    https://jam.dev/c/988b51b9-580e-435a-9e30-45260d534443

Vanessa: I believe this is fixed on the new preview link above. ✅

  1. It might be an issue with my original implementation of this menu on the icon button in the table, but it's not accessible via keyboard. You can open it, and then focus does not move to it.
    https://jam.dev/c/5922bf6e-0370-4c2f-a65d-9a95376abfbb

Vanessa: Yes, it isn't an issue with this PR. The MenuButton from Chris will handle the arrow key well if Popover is opened (this is a part of a reason why I don't add logic to the angular workspace demo), let's wait till @chrisolsen PR merged to alpha, and we can integrate later.✅

  1. Again, might be an issue with how I implemented it originally, but the Link to the detail page from the Name is not able to be selected through the focus moving through the data grid
    https://jam.dev/c/6f25727d-8c23-49ac-93ff-f96c313b3e94

Vanessa: Yes on the implementation we are using <goab-link (click)="navigateToResult(result)" >{{ result.name }}</goab-link> which doesn't make use of a

The preview link above, I am using this: <td data-grid="cell" data-testid={cell-${user.idNumber}-idNumber}><GoabLink><a href={mailto: ${user.email}}>{user.email}</a></GoabLink></td>

Note: The focus border is blue now, is same vs our Link, so you don't see the yellow border, unless this ticket wants to cover focus css for GoabLink too. ✅

image

Also, when I tab when inside the table, it should go between the interactive elements. It does not, it skips everything in the table other than the sortable headers. https://jam.dev/c/88e6ba53-603b-4bd7-a212-b35415afad07

Vanessa: I fixed this, let me know if it happens again ✅

  1. It's hard to tell exactly because the dropdown is the last item in the container, but in the data grid when you press down-arrow it moves to the next card. But when you press down on the dropdown it opens up the dropdown menu and goes to the option. Should this open the menu or move past it?
    https://jam.dev/c/7acce053-243e-4430-b579-63b9c41ac700

Vanessa: According to the daily standup meeting, we will accept that dropdown will override the arrow key keyboard behaviour, until user press Esc to exit the focus from the popover, then arrow up and down will continue to work as usual. ✅

  1. Regarding the keyboard navigation on the data-grid when using a table: When the user gets to the edge of the table and clicks to the edge again, it should not move.

Vanessa: Yes I fixed it, now if you focus on Open, use click to click on the cell, Open still remains focused.

image

Data-grid:

  • When focus is on the last cell (on an edge) focus does not move.

Vanessa: Since we are using this https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/data-grids/, I will not move the focus when we already end of the row (or first cell of the row)✅

@vanessatran-ddi vanessatran-ddi force-pushed the vanessa/2609-data-table branch 4 times, most recently from 371b40e to 7489c6e Compare September 29, 2025 22:23
@vanessatran-ddi vanessatran-ddi marked this pull request as ready for review September 29, 2025 22:27
@vanessatran-ddi vanessatran-ddi force-pushed the vanessa/2609-data-table branch from 7489c6e to 34194eb Compare October 1, 2025 21:51
@vanessatran-ddi vanessatran-ddi mentioned this pull request Oct 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Data table: base component

2 participants