Skip to content

[FEAT]: Tab Index for Inputs #1683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 14, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
import styled, { css } from "styled-components";
import { UICompBuilder } from "../../generators";
import { FormDataPropertyView } from "../formComp/formDataConstants";
import { jsonControl } from "comps/controls/codeControl";
import { jsonControl, NumberControl } from "comps/controls/codeControl";
import { dropdownControl } from "comps/controls/dropdownControl";
import {
getStyle,
Expand Down Expand Up @@ -92,6 +92,7 @@ const childrenMap = {
inputFieldStyle: styleControl(InputLikeStyle , 'inputFieldStyle'),
childrenInputFieldStyle: styleControl(ChildrenMultiSelectStyle, 'childrenInputFieldStyle'),
animationStyle: styleControl(AnimationStyle , 'animationStyle'),
tabIndex: NumberControl,
};

const getValidate = (value: any): "" | "warning" | "error" | undefined => {
Expand Down Expand Up @@ -271,6 +272,7 @@ let AutoCompleteCompBase = (function () {
suffix={hasIcon(props.suffixIcon) && props.suffixIcon}
status={getValidate(validateState)}
onPressEnter={undefined}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
/>
</AutoComplete>
</>
Expand Down Expand Up @@ -354,6 +356,9 @@ let AutoCompleteCompBase = (function () {
>
{children.animationStyle.getPropertyView()}
</Section>
<Section name={sectionNames.advanced}>
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>
</>
);
})
Expand Down
10 changes: 9 additions & 1 deletion client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"
import {
BoolCodeControl,
CustomRuleControl,
NumberControl,
RangeControl,
StringControl,
} from "../../controls/codeControl";
Expand Down Expand Up @@ -99,6 +100,7 @@ const commonChildren = {
childrenInputFieldStyle: styleControl(ChildrenMultiSelectStyle, 'childrenInputFieldStyle'),
timeZone: dropdownControl(timeZoneOptions, Intl.DateTimeFormat().resolvedOptions().timeZone),
pickerMode: dropdownControl(PickerModeOptions, 'date'),
tabIndex: NumberControl,
};
type CommonChildrenType = RecordConstructorToComp<typeof commonChildren>;

Expand Down Expand Up @@ -185,6 +187,7 @@ export type DateCompViewProps = Pick<
disabledTime: () => ReturnType<typeof disabledTime>;
suffixIcon: ReactNode;
placeholder?: string | [string, string];
tabIndex?: number;
};

const getFormattedDate = (
Expand Down Expand Up @@ -281,6 +284,7 @@ const DatePickerTmpCmp = new UICompBuilder(childrenMap, (props) => {
onFocus={() => props.onEvent("focus")}
onBlur={() => props.onEvent("blur")}
suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
/>
),
showValidationWhenEmpty: props.showValidationWhenEmpty,
Expand Down Expand Up @@ -340,6 +344,7 @@ const DatePickerTmpCmp = new UICompBuilder(childrenMap, (props) => {
<><Section name={sectionNames.advanced}>
{timeFields(children, isMobile)}
{children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section></>
)}
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && !isMobile && commonAdvanceSection(children)}
Expand Down Expand Up @@ -475,7 +480,9 @@ let DateRangeTmpCmp = (function () {
}}
onFocus={() => props.onEvent("focus")}
onBlur={() => props.onEvent("blur")}
suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} />
suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
/>
);

const startResult = validate({ ...props, value: props.start });
Expand Down Expand Up @@ -553,6 +560,7 @@ let DateRangeTmpCmp = (function () {
<><Section name={sectionNames.advanced}>
{timeFields(children, isMobile)}
{children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section></>
)}
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && commonAdvanceSection(children)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export interface DateRangeUIViewProps extends DateCompViewProps {
placeholder?: string | [string, string];
onChange: (start?: dayjs.Dayjs | null, end?: dayjs.Dayjs | null) => void;
onPanelChange: (value: any, mode: [string, string]) => void;
onClickDateRangeTimeZone:(value:any)=>void
onClickDateRangeTimeZone:(value:any)=>void;
tabIndex?: number;
}

export const DateRangeUIView = (props: DateRangeUIViewProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface DataUIViewProps extends DateCompViewProps {
onChange: DatePickerProps<Dayjs>['onChange'];
onPanelChange: () => void;
onClickDateTimeZone:(value:any)=>void;

tabIndex?: number;
}

const DateMobileUIView = React.lazy(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"
import {
BoolCodeControl,
CustomRuleControl,
NumberControl,
RangeControl,
StringControl,
} from "../../controls/codeControl";
Expand Down Expand Up @@ -92,6 +93,7 @@ const commonChildren = {
suffixIcon: withDefault(IconControl, "/icon:regular/clock"),
timeZone: dropdownControl(timeZoneOptions, Intl.DateTimeFormat().resolvedOptions().timeZone),
viewRef: RefControl<CommonPickerMethods>,
tabIndex: NumberControl,
...validationChildren,
};

Expand Down Expand Up @@ -212,6 +214,7 @@ const TimePickerTmpCmp = new UICompBuilder(childrenMap, (props) => {
onFocus={() => props.onEvent("focus")}
onBlur={() => props.onEvent("blur")}
suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
/>
),
showValidationWhenEmpty: props.showValidationWhenEmpty,
Expand Down Expand Up @@ -263,6 +266,7 @@ const TimePickerTmpCmp = new UICompBuilder(childrenMap, (props) => {
{commonAdvanceSection(children)}
{children.use12Hours.propertyView({ label: trans("prop.use12Hours") })}
{children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>
)}

Expand Down Expand Up @@ -368,6 +372,7 @@ const TimeRangeTmpCmp = (function () {
onFocus={() => props.onEvent("focus")}
onBlur={() => props.onEvent("blur")}
suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
/>
);

Expand Down Expand Up @@ -439,6 +444,7 @@ const TimeRangeTmpCmp = (function () {
{commonAdvanceSection(children)}
{children.use12Hours.propertyView({ label: trans("prop.use12Hours") })}
{children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface TimeRangeUIViewProps extends TimeCompViewProps {
placeholder?: string | [string, string];
onChange: (start?: dayjs.Dayjs | null, end?: dayjs.Dayjs | null) => void;
handleTimeRangeZoneChange: (value:any) => void;
tabIndex?: number;
}

export const TimeRangeUIView = (props: TimeRangeUIViewProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface TimeUIViewProps extends TimeCompViewProps {
value: dayjs.Dayjs | null;
onChange: (value: dayjs.Dayjs | null) => void;
handleTimeZoneChange: (value:any) => void;
tabIndex?: number;
}

export const TimeUIView = (props: TimeUIViewProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ const childrenMap = {
min: UndefinedNumberControl,
max: UndefinedNumberControl,
customRule: CustomRuleControl,
tabIndex: NumberControl,

...formDataChildren,
};
Expand Down Expand Up @@ -330,6 +331,7 @@ const CustomInputNumber = (props: RecordConstructorToView<typeof childrenMap>) =
precision={props.precision}
$style={props.inputFieldStyle}
prefix={hasIcon(props.prefixIcon) ? props.prefixIcon : props.prefixText.value}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
onPressEnter={() => {
handleFinish();
props.onEvent("submit");
Expand Down Expand Up @@ -436,6 +438,7 @@ let NumberInputTmpComp = (function () {
})}
{children.controls.propertyView({ label: trans("numberInput.controls") })}
{readOnlyPropertyView(children)}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generat
import { SliderChildren, SliderPropertyView, SliderStyled, SliderWrapper } from "./sliderCompConstants";
import { hasIcon } from "comps/utils";
import { BoolControl } from "comps/controls/boolControl";
import { NumberControl } from "comps/controls/codeControl";

const RangeSliderBasicComp = (function () {
const childrenMap = {
...SliderChildren,
start: numberExposingStateControl("start", 10),
end: numberExposingStateControl("end", 60),
vertical: BoolControl,
tabIndex: NumberControl,
};
return new UICompBuilder(childrenMap, (props, dispatch) => {
return props.label({
Expand All @@ -36,6 +38,7 @@ const RangeSliderBasicComp = (function () {
$style={props.inputFieldStyle}
style={{ margin: 0 }}
$vertical={Boolean(props.vertical) || false}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
onChange={([start, end]) => {
props.start.onChange(start);
props.end.onChange(end);
Expand All @@ -60,6 +63,7 @@ const RangeSliderBasicComp = (function () {
tooltip: trans("rangeSlider.stepTooltip"),
})}
{children.vertical.propertyView({ label: trans("slider.vertical") })}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>

<SliderPropertyView {...children} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { formDataChildren, FormDataPropertyView } from "../formComp/formDataCons
import { SliderChildren, SliderPropertyView, SliderStyled, SliderWrapper } from "./sliderCompConstants";
import { hasIcon } from "comps/utils";
import { BoolControl } from "comps/controls/boolControl";
import { NumberControl } from "comps/controls/codeControl";

const SliderBasicComp = (function () {
/**
Expand All @@ -16,6 +17,7 @@ const SliderBasicComp = (function () {
...SliderChildren,
value: numberExposingStateControl("value", 60),
vertical: BoolControl,
tabIndex: NumberControl,
...formDataChildren,
};
return new UICompBuilder(childrenMap, (props) => {
Expand All @@ -39,6 +41,7 @@ const SliderBasicComp = (function () {
$style={props.inputFieldStyle}
style={{margin: 0}}
$vertical={Boolean(props.vertical) || false}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
onChange={(e) => {
props.value.onChange(e);
props.onEvent("change");
Expand All @@ -61,6 +64,7 @@ const SliderBasicComp = (function () {
tooltip: trans("slider.stepTooltip"),
})}
{children.vertical.propertyView({ label: trans("slider.vertical") })}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>
<FormDataPropertyView {...children} />
<SliderPropertyView {...children} />
Expand Down
15 changes: 14 additions & 1 deletion client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StringControl } from "comps/controls/codeControl";
import { StringControl, NumberControl } from "comps/controls/codeControl";
import { BoolControl } from "comps/controls/boolControl";
import { BoolCodeControl } from "../controls/codeControl";
import { stringExposingStateControl } from "comps/controls/codeStateControl";
Expand Down Expand Up @@ -180,6 +180,7 @@ const childrenMap = {
toolbar: withDefault(StringControl, JSON.stringify(toolbarOptions)),
onEvent: ChangeEventHandlerControl,
style: styleControl(RichTextEditorStyle , 'style'),
tabIndex: NumberControl,

...formDataChildren,
};
Expand All @@ -196,6 +197,7 @@ interface IProps {
onChange: (value: string) => void;
$style: RichTextEditorStyleType;
contentScrollBar: boolean;
tabIndex?: number;
}

const ReactQuillEditor = React.lazy(() => import("react-quill"));
Expand Down Expand Up @@ -226,6 +228,15 @@ function RichTextEditor(props: IProps) {
[props.placeholder]
);

useEffect(() => {
if (editorRef.current && props.tabIndex !== undefined) {
const editor = editorRef.current.getEditor();
if (editor && editor.scroll && editor.scroll.domNode) {
(editor.scroll.domNode as HTMLElement).tabIndex = props.tabIndex;
}
}
}, [props.tabIndex, key]); // Also re-run when key changes due to placeholder update

const contains = (parent: HTMLElement, descendant: HTMLElement) => {
try {
// Firefox inserts inaccessible nodes around video elements
Expand Down Expand Up @@ -316,6 +327,7 @@ const RichTextEditorCompBase = new UICompBuilder(childrenMap, (props) => {
onChange={handleChange}
$style={props.style}
contentScrollBar={props.contentScrollBar}
tabIndex={props.tabIndex}
/>
);
})
Expand All @@ -334,6 +346,7 @@ const RichTextEditorCompBase = new UICompBuilder(childrenMap, (props) => {
{children.onEvent.getPropertyView()}
{hiddenPropertyView(children)}
{readOnlyPropertyView(children)}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
{showDataLoadingIndicatorsPropertyView(children)}
</Section>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { default as AntdCheckboxGroup } from "antd/es/checkbox/Group";
import { SelectInputOptionControl } from "comps/controls/optionsControl";
import { BoolCodeControl } from "../../controls/codeControl";
import { BoolCodeControl, NumberControl } from "../../controls/codeControl";
import { arrayStringExposingStateControl } from "../../controls/codeStateControl";
import { LabelControl } from "../../controls/labelControl";
import { ChangeEventHandlerControl } from "../../controls/eventHandlerControl";
Expand Down Expand Up @@ -115,6 +115,7 @@ export const getStyle = (style: CheckboxStyleType) => {
const CheckboxGroup = styled(AntdCheckboxGroup) <{
$style: CheckboxStyleType;
$layout: ValueFromOption<typeof RadioLayoutOptions>;
tabIndex?: number;
}>`
min-height: 32px;
${(props) => props.$style && getStyle(props.$style)}
Expand Down Expand Up @@ -156,6 +157,7 @@ let CheckboxBasicComp = (function () {
viewRef: RefControl<HTMLDivElement>,
inputFieldStyle: styleControl(CheckboxStyle , 'inputFieldStyle'),
animationStyle: styleControl(AnimationStyle , 'animationStyle'),
tabIndex: NumberControl,
...SelectInputValidationChildren,
...formDataChildren,
};
Expand Down Expand Up @@ -184,6 +186,7 @@ let CheckboxBasicComp = (function () {
value: option.value,
disabled: option.disabled,
}))}
tabIndex={typeof props.tabIndex === 'number' ? props.tabIndex : undefined}
onChange={(values) => {
handleChange(values as string[]);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { EllipsisTextCss, ValueFromOption } from "lowcoder-design";
import { trans } from "i18n";
import { fixOldInputCompData } from "../textInputComp/textInputConstants";
import { migrateOldData } from "comps/generators/simpleGenerators";
import { useEffect, useRef } from "react";

const getStyle = (style: RadioStyleType, inputFieldStyle?:RadioStyleType ) => {
return css`
Expand Down Expand Up @@ -102,6 +103,18 @@ let RadioBasicComp = (function () {
validateState,
handleChange,
] = useSelectInputValidate(props);

const radioRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
if (radioRef.current && typeof props.tabIndex === 'number') {
const firstRadioInput = radioRef.current.querySelector('input[type="radio"]');
if (firstRadioInput) {
firstRadioInput.setAttribute('tabindex', props.tabIndex.toString());
}
}
}, [props.tabIndex, props.options]);

return props.label({
required: props.required,
style: props.style,
Expand All @@ -110,7 +123,12 @@ let RadioBasicComp = (function () {
animationStyle:props.animationStyle,
children: (
<Radio
ref={props.viewRef}
ref={(el) => {
if (el) {
props.viewRef(el);
radioRef.current = el;
}
}}
disabled={props.disabled}
value={props.value.value}
$style={props.style}
Expand Down
Loading
Loading