Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5117535
Auto-link to study input field labels
labkey-susanh May 18, 2026
3ca1385
Remove tabIndex value from DomainRow component
labkey-susanh May 18, 2026
e4bfece
Auto-link to study input field labels
labkey-susanh May 18, 2026
e49b72c
Add labels for text choice input fields
labkey-susanh May 18, 2026
367b1a8
Use button instead of i for expandable container chevrons
labkey-susanh May 18, 2026
7aab214
Return null for label if showLabel is false
labkey-susanh May 18, 2026
05baa51
Reset positive tabIndex to 0
labkey-susanh May 19, 2026
45b58a9
Add text area label references
labkey-susanh May 19, 2026
1c39505
Add aria-label
labkey-susanh May 19, 2026
ccb46d8
add ariaLabelledBy for DatePickerInput
labkey-susanh May 19, 2026
da83c18
Add labelId method to QueryColumn and use it for tying labels and inp…
labkey-susanh May 19, 2026
a3c3f42
release notes
labkey-susanh May 19, 2026
a089d84
Don't render as label if no inputId is provided
labkey-susanh May 19, 2026
092effa
bold font for label, even if not in a <label> element
labkey-susanh May 19, 2026
54fb71f
Add aria-labelledby for CheckboxInput when no label is provided
labkey-susanh May 19, 2026
6750ab9
Space
labkey-susanh May 19, 2026
38c635d
Font weight update for control-label
labkey-susanh May 19, 2026
2bccb16
Remove inputId for SelectInput labelOverlayProps to avoid orphaned label
labkey-susanh May 19, 2026
3ec468d
@labkey/components v7.37.1-moreAccessibility.0
labkey-susanh May 19, 2026
81ba821
Add `data-fieldkey` to some labels to help with locators
labkey-susanh May 20, 2026
02486e1
@labkey/components v7.37.1-moreAccessibility.1
labkey-susanh May 20, 2026
bf4191e
Merge from develop
labkey-susanh May 20, 2026
bf0c757
@labkey/components v7.37.2-moreAccessibility.2
labkey-susanh May 20, 2026
e91de93
Update snapshot
labkey-susanh May 20, 2026
c092c0c
put data-fieldKey attribute on inner span for consistency
labkey-susanh May 21, 2026
9396acf
@labkey/components v7.37.2-moreAccessibility.3
labkey-susanh May 21, 2026
b0cf2cd
Data key instead of inputId for labelOverlayProps
labkey-susanh May 21, 2026
0a2486b
Add id to label as well to avoid broken aria references
labkey-susanh May 21, 2026
61fa30f
@labkey/components v7.37.2-moreAccessibility.4
labkey-susanh May 21, 2026
5387462
Add data-fieldkey for amount and units fields
labkey-susanh May 21, 2026
52a90a0
merge from develop
labkey-susanh May 21, 2026
f1bed9c
Casing updates for data-fieldkey attribute
labkey-susanh May 22, 2026
af80d50
Test updates
labkey-susanh May 22, 2026
194a0ed
@labkey/components v7.38.1-moreAccessibility.6
labkey-susanh May 22, 2026
6693049
Empty button
labkey-susanh May 22, 2026
9cce6c4
Remove redundant (I hope) data-fieldkey attribute
labkey-susanh May 22, 2026
a57e4b7
Unique ids for description fields
labkey-susanh May 22, 2026
5be9ccd
Fix empty toggle buttons
labkey-susanh May 22, 2026
6fef7a1
Fix styling for control-labels that are spans in content-form
labkey-susanh May 22, 2026
652fec0
Use aria-label instead of labelledby for more consistent display
labkey-susanh May 22, 2026
f5d3dac
@labkey/components v7.38.1-moreAccessibility.7
labkey-susanh May 22, 2026
0a4f3f1
Add aria-pressed attribut for toggle buttons
labkey-susanh May 22, 2026
0c7c412
Explicitly set aria-label to undefined when label is falsy
labkey-susanh May 22, 2026
6dacdb6
Make ids unique
labkey-susanh May 22, 2026
a86e1f9
@labkey/components v7.38.1-moreAccessibility.8
labkey-susanh May 22, 2026
a4fceee
Add span for ease of locating
labkey-susanh May 25, 2026
eb2ba56
Remove redundant null return
labkey-susanh May 25, 2026
cb2a153
@labkey/components v7.38.1-moreAccessibility.9
labkey-susanh May 25, 2026
c9b6401
Update SR text to be more descriptive
labkey-susanh May 25, 2026
f2e4de1
Restore showLab
labkey-susanh May 25, 2026
f8a87dc
@labkey/components v7.38.1-moreAccessibility.11
labkey-susanh May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "7.38.0",
"version": "7.38.1-moreAccessibility.11",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
11 changes: 11 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version TBD
*Released*: TBD
- Misc. accessibility improvements
- Auto-link to study input field labels
- Remove tabIndex value from `DomainRow`
- Add labels for text choice input fields
- Use button instead of i for expandable container chevrons
- Update LabelOverlay and DetailEditor with ids for labeling elements
- Add `labelId` getter method in `QueryColumn`
- Add `data-fieldkey` to some labels to help with locators

