Teact: Support autoFocus attribute (#6948)

This commit is contained in:
Alexander Zinchuk 2026-05-15 18:37:55 +02:00
parent a99d7bbdf9
commit 480b09e9aa
11 changed files with 193 additions and 290 deletions

View File

@ -99,61 +99,19 @@
}
.day-button {
--button-text-color: var(--color-text);
position: relative;
margin: 0.125rem 0.625rem;
border-radius: 4rem;
font-weight: var(--font-weight-medium);
outline: none !important;
&::before {
content: "";
display: block;
padding-top: 100%;
}
font-variant-numeric: tabular-nums;
&.weekday {
height: 1rem;
margin-bottom: 0;
}
&.clickable {
cursor: var(--custom-cursor, pointer);
&:hover {
background-color: var(--color-interactive-element-hover);
}
&.selected {
color: white;
background-color: var(--color-primary);
}
}
&.disabled {
pointer-events: none;
opacity: 0.25;
}
span {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 0.875rem;
}
@media (max-width: 600px) {
margin: 0.25rem 0.375rem;
&.selected {
--button-text-color: revert-layer;
}
}
@ -163,12 +121,15 @@
justify-content: center;
min-height: 17rem;
margin: 1.5rem -0.5rem 0.5rem;
margin: 0rem -0.5rem 1rem;
}
.calendar-grid {
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(7, 1fr);
place-items: center;
width: 100%;
}
}

View File

