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
Merged
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -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,
@@ -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 => {
@@ -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>
</>
@@ -354,6 +356,9 @@ let AutoCompleteCompBase = (function () {
>
{children.animationStyle.getPropertyView()}
</Section>
<Section name={sectionNames.advanced}>
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>
</>
);
})
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
@@ -4,6 +4,7 @@ import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"
import {
BoolCodeControl,
CustomRuleControl,
NumberControl,
RangeControl,
StringControl,
} from "../../controls/codeControl";
@@ -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>;

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

const getFormattedDate = (
@@ -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,
@@ -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)}
@@ -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 });
@@ -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)}
Original file line number Diff line number Diff line change
@@ -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) => {
Original file line number Diff line number Diff line change
@@ -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(() =>
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"
import {
BoolCodeControl,
CustomRuleControl,
NumberControl,
RangeControl,
StringControl,
} from "../../controls/codeControl";
@@ -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,
};

@@ -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,
@@ -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>
)}

@@ -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}
/>
);

@@ -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>
)}

Original file line number Diff line number Diff line change
@@ -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) => {
Original file line number Diff line number Diff line change
@@ -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) => {
Original file line number Diff line number Diff line change
@@ -272,6 +272,7 @@ const childrenMap = {
min: UndefinedNumberControl,
max: UndefinedNumberControl,
customRule: CustomRuleControl,
tabIndex: NumberControl,

...formDataChildren,
};
@@ -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");
@@ -436,6 +438,7 @@ let NumberInputTmpComp = (function () {
})}
{children.controls.propertyView({ label: trans("numberInput.controls") })}
{readOnlyPropertyView(children)}
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
</Section>
)}

Original file line number Diff line number Diff line change
@@ -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({
@@ -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);
@@ -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} />
Original file line number Diff line number Diff line change
@@ -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 () {
/**
@@ -16,6 +17,7 @@ const SliderBasicComp = (function () {
...SliderChildren,
value: numberExposingStateControl("value", 60),
vertical: BoolControl,
tabIndex: NumberControl,
...formDataChildren,
};
return new UICompBuilder(childrenMap, (props) => {
@@ -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");
@@ -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} />
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";
@@ -180,6 +180,7 @@ const childrenMap = {
toolbar: withDefault(StringControl, JSON.stringify(toolbarOptions)),
onEvent: ChangeEventHandlerControl,
style: styleControl(RichTextEditorStyle , 'style'),
tabIndex: NumberControl,

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

const ReactQuillEditor = React.lazy(() => import("react-quill"));
@@ -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
@@ -316,6 +327,7 @@ const RichTextEditorCompBase = new UICompBuilder(childrenMap, (props) => {
onChange={handleChange}
$style={props.style}
contentScrollBar={props.contentScrollBar}
tabIndex={props.tabIndex}
/>
);
})
@@ -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>
)}
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";
@@ -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)}
@@ -156,6 +157,7 @@ let CheckboxBasicComp = (function () {
viewRef: RefControl<HTMLDivElement>,
inputFieldStyle: styleControl(CheckboxStyle , 'inputFieldStyle'),
animationStyle: styleControl(AnimationStyle , 'animationStyle'),
tabIndex: NumberControl,
...SelectInputValidationChildren,
...formDataChildren,
};
@@ -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[]);
}}
Original file line number Diff line number Diff line change
@@ -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`
@@ -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,
@@ -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}
Loading
Loading