### version 7.38.0
*Released*: 21 May 2026
- Accessibility improvements for app pages: Keyboard Interactions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,24 @@ export const ToggleIcon: FC<Props> = memo(props => {
const body = (
<>
{firstActive && (
<button className="clickable-text fa fa-toggle-on" onClick={secondBtnClick} type="button" />
<button
aria-pressed="true"
className="clickable-text fa fa-toggle-on"
onClick={secondBtnClick}
type="button"
>
<span className="sr-only">Toggle On {inputFieldName}</span>
</button>
)}
{secondActive && (
<button className="clickable-text fa fa-toggle-off" onClick={firstBtnClick} type="button" />
<button
aria-pressed="true"
className="clickable-text fa fa-toggle-off"
onClick={firstBtnClick}
type="button"
>
<span className="sr-only">Toggle Off {inputFieldName}</span>
</button>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ import { Container } from '../base/models/Container';
import { LoadingSpinner } from '../base/LoadingSpinner';

interface Props {
ariaLabelledBy: string;
autoLinkTarget: string;
containers: Container[];
onChange: (evt: any) => void;
value: string;
}

export const AutoLinkToStudyDropdown: FC<Props> = memo(({ autoLinkTarget, containers, onChange, value }) => {
export const AutoLinkToStudyDropdown: FC<Props> = memo(({ ariaLabelledBy, autoLinkTarget, containers, onChange, value }) => {
if (containers === undefined) return <LoadingSpinner />;
return (
<select className="form-control" id={autoLinkTarget} onChange={onChange} value={value || ''}>
<select
aria-labelledby={ariaLabelledBy}
className="form-control"
id={autoLinkTarget}
onChange={onChange}
value={value || ''}
>
<option value={null} />
{containers.map(container => (
<option key={container.id} value={container.id}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,6 @@ export class DomainRow extends React.PureComponent<DomainRowProps, DomainRowStat
className={this.getRowCssClasses(expanded, dragging, selected, fieldError)}
{...provided.draggableProps}
ref={provided.innerRef}
tabIndex={index}
>
<div
className="row domain-row-container"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export const TextChoiceOptionsImpl: FC<ImplProps> = memo(props => {
<div className="list-group domain-text-choices-list">
{validValues.length > MIN_VALUES_FOR_SEARCH_COUNT && (
<input
aria-label="Search for a value"
autoFocus
className="form-control domain-text-choices-search"
onChange={onSearchChange}
Expand Down Expand Up @@ -319,10 +320,11 @@ export const TextChoiceOptionsImpl: FC<ImplProps> = memo(props => {
{selectedIndex !== undefined && (!currentInUse || !isMultiChoiceField) && (
<>
<div className="domain-field-label">
<DomainFieldLabel label="Value" />
<DomainFieldLabel id={'text-choice-value-label-' + selectedIndex} label="Value" />
</div>
<div className="domain-field-padding-bottom">
<DisableableInput
aria-labelledby={'text-choice-value-label-' + selectedIndex}
className="form-control full-width"
disabledMsg={currentLocked ? LOCKED_TIP : undefined}
name="value"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,11 @@ export class AutoLinkDataInput extends React.PureComponent<InputProps, AutoLinkD
</p>
</>
}
id="auto-link-data-input"
label="Auto-Link Data to Study"
>
<AutoLinkToStudyDropdown
ariaLabelledBy="auto-link-data-input"
autoLinkTarget={FORM_IDS.AUTO_LINK_TARGET}
containers={containers}
onChange={onChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,12 +547,14 @@ class SampleTypePropertiesPanelImpl extends PureComponent<InjectedDomainProperti
<div className="row margin-top">
<div className="col-xs-2">
<DomainFieldLabel
id="linked-study-label"
helpTipBody={<AutoLinkDataToStudyHelpTip />}
label="Auto-Link Data to Study"
/>
</div>
<div className="col-xs-5">
<AutoLinkToStudyDropdown
ariaLabelledBy="linked-study-label"
autoLinkTarget={ENTITY_FORM_IDS.AUTO_LINK_TARGET}
containers={containers}
onChange={this.onFormChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,51 @@ import { LabelOverlay } from './LabelOverlay';
describe('LabelOverlay', () => {
it('renders label with overlay, not formsy', () => {
render(<LabelOverlay isFormsy={false} label="Test Label" />);
expect(document.querySelector('label')?.textContent).toBe('Test Label ');
expect(document.querySelector('.control-label')?.textContent).toBe('Test Label ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
});

it('renders label with overlay and required symbol when required, not formsy', () => {
render(<LabelOverlay isFormsy={false} label="Test Label" required={true} />);
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
expect(document.querySelector('.control-label')?.textContent).toBe('Test Label * ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
});

it('renders label with overlay and required, but addLabelAsterisk = false, not formsy', () => {
render(<LabelOverlay addLabelAsterisk={false} isFormsy={false} label="Test Label" required={true} />);
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
expect(document.querySelector('.control-label')?.textContent).toBe('Test Label * ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
});

it('renders label with overlay, required = false, and addLabelAsterisk = true, not formsy', () => {
render(<LabelOverlay addLabelAsterisk={true} isFormsy={false} label="Test Label" required={false} />);
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
expect(document.querySelector('.control-label')?.textContent).toBe('Test Label * ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
});

it('renders label with no overlay and required symbol when required, not formsy', () => {
render(<LabelOverlay helpTipRenderer="NONE" isFormsy={false} label="Test Label" required={true} />);
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
expect(document.querySelector('.control-label')?.textContent).toBe('Test Label * ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(0);
});

it('renders label with overlay, isFormsy = true (default)', () => {
render(<LabelOverlay label="Test Label" />);
expect(document.querySelector('label')).toBeNull();
expect(document.querySelector('.control-label')).toBeNull();
expect(document.querySelector('span')?.textContent).toBe('Test Label ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
});

it('renders label with overlay and required', () => {
render(<LabelOverlay isFormsy={true} label="Test Label" required={true} />);
expect(document.querySelector('label')).toBeNull();
expect(document.querySelector('.control-label')).toBeNull();
expect(document.querySelector('span')?.textContent).toBe('Test Label * ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
});

it('renders label with overlay and addLabelAsterisk', () => {
render(<LabelOverlay addLabelAsterisk={true} isFormsy={true} label="Test Label" />);
expect(document.querySelector('label')).toBeNull();
expect(document.querySelector('.control-label')).toBeNull();
expect(document.querySelector('span')?.textContent).toBe('Test Label * ');
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
});
Expand Down
24 changes: 18 additions & 6 deletions packages/components/src/internal/components/forms/LabelOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { DOMAIN_FIELD, DomainFieldHelpTipContents } from './DomainFieldHelpTipCo
export interface LabelOverlayProps extends PropsWithChildren {
addLabelAsterisk?: boolean;
column?: QueryColumn;
dataKey?: string;
description?: string;
helpTipRenderer?: string;
inputId?: string;
Expand Down Expand Up @@ -103,28 +104,39 @@ export class LabelOverlay extends React.Component<LabelOverlayProps> {
}

render(): ReactNode {
const { column, inputId, isFormsy, labelClass, required, addLabelAsterisk } = this.props;
const { column, dataKey, inputId, isFormsy, labelClass, required, addLabelAsterisk } = this.props;
const label = this.props.label ? this.props.label : column ? column.caption : null;

const overlay = this.getOverlay();

if (isFormsy) {
// when being used as a label for a formsy component directly this will use just a span without the
// classes applied as well as not needing to handle 'required' display
// TODO: remove space for required-symbol after *
return (
<span>
<span data-fieldkey={dataKey ?? column?.fieldKey} id={column?.labelId}>
{label}&nbsp;
{overlay}
{required || addLabelAsterisk ? <span className="required-symbol">* </span> : null}
</span>
);
}

// TODO: remove space for required-symbol after *
if (!inputId) {
return (
<span className={(labelClass ? labelClass + ' ' : '') + 'text__truncate-and-wrap'} id={column?.labelId}>
<span data-fieldkey={dataKey ?? column?.fieldKey}>{label}</span>&nbsp;
{overlay}
{required || addLabelAsterisk ? <span className="required-symbol">* </span> : null}
</span>
);
}
return (
<label className={(labelClass ? labelClass + ' ' : '') + 'text__truncate-and-wrap'} htmlFor={inputId}>
<span>{label}</span>&nbsp;
<label
className={(labelClass ? labelClass + ' ' : '') + 'text__truncate-and-wrap'}
htmlFor={inputId}
id={column?.labelId}
>
<span data-fieldkey={inputId}>{label}</span>&nbsp;
{overlay}
{required || addLabelAsterisk ? <span className="required-symbol">* </span> : null}
</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/
import React, { FC, memo } from 'react';
import { Icon } from '../../../Icon';

interface DetailPanelHeaderProps {
editing?: boolean;
Expand All @@ -30,7 +31,9 @@ export const DetailPanelHeader: FC<DetailPanelHeaderProps> = memo(props => {
if (editing) {
return (
<h2 className="panel-heading">
{verb} {title}
<span>
{verb} {title}
</span>
<span className="detail__edit--heading">
{warning !== undefined && (
<span>
Expand All @@ -45,12 +48,12 @@ export const DetailPanelHeader: FC<DetailPanelHeaderProps> = memo(props => {

return (
<h2 className="panel-heading">
{title}
<span>{title}</span>
<span className="detail__edit--heading">
{isEditable && (
<>
<button className="clickable-text detail__edit-button" onClick={onClick} type="button">
<i className="fa fa-pencil-square-o" />
<Icon iconClass="fa fa-pencil-square-o" srText={'Edit ' + title} />
</button>
<div className="clearfix" />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ describe('AmountUnitInput', () => {
expect(document.querySelectorAll('.form-group.row')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')[0].textContent).toBe('Amount and Units');
expect(document.querySelectorAll('label')).toHaveLength(1);
expect(document.querySelectorAll('label')[0].textContent).toBe('');
expect(document.querySelectorAll('label')).toHaveLength(0);
expect(document.querySelectorAll('.fa-toggle-on')).toHaveLength(1);
expect(document.querySelectorAll('.fa-toggle-off')).toHaveLength(0);
const inputs = document.querySelectorAll('input');
Expand All @@ -105,8 +104,7 @@ describe('AmountUnitInput', () => {
expect(document.querySelectorAll('.form-group.row')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')[0].textContent).toBe('Amount and Units');
expect(document.querySelectorAll('label')).toHaveLength(1);
expect(document.querySelectorAll('label')[0].textContent).toBe('');
expect(document.querySelectorAll('label')).toHaveLength(0);
expect(document.querySelectorAll('.fa-toggle-on')).toHaveLength(0);
expect(document.querySelectorAll('.fa-toggle-off')).toHaveLength(1);
const inputs = document.querySelectorAll('input');
Expand All @@ -133,8 +131,7 @@ describe('AmountUnitInput', () => {
expect(document.querySelectorAll('.form-group.row')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')[0].textContent).toBe('Amount and Units');
expect(document.querySelectorAll('label')).toHaveLength(1);
expect(document.querySelectorAll('label')[0].textContent).toBe('');
expect(document.querySelectorAll('label')).toHaveLength(0);
expect(document.querySelectorAll('.fa-toggle-on')).toHaveLength(0);
expect(document.querySelectorAll('.fa-toggle-off')).toHaveLength(1);
const inputs = document.querySelectorAll('input');
Expand All @@ -161,8 +158,7 @@ describe('AmountUnitInput', () => {
expect(document.querySelectorAll('.form-group.row')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')).toHaveLength(1);
expect(document.querySelectorAll('.control-label')[0].textContent).toBe('Amount and Units');
expect(document.querySelectorAll('label')).toHaveLength(1);
expect(document.querySelectorAll('label')[0].textContent).toBe('');
expect(document.querySelectorAll('label')).toHaveLength(0);
expect(document.querySelectorAll('.fa-toggle-on')).toHaveLength(0);
expect(document.querySelectorAll('.fa-toggle-off')).toHaveLength(0);
const inputs = document.querySelectorAll('input');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ export const AmountUnitInput: FC<InputRendererProps> = memo(props => {
id={id}
isDisabled={disabled}
label={
<div className={inputLabelClass + ' bold-text text__truncate-and-wrap'}>
<span
className={inputLabelClass + ' bold-text text__truncate-and-wrap'}
data-fieldkey={amountCol.name}
>
Amount and Units
</div>
</span>
}
labelOverlayProps={{
description: 'The amount and units of this sample currently on hand.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ class CheckboxInputImpl extends DisableableInput<CheckboxInputImplProps, Checkbo
/>
) : (
<input
aria-label={label}
aria-label={label || undefined}
aria-labelledby={label ? undefined : queryColumn.labelId}
checked={checked}
disabled={isDisabled}
id={queryColumn.fieldKey}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ export class DatePickerInputImpl extends DisableableInput<DatePickerInputImplPro
: (placeholderText ?? `Select ${queryColumn.caption.toLowerCase()}`);
const picker = (
<DatePicker
ariaLabelledBy={
!showLabel && !renderFieldLabel ? queryColumn.labelId : undefined
}
autoComplete="off"
autoFocus={autoFocus}
className={inputClassName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {

const labelOverlayProps = {
addLabelAsterisk,
inputId,
dataKey: inputId,
// While this component supports binding Formsy it does not use a Formsy component
// to render the associated label. As such, the label overlay is always configured as isFormsy={false}.
isFormsy: false,
Expand Down
Loading