@ -203,7 +203,7 @@ const CalendarModal = ({
message: lang('MessageScheduledRepeatPremium'),
action: {
action: 'openPremiumModal',
payload: { },
payload: {},
},
actionText: lang('PremiumMore'),
});
@ -399,6 +399,8 @@ const CalendarModal = ({
color="translucent"
iconName="previous"
disabled={shouldDisablePrevMonth}
noFastClick
noPreventDefault
onClick={shouldDisablePrevMonth ? undefined : handlePrevMonth}
/>
@ -408,6 +410,8 @@ const CalendarModal = ({
color="translucent"
iconName="next"
disabled={shouldDisableNextMonth}
noFastClick
noPreventDefault
onClick={shouldDisableNextMonth ? undefined : handleNextMonth}
/>
</div>
@ -417,35 +421,53 @@ const CalendarModal = ({
<div className="calendar-grid">
{WEEKDAY_LETTERS.map((day) => (
<div className="day-button faded weekday">
<span>{oldLang(day)}</span>
{oldLang(day)}
</div>
))}
{prevMonthGrid.map((gridDate) => (
<div className="day-button disabled"><span>{gridDate}</span></div>
))}
{currentMonthGrid.map((gridDate) => (
<div
role="button"
tabIndex={0}
onClick={() => handleDateSelect(gridDate)}
className={buildClassName(
'day-button',
'div-button',
isDisabledDay(
currentYear, currentMonth, gridDate, minDate, maxDate,
)
? 'disabled'
: gridDate ? 'clickable' : '',
selectedDay === formatDay(currentYear, currentMonth, gridDate) && 'selected',
)}
<Button
key={`prev-month-${gridDate}`}
round
size="smaller"
color="translucent"
disabled
>
{Boolean(gridDate) && (
<span>{gridDate}</span>
)}
</div>
{gridDate}
</Button>
))}
{currentMonthGrid.map((gridDate) => {
const isSelected = selectedDay === formatDay(currentYear, currentMonth, gridDate);
return (
<Button
key={`current-month-${gridDate}`}
round
autoFocus={isSelected}
ariaSelected={isSelected}
onClick={() => handleDateSelect(gridDate)}
disabled={isDisabledDay(
currentYear, currentMonth, gridDate, minDate, maxDate,
)}
noFastClick
noPreventDefault
nonInteractive={!gridDate}
size="smaller"
color={isSelected ? 'primary' : 'translucent'}
className={buildClassName('day-button div-button', isSelected && 'selected')}
>
{gridDate}
</Button>
);
})}
{nextMonthGrid.map((gridDate) => (
<div className="day-button disabled"><span>{gridDate}</span></div>
<Button
key={`next-month-${gridDate}`}
round
size="smaller"
color="translucent"
disabled
>
{gridDate}
</Button>
))}
</div>
</div>

View File

@ -292,8 +292,7 @@
padding-bottom: 1.25rem;
.unpin-all-button {
color: var(--color-primary);
text-transform: capitalize;
--button-text-color: var(--color-primary);
.icon-unpin {
margin-inline-start: -0.4375rem;
@ -304,27 +303,6 @@
transition: color 0.15s;
}
@media (hover: hover) {
&:hover {
color: var(--color-white);
.icon-unpin {
color: var(--color-white);
}
}
}
@media (max-width: 600px) {
&:active,
&:focus {
color: var(--color-white);
.icon-unpin {
color: var(--color-white);
}
}
}
}
.composer-button {
@ -339,20 +317,7 @@
}
.open-chat-button {
color: var(--color-primary);
@media (hover: hover) {
&:hover {
color: var(--color-white);
}
}
@media (max-width: 600px) {
&:active,
&:focus {
color: var(--color-white);
}
}
--button-text-color: var(--color-primary);
}
.mask-image-disabled &::before {

View File

@ -608,6 +608,7 @@ function MiddleColumn({
fluid
color="secondary"
className="composer-button unpin-all-button"
noForcedUpperCase
onClick={handleOpenUnpinModal}
iconName="unpin"
>

View File

@ -154,7 +154,6 @@ const PollModal = ({
const lang = useLang();
const questionInputRef = useRef<HTMLInputElement>();
const mainButtonRef = useRef<HTMLButtonElement>();
const optionListRef = useRef<HTMLDivElement>();
const durationMenuRef = useRef<HTMLDivElement>();
@ -206,14 +205,6 @@ const PollModal = ({
true,
);
useEffect(() => {
if (!isOpen) {
return;
}
questionInputRef.current?.focus();
}, [isOpen]);
useEffect(() => {
if (isChannel) {
setIsPublic(false);
@ -578,11 +569,11 @@ const PollModal = ({
<IslandTitle>{lang('PollModalQuestionTitle')}</IslandTitle>
<Island>
<InputText
ref={questionInputRef}
className={styles.input}
label={lang('AskAQuestion')}
value={question}
maxLength={MAX_QUESTION_LENGTH}
autoFocus
error={hasSubmitted && !trimmedQuestion ? lang('PollsChooseQuestion') : undefined}
onChange={handleQuestionChange}
/>

View File

@ -27,6 +27,8 @@
@layer ui.button {
.Button {
--premium-gradient: linear-gradient(88.39deg, #6c93ff -2.56%, #976fff 51.27%, #df69d1 107.39%);
--button-text-color: white;
--button-background-color: transparent;
cursor: var(--custom-cursor, pointer);
@ -46,11 +48,11 @@
font-weight: var(--font-weight-medium);
line-height: 1.2;
color: white;
color: var(--button-text-color);
text-decoration: none !important;
text-transform: uppercase;
background-color: transparent;
background-color: var(--button-background-color);
background-size: cover;
outline: none !important;
@ -88,66 +90,71 @@
}
}
@include active-styles() {
color:
var(
--button-active-text-color,
var(--button-text-color)
);
background-color:
var(
--button-active-background-color,
var(--button-background-color)
);
}
@include no-ripple-styles() {
color:
var(
--button-no-ripple-text-color,
var(
--button-active-text-color,
var(--button-text-color)
)
);
background-color:
var(
--button-no-ripple-background-color,
var(
--button-active-background-color,
var(--button-background-color)
)
);
}
&.primary {
--ripple-color: rgba(0, 0, 0, 0.08);
color: var(--color-white);
background-color: var(--color-primary);
@include active-styles() {
background-color: rgba(var(--color-primary-shade-rgb), 0.9);
}
@include no-ripple-styles() {
background-color: var(--color-primary-shade-darker);
}
--button-text-color: var(--color-white);
--button-background-color: var(--color-primary);
--button-active-text-color: var(--color-white);
--button-active-background-color: rgba(var(--color-primary-shade-rgb), 0.9);
--button-no-ripple-background-color: var(--color-primary-shade-darker);
}
&.secondary {
--ripple-color: rgba(0, 0, 0, 0.08);
color: rgba(var(--color-text-secondary-rgb), 0.75);
background-color: var(--color-background);
@include active-styles() {
color: white;
background-color: var(--color-primary);
}
@include no-ripple-styles() {
background-color: var(--color-primary-shade);
}
--button-text-color: rgba(var(--color-text-secondary-rgb), 0.75);
--button-background-color: var(--color-background);
--button-active-text-color: white;
--button-active-background-color: var(--color-primary);
--button-no-ripple-background-color: var(--color-primary-shade);
}
&.gray {
--ripple-color: rgba(0, 0, 0, 0.08);
color: var(--color-text-secondary);
background-color: var(--color-background);
@include active-styles() {
color: var(--color-primary);
}
@include no-ripple-styles() {
background-color: var(--color-chat-hover);
}
--button-text-color: var(--color-text-secondary);
--button-background-color: var(--color-background);
--button-active-text-color: var(--color-primary);
--button-no-ripple-background-color: var(--color-chat-hover);
}
&.danger {
--ripple-color: rgba(var(--color-error-rgb), 0.16);
color: var(--color-error);
background-color: var(--color-background);
@include active-styles() {
color: var(--color-white);
background-color: var(--color-error);
}
@include no-ripple-styles() {
background-color: var(--color-error-shade);
}
--button-text-color: var(--color-error);
--button-background-color: var(--color-background);
--button-active-text-color: var(--color-white);
--button-active-background-color: var(--color-error);
--button-no-ripple-background-color: var(--color-error-shade);
}
&.faded {
@ -165,154 +172,102 @@
&.translucent-primary,
&.translucent {
--ripple-color: var(--color-interactive-element-hover);
color: var(--color-text-secondary);
background-color: transparent;
@include active-styles() {
background-color: var(--color-interactive-element-hover);
}
@include no-ripple-styles() {
background-color: rgba(var(--color-text-secondary-rgb), 0.16);
}
--button-text-color: var(--color-text-secondary);
--button-background-color: transparent;
--button-active-background-color: var(--color-interactive-element-hover);
--button-no-ripple-background-color: rgba(var(--color-text-secondary-rgb), 0.16);
&.activated {
color: var(--color-primary);
--button-active-text-color: var(--color-primary);
color: var(--button-active-text-color, var(--color-primary));
}
}
&.translucent-white {
--ripple-color: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.5);
background-color: transparent;
@include active-styles() {
color: white;
background-color: rgba(255, 255, 255, 0.08);
}
@include no-ripple-styles() {
background-color: rgba(255, 255, 255, 0.16);
}
--button-text-color: rgba(255, 255, 255, 0.5);
--button-background-color: transparent;
--button-active-text-color: white;
--button-active-background-color: rgba(255, 255, 255, 0.08);
--button-no-ripple-background-color: rgba(255, 255, 255, 0.16);
}
&.translucent-black {
--ripple-color: rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.8);
background-color: transparent;
@include active-styles() {
background-color: rgba(0, 0, 0, 0.08);
}
@include no-ripple-styles() {
background-color: rgba(0, 0, 0, 0.16);
}
--button-text-color: rgba(0, 0, 0, 0.8);
--button-background-color: transparent;
--button-active-background-color: rgba(0, 0, 0, 0.08);
--button-no-ripple-background-color: rgba(0, 0, 0, 0.16);
}
&.translucent-primary {
color: var(--color-primary);
--button-text-color: var(--color-primary);
}
&.translucent-bordered {
--ripple-color: rgba(0, 0, 0, 0.08);
--button-text-color: var(--accent-color);
--button-background-color: transparent;
--button-active-text-color: var(--color-white);
--button-active-background-color: var(--accent-color);
--button-no-ripple-background-color: var(--active-color);
border: 1px solid var(--accent-color);
color: var(--accent-color);
background-color: transparent;
@include active-styles() {
color: var(--color-white);
background-color: var(--accent-color);
}
@include no-ripple-styles() {
background-color: var(--active-color);
}
}
&.adaptive {
--ripple-color: var(--accent-background-active-color);
color: var(--accent-color);
background-color: var(--accent-background-color);
@include active-styles() {
background-color: var(--accent-background-active-color);
}
@include no-ripple-styles() {
background-color: var(--accent-background-active-color);
}
--button-text-color: var(--accent-color);
--button-background-color: var(--accent-background-color);
--button-active-background-color: var(--accent-background-active-color);
--button-no-ripple-background-color: var(--accent-background-active-color);
}
&.dark {
--ripple-color: rgba(255, 255, 255, 0.08);
color: white;
background-color: rgba(0, 0, 0, 0.75);
@include active-styles() {
color: white;
background-color: rgba(0, 0, 0, 0.85);
}
@include no-ripple-styles() {
background-color: rgba(0, 0, 0, 0.95);
}
--button-text-color: white;
--button-background-color: rgba(0, 0, 0, 0.75);
--button-active-text-color: white;
--button-active-background-color: rgba(0, 0, 0, 0.85);
--button-no-ripple-background-color: rgba(0, 0, 0, 0.95);
}
&.green {
--ripple-color: rgba(0, 0, 0, 0.08);
color: var(--color-white);
background-color: var(--color-green);
@include active-styles() {
background-color: var(--color-green-darker);
}
@include no-ripple-styles() {
background-color: var(--color-green);
}
--button-text-color: var(--color-white);
--button-background-color: var(--color-green);
--button-active-text-color: var(--color-white);
--button-active-background-color: var(--color-green-darker);
--button-no-ripple-background-color: var(--color-green);
}
&.stars {
--ripple-color: rgba(0, 0, 0, 0.08);
color: var(--color-white);
background-color: #ffb727;
--button-text-color: var(--color-white);
--button-background-color: #ffb727;
--button-active-text-color: var(--color-white);
--button-active-background-color: #ffb727cc;
--button-no-ripple-background-color: #ffb727;
.theme-dark & {
background-color: #cf8920;
}
@include active-styles() {
background-color: #ffb727cc;
}
@include no-ripple-styles() {
background-color: #ffb727;
--button-background-color: #cf8920;
}
}
&.bluredStarsBadge {
color: var(--color-white);
--button-text-color: var(--color-white);
background: rgba(0, 0, 0, 0.2) !important;
backdrop-filter: blur(50px);
}
&.transparentBlured {
color: white;
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(0.5rem);
--button-text-color: white;
--button-background-color: rgba(255, 255, 255, 0.1);
--button-active-background-color: rgba(255, 255, 255, 0.2);
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
backdrop-filter: blur(0.5rem);
}
&.smaller {
@ -501,35 +456,23 @@
}
&.text {
background-color: transparent;
--button-background-color: transparent;
&.primary {
color: var(--color-primary);
background-color: transparent;
@include active-styles() {
background-color: rgba(var(--color-primary-shade-rgb), 0.08);
}
@include no-ripple-styles() {
background-color: rgba(var(--color-primary-shade-rgb), 0.16);
}
--button-text-color: var(--color-primary);
--button-active-text-color: var(--color-primary);
--button-active-background-color: rgba(var(--color-primary-shade-rgb), 0.08);
--button-no-ripple-background-color: rgba(var(--color-primary-shade-rgb), 0.16);
}
&.secondary {
color: var(--color-text-secondary);
background-color: transparent;
--button-text-color: var(--color-text-secondary);
}
&.danger {
@include active-styles() {
color: var(--color-error);
background-color: rgba(var(--color-error-rgb), 0.08);
}
@include no-ripple-styles() {
background-color: rgba(var(--color-error-rgb), 0.16);
}
--button-active-text-color: var(--color-error);
--button-active-background-color: rgba(var(--color-error-rgb), 0.08);
--button-no-ripple-background-color: rgba(var(--color-error-rgb), 0.16);
}
}
}

View File

@ -40,6 +40,7 @@ export type OwnProps = {
isLoading?: boolean;
ariaLabel?: string;
ariaControls?: string;
ariaSelected?: boolean;
hasPopup?: boolean;
href?: string;
download?: string;
@ -60,6 +61,7 @@ export type OwnProps = {
noForcedUpperCase?: boolean;
shouldStopPropagation?: boolean;
style?: string;
autoFocus?: boolean;
iconName?: IconName;
iconAlignment?: 'top' | 'bottom' | 'start' | 'end';
iconClassName?: string;
@ -98,6 +100,7 @@ const Button = ({
noSparkleAnimation,
ariaLabel,
ariaControls,
ariaSelected,
hasPopup,
href,
download,
@ -114,6 +117,7 @@ const Button = ({
shouldStopPropagation,
noForcedUpperCase,
style,
autoFocus,
iconName,
iconAlignment = 'start',
iconClassName,
@ -241,6 +245,7 @@ const Button = ({
title={ariaLabel}
download={download}
tabIndex={tabIndex}
autoFocus={autoFocus}
dir={isRtl ? 'rtl' : undefined}
aria-label={ariaLabel}
aria-controls={ariaControls}
@ -268,9 +273,12 @@ const Button = ({
onMouseLeave={onMouseLeave && !isNotInteractive ? onMouseLeave : undefined}
onTransitionEnd={onTransitionEnd}
onFocus={onFocus && !isNotInteractive ? onFocus : undefined}
disabled={disabled}
autoFocus={autoFocus}
aria-label={ariaLabel}
aria-controls={ariaControls}
aria-haspopup={hasPopup}
aria-selected={ariaSelected}
title={ariaLabel}
tabIndex={tabIndex}
dir={isRtl ? 'rtl' : undefined}

View File

@ -68,6 +68,7 @@ const ConfirmDialog: FC<OwnProps> = ({
onClose={onClose}
isNativeDialog
onCloseAnimationEnd={onCloseAnimationEnd}
noTitleAutoFocus
>
{text && text.split('\\n').map((textPart) => (
<p>{textPart}</p>
@ -85,10 +86,15 @@ const ConfirmDialog: FC<OwnProps> = ({
onClick={confirmHandler}
color={confirmIsDestructive ? 'danger' : 'primary'}
disabled={isConfirmDisabled}
autoFocus={!confirmIsDestructive}
>
{confirmLabel || lang('GeneralConfirm')}
</Button>
{!isOnlyConfirm && <Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>}
{!isOnlyConfirm && (
<Button className="confirm-dialog-button" isText onClick={onClose} autoFocus={confirmIsDestructive}>
{lang('Cancel')}
</Button>
)}
</div>
</Modal>
);

View File

@ -24,6 +24,7 @@ type OwnProps = {
maxLength?: number;
tabIndex?: number;
title?: string;
autoFocus?: boolean;
teactExperimentControlled?: boolean;
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
@ -51,6 +52,7 @@ const InputText = ({
maxLength,
tabIndex,
title,
autoFocus,
teactExperimentControlled,
onChange,
onInput,
@ -99,6 +101,7 @@ const InputText = ({
title={title}
teactExperimentControlled={teactExperimentControlled}
onClick={onClick}
autoFocus={autoFocus}
/>
{labelText && (
<label htmlFor={id}>{labelText}</label>

View File

@ -44,6 +44,7 @@ export type OwnProps = {
noBackdrop?: boolean;
noBackdropClose?: boolean;
isNativeDialog?: boolean;
noTitleAutoFocus?: boolean;
children: React.ReactNode;
style?: string;
dialogStyle?: string;
@ -68,6 +69,7 @@ const Modal = (props: OwnProps) => {
noBackdropClose,
noFreezeOnClose,
isNativeDialog,
noTitleAutoFocus,
onClose,
onCloseAnimationEnd,
onEnter,
@ -251,7 +253,7 @@ const Modal = (props: OwnProps) => {
return title ? (
<div className={buildClassName('modal-header', headerClassName, isCondensedHeader && 'modal-header-condensed')}>
{closeButton}
<div className="modal-title">{title}</div>
<div className="modal-title" autoFocus={!noTitleAutoFocus}>{title}</div>
</div>
) : closeButton;
}

View File

@ -52,6 +52,7 @@ const MAPPED_ATTRIBUTES: Partial<Record<string, string>> = {
autoCorrect: 'autocorrect',
autoPlay: 'autoplay',
spellCheck: 'spellcheck',
autoFocus: 'autofocus',
};
const INDEX_KEY_PREFIX = '__indexKey#';
const SELECTION_STATE_ATTRIBUTE = '__teactSelectionState';