From 92ed115d5b52115ba7094bd5395ce4b1947a4d4d Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 7 May 2026 16:07:06 -0500 Subject: [PATCH 01/68] release notes --- packages/components/releaseNotes/components.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 5f4f0e47c5..e30d716108 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,14 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version TBD +*Released*: TBD +- Accessibility improvements for app pages: Keyboard Interactions + +### version 7.35.1 +*Released*: 7 May 2026 +- Update @labkey/build + ### version 7.35.0 *Released*: 7 May 2026 - Fix accessibility issues for empty links and buttons From 5b77cf5907a1dd2ec126ae541ad5941e786fad0f Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 7 May 2026 16:07:50 -0500 Subject: [PATCH 02/68] Global Header: focus indicators for user menu buttons --- packages/components/src/theme/navbar.scss | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/components/src/theme/navbar.scss b/packages/components/src/theme/navbar.scss index d9d947eee5..4df5ae863a 100644 --- a/packages/components/src/theme/navbar.scss +++ b/packages/components/src/theme/navbar.scss @@ -131,14 +131,23 @@ border: none; background-color: transparent; box-shadow: none; + border-radius: 2px; + + &:focus { + background-color: transparent !important; + } + &:focus-visible { + outline: 1px solid $white; + outline-offset: 2px; + } - &:focus, &:active, &:hover, &:hover:active { box-shadow: none; border: none; background-color: transparent; + outline: none; } } @@ -574,6 +583,11 @@ & .caret { color: $white; } + + a:focus-visible { + outline: 1px solid $white; + outline-offset: 2px; + } } .user-name { From 2996a6f6441687b7752d87a4ee37ad2ad202972c Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 7 May 2026 16:08:11 -0500 Subject: [PATCH 03/68] Mega menu: allow tab to folders --- .../internal/components/navigation/FolderMenu.tsx | 4 ++-- packages/components/src/theme/navbar.scss | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/components/src/internal/components/navigation/FolderMenu.tsx b/packages/components/src/internal/components/navigation/FolderMenu.tsx index c81a5eeb68..3b6df3255f 100644 --- a/packages/components/src/internal/components/navigation/FolderMenu.tsx +++ b/packages/components/src/internal/components/navigation/FolderMenu.tsx @@ -54,9 +54,9 @@ export const FolderMenuItems: FC = memo(props => { 'col-xs-10': !user.isAdmin, })} > - onClick(item)}> +
Date: Thu, 7 May 2026 16:08:45 -0500 Subject: [PATCH 04/68] 7.35.2-fb-keyboardInteraction.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index bb0012c64a..e6b91ba7b3 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.1", + "version": "7.35.2-fb-keyboardInteraction.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.1", + "version": "7.35.2-fb-keyboardInteraction.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 25f2ed57d6..395e810108 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.1", + "version": "7.35.2-fb-keyboardInteraction.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From cbed43b88cbc7d43b4c4bb8fb08f863a39c2a7b7 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Thu, 7 May 2026 16:09:19 -0700 Subject: [PATCH 05/68] Make ActionButton a button so it can be tabbed to --- packages/components/releaseNotes/components.md | 1 + .../src/internal/components/buttons/ActionButton.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index e30d716108..6cd417598e 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -4,6 +4,7 @@ Components, models, actions, and utility functions for LabKey applications and p ### version TBD *Released*: TBD - Accessibility improvements for app pages: Keyboard Interactions + - Make ActionButton a button so it can be tabbed to ### version 7.35.1 *Released*: 7 May 2026 diff --git a/packages/components/src/internal/components/buttons/ActionButton.tsx b/packages/components/src/internal/components/buttons/ActionButton.tsx index 763e323386..6b7cd93d66 100644 --- a/packages/components/src/internal/components/buttons/ActionButton.tsx +++ b/packages/components/src/internal/components/buttons/ActionButton.tsx @@ -42,9 +42,9 @@ export class ActionButton extends React.PureComponent { return (
- + {helperBody && {helperBody}}
From b59afa978390dcb46781201f2494bcad3175ec59 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Thu, 7 May 2026 16:11:35 -0700 Subject: [PATCH 06/68] @labkey/components v7.35.2-fb-keyboardInteraction.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index e6b91ba7b3..5cbf85b7d8 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.0", + "version": "7.35.2-fb-keyboardInteraction.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.0", + "version": "7.35.2-fb-keyboardInteraction.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 395e810108..8b2b2b1f3e 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.0", + "version": "7.35.2-fb-keyboardInteraction.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 6ec62ea2a5be661b5cf3dfccd589c7ef0983b297 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 09:41:34 -0500 Subject: [PATCH 07/68] Modals to have focus on open, allow tab only within modal elements, and ESCAPE to close --- .../components/releaseNotes/components.md | 2 + packages/components/src/internal/Modal.tsx | 53 ++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 6cd417598e..10610c9fc0 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -5,6 +5,8 @@ Components, models, actions, and utility functions for LabKey applications and p *Released*: TBD - Accessibility improvements for app pages: Keyboard Interactions - Make ActionButton a button so it can be tabbed to + - Allow tab to app main menu folder items + - Modals to have focus on open, allow tab only within modal elements, and ESCAPE to close ### version 7.35.1 *Released*: 7 May 2026 diff --git a/packages/components/src/internal/Modal.tsx b/packages/components/src/internal/Modal.tsx index d8c678e2f0..ff7061796e 100644 --- a/packages/components/src/internal/Modal.tsx +++ b/packages/components/src/internal/Modal.tsx @@ -1,14 +1,19 @@ -import React, { FC, memo, PropsWithChildren, ReactNode, useEffect } from 'react'; +import React, { FC, memo, PropsWithChildren, ReactNode, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { usePortalRef } from './hooks'; import { ModalButtons, ModalButtonsProps } from './ModalButtons'; +import { Key } from '../public/useEnterEscape'; + +const FOCUSABLE_SELECTORS = + 'a, button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; interface BaseModalProps extends PropsWithChildren { bsSize?: 'lg' | 'sm'; className?: string; + onCancel?: () => void; } /** @@ -16,8 +21,9 @@ interface BaseModalProps extends PropsWithChildren { * component, instead you should probably be using Modal, which has a bunch of props to make it easier to render a * typical modal with save/close buttons and the appropriate logic for those buttons. */ -export const BaseModal: FC = ({ bsSize, children, className }) => { +export const BaseModal: FC = ({ bsSize, children, className, onCancel }) => { const portalRef = usePortalRef('modal'); + const modalRef = useRef(null); const className_ = classNames('modal-dialog', className, { 'modal-sm': bsSize === 'sm', 'modal-lg': bsSize === 'lg', @@ -31,13 +37,46 @@ export const BaseModal: FC = ({ bsSize, children, className }) = }; }, []); + useEffect(() => { + // Focus the modal on open so keyboard navigation starts within it rather than behind it + modalRef.current?.focus(); + }, []); + + useEffect(() => { + // Trap focus within the modal so Tab/Shift+Tab cycle only through modal elements, + // and close the modal on Escape + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === Key.ESCAPE) { + onCancel?.(); + } else if (e.key === Key.TAB) { + const focusable = Array.from( + modalRef.current?.querySelectorAll(FOCUSABLE_SELECTORS) ?? [] + ); + if (!focusable.length) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onCancel]); + const modal = (
-
{children}
+
+ {children} +
@@ -96,7 +135,7 @@ export const Modal: FC = memo(props => { } = props; const showHeader = onCancel || title; return ( - + {showHeader && }
{children}
@@ -107,12 +146,12 @@ export const Modal: FC = memo(props => { cancelText={cancelText} canConfirm={canConfirm} confirmClass={confirmClass} - confirmText={confirmText} confirmingText={confirmingText} + confirmText={confirmText} isConfirming={isConfirming} - onConfirm={onConfirm} - onCommentChange={onCommentChange} onCancel={onCancel} + onCommentChange={onCommentChange} + onConfirm={onConfirm} requiresUserComment={requiresUserComment} > {footerContent} From 306a5b3196393e7fe8a75e9c9b3edd04090eba27 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 09:42:17 -0500 Subject: [PATCH 08/68] 7.35.2-fb-keyboardInteraction.2 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 5cbf85b7d8..36d349d5bd 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.1", + "version": "7.35.2-fb-keyboardInteraction.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.1", + "version": "7.35.2-fb-keyboardInteraction.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 8b2b2b1f3e..b7d97b545a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.1", + "version": "7.35.2-fb-keyboardInteraction.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 9e55e5427cdf27a3ebf4c022cc5aab48c59739ae Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 09:51:38 -0500 Subject: [PATCH 09/68] fix jest tests for related changes --- .../components/buttons/ActionButton.test.tsx | 14 ++++---- .../domainproperties/DomainForm.test.tsx | 2 +- .../TextChoiceOptions.test.tsx | 6 ++-- .../SampleTypePropertiesPanel.test.tsx.snap | 32 +++++++++---------- .../BarTenderSettingsForm.test.tsx | 9 ++++-- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/components/src/internal/components/buttons/ActionButton.test.tsx b/packages/components/src/internal/components/buttons/ActionButton.test.tsx index 50f7b433d8..fa0b25e40d 100644 --- a/packages/components/src/internal/components/buttons/ActionButton.test.tsx +++ b/packages/components/src/internal/components/buttons/ActionButton.test.tsx @@ -19,11 +19,11 @@ import { userEvent } from '@testing-library/user-event'; import { ActionButton } from './ActionButton'; -describe('', () => { +describe('ActionButton', () => { test('Default properties', async () => { const onClick = jest.fn(); render(); - await userEvent.click(document.querySelector('span')); + await userEvent.click(document.querySelector('button')); expect(onClick).toHaveBeenCalledTimes(1); }); @@ -41,10 +41,10 @@ describe('', () => { ); // Customized attributes should all be valid click targets - await userEvent.click(document.querySelector('span')); - await userEvent.click(document.querySelector('.test-button-class span')); - await userEvent.click(document.querySelector('.test-container-class span')); - await userEvent.click(document.querySelector('[title="test-title"] span')); + await userEvent.click(document.querySelector('button')); + await userEvent.click(document.querySelector('.test-button-class button')); + await userEvent.click(document.querySelector('.test-container-class button')); + await userEvent.click(document.querySelector('[title="test-title"] button')); expect(onClick).toHaveBeenCalledTimes(4); }); @@ -71,7 +71,7 @@ describe('', () => { test('Disabled', async () => { const onClick = jest.fn(); render(); - await userEvent.click(document.querySelector('span')); + await userEvent.click(document.querySelector('button')); expect(onClick).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/components/src/internal/components/domainproperties/DomainForm.test.tsx b/packages/components/src/internal/components/domainproperties/DomainForm.test.tsx index 10d3eddfc1..ca1a9dcc0c 100644 --- a/packages/components/src/internal/components/domainproperties/DomainForm.test.tsx +++ b/packages/components/src/internal/components/domainproperties/DomainForm.test.tsx @@ -458,7 +458,7 @@ describe('DomainForm', () => { expect(document.getElementsByClassName('domain-form-manual-btn')).toHaveLength(1); expect(document.getElementsByClassName('domain-field-row')).toHaveLength(0); await act(async () => { - await userEvent.click(document.querySelector('.domain-form-manual-btn>span')); + await userEvent.click(document.querySelector('.domain-form-manual-btn>button')); }); expect(document.getElementsByClassName('translator--toggle__wizard')).toHaveLength(0); expect(document.getElementsByClassName('domain-form-manual-btn')).toHaveLength(0); diff --git a/packages/components/src/internal/components/domainproperties/TextChoiceOptions.test.tsx b/packages/components/src/internal/components/domainproperties/TextChoiceOptions.test.tsx index d15fc311ca..b00e0ae502 100644 --- a/packages/components/src/internal/components/domainproperties/TextChoiceOptions.test.tsx +++ b/packages/components/src/internal/components/domainproperties/TextChoiceOptions.test.tsx @@ -65,7 +65,7 @@ describe('TextChoiceOptions', () => { } expect(document.querySelectorAll('.choices-list__locked').length).toBe(inUse); - const addBtn = document.querySelector('span.container--action-button'); + const addBtn = document.querySelector('.container--action-button'); expect(addBtn.textContent).toBe(' Add Values'); if (validValuesCount > 0 && !hasSelection) { @@ -91,7 +91,7 @@ describe('TextChoiceOptions', () => { test('default props', () => { render(); validate(); - const addBtn = document.querySelector('span.container--action-button'); + const addBtn = document.querySelector('.container--action-button'); expect(addBtn.textContent).toBe(' Add Values'); expect(addBtn.getAttribute('class').indexOf('disabled')).toBe(-1); @@ -304,7 +304,7 @@ describe('TextChoiceOptions', () => { test('AddEntityButton disabled if max reached', () => { render(); validate(false, 2); - const addBtn = document.querySelector('span.container--action-button'); + const addBtn = document.querySelector('.container--action-button'); expect(addBtn.textContent).toBe(' Add Values'); expect(addBtn.getAttribute('class')).toContain(' disabled'); }); diff --git a/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap b/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap index bff0d5958c..9476df16ba 100644 --- a/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap +++ b/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap @@ -185,7 +185,7 @@ exports[`SampleTypePropertiesPanel appPropertiesOnly 1`] = ` class="form-group" >
- Add a undefined - +
@@ -467,7 +467,7 @@ exports[`SampleTypePropertiesPanel appPropertiesOnly 1`] = ` class="form-group" >
- Add a undefined - +
@@ -786,7 +786,7 @@ exports[`SampleTypePropertiesPanel default props 1`] = ` class="form-group" >
- Add a undefined - +
@@ -1000,7 +1000,7 @@ exports[`SampleTypePropertiesPanel default props 1`] = ` class="form-group" >
- Add a undefined - +
@@ -1261,7 +1261,7 @@ exports[`SampleTypePropertiesPanel include dataclass and use custom labels 1`] = class="form-group" >
- Add a undefined - +
@@ -1289,7 +1289,7 @@ exports[`SampleTypePropertiesPanel include dataclass and use custom labels 1`] = class="form-group" >
- Add a undefined - +
@@ -1503,7 +1503,7 @@ exports[`SampleTypePropertiesPanel include dataclass and use custom labels 1`] = class="form-group" >
- Add a undefined - +
@@ -1531,7 +1531,7 @@ exports[`SampleTypePropertiesPanel include dataclass and use custom labels 1`] = class="form-group" >
- Add a undefined - +
diff --git a/packages/components/src/internal/components/labelPrinting/BarTenderSettingsForm.test.tsx b/packages/components/src/internal/components/labelPrinting/BarTenderSettingsForm.test.tsx index ee1d8621a7..9176f8f888 100644 --- a/packages/components/src/internal/components/labelPrinting/BarTenderSettingsForm.test.tsx +++ b/packages/components/src/internal/components/labelPrinting/BarTenderSettingsForm.test.tsx @@ -40,9 +40,9 @@ describe('BarTenderSettingsForm', () => { expect(document.querySelectorAll('.label-printing--help-link')).toHaveLength(1); } - function validateButtons(canTest?: boolean, canSave?: boolean): void { + function validateButtons(canTest?: boolean, canSave?: boolean, canAdd = true): void { const buttons = document.querySelectorAll('button'); - expect(buttons).toHaveLength(1); + expect(buttons).toHaveLength(canTest || canSave || canAdd ? 2 : 1); const button = buttons.item(0); if (canTest) { expect(button).toHaveTextContent('Test Connection'); @@ -56,6 +56,9 @@ describe('BarTenderSettingsForm', () => { expect(button).toBeDisabled(); } } + if (canAdd) { + expect(buttons.item(1).textContent).toBe(' Add New Label Template'); + } } test('default props, home project', async () => { @@ -85,7 +88,7 @@ describe('BarTenderSettingsForm', () => { expect(document.querySelectorAll('.label-templates-container')).toHaveLength(0); expect(document.querySelector('input').getAttribute('type')).toBe('url'); validate(true); - validateButtons(false, false); + validateButtons(false, false, false); }); test('default props, subfolder without folders', async () => { From 49b3ccb3384ca31eaec979b6f2a77a52df24fd23 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 8 May 2026 08:54:31 -0700 Subject: [PATCH 10/68] For ManageViewsModal and ColumnSelectionModal, use buttons with clickable-text styling for actionable elements --- .../components/releaseNotes/components.md | 1 + .../components/ColumnSelectionModal.tsx | 24 ++++++++-------- .../public/QueryModel/ManageViewsModal.tsx | 28 +++++++++++-------- .../components/src/theme/notification.scss | 5 ++++ 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 10610c9fc0..4b5bd42361 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -7,6 +7,7 @@ Components, models, actions, and utility functions for LabKey applications and p - Make ActionButton a button so it can be tabbed to - Allow tab to app main menu folder items - Modals to have focus on open, allow tab only within modal elements, and ESCAPE to close + - Use buttons with clickable-text styling instead of spans and divs with onClick properties ### version 7.35.1 *Released*: 7 May 2026 diff --git a/packages/components/src/internal/components/ColumnSelectionModal.tsx b/packages/components/src/internal/components/ColumnSelectionModal.tsx index 7e03435c26..cdde862702 100644 --- a/packages/components/src/internal/components/ColumnSelectionModal.tsx +++ b/packages/components/src/internal/components/ColumnSelectionModal.tsx @@ -101,7 +101,7 @@ export interface ColumnChoiceProps { // exported for jest tests export const ColumnChoice: FC = memo(props => { const { column, disabledMsg, isExpanded, isInView, onAddColumn, onExpandColumn, onCollapseColumn } = props; - const { onMouseEnter, onMouseLeave, portalEl, show, targetRef } = useOverlayTriggerState( + const { onMouseEnter, onMouseLeave, portalEl, show, targetRef } = useOverlayTriggerState( 'disabled-button-overlay', disabledMsg !== undefined, false @@ -145,10 +145,10 @@ export const ColumnChoice: FC = memo(props => { ))}
{column.isLookup() && !isExpanded && ( - +
@@ -160,8 +160,8 @@ export const ColumnChoice: FC = memo(props => { )} {!isInView && column.selectable && ( -
= memo(props => { > {show && createPortal(popover, portalEl)} -
+ )} ); @@ -315,22 +315,22 @@ export const ColumnInView: FC = memo(props => { {!editing && ( {allowEditLabel && ( - - + )} {!disableDelete && ( - - + )} {disableDelete && } diff --git a/packages/components/src/public/QueryModel/ManageViewsModal.tsx b/packages/components/src/public/QueryModel/ManageViewsModal.tsx index c14a9f686a..386b56614a 100644 --- a/packages/components/src/public/QueryModel/ManageViewsModal.tsx +++ b/packages/components/src/public/QueryModel/ManageViewsModal.tsx @@ -231,35 +231,40 @@ export const ManageViewsModal: FC = memo(props => { } > {view.isSaved ? ( - + ) : ( Revert )} )} {!isDefault && !isRenaming && ( - Make default - + )} {canEdit && ( - - - - - - + + + )} @@ -299,3 +304,4 @@ export const ManageViewsModal: FC = memo(props => { ); }); +ManageViewsModal.displayName = 'ManageViewsModal'; diff --git a/packages/components/src/theme/notification.scss b/packages/components/src/theme/notification.scss index 661df66b0f..6c19cd530d 100644 --- a/packages/components/src/theme/notification.scss +++ b/packages/components/src/theme/notification.scss @@ -128,6 +128,11 @@ button.clickable-text { padding: 0; border: none; background-color: transparent; + + &.fa { + color: $text-color-light; + padding-left: 5px; + } } .page-detail-header-title-padding { From 19c209fc076d6b5c3af25327660330aae594e243 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 8 May 2026 08:59:07 -0700 Subject: [PATCH 11/68] @labkey/components v7.35.2-fb-keyboardInteraction.3 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 36d349d5bd..cb9cf4040f 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.2", + "version": "7.35.2-fb-keyboardInteraction.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.2", + "version": "7.35.2-fb-keyboardInteraction.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index b7d97b545a..88d9e2d7da 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.2", + "version": "7.35.2-fb-keyboardInteraction.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 0c41ed332b00a20106a6cbb1fc73209d380df26f Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 8 May 2026 09:28:16 -0700 Subject: [PATCH 12/68] Add focus background color change for content tab nav tabs --- packages/components/src/theme/tabs.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/src/theme/tabs.scss b/packages/components/src/theme/tabs.scss index b9509f5687..cc9da027b2 100644 --- a/packages/components/src/theme/tabs.scss +++ b/packages/components/src/theme/tabs.scss @@ -18,6 +18,9 @@ text-align: center; text-transform: uppercase; white-space: nowrap; + &:focus { + background-color: $gray-shadow; + } } } From af4e0b6a93a6006844370899c98c3bac776154e4 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 8 May 2026 09:39:00 -0700 Subject: [PATCH 13/68] Use buttons for field enabling toggle --- .../src/internal/components/buttons/ToggleButtons.tsx | 4 ++-- packages/components/src/theme/fields.scss | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/buttons/ToggleButtons.tsx b/packages/components/src/internal/components/buttons/ToggleButtons.tsx index 5687c6d107..2f77ac7991 100644 --- a/packages/components/src/internal/components/buttons/ToggleButtons.tsx +++ b/packages/components/src/internal/components/buttons/ToggleButtons.tsx @@ -104,8 +104,8 @@ export const ToggleIcon: FC = memo(props => { const body = ( <> - {firstActive && } - {secondActive && } + {firstActive && {showDetails && ( Date: Fri, 8 May 2026 12:09:42 -0700 Subject: [PATCH 16/68] Hide outline on focus for folder listing --- packages/components/src/theme/product-navigation.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/src/theme/product-navigation.scss b/packages/components/src/theme/product-navigation.scss index 4722db0cd9..4d4f964607 100644 --- a/packages/components/src/theme/product-navigation.scss +++ b/packages/components/src/theme/product-navigation.scss @@ -213,5 +213,8 @@ cursor: default; } } + a:focus { + outline: none; + } } } From b4d742959fa1e5602f5a3711f10bc5b803b71788 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 8 May 2026 12:12:25 -0700 Subject: [PATCH 17/68] Update `useEnterEscape` to allow optional event argument to callbacks --- packages/components/releaseNotes/components.md | 1 + packages/components/src/public/useEnterEscape.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 4b5bd42361..6f35768b1c 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -8,6 +8,7 @@ Components, models, actions, and utility functions for LabKey applications and p - Allow tab to app main menu folder items - Modals to have focus on open, allow tab only within modal elements, and ESCAPE to close - Use buttons with clickable-text styling instead of spans and divs with onClick properties + - Update `useEnterEscape` to allow optional event argument to callbacks ### version 7.35.1 *Released*: 7 May 2026 diff --git a/packages/components/src/public/useEnterEscape.ts b/packages/components/src/public/useEnterEscape.ts index edfd02c890..ac0260e71e 100644 --- a/packages/components/src/public/useEnterEscape.ts +++ b/packages/components/src/public/useEnterEscape.ts @@ -25,7 +25,7 @@ type KeyHandler = (evt: KeyboardEvent) => void; * @param onEnter: function to call when the enter key is pressed. * @param onEscape: function to call when the escape key is pressed. */ -export const useEnterEscape = (onEnter?: () => void, onEscape?: () => void): any => { +export const useEnterEscape = (onEnter?: (evt?: any) => void, onEscape?: (evt?: any) => void): any => { return useCallback( (evt: KeyboardEvent) => { if (evt.shiftKey || evt.metaKey) return; @@ -34,12 +34,12 @@ export const useEnterEscape = (onEnter?: () => void, onEscape?: () => void): any case Key.ENTER: evt.stopPropagation(); evt.preventDefault(); - onEnter?.(); + onEnter?.(evt); break; case Key.ESCAPE: evt.stopPropagation(); evt.preventDefault(); - onEscape?.(); + onEscape?.(evt); break; default: break; From 0d2e643d369fd93d487a5b5df9495c04450953e0 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 8 May 2026 12:14:07 -0700 Subject: [PATCH 18/68] @labkey/components v7.35.2-fb-keyboardInteraction.4 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index cb9cf4040f..1cfa8d40b2 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.3", + "version": "7.35.2-fb-keyboardInteraction.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.3", + "version": "7.35.2-fb-keyboardInteraction.4", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 88d9e2d7da..797f4331c8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.3", + "version": "7.35.2-fb-keyboardInteraction.4", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 79bc742b793914bbe59b20b7f1fffe5500ec025a Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 14:27:43 -0500 Subject: [PATCH 19/68] Change a tag with onclick to button tag with className clickable-text --- .../internal/components/picklist/ChoosePicklistModal.tsx | 6 +++++- packages/components/src/internal/util/messaging.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/picklist/ChoosePicklistModal.tsx b/packages/components/src/internal/components/picklist/ChoosePicklistModal.tsx index 897558f0e1..b0e0a52ce2 100644 --- a/packages/components/src/internal/components/picklist/ChoosePicklistModal.tsx +++ b/packages/components/src/internal/components/picklist/ChoosePicklistModal.tsx @@ -291,7 +291,11 @@ export const ChoosePicklistModalDisplay: FC - Do you want to create a new one? + Do you want to{' '} + + ? ); diff --git a/packages/components/src/internal/util/messaging.tsx b/packages/components/src/internal/util/messaging.tsx index a97c1a6228..bdfee8e7a3 100644 --- a/packages/components/src/internal/util/messaging.tsx +++ b/packages/components/src/internal/util/messaging.tsx @@ -12,7 +12,11 @@ export function getActionErrorMessage(problemStatement: string, noun: string, sh  Your session may have expired or the {noun} may no longer be valid. {showRefresh && ( <> -  Try window.location.reload()}>refreshing the page. +  Try{' '} + + . )} From 047edc0e5946ef31220de5738f9b89630f33eddb Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 14:28:14 -0500 Subject: [PATCH 20/68] More usage of useEnterEscape with onKeyDown for tab focus --- .../components/notifications/NotificationItem.tsx | 12 +++++++++++- .../src/public/QueryModel/TabbedGridPanel.tsx | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/notifications/NotificationItem.tsx b/packages/components/src/internal/components/notifications/NotificationItem.tsx index b1152d9f18..7f6dbab0aa 100644 --- a/packages/components/src/internal/components/notifications/NotificationItem.tsx +++ b/packages/components/src/internal/components/notifications/NotificationItem.tsx @@ -17,6 +17,7 @@ import React, { FC, useCallback } from 'react'; import { NotificationItemModel } from './model'; import { useNotificationsContext } from './NotificationsContext'; +import { useEnterEscape } from '../../../public/useEnterEscape'; interface ItemProps { item: NotificationItemModel; @@ -26,11 +27,20 @@ export const NotificationItem: FC = ({ item }) => { const { dismissNotifications } = useNotificationsContext(); const { data, id, message, isDismissible } = item; const onClick = useCallback(() => dismissNotifications(id), [dismissNotifications, id]); + const onKeyDown = useEnterEscape(onClick); return (
{typeof message === 'function' ? message(item, data) : message} - {isDismissible && } + {isDismissible && ( + + )}
); }; diff --git a/packages/components/src/public/QueryModel/TabbedGridPanel.tsx b/packages/components/src/public/QueryModel/TabbedGridPanel.tsx index ef31a951a4..32086ff50b 100644 --- a/packages/components/src/public/QueryModel/TabbedGridPanel.tsx +++ b/packages/components/src/public/QueryModel/TabbedGridPanel.tsx @@ -26,6 +26,7 @@ import { GridPanel, GridPanelProps } from './GridPanel'; import { InjectedQueryModels } from './withQueryModels'; import { QueryModel } from './QueryModel'; import { getQueryModelExportParams } from './utils'; +import { useEnterEscape } from '../useEnterEscape'; interface GridTabProps { isActive: boolean; @@ -43,6 +44,7 @@ const GridTab: FC = memo(({ isActive, model, onSelect, pullRight, 'pull-right': pullRight, }); const onClick = useCallback(() => onSelect(id), [id, onSelect]); + const onKeyDown = useEnterEscape(onClick); const rowCountDisplay = useMemo(() => { if (rowCount === undefined && !model.isActivelyLoadingTotalCount) return tabRowCount?.toLocaleString(); @@ -51,7 +53,7 @@ const GridTab: FC = memo(({ isActive, model, onSelect, pullRight, return (
  • - + {title || queryInfo?.queryLabel || queryInfo?.name} {showRowCount && rowCountDisplay !== undefined && ({rowCountDisplay})} From 11dec30af7f6ec481a2881825de81acd3fd27be5 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 14:28:39 -0500 Subject: [PATCH 21/68] EditableGrid Cell to allow tab focus with tabIndex 0 --- packages/components/releaseNotes/components.md | 1 + .../components/src/internal/components/editable/Cell.tsx | 2 +- packages/components/src/theme/app/overrides.scss | 7 +++++-- packages/components/src/theme/grid.scss | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 6f35768b1c..b4ba0d04c2 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -9,6 +9,7 @@ Components, models, actions, and utility functions for LabKey applications and p - Modals to have focus on open, allow tab only within modal elements, and ESCAPE to close - Use buttons with clickable-text styling instead of spans and divs with onClick properties - Update `useEnterEscape` to allow optional event argument to callbacks + - EditableGrid Cell to allow tab focus with tabIndex 0 ### version 7.35.1 *Released*: 7 May 2026 diff --git a/packages/components/src/internal/components/editable/Cell.tsx b/packages/components/src/internal/components/editable/Cell.tsx index 3f3e2f0c15..792060fb04 100644 --- a/packages/components/src/internal/components/editable/Cell.tsx +++ b/packages/components/src/internal/components/editable/Cell.tsx @@ -161,7 +161,7 @@ const DisplayCell: FC = memo(props => { onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} ref={targetRef} - tabIndex={-1} + tabIndex={0} > {body} {show && createPortal(popover, portalEl)} diff --git a/packages/components/src/theme/app/overrides.scss b/packages/components/src/theme/app/overrides.scss index 34a7df56b0..606b75159c 100644 --- a/packages/components/src/theme/app/overrides.scss +++ b/packages/components/src/theme/app/overrides.scss @@ -28,10 +28,13 @@ body { margin: 20px 0; } -// Remove :focus browser outline for nav-tabs -.nav-tabs li a:focus { +// Remove :focus browser outline for nav-tabs when active +.nav-tabs li.active a:focus { outline: none; } +.nav-tabs li.active a:focus-visible { + outline: 5px auto -webkit-focus-ring-color; +} // resolve issue 32093 // bootstrap attempts to make room to the right of an input for an error icon, diff --git a/packages/components/src/theme/grid.scss b/packages/components/src/theme/grid.scss index 4c2239d645..a27e9be596 100644 --- a/packages/components/src/theme/grid.scss +++ b/packages/components/src/theme/grid.scss @@ -274,7 +274,7 @@ $table-cell-padding: 4px 2px; background-color: $table-cell-selection-bg-color; } - &.cell-selected { + &.cell-selected, &:focus { border: $table-cell-selected-border; cursor: default; outline: none; From a7007d6650a5ed427e73d2ab3b72851ae77cd113 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 14:44:28 -0500 Subject: [PATCH 22/68] jest updates to match changes --- .../components/ColumnSelectionModal.test.tsx | 2 +- .../ExpandableContainer.test.tsx.snap | 8 +- .../__snapshots__/TimelineView.test.tsx.snap | 168 +++++++++--------- .../components/buttons/ToggleButtons.test.tsx | 4 +- .../components/user/UserLink.test.tsx | 16 +- .../QueryModel/ManageViewsModal.test.tsx | 6 +- 6 files changed, 101 insertions(+), 103 deletions(-) diff --git a/packages/components/src/internal/components/ColumnSelectionModal.test.tsx b/packages/components/src/internal/components/ColumnSelectionModal.test.tsx index 2390c646e6..3b2ef54822 100644 --- a/packages/components/src/internal/components/ColumnSelectionModal.test.tsx +++ b/packages/components/src/internal/components/ColumnSelectionModal.test.tsx @@ -123,7 +123,7 @@ describe('ColumnSelectionModal', () => { } else { expect(removeIcon).toBeTruthy(); const iconParent = removeIcon.parentElement; - expect(iconParent.className).toContain('view-field__action clickable'); + expect(iconParent.className).toContain('clickable-text view-field__action'); expect(iconParent.onclick).toBeDefined(); } } diff --git a/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap b/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap index 6986c0ff47..20f5bc41c2 100644 --- a/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap +++ b/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap @@ -19,8 +19,8 @@ exports[` custom props 1`] = `
    -
    default props 1`] = `
    -
    Disable selection 1`] = `
    @@ -110,11 +110,11 @@ exports[` Disable selection 1`] = `
    @@ -171,11 +171,11 @@ exports[` Disable selection 1`] = ` @@ -232,11 +232,11 @@ exports[` Disable selection 1`] = ` @@ -293,11 +293,11 @@ exports[` Disable selection 1`] = ` @@ -354,11 +354,11 @@ exports[` Disable selection 1`] = ` @@ -415,11 +415,11 @@ exports[` Disable selection 1`] = ` @@ -552,11 +552,11 @@ exports[` Hide user link 1`] = ` @@ -613,11 +613,11 @@ exports[` Hide user link 1`] = ` @@ -674,11 +674,11 @@ exports[` Hide user link 1`] = ` @@ -735,11 +735,11 @@ exports[` Hide user link 1`] = ` @@ -796,11 +796,11 @@ exports[` Hide user link 1`] = ` @@ -857,11 +857,11 @@ exports[` Hide user link 1`] = ` @@ -918,11 +918,11 @@ exports[` Hide user link 1`] = ` @@ -1055,11 +1055,11 @@ exports[` with selection, completed entity 1`] = ` @@ -1122,11 +1122,11 @@ exports[` with selection, completed entity 1`] = ` @@ -1183,11 +1183,11 @@ exports[` with selection, completed entity 1`] = ` @@ -1244,11 +1244,11 @@ exports[` with selection, completed entity 1`] = ` @@ -1305,11 +1305,11 @@ exports[` with selection, completed entity 1`] = ` @@ -1372,11 +1372,11 @@ exports[` with selection, completed entity 1`] = ` @@ -1433,11 +1433,11 @@ exports[` with selection, completed entity 1`] = ` @@ -1570,11 +1570,11 @@ exports[` with selection, open entity 1`] = ` @@ -1631,11 +1631,11 @@ exports[` with selection, open entity 1`] = ` @@ -1698,11 +1698,11 @@ exports[` with selection, open entity 1`] = ` @@ -1759,11 +1759,11 @@ exports[` with selection, open entity 1`] = ` @@ -1820,11 +1820,11 @@ exports[` with selection, open entity 1`] = ` @@ -1881,11 +1881,11 @@ exports[` with selection, open entity 1`] = ` @@ -1942,11 +1942,11 @@ exports[` with selection, open entity 1`] = ` diff --git a/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx b/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx index 37215e4adc..3680c22322 100644 --- a/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx +++ b/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx @@ -103,7 +103,7 @@ describe('ToggleIcon', () => { expect(document.getElementsByClassName('toggle-off').length).toBe(1); expect(document.getElementsByClassName('fa-toggle-off').length).toBe(1); - await userEvent.click(document.getElementsByTagName('i')[0]); + await userEvent.click(document.getElementsByTagName('button')[0]); expect(onClickFn).toHaveBeenCalledTimes(1); expect(onClickFn).toHaveBeenCalledWith('on'); }); @@ -141,7 +141,7 @@ describe('ToggleIcon', () => { expect(document.getElementsByClassName('overlay-trigger').length).toBe(1); - await userEvent.click(document.getElementsByTagName('i')[0]); + await userEvent.click(document.getElementsByTagName('button')[0]); expect(onClickFn).toHaveBeenCalledTimes(1); expect(onClickFn).toHaveBeenCalledWith('on'); }); diff --git a/packages/components/src/internal/components/user/UserLink.test.tsx b/packages/components/src/internal/components/user/UserLink.test.tsx index e8c480b2ff..d4a15ec3c9 100644 --- a/packages/components/src/internal/components/user/UserLink.test.tsx +++ b/packages/components/src/internal/components/user/UserLink.test.tsx @@ -43,13 +43,12 @@ describe('UserLink', () => { { serverContext: { user: TEST_USER_APP_ADMIN } } ); await waitFor(() => { - expect(container.querySelectorAll('a')).toHaveLength(1); + expect(container.querySelectorAll('button')).toHaveLength(1); }); - expect(container.querySelectorAll('a')).toHaveLength(1); - expect(container.querySelectorAll('.clickable')).toHaveLength(1); + expect(container.querySelectorAll('.clickable-text')).toHaveLength(1); expect(container.querySelectorAll('span')).toHaveLength(0); expect(container.querySelectorAll('.gray-text')).toHaveLength(0); - expect(container.querySelector('a').textContent).toBe('Test display'); + expect(container.querySelector('button').textContent).toBe('Test display'); }); test('user cannot ReadUserDetails, not self', async () => { @@ -61,7 +60,7 @@ describe('UserLink', () => { expect(container.querySelectorAll('span')).toHaveLength(1); }); expect(container.querySelectorAll('a')).toHaveLength(0); - expect(container.querySelectorAll('.clickable')).toHaveLength(0); + expect(container.querySelectorAll('.clickable-text')).toHaveLength(0); expect(container.querySelectorAll('span')).toHaveLength(1); expect(container.querySelectorAll('.gray-text')).toHaveLength(0); expect(container.querySelector('span').textContent).toBe('Test display'); @@ -73,13 +72,12 @@ describe('UserLink', () => { { serverContext: { user: TEST_USER_READER } } ); await waitFor(() => { - expect(container.querySelectorAll('a')).toHaveLength(1); + expect(container.querySelectorAll('button')).toHaveLength(1); }); - expect(container.querySelectorAll('a')).toHaveLength(1); - expect(container.querySelectorAll('.clickable')).toHaveLength(1); + expect(container.querySelectorAll('.clickable-text')).toHaveLength(1); expect(container.querySelectorAll('span')).toHaveLength(0); expect(container.querySelectorAll('.gray-text')).toHaveLength(0); - expect(container.querySelector('a').textContent).toBe('Test display'); + expect(container.querySelector('button').textContent).toBe('Test display'); }); }); diff --git a/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx b/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx index e28b785a30..63813752cc 100644 --- a/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx +++ b/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx @@ -206,7 +206,7 @@ describe('ManageViewsModal', () => { expect(rows[1].querySelector('.col-xs-8').textContent.trim()).toBe('View 1'); expect(rows[1].querySelectorAll('.fa-pencil')).toHaveLength(1); expect(rows[1].querySelectorAll('.fa-trash-o')).toHaveLength(1); - expect(rows[1].querySelectorAll('.clickable-text')).toHaveLength(1); + expect(rows[1].querySelectorAll('.clickable-text')).toHaveLength(3); expect(rows[1].querySelector('.clickable-text').textContent).toBe('Make default'); expect(rows[2].querySelector('.col-xs-8').textContent.trim()).toBe('View 2 (edited)'); @@ -219,7 +219,7 @@ describe('ManageViewsModal', () => { expect(rows[3].querySelectorAll('.fa-pencil')).toHaveLength(1); expect(rows[3].querySelectorAll('.fa-trash-o')).toHaveLength(1); expect(rows[0].querySelectorAll('.gray-text')).toHaveLength(0); - expect(rows[3].querySelectorAll('.clickable-text')).toHaveLength(1); + expect(rows[3].querySelectorAll('.clickable-text')).toHaveLength(3); expect(rows[3].querySelector('.clickable-text').textContent).toBe('Make default'); expect(document.querySelector('button.btn-default').textContent).toEqual('Done'); @@ -277,7 +277,7 @@ describe('ManageViewsModal', () => { expect(rows[1].querySelector('.col-xs-8').textContent.trim()).toBe('View 1'); expect(rows[1].querySelectorAll('.fa-pencil')).toHaveLength(1); expect(rows[1].querySelectorAll('.fa-trash-o')).toHaveLength(1); - expect(rows[1].querySelectorAll('.clickable-text')).toHaveLength(0); + expect(rows[1].querySelectorAll('.clickable-text')).toHaveLength(2); expect(rows[2].querySelector('.col-xs-8').textContent.trim()).toBe('View 2 (edited)'); expect(rows[2].querySelectorAll('.fa-pencil')).toHaveLength(0); From 003b2c47bcbe8d835b8c428f9254893c9b80d9f3 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 14:45:03 -0500 Subject: [PATCH 23/68] 7.35.2-fb-keyboardInteraction.5 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 1cfa8d40b2..b5221393eb 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.4", + "version": "7.35.2-fb-keyboardInteraction.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.4", + "version": "7.35.2-fb-keyboardInteraction.5", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 797f4331c8..30d01ce9de 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.4", + "version": "7.35.2-fb-keyboardInteraction.5", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 8efd0712e711211bc730e1990b2d3fff38c41fac Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 17:01:00 -0500 Subject: [PATCH 24/68] another round of useEnterEscape and button element changes for tab navigation --- .../src/internal/components/EditInlineField.tsx | 2 +- .../components/forms/detail/DetailPanelHeader.tsx | 4 ++-- .../src/internal/renderers/AttachmentCard.tsx | 13 ++++++++++++- packages/components/src/theme/attachment-card.scss | 3 +++ packages/components/src/theme/detail.scss | 7 +++---- packages/components/src/theme/fields.scss | 2 +- packages/components/src/theme/notification.scss | 5 +++++ 7 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/components/src/internal/components/EditInlineField.tsx b/packages/components/src/internal/components/EditInlineField.tsx index 32afb584ea..1c1f03b69f 100644 --- a/packages/components/src/internal/components/EditInlineField.tsx +++ b/packages/components/src/internal/components/EditInlineField.tsx @@ -315,7 +315,7 @@ export const EditInlineField: FC = memo(props => { className={classNames({ 'edit-inline-field__toggle': allowEdit, 'ws-pre-wrap': isTextArea })} onClick={toggleEdit} onKeyDown={toggleKeyDown} - tabIndex={1} + tabIndex={0} > {allowEdit && pullRight && } {!isUser && displayValue} diff --git a/packages/components/src/internal/components/forms/detail/DetailPanelHeader.tsx b/packages/components/src/internal/components/forms/detail/DetailPanelHeader.tsx index 7a52a986a0..f982579fd6 100644 --- a/packages/components/src/internal/components/forms/detail/DetailPanelHeader.tsx +++ b/packages/components/src/internal/components/forms/detail/DetailPanelHeader.tsx @@ -49,9 +49,9 @@ export const DetailPanelHeader: FC = memo(props => { {isEditable && ( <> -
    +
    +
    )} diff --git a/packages/components/src/internal/renderers/AttachmentCard.tsx b/packages/components/src/internal/renderers/AttachmentCard.tsx index 6b9aa71863..3d44b2311e 100644 --- a/packages/components/src/internal/renderers/AttachmentCard.tsx +++ b/packages/components/src/internal/renderers/AttachmentCard.tsx @@ -6,6 +6,7 @@ import { formatBytes, getIconFontCls, isImage } from '../util/utils'; import { isLoading, LoadingState } from '../../public/LoadingState'; import { LoadingSpinner } from '../components/base/LoadingSpinner'; import { DropdownMenu, MenuItem } from '../dropdowns'; +import { useEnterEscape } from '../../public/useEnterEscape'; const now = (): number => new Date().valueOf(); @@ -76,6 +77,14 @@ export const AttachmentCard: FC = memo(props => { } }, [allowRemove, attachment, onRemove]); + const _onBodyAction = useCallback(() => { + if (!attachment || attachment.unavailable || isLoading(attachment.loadingState)) return; + if (isImage(attachment.name)) _showModal(); + else _onDownload(); + }, [attachment, _showModal, _onDownload]); + + const onBodyKeyDown = useEnterEscape(_onBodyAction); + const showMenu = useMemo(() => { return ((onCopyLink || allowDownload) && !attachment?.unavailable) || allowRemove; }, [onCopyLink, allowDownload, attachment, allowRemove]); @@ -106,7 +115,9 @@ export const AttachmentCard: FC = memo(props => { >
    {_isImage && !isLoaded && } diff --git a/packages/components/src/theme/attachment-card.scss b/packages/components/src/theme/attachment-card.scss index 991a684ca3..c781db786a 100644 --- a/packages/components/src/theme/attachment-card.scss +++ b/packages/components/src/theme/attachment-card.scss @@ -83,6 +83,9 @@ &:focus, &:hover { color: $gray-dark; } + &:focus-visible { + outline: 1px auto -webkit-focus-ring-color; + } } } .attachment-card__name { diff --git a/packages/components/src/theme/detail.scss b/packages/components/src/theme/detail.scss index 6aed171bcb..079336056d 100644 --- a/packages/components/src/theme/detail.scss +++ b/packages/components/src/theme/detail.scss @@ -151,11 +151,10 @@ float: right; } -.detail__edit-button { - cursor: pointer; - +.detail__edit-button .fa { + color: $text-color-light; &:hover { - color: #adabab; + color: $gray-dark; } } diff --git a/packages/components/src/theme/fields.scss b/packages/components/src/theme/fields.scss index e4a58791e3..0f147bd918 100644 --- a/packages/components/src/theme/fields.scss +++ b/packages/components/src/theme/fields.scss @@ -17,7 +17,7 @@ display: inline-block; &:hover { - color: $gray-base; + color: $gray-dark;; cursor: pointer; } } diff --git a/packages/components/src/theme/notification.scss b/packages/components/src/theme/notification.scss index 6c19cd530d..91a6cfcfad 100644 --- a/packages/components/src/theme/notification.scss +++ b/packages/components/src/theme/notification.scss @@ -132,6 +132,11 @@ button.clickable-text { &.fa { color: $text-color-light; padding-left: 5px; + text-decoration: none; + + &:hover { + color: $gray-dark; + } } } From 77eafdc061f93a61b196ebb071a9a03b5c54fe5f Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 8 May 2026 17:21:44 -0500 Subject: [PATCH 25/68] 7.35.2-fb-keyboardInteraction.6 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index b5221393eb..8c44b05a9f 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.5", + "version": "7.35.2-fb-keyboardInteraction.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.5", + "version": "7.35.2-fb-keyboardInteraction.6", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 30d01ce9de..5f72baafc5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.5", + "version": "7.35.2-fb-keyboardInteraction.6", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 00892f89819a7ecb8ff3a4a834ead9b4fd9756f3 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Mon, 11 May 2026 09:24:24 -0700 Subject: [PATCH 26/68] Add focus-visible styling for dropdown toggles --- packages/components/src/theme/dropdowns.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/components/src/theme/dropdowns.scss b/packages/components/src/theme/dropdowns.scss index ef12f9c2a5..8e8277025c 100644 --- a/packages/components/src/theme/dropdowns.scss +++ b/packages/components/src/theme/dropdowns.scss @@ -34,3 +34,10 @@ color: color.adjust(lightgray, $lightness: -10%); } } + +.lk-dropdown .dropdown-toggle { + &:focus-visible { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: 2px; + } +} From 864a154de037d9ffd62caaf8522a3b7f026b27b9 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Mon, 11 May 2026 09:25:42 -0700 Subject: [PATCH 27/68] Add onKeyDown property for menu properties that uses the onClick method --- packages/components/src/internal/dropdowns.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/components/src/internal/dropdowns.tsx b/packages/components/src/internal/dropdowns.tsx index 172fe58113..70ba320210 100644 --- a/packages/components/src/internal/dropdowns.tsx +++ b/packages/components/src/internal/dropdowns.tsx @@ -20,7 +20,7 @@ import { generateId } from './util/utils'; import { cancelEvent } from './events'; import { AppLink } from './url/AppLink'; import { AppURL } from './url/AppURL'; -import { Icon } from './Icon'; +import { useEnterEscape } from '../public/useEnterEscape'; export type BSStyle = 'success' | 'danger' | 'default' | 'primary' | 'info'; const DROPDOWN_MENU_CLASS = 'dropdown-menu'; @@ -94,14 +94,18 @@ export const DropdownMenu: FC = props => { const className = classNames('lk-dropdown', 'dropdown', props.className, { open }); const menuClassName = classNames(DROPDOWN_MENU_CLASS, { 'dropdown-menu-right': pullRight }); + const onKeyDown = useEnterEscape(onClick); + const elemProps = { 'aria-haspopup': true, 'aria-expanded': open, className: 'dropdown-toggle', id, onClick, + onKeyDown, ref: toggleRef, role: 'button', + tabIndex: 0, title: label, }; @@ -310,6 +314,7 @@ export interface MenuItemProps { disabled?: boolean; href?: string | AppURL; onClick?: () => void; + onKeyDown?: (e: React.KeyboardEvent) => void; onMouseEnter?: () => void; onMouseLeave?: () => void; rel?: string; @@ -327,6 +332,7 @@ export const MenuItem = forwardRef((props, ref) => disabled, href = '#', onClick, + onKeyDown, onMouseEnter, onMouseLeave, rel, @@ -356,6 +362,7 @@ export const MenuItem = forwardRef((props, ref) => Date: Mon, 11 May 2026 11:14:45 -0700 Subject: [PATCH 28/68] Announcement and Thread component updates for keyboard navigation --- .../components/releaseNotes/components.md | 2 ++ .../internal/announcements/Discussions.tsx | 4 ++- .../announcements/ThreadAttachments.tsx | 4 +++ .../internal/announcements/ThreadBlock.tsx | 4 ++- .../internal/announcements/ThreadEditor.tsx | 12 +++++-- .../src/internal/renderers/AttachmentCard.tsx | 35 ++++++++++++------- .../components/src/theme/announcements.scss | 23 ++++++++++-- 7 files changed, 65 insertions(+), 19 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index b4ba0d04c2..cd352e6d3c 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -10,6 +10,8 @@ Components, models, actions, and utility functions for LabKey applications and p - Use buttons with clickable-text styling instead of spans and divs with onClick properties - Update `useEnterEscape` to allow optional event argument to callbacks - EditableGrid Cell to allow tab focus with tabIndex 0 + - Update styling for file inputs on `AttachmentCard` so input field is not hidden + - Add `tabIndex` and `onKeyDown` callback for thread components ### version 7.35.1 *Released*: 7 May 2026 diff --git a/packages/components/src/internal/announcements/Discussions.tsx b/packages/components/src/internal/announcements/Discussions.tsx index 3f69b544c2..6128a9c94c 100644 --- a/packages/components/src/internal/announcements/Discussions.tsx +++ b/packages/components/src/internal/announcements/Discussions.tsx @@ -9,6 +9,7 @@ import { AnnouncementsAPIWrapper, getDefaultAnnouncementsAPIWrapper } from './AP import { AnnouncementModel } from './model'; import { Thread } from './Thread'; import { ThreadEditor } from './ThreadEditor'; +import { useEnterEscape } from '../../public/useEnterEscape'; interface Props { api?: AnnouncementsAPIWrapper; @@ -67,6 +68,7 @@ export const Discussions: FC = memo(props => { const onShow = useCallback(() => { setShowEditor(true); }, []); + const onShowKeyDown = useEnterEscape(onShow); const updatePendingThread = useCallback( (threadId: number, hasPendingChange: boolean) => { @@ -117,7 +119,7 @@ export const Discussions: FC = memo(props => { ))} {allowCreateThread && !showEditor && ( - + Start a thread diff --git a/packages/components/src/internal/announcements/ThreadAttachments.tsx b/packages/components/src/internal/announcements/ThreadAttachments.tsx index 3606afe7b4..a75148c2c7 100644 --- a/packages/components/src/internal/announcements/ThreadAttachments.tsx +++ b/packages/components/src/internal/announcements/ThreadAttachments.tsx @@ -4,6 +4,7 @@ import { Alert } from '../components/base/Alert'; import { Modal } from '../Modal'; import { Attachment, getAttachmentURL } from './model'; +import { useEnterEscape } from '../../public/useEnterEscape'; interface ThreadAttachmentProps { attachment: Attachment; @@ -13,6 +14,7 @@ interface ThreadAttachmentProps { const ThreadAttachment: FC = memo(({ attachment, containerPath, onRemove }) => { const _onRemove = useCallback(() => onRemove(attachment.name), [attachment, onRemove]); + const onRemoveKeyDown = useEnterEscape(_onRemove); // Only generate a URL if the file has been uploaded. const url = attachment.created !== undefined ? getAttachmentURL(attachment, containerPath) : undefined; @@ -22,6 +24,8 @@ const ThreadAttachment: FC = memo(({ attachment, containe )} diff --git a/packages/components/src/internal/announcements/ThreadBlock.tsx b/packages/components/src/internal/announcements/ThreadBlock.tsx index 69a8f881ff..de052e3ae7 100644 --- a/packages/components/src/internal/announcements/ThreadBlock.tsx +++ b/packages/components/src/internal/announcements/ThreadBlock.tsx @@ -17,6 +17,7 @@ import { fromNow, parseDate } from '../util/Date'; import { AnnouncementModel } from './model'; import { ThreadEditor, ThreadEditorProps } from './ThreadEditor'; import { ThreadAttachments } from './ThreadAttachments'; +import { useEnterEscape } from '../../public/useEnterEscape'; interface DeleteThreadBSModalProps { cancel: () => void; @@ -184,6 +185,7 @@ export const ThreadBlock: FC = props => { const onReply = useCallback(() => { setReplying(true); }, []); + const onReplyKeyDown = useEnterEscape(onReply); const onReplied = useCallback((thread: AnnouncementModel) => { clearTimeout(recentTimeout); @@ -219,7 +221,7 @@ export const ThreadBlock: FC = props => { {allowReply && ( - + Reply )} diff --git a/packages/components/src/internal/announcements/ThreadEditor.tsx b/packages/components/src/internal/announcements/ThreadEditor.tsx index 94cfd857c3..b099997548 100644 --- a/packages/components/src/internal/announcements/ThreadEditor.tsx +++ b/packages/components/src/internal/announcements/ThreadEditor.tsx @@ -16,7 +16,7 @@ import { handleFileInputChange } from '../util/utils'; import { isLoading, LoadingState } from '../../public/LoadingState'; import { resolveErrorMessage } from '../util/messaging'; import { LoadingSpinner } from '../components/base/LoadingSpinner'; -import { Key } from '../../public/useEnterEscape'; +import { Key, useEnterEscape } from '../../public/useEnterEscape'; import { DropdownMenu, MenuItem } from '../dropdowns'; @@ -439,6 +439,7 @@ export const ThreadEditor: FC = props => { onCancel?.(); handlePendingChange(); }, [thread?.rowId, onCancel, handlePendingChange]); + const onCancelKeyDown = useEnterEscape(handleCancel); const onSubmit = useCallback(() => { if (submitting) return; @@ -518,10 +519,15 @@ export const ThreadEditor: FC = props => { - + Cancel diff --git a/packages/components/src/internal/renderers/AttachmentCard.tsx b/packages/components/src/internal/renderers/AttachmentCard.tsx index 3d44b2311e..266d93f467 100644 --- a/packages/components/src/internal/renderers/AttachmentCard.tsx +++ b/packages/components/src/internal/renderers/AttachmentCard.tsx @@ -64,18 +64,21 @@ export const AttachmentCard: FC = memo(props => { }, [setShowModal]); const _onCopyLink = useCallback((): void => onCopyLink(attachment), [attachment, onCopyLink]); + const onCopyKeyDown = useEnterEscape(_onCopyLink); const _onDownload = useCallback((): void => { if (allowDownload) { onDownload?.(attachment); } }, [allowDownload, attachment, onDownload]); + const onDownloadKeyDown = useEnterEscape(_onDownload); const _onRemove = useCallback(() => { if (allowRemove) { onRemove?.(attachment); } }, [allowRemove, attachment, onRemove]); + const onRemoveKeyDown = useEnterEscape(_onRemove); const _onBodyAction = useCallback(() => { if (!attachment || attachment.unavailable || isLoading(attachment.loadingState)) return; @@ -99,7 +102,7 @@ export const AttachmentCard: FC = memo(props => { const recentlyCreated = attachment.created ? attachment.created > now() - 30000 : false; const _isImage = isImage(attachment.name); const modalTitle = ( - + {title ?? name} ); @@ -113,16 +116,11 @@ export const AttachmentCard: FC = memo(props => { })} title={name + (unavailable ? ' (unavailable)' : '')} > -
    +
    {_isImage && !isLoaded && } {_isImage && isLoaded && !unavailable && ( - {name} + {name} )} {(!_isImage || unavailable) && }
    @@ -149,18 +147,31 @@ export const AttachmentCard: FC = memo(props => { pullRight title={} > - {onCopyLink && !unavailable && Copy {copyNoun}} - {allowDownload && !unavailable && Download} - {allowRemove && Remove {noun}} + {onCopyLink && !unavailable && ( + + Copy {copyNoun} + + )} + {allowDownload && !unavailable && ( + + Download + + )} + {allowRemove && ( + + Remove {noun} + + )} )}
    {showModal && ( - {`${name} + {`${name} )} ); }); +AttachmentCard.displayName = 'AttachmentCard'; diff --git a/packages/components/src/theme/announcements.scss b/packages/components/src/theme/announcements.scss index ca4291089f..e0d2e647c7 100644 --- a/packages/components/src/theme/announcements.scss +++ b/packages/components/src/theme/announcements.scss @@ -52,11 +52,18 @@ $editor-padding-horizontal: 15px; .insert-menu__attachment_input { cursor: pointer; font-weight: normal; + position: relative; margin: 0; width: 100%; input[type=file] { - display: none; + cursor: pointer; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; } } } @@ -148,9 +155,21 @@ $editor-padding-horizontal: 15px; .thread-editor__attachment-input { cursor: pointer; margin-left: 15px; + position: relative; input[type=file] { - display: none; + cursor: pointer; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; + } + + &:has(input[type=file]:focus-visible) { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: 2px; } } .thread-editor-attachments { From 936dea2c6ce073275db858c6bf80e3a170a86ae7 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Mon, 11 May 2026 11:19:32 -0700 Subject: [PATCH 29/68] @labkey/components v7.35.2-fb-keyboardInteraction.7 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 8c44b05a9f..1e296237a9 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.6", + "version": "7.35.2-fb-keyboardInteraction.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.6", + "version": "7.35.2-fb-keyboardInteraction.7", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 5f72baafc5..b5a81bfd52 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.6", + "version": "7.35.2-fb-keyboardInteraction.7", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 6e8e8a143daab4ae353550fdbee5cbf44d93b25d Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 11 May 2026 15:49:01 -0500 Subject: [PATCH 30/68] button element to set type="button" --- .../src/internal/components/ColumnSelectionModal.tsx | 7 +++++-- .../src/internal/components/ExpandableContainer.tsx | 1 + .../src/internal/components/buttons/ActionButton.tsx | 2 +- .../src/internal/components/buttons/ToggleButtons.tsx | 4 ++-- .../internal/components/forms/detail/DetailPanelHeader.tsx | 2 +- .../src/internal/components/navigation/FolderMenu.tsx | 2 +- .../components/src/internal/components/user/UserLink.tsx | 2 +- .../components/src/public/QueryModel/ManageViewsModal.tsx | 5 ++++- 8 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/components/src/internal/components/ColumnSelectionModal.tsx b/packages/components/src/internal/components/ColumnSelectionModal.tsx index cdde862702..304469bcd4 100644 --- a/packages/components/src/internal/components/ColumnSelectionModal.tsx +++ b/packages/components/src/internal/components/ColumnSelectionModal.tsx @@ -145,10 +145,10 @@ export const ColumnChoice: FC = memo(props => { ))}
    {column.isLookup() && !isExpanded && ( -
    @@ -167,6 +167,7 @@ export const ColumnChoice: FC = memo(props => { onMouseLeave={onMouseLeave} ref={targetRef} title={disabled ? undefined : 'Add this field to the view.'} + type="button" > {show && createPortal(popover, portalEl)} @@ -319,6 +320,7 @@ export const ColumnInView: FC = memo(props => { className="clickable-text edit-inline-field__toggle" onClick={_onEditTitle} title="Edit the field's label for this view." + type="button" > @@ -328,6 +330,7 @@ export const ColumnInView: FC = memo(props => { className="clickable-text view-field__action" onClick={_onRemoveColumn} title="Remove this field from the view." + type="button" > diff --git a/packages/components/src/internal/components/ExpandableContainer.tsx b/packages/components/src/internal/components/ExpandableContainer.tsx index d0c4059d05..10e17d4978 100644 --- a/packages/components/src/internal/components/ExpandableContainer.tsx +++ b/packages/components/src/internal/components/ExpandableContainer.tsx @@ -107,6 +107,7 @@ export const ExpandableContainer: FC = memo(props => { 'fa-chevron-down': visible, 'fa-chevron-right': !visible, })} + type="button" />
    diff --git a/packages/components/src/internal/components/buttons/ActionButton.tsx b/packages/components/src/internal/components/buttons/ActionButton.tsx index 6b7cd93d66..b405002dae 100644 --- a/packages/components/src/internal/components/buttons/ActionButton.tsx +++ b/packages/components/src/internal/components/buttons/ActionButton.tsx @@ -42,7 +42,7 @@ export class ActionButton extends React.PureComponent { return (
    - {helperBody && {helperBody}} diff --git a/packages/components/src/internal/components/buttons/ToggleButtons.tsx b/packages/components/src/internal/components/buttons/ToggleButtons.tsx index 2f77ac7991..126e6cec84 100644 --- a/packages/components/src/internal/components/buttons/ToggleButtons.tsx +++ b/packages/components/src/internal/components/buttons/ToggleButtons.tsx @@ -104,8 +104,8 @@ export const ToggleIcon: FC = memo(props => { const body = ( <> - {firstActive &&
    diff --git a/packages/components/src/internal/components/navigation/FolderMenu.tsx b/packages/components/src/internal/components/navigation/FolderMenu.tsx index 3b6df3255f..d9e46dd7f9 100644 --- a/packages/components/src/internal/components/navigation/FolderMenu.tsx +++ b/packages/components/src/internal/components/navigation/FolderMenu.tsx @@ -54,7 +54,7 @@ export const FolderMenuItems: FC = memo(props => { 'col-xs-10': !user.isAdmin, })} > -
    diff --git a/packages/components/src/internal/components/user/UserLink.tsx b/packages/components/src/internal/components/user/UserLink.tsx index 9801c2295e..2ce0d7a3e8 100644 --- a/packages/components/src/internal/components/user/UserLink.tsx +++ b/packages/components/src/internal/components/user/UserLink.tsx @@ -72,7 +72,7 @@ export const UserLink: FC = props => { return ( <> - {showDetails && ( diff --git a/packages/components/src/public/QueryModel/ManageViewsModal.tsx b/packages/components/src/public/QueryModel/ManageViewsModal.tsx index 386b56614a..6587da147b 100644 --- a/packages/components/src/public/QueryModel/ManageViewsModal.tsx +++ b/packages/components/src/public/QueryModel/ManageViewsModal.tsx @@ -231,7 +231,7 @@ export const ManageViewsModal: FC = memo(props => { } > {view.isSaved ? ( - ) : ( @@ -244,6 +244,7 @@ export const ManageViewsModal: FC = memo(props => { onClick={setDefaultView} id={'setDefault-' + ind} className="clickable-text" + type="button" > Make default @@ -255,6 +256,7 @@ export const ManageViewsModal: FC = memo(props => { className="clickable-text edit-inline-field__toggle small-right-padding" id={'select-' + ind} onClick={onSelectView} + type="button" > @@ -262,6 +264,7 @@ export const ManageViewsModal: FC = memo(props => { className="clickable-text edit-inline-field__toggle" id={'delete-' + ind} onClick={onDeleteView} + type="button" > From fe5c90af74aa5e410cd5fabebae24194b027531f Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 11 May 2026 15:49:32 -0500 Subject: [PATCH 31/68] 7.35.2-fb-keyboardInteraction.8 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 1e296237a9..f3c1d2b581 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.7", + "version": "7.35.2-fb-keyboardInteraction.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.7", + "version": "7.35.2-fb-keyboardInteraction.8", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index b5a81bfd52..5d45a28393 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.2-fb-keyboardInteraction.7", + "version": "7.35.2-fb-keyboardInteraction.8", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 200642ac439469302cf9197055bb0a23ee01942e Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 11 May 2026 15:50:55 -0500 Subject: [PATCH 32/68] jest snapshot updates to match --- .../ExpandableContainer.test.tsx.snap | 2 ++ .../__snapshots__/TimelineView.test.tsx.snap | 28 +++++++++++++++++++ .../SampleTypePropertiesPanel.test.tsx.snap | 8 ++++++ 3 files changed, 38 insertions(+) diff --git a/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap b/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap index 20f5bc41c2..9d81e72a07 100644 --- a/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap +++ b/packages/components/src/internal/components/__snapshots__/ExpandableContainer.test.tsx.snap @@ -21,6 +21,7 @@ exports[` custom props 1`] = ` >
    default props 1`] = ` >
    Disable selection 1`] = ` > @@ -112,6 +113,7 @@ exports[` Disable selection 1`] = ` > @@ -173,6 +175,7 @@ exports[` Disable selection 1`] = ` > @@ -234,6 +237,7 @@ exports[` Disable selection 1`] = ` > @@ -295,6 +299,7 @@ exports[` Disable selection 1`] = ` > @@ -356,6 +361,7 @@ exports[` Disable selection 1`] = ` > @@ -417,6 +423,7 @@ exports[` Disable selection 1`] = ` > @@ -554,6 +561,7 @@ exports[` Hide user link 1`] = ` > @@ -615,6 +623,7 @@ exports[` Hide user link 1`] = ` > @@ -676,6 +685,7 @@ exports[` Hide user link 1`] = ` > @@ -737,6 +747,7 @@ exports[` Hide user link 1`] = ` > @@ -798,6 +809,7 @@ exports[` Hide user link 1`] = ` > @@ -859,6 +871,7 @@ exports[` Hide user link 1`] = ` > @@ -920,6 +933,7 @@ exports[` Hide user link 1`] = ` > @@ -1057,6 +1071,7 @@ exports[` with selection, completed entity 1`] = ` > @@ -1124,6 +1139,7 @@ exports[` with selection, completed entity 1`] = ` > @@ -1185,6 +1201,7 @@ exports[` with selection, completed entity 1`] = ` > @@ -1246,6 +1263,7 @@ exports[` with selection, completed entity 1`] = ` > @@ -1307,6 +1325,7 @@ exports[` with selection, completed entity 1`] = ` > @@ -1374,6 +1393,7 @@ exports[` with selection, completed entity 1`] = ` > @@ -1435,6 +1455,7 @@ exports[` with selection, completed entity 1`] = ` > @@ -1572,6 +1593,7 @@ exports[` with selection, open entity 1`] = ` > @@ -1633,6 +1655,7 @@ exports[` with selection, open entity 1`] = ` > @@ -1700,6 +1723,7 @@ exports[` with selection, open entity 1`] = ` > @@ -1761,6 +1785,7 @@ exports[` with selection, open entity 1`] = ` > @@ -1822,6 +1847,7 @@ exports[` with selection, open entity 1`] = ` > @@ -1883,6 +1909,7 @@ exports[` with selection, open entity 1`] = ` > @@ -1944,6 +1971,7 @@ exports[` with selection, open entity 1`] = ` > diff --git a/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap b/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap index 9476df16ba..f6676f21c8 100644 --- a/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap +++ b/packages/components/src/internal/components/domainproperties/samples/__snapshots__/SampleTypePropertiesPanel.test.tsx.snap @@ -187,6 +187,7 @@ exports[`SampleTypePropertiesPanel appPropertiesOnly 1`] = `
    )} {onRemoveAll && showRemoveAll && ( - + )}
    ); From a8a1baf9c966fc4bccbe2eb10baa9f9d77b39c88 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 12 May 2026 11:39:41 -0500 Subject: [PATCH 41/68] Filter value component keyboard accessible with useEnterEscape --- packages/components/src/public/QueryModel/grid/Value.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/components/src/public/QueryModel/grid/Value.tsx b/packages/components/src/public/QueryModel/grid/Value.tsx index d5e5f8a539..8d886a54df 100644 --- a/packages/components/src/public/QueryModel/grid/Value.tsx +++ b/packages/components/src/public/QueryModel/grid/Value.tsx @@ -16,6 +16,8 @@ import React, { FC, memo, useCallback, useState } from 'react'; import classNames from 'classnames'; +import { useEnterEscape } from '../../useEnterEscape'; + import { ActionValue } from './actions/Action'; interface ValueProps { @@ -65,6 +67,7 @@ export const Value: FC = memo(({ actionValue, index, lockReadOnlyFor const onMouseEnter = useCallback((): void => setIsActive(true), []); const onMouseLeave = useCallback((): void => setIsActive(false), []); + const onKeyDown = useEnterEscape(onValueClick); const showRemoveIcon = isActive && isRemovable !== false && action.keyword !== 'view'; @@ -84,8 +87,10 @@ export const Value: FC = memo(({ actionValue, index, lockReadOnlyFor
    {(!lockReadOnlyForDelete || !isReadOnly) && } From 5999f8ea584ded5ca1149d8145f060662ed97311 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 12 May 2026 12:03:26 -0500 Subject: [PATCH 42/68] File upload container to get focus outline when "hidden" file input tag has focus --- packages/components/src/theme/fileupload.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/components/src/theme/fileupload.scss b/packages/components/src/theme/fileupload.scss index dc56065846..2b4c5e8e04 100644 --- a/packages/components/src/theme/fileupload.scss +++ b/packages/components/src/theme/fileupload.scss @@ -9,6 +9,11 @@ flex-grow: 2; } +.file-upload__container:has(:focus-visible) { + outline: 2px solid $brand-primary; + outline-offset: -1px; +} + .file-upload__label { cursor: pointer; color: #116596; From 61e6e79c5c53e7b862205deb21e2735332394136 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 12 May 2026 12:09:46 -0500 Subject: [PATCH 43/68] FormStep.tsx convert to FC and add useEnterEscape to FormTabs --- .../internal/components/forms/FormStep.tsx | 124 ++++++++++-------- 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/packages/components/src/internal/components/forms/FormStep.tsx b/packages/components/src/internal/components/forms/FormStep.tsx index bb611d63c7..45d6064b0b 100644 --- a/packages/components/src/internal/components/forms/FormStep.tsx +++ b/packages/components/src/internal/components/forms/FormStep.tsx @@ -2,9 +2,11 @@ * Copyright (c) 2017-2018 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ -import React, { PropsWithChildren, ReactNode } from 'react'; +import React, { FC, PropsWithChildren, ReactNode, useCallback, useContext } from 'react'; import classNames from 'classnames'; +import { useEnterEscape } from '../../../public/useEnterEscape'; + interface IFormStepContext { currentStep?: number; furthestStep?: number; @@ -70,65 +72,79 @@ export class FormStep extends React.Component { } } +interface FormTabItemProps { + active: boolean; + disabled: boolean; + onTabChange?: (stepIndex?: number) => any; + selectStep: (requestedStep?: number) => boolean; + step: number; + title: string; +} + +const FormTabItem: FC = ({ active, disabled, onTabChange, selectStep, step, title }) => { + const onSelectStep = useCallback(() => { + if (selectStep(step) !== false && onTabChange) { + onTabChange(step); + } + }, [onTabChange, selectStep, step]); + + const onKeyDown = useEnterEscape(disabled ? undefined : onSelectStep); + + return ( +
  • + {title} +
  • + ); +}; +FormTabItem.displayName = 'FormTabItem'; + interface FormTabsProps { onTabChange?: (stepIndex?: number) => any; tabs: string[]; } -export class FormTabs extends React.Component { - render() { - const { onTabChange, tabs } = this.props; +export const FormTabs: FC = ({ onTabChange, tabs }) => { + const context = useContext(FormStepContext); + if (!context) return null; + const { currentStep, furthestStep, hasDependentSteps, selectStep } = context; + + return ( +
    +
    +
      + {tabs.map((title, i) => { + const step = i + 1; + const disabled = + furthestStep === undefined + ? true + : hasDependentSteps + ? step > currentStep + : furthestStep < step; - return ( - - {(context: IFormStepContext) => { - if (!context) return null; - const { currentStep, furthestStep, hasDependentSteps, selectStep } = context; - - return ( -
      -
      -
        - {tabs.map((title, i) => { - const step = i + 1; - const disabled = - furthestStep === undefined - ? true - : hasDependentSteps - ? step > currentStep - : furthestStep < step; - - return ( -
      • { - if (selectStep(step) !== false && onTabChange) { - onTabChange(step); - } - } - } - > - {title} -
      • - ); - })} -
      -
      -
      -
      - ); - }} - - ); - } -} + return ( + + ); + })} +
    +
    +
    +
    + ); +}; +FormTabs.displayName = 'FormTabs'; export interface WithFormStepsState { currentStep?: number; From 0b417cb9558d4f056be82da26a8009eaec0083b5 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 12 May 2026 14:29:32 -0500 Subject: [PATCH 44/68] 7.35.3-fb-keyboardInteraction.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index a580521ca4..0bb8470d5f 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.3-fb-keyboardInteraction.0", + "version": "7.35.3-fb-keyboardInteraction.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.3-fb-keyboardInteraction.0", + "version": "7.35.3-fb-keyboardInteraction.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index fba436680b..e2d536b797 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.3-fb-keyboardInteraction.0", + "version": "7.35.3-fb-keyboardInteraction.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 2f4aca36c0d4c83631c4d65aefc48880774f6dcd Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 12 May 2026 12:47:50 -0700 Subject: [PATCH 45/68] Use button instead of div for Select/Deselect All --- .../src/internal/components/entities/DataTypeSelector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/entities/DataTypeSelector.tsx b/packages/components/src/internal/components/entities/DataTypeSelector.tsx index 7222e06067..f8bdffbb54 100644 --- a/packages/components/src/internal/components/entities/DataTypeSelector.tsx +++ b/packages/components/src/internal/components/entities/DataTypeSelector.tsx @@ -360,9 +360,9 @@ export const DataTypeSelector: FC = memo(props => { {toggleSelectAll && !disabled && dataTypes?.length > 0 && (
    -
    +
    +
    )} From f4ca746f9b796cfca18b88d6b1e2f36b3562a6eb Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 12 May 2026 13:00:10 -0700 Subject: [PATCH 46/68] Convert to FC to use useEnterEscape hook --- .../CollapsiblePanelHeader.tsx | 192 +++++++++--------- 1 file changed, 93 insertions(+), 99 deletions(-) diff --git a/packages/components/src/internal/components/domainproperties/CollapsiblePanelHeader.tsx b/packages/components/src/internal/components/domainproperties/CollapsiblePanelHeader.tsx index 59734dfaf7..f579506b78 100644 --- a/packages/components/src/internal/components/domainproperties/CollapsiblePanelHeader.tsx +++ b/packages/components/src/internal/components/domainproperties/CollapsiblePanelHeader.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, ReactNode } from 'react'; +import React, { FC, memo, PropsWithChildren, ReactNode } from 'react'; import classNames from 'classnames'; import { isApp } from '../../app/utils'; @@ -6,6 +6,7 @@ import { isApp } from '../../app/utils'; import { LabelHelpTip } from '../base/LabelHelpTip'; import { DomainPanelStatus } from './models'; +import { useEnterEscape } from '../../../public/useEnterEscape'; interface Props extends PropsWithChildren { collapsed: boolean; @@ -22,105 +23,98 @@ interface Props extends PropsWithChildren { togglePanel: () => void; } -export class CollapsiblePanelHeader extends React.PureComponent { - getHeaderIconHelpMsg = (): string => { - const { isValid, panelStatus, iconHelpMsg, todoIconHelpMsg } = this.props; - +export const CollapsiblePanelHeader: FC = memo(props => { + const { + children, + collapsed, + collapsible, + controlledCollapse, + headerDetails, + iconHelpMsg, + id, + isValid, + panelStatus, + title, + titlePrefix, + todoIconHelpMsg, + togglePanel, + } = props; + const isApp_ = isApp(); + const onKeyDown = useEnterEscape(togglePanel); + + let iconHelpMsgStr: string; + if (panelStatus && panelStatus !== 'NONE') { if (!isValid) { - return iconHelpMsg; - } - - if (panelStatus === 'TODO') { - return todoIconHelpMsg || 'This section does not contain any user-defined fields. You may want to review.'; - } - - return undefined; - }; - - getHeaderIconComponent = (): ReactNode => { - const { collapsed, isValid, panelStatus } = this.props; - const validComplete = isValid && panelStatus === 'COMPLETE'; - const wrapperClassName = classNames('domain-panel-status-icon', { - 'domain-panel-status-icon-green': collapsed && validComplete, - 'domain-panel-status-icon-blue': collapsed && !validComplete, - }); - const iconClassName = !isValid || panelStatus === 'TODO' ? 'fa fa-exclamation-circle' : 'fa fa-check-circle'; - - return ( - - - - ); - }; - - getTitlePrefix = (): string => { - let prefix = this.props.titlePrefix; - - // ellipsis after certain length - if (prefix && prefix.length > 70) { - prefix = prefix.substr(0, 70) + '...'; + iconHelpMsgStr = iconHelpMsg; + } else if (panelStatus === 'TODO') { + iconHelpMsgStr = + todoIconHelpMsg || 'This section does not contain any user-defined fields. You may want to review.'; } + } - return prefix ? prefix + ' - ' : ''; - }; - - render() { - const { - children, - collapsed, - collapsible, - controlledCollapse, - headerDetails, - id, - panelStatus, - title, - togglePanel, - } = this.props; - const isApp_ = isApp(); - const iconHelpMsg = panelStatus && panelStatus !== 'NONE' ? this.getHeaderIconHelpMsg() : undefined; - const collapsedIconClass = classNames('fa', 'fa-lg', { - 'fa-chevron-right': collapsed, - 'fa-chevron-down': !collapsed, - 'domain-form-expand-btn': collapsed, - 'domain-form-collapse-btn': !collapsed, - }); - const panelHeaderClass = classNames('domain-panel-header', { - 'panel-heading': isApp_, - 'domain-heading-collapsible': collapsible || controlledCollapse, - 'domain-panel-header-expanded': !collapsed, - 'domain-panel-header-collapsed': collapsed, - 'labkey-page-nav': !collapsed && !isApp_, - 'domain-panel-header-no-theme': !collapsed && isApp_, - }); - - return ( -
    - {/* Header help icon*/} - {iconHelpMsg && ( - - {iconHelpMsg} - - )} - {panelStatus && panelStatus !== 'NONE' && !iconHelpMsg && this.getHeaderIconComponent()} - - {/* Header name*/} - {this.getTitlePrefix() + title} - - {/* Expand/Collapse Icon*/} - {(controlledCollapse || collapsible) && ( - - - - )} - - {/* Help tip*/} - {children && {children}} - - {/* Header details, shown on the right side*/} - {controlledCollapse && headerDetails && ( - {headerDetails} - )} -
    - ); + const validComplete = isValid && panelStatus === 'COMPLETE'; + const headerIconComponent: ReactNode = ( + + + + ); + + let prefix = titlePrefix; + if (prefix && prefix.length > 70) { + prefix = prefix.substring(0, 70) + '...'; } -} + const titlePrefixStr = prefix ? prefix + ' - ' : ''; + + const collapsedIconClass = classNames('fa', 'fa-lg', { + 'fa-chevron-right': collapsed, + 'fa-chevron-down': !collapsed, + 'domain-form-expand-btn': collapsed, + 'domain-form-collapse-btn': !collapsed, + }); + const panelHeaderClass = classNames('domain-panel-header', { + 'panel-heading': isApp_, + 'domain-heading-collapsible': collapsible || controlledCollapse, + 'domain-panel-header-expanded': !collapsed, + 'domain-panel-header-collapsed': collapsed, + 'labkey-page-nav': !collapsed && !isApp_, + 'domain-panel-header-no-theme': !collapsed && isApp_, + }); + + return ( +
    + {iconHelpMsgStr && ( + + {iconHelpMsgStr} + + )} + {panelStatus && panelStatus !== 'NONE' && !iconHelpMsgStr && headerIconComponent} + + {titlePrefixStr + title} + + {(controlledCollapse || collapsible) && ( + + + + )} + + {children && {children}} + + {controlledCollapse && headerDetails && ( + {headerDetails} + )} +
    + ); +}); +CollapsiblePanelHeader.displayName = 'CollapsiblePanelHeader'; From 4bd679a7dc9c389ea7cdf96f6b6b75725ce493b5 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 12 May 2026 14:04:07 -0700 Subject: [PATCH 47/68] Move onEnterKeyDown to public/useEnterEscape --- packages/components/src/index.ts | 3 ++- packages/components/src/public/useEnterEscape.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index ba0b3643db..db273400e9 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -849,7 +849,7 @@ import { WORKFLOW_HOME_HREF, WORKFLOW_KEY, } from './internal/app/constants'; -import { Key, useEnterEscape } from './public/useEnterEscape'; +import { Key, useEnterEscape, onEnterKeyDown } from './public/useEnterEscape'; import { DateInput } from './internal/components/DateInput'; import { EditInlineField } from './internal/components/EditInlineField'; import { FileAttachmentArea } from './internal/components/files/FileAttachmentArea'; @@ -1574,6 +1574,7 @@ export { NOT_IN_EXP_DESCENDANTS_OF_FILTER_TYPE, Notifications, NotificationsContextProvider, + onEnterKeyDown, OntologyBrowserFilterPanel, OntologyBrowserPage, OntologyConceptOverviewPanel, diff --git a/packages/components/src/public/useEnterEscape.ts b/packages/components/src/public/useEnterEscape.ts index 051b34b69c..7feaf84bfe 100644 --- a/packages/components/src/public/useEnterEscape.ts +++ b/packages/components/src/public/useEnterEscape.ts @@ -48,3 +48,18 @@ export const useEnterEscape = (onEnter?: (evt?: any) => void, onEscape?: (evt?: [allowMultiSelect, onEnter, onEscape] ); }; + +// For use with PureComponents that can't use the above hook +export const onEnterKeyDown = (onClick: () => any): ((evt: any) => void) => { + return (evt: KeyboardEvent) => { + if (evt.shiftKey || evt.metaKey) return; + + switch (evt.key) { + case Key.ENTER: + evt.stopPropagation(); + evt.preventDefault(); + onClick(); + break; + } + }; +}; From 82540bb1298f8edeec05d1af57997847986d0f6f Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 12 May 2026 14:04:25 -0700 Subject: [PATCH 48/68] Use button instead of div --- .../internal/components/base/ExpandableFilterToggle.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/internal/components/base/ExpandableFilterToggle.tsx b/packages/components/src/internal/components/base/ExpandableFilterToggle.tsx index 02466d8d2e..52a56c8ac2 100644 --- a/packages/components/src/internal/components/base/ExpandableFilterToggle.tsx +++ b/packages/components/src/internal/components/base/ExpandableFilterToggle.tsx @@ -19,7 +19,7 @@ export class ExpandableFilterToggle extends PureComponent { return ( <> -
    +
    + {hasFilter && ( - + )} ); From a2502e8c7afda3f65554099769f811a3a519004c Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 12 May 2026 14:05:05 -0700 Subject: [PATCH 49/68] add onKeyDown handlers --- .../internal/components/auditlog/TimelineView.tsx | 13 +++++++++---- .../components/domainproperties/SystemFields.tsx | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/components/src/internal/components/auditlog/TimelineView.tsx b/packages/components/src/internal/components/auditlog/TimelineView.tsx index 2969999167..f7100ece1b 100644 --- a/packages/components/src/internal/components/auditlog/TimelineView.tsx +++ b/packages/components/src/internal/components/auditlog/TimelineView.tsx @@ -9,6 +9,7 @@ import { UserLink } from '../user/UserLink'; import { TimelineEventModel, TimelineGroupedEventInfo } from './models'; import { getEventDataValueDisplay } from './utils'; +import { onEnterKeyDown } from '../../../public/useEnterEscape'; interface Props { events: TimelineEventModel[]; @@ -57,17 +58,21 @@ export class TimelineView extends React.Component { } else if (info.firstEvent && event.getRowKey() === info.firstEvent.getRowKey()) isFirstEvent = true; }); } + const onClick = () => { + if (event.rowId) this.selectEvent(event); + }; + const onKeyDown = onEnterKeyDown(onClick); return ( { - if (event.rowId) this.selectEvent(event); - }} className={classNames({ 'timeline-event-row': event.rowId !== 0, 'timeline-row-selected': eventSelected, })} + key={event.getRowKey()} + onClick={onClick} + onKeyDown={onKeyDown} + tabIndex={event.rowId ? 0 : -1} > {this.renderTimestampCol(event.timestamp)} {this.renderIconCol( diff --git a/packages/components/src/internal/components/domainproperties/SystemFields.tsx b/packages/components/src/internal/components/domainproperties/SystemFields.tsx index 429cf2994e..68436986d9 100644 --- a/packages/components/src/internal/components/domainproperties/SystemFields.tsx +++ b/packages/components/src/internal/components/domainproperties/SystemFields.tsx @@ -9,6 +9,7 @@ import { GridColumn } from '../base/models/GridColumn'; import { SystemField } from './models'; import { Collapsible } from './Collapsible'; +import { useEnterEscape } from '../../../public/useEnterEscape'; interface Props { disabledSystemFields?: string[]; @@ -57,6 +58,7 @@ export const SystemFields: FC = memo(({ fields, disabledSystemFields, onS const onToggle = useCallback(() => { setCollapsed(!collapsed); }, [collapsed]); + const onKeyDown = useEnterEscape(onToggle); const gridData: SystemField[] = useMemo(() => { const data: SystemField[] = []; @@ -106,7 +108,7 @@ export const SystemFields: FC = memo(({ fields, disabledSystemFields, onS
    Default System Fields
    -
    +
    From 05ab27b13242c04cd2aa69b832608790ae7a0fb3 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 12 May 2026 14:23:05 -0700 Subject: [PATCH 50/68] @labkey/components v7.35.3-fb-keyboardInteraction.2 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 0bb8470d5f..423d2b2c6c 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.35.3-fb-keyboardInteraction.1", + "version": "7.35.3-fb-keyboardInteraction.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.35.3-fb-keyboardInteraction.1", + "version": "7.35.3-fb-keyboardInteraction.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index e2d536b797..342643179c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.35.3-fb-keyboardInteraction.1", + "version": "7.35.3-fb-keyboardInteraction.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From bb597dbf7fd507510c7db38d0a320296eb08c9d2 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Wed, 13 May 2026 08:37:46 -0700 Subject: [PATCH 51/68] Put keyDown on timestamp part of timeline row --- .../components/auditlog/TimelineView.tsx | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/components/src/internal/components/auditlog/TimelineView.tsx b/packages/components/src/internal/components/auditlog/TimelineView.tsx index f7100ece1b..c2cd19b5d7 100644 --- a/packages/components/src/internal/components/auditlog/TimelineView.tsx +++ b/packages/components/src/internal/components/auditlog/TimelineView.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { FC, ReactNode } from 'react'; import classNames from 'classnames'; import { SVGIcon } from '../base/SVGIcon'; @@ -9,7 +9,29 @@ import { UserLink } from '../user/UserLink'; import { TimelineEventModel, TimelineGroupedEventInfo } from './models'; import { getEventDataValueDisplay } from './utils'; -import { onEnterKeyDown } from '../../../public/useEnterEscape'; +import { useEnterEscape } from '../../../public/useEnterEscape'; + +interface TimelineTimestampProps { + event: TimelineEventModel; + onSelect: (event: TimelineEventModel) => void; +} + +const TimelineTimestamp: FC = ({ event, onSelect }) => { + const onClick = () => { + if (event.rowId) onSelect(event); + }; + const onKeyDown = useEnterEscape(onClick); + return ( + + {getEventDataValueDisplay(event.timestamp)} + + ); +}; interface Props { events: TimelineEventModel[]; @@ -61,7 +83,6 @@ export class TimelineView extends React.Component { const onClick = () => { if (event.rowId) this.selectEvent(event); }; - const onKeyDown = onEnterKeyDown(onClick); return ( { })} key={event.getRowKey()} onClick={onClick} - onKeyDown={onKeyDown} - tabIndex={event.rowId ? 0 : -1} > - {this.renderTimestampCol(event.timestamp)} + {this.renderIconCol( event.getIcon(), eventSelected, @@ -88,14 +107,6 @@ export class TimelineView extends React.Component { ); } - renderTimestampCol(timestamp) { - return ( - - {getEventDataValueDisplay(timestamp)} - - ); - } - renderIconCol( iconSrc: string, isSelected?: boolean, From 557cc7c742fdadddc51a8d8eee77bfbfeb0a88bb Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 13 May 2026 10:50:21 -0500 Subject: [PATCH 52/68] anchor tag to button with clickable-text class --- .../internal/components/files/FileAttachmentEntry.tsx | 4 ++-- packages/components/src/theme/fileupload.scss | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/components/src/internal/components/files/FileAttachmentEntry.tsx b/packages/components/src/internal/components/files/FileAttachmentEntry.tsx index f609946db5..d7ea822641 100644 --- a/packages/components/src/internal/components/files/FileAttachmentEntry.tsx +++ b/packages/components/src/internal/components/files/FileAttachmentEntry.tsx @@ -9,10 +9,10 @@ interface Props { export const FileAttachmentEntry: FC = memo(props => { const { downloadUrl, onDelete, name } = props; const onClick = useCallback(() => onDelete(name), [onDelete, name]); - const deleteIconClassName = 'fa fa-times-circle attached-file__remove-icon'; + const deleteIconClassName = 'fa fa-times-circle clickable-text attached-file__remove-icon'; return (
    - {onDelete && } + {onDelete && {showChild ?
  • {child}
  • : null} @@ -131,14 +131,15 @@ const DetailsListLineageItem: FC = memo(({ highligh {context => { if (context.isNodeInGraph(item)) { return ( - context.onNodeClick(item)} onMouseOver={e => context.onNodeMouseOver(item)} onMouseOut={e => context.onNodeMouseOut(item)} + type="button" > {item.name} - + ); } diff --git a/packages/components/src/theme/lineage.scss b/packages/components/src/theme/lineage.scss index cbcad4e432..1ee6de497c 100644 --- a/packages/components/src/theme/lineage.scss +++ b/packages/components/src/theme/lineage.scss @@ -7,7 +7,6 @@ } .lineage-name { - outline: none; h6 { display: inline-block; From 2b1c4514bea2bf09ab96b22c648e26a545c65eb9 Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 13 May 2026 16:08:16 -0500 Subject: [PATCH 57/68] ManageViewsDialog fix for getting target id attribute for parent button --- packages/components/src/public/QueryModel/ManageViewsModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/public/QueryModel/ManageViewsModal.tsx b/packages/components/src/public/QueryModel/ManageViewsModal.tsx index 6587da147b..466466edcd 100644 --- a/packages/components/src/public/QueryModel/ManageViewsModal.tsx +++ b/packages/components/src/public/QueryModel/ManageViewsModal.tsx @@ -91,7 +91,8 @@ export const ManageViewsModal: FC = memo(props => { const getActionView = useCallback( event => { - const targetId = event.target.id; + const target = event.target.tagName === 'I' ? event.target.parentElement : event.target; + const targetId = target.id; const viewInd = parseInt(targetId.split('-')[1]); return views[viewInd]; }, From 70c7ee786db245b3e6e14a02003e9f935787d664 Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 13 May 2026 16:08:55 -0500 Subject: [PATCH 58/68] Fix warning about ariaLabelledBy (remove props via spread) --- .../internal/components/domainproperties/Lookup/Fields.tsx | 4 ++-- packages/components/src/theme/fields.scss | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/domainproperties/Lookup/Fields.tsx b/packages/components/src/internal/components/domainproperties/Lookup/Fields.tsx index 7d1ce4e1f7..06890f628d 100644 --- a/packages/components/src/internal/components/domainproperties/Lookup/Fields.tsx +++ b/packages/components/src/internal/components/domainproperties/Lookup/Fields.tsx @@ -74,11 +74,11 @@ class FolderSelectImpl extends React.Component +