mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 17:26:26 +01:00
✨ Add autofocus for searchbar (#1408)
This commit is contained in:
@@ -86,6 +86,7 @@ model UserSettings {
|
||||
disablePingPulse Boolean @default(false)
|
||||
replacePingWithIcons Boolean @default(false)
|
||||
useDebugLanguage Boolean @default(false)
|
||||
autoFocusSearch Boolean @default(false)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId])
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
"newTab": {
|
||||
"label": "Open search results in a new tab"
|
||||
},
|
||||
"autoFocus": {
|
||||
"label": "Focus search bar on page load.",
|
||||
"description": "This will automatically focus the search bar, when you navigate to the board pages. It will only work on desktop devices."
|
||||
},
|
||||
"template": {
|
||||
"label": "Query URL",
|
||||
"description": "Use %s as a placeholder for the query"
|
||||
|
||||
@@ -31,6 +31,11 @@ export const SearchEngineSettings = () => {
|
||||
label={t('searchEngine.newTab.label')}
|
||||
{...form.getInputProps('openSearchInNewTab', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label={t('searchEngine.autoFocus.label')}
|
||||
description={t('searchEngine.autoFocus.description')}
|
||||
{...form.getInputProps('autoFocusSearch', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={t('searchEngine.template.label')}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/grid
|
||||
import { BoardHeadOverride } from '~/components/layout/Meta/BoardHeadOverride';
|
||||
import { HeaderActionButton } from '~/components/layout/header/ActionButton';
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
import { MainLayout } from './MainLayout';
|
||||
@@ -30,9 +31,13 @@ type BoardLayoutProps = {
|
||||
|
||||
export const BoardLayout = ({ children, dockerEnabled }: BoardLayoutProps) => {
|
||||
const { config } = useConfigContext();
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<MainLayout headerActions={<HeaderActions dockerEnabled={dockerEnabled} />}>
|
||||
<MainLayout
|
||||
autoFocusSearch={session?.user.autoFocusSearch}
|
||||
headerActions={<HeaderActions dockerEnabled={dockerEnabled} />}
|
||||
>
|
||||
<BoardHeadOverride />
|
||||
<BackgroundImage />
|
||||
{children}
|
||||
|
||||
@@ -6,9 +6,16 @@ type MainLayoutProps = {
|
||||
headerActions?: React.ReactNode;
|
||||
contentComponents?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
autoFocusSearch?: boolean;
|
||||
};
|
||||
|
||||
export const MainLayout = ({ showExperimental, headerActions, contentComponents, children }: MainLayoutProps) => {
|
||||
export const MainLayout = ({
|
||||
showExperimental,
|
||||
headerActions,
|
||||
contentComponents,
|
||||
children,
|
||||
autoFocusSearch,
|
||||
}: MainLayoutProps) => {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
@@ -18,7 +25,14 @@ export const MainLayout = ({ showExperimental, headerActions, contentComponents,
|
||||
background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
||||
},
|
||||
}}
|
||||
header={<MainHeader headerActions={headerActions} contentComponents={contentComponents} showExperimental={showExperimental} />}
|
||||
header={
|
||||
<MainHeader
|
||||
autoFocusSearch={autoFocusSearch}
|
||||
headerActions={headerActions}
|
||||
contentComponents={contentComponents}
|
||||
showExperimental={showExperimental}
|
||||
/>
|
||||
}
|
||||
className="dashboard-app-shell"
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -23,6 +23,7 @@ type MainHeaderProps = {
|
||||
headerActions?: React.ReactNode;
|
||||
contentComponents?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
autoFocusSearch?: boolean;
|
||||
};
|
||||
|
||||
export const MainHeader = ({
|
||||
@@ -31,6 +32,7 @@ export const MainHeader = ({
|
||||
headerActions,
|
||||
leftIcon,
|
||||
contentComponents,
|
||||
autoFocusSearch,
|
||||
}: MainHeaderProps) => {
|
||||
const { breakpoints } = useMantineTheme();
|
||||
const isSmallerThanMd = useMediaQuery(`(max-width: ${breakpoints.sm})`);
|
||||
@@ -51,7 +53,7 @@ export const MainHeader = ({
|
||||
</UnstyledButton>
|
||||
</Group>
|
||||
|
||||
{!isSmallerThanMd && <Search />}
|
||||
{!isSmallerThanMd && <Search autoFocus={autoFocusSearch} />}
|
||||
|
||||
<Group noWrap style={{ flex: 1 }} position="right">
|
||||
<Group noWrap spacing={8}>
|
||||
|
||||
@@ -19,9 +19,10 @@ import { MovieModal } from './Search/MovieModal';
|
||||
|
||||
type SearchProps = {
|
||||
isMobile?: boolean;
|
||||
autoFocus?: boolean;
|
||||
};
|
||||
|
||||
export const Search = ({ isMobile }: SearchProps) => {
|
||||
export const Search = ({ isMobile, autoFocus }: SearchProps) => {
|
||||
const { t } = useTranslation('layout/header');
|
||||
const [search, setSearch] = useState('');
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
@@ -62,6 +63,7 @@ export const Search = ({ isMobile }: SearchProps) => {
|
||||
variant="filled"
|
||||
placeholder={`${t('search.label')}...`}
|
||||
hoverOnSearchChange
|
||||
autoFocus={autoFocus}
|
||||
rightSection={
|
||||
<IconSearch
|
||||
onClick={() => ref.current?.focus()}
|
||||
|
||||
@@ -89,6 +89,7 @@ const SettingsComponent = ({
|
||||
replaceDotsWithIcons: settings.replacePingWithIcons,
|
||||
searchTemplate: settings.searchTemplate,
|
||||
openSearchInNewTab: settings.openSearchInNewTab,
|
||||
autoFocusSearch: settings.autoFocusSearch,
|
||||
},
|
||||
validate: i18nZodResolver(updateSettingsValidationSchema),
|
||||
validateInputOnBlur: true,
|
||||
|
||||
@@ -221,6 +221,7 @@ export const userRouter = createTRPCRouter({
|
||||
firstDayOfWeek: input.firstDayOfWeek,
|
||||
searchTemplate: input.searchTemplate,
|
||||
openSearchInNewTab: input.openSearchInNewTab,
|
||||
autoFocusSearch: input.autoFocusSearch,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ declare module 'next-auth' {
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
colorScheme: 'light' | 'dark' | 'environment';
|
||||
autoFocusSearch: boolean;
|
||||
language: string;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
@@ -33,6 +34,7 @@ declare module 'next-auth' {
|
||||
interface User {
|
||||
isAdmin: boolean;
|
||||
colorScheme: 'light' | 'dark' | 'environment';
|
||||
autoFocusSearch: boolean;
|
||||
language: string;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
@@ -75,6 +77,7 @@ export const constructAuthOptions = (
|
||||
select: {
|
||||
colorScheme: true,
|
||||
language: true,
|
||||
autoFocusSearch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -83,6 +86,7 @@ export const constructAuthOptions = (
|
||||
session.user.isAdmin = userFromDatabase.isAdmin;
|
||||
session.user.colorScheme = colorSchemeParser.parse(userFromDatabase.settings?.colorScheme);
|
||||
session.user.language = userFromDatabase.settings?.language ?? 'en';
|
||||
session.user.autoFocusSearch = userFromDatabase.settings?.autoFocusSearch ?? false;
|
||||
}
|
||||
|
||||
return session;
|
||||
@@ -148,6 +152,7 @@ export const constructAuthOptions = (
|
||||
select: {
|
||||
colorScheme: true,
|
||||
language: true,
|
||||
autoFocusSearch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -173,6 +178,7 @@ export const constructAuthOptions = (
|
||||
isAdmin: false,
|
||||
colorScheme: colorSchemeParser.parse(user.settings?.colorScheme),
|
||||
language: user.settings?.language ?? 'en',
|
||||
autoFocusSearch: user.settings?.autoFocusSearch ?? false,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -49,4 +49,5 @@ export const updateSettingsValidationSchema = z.object({
|
||||
replaceDotsWithIcons: z.boolean(),
|
||||
searchTemplate: z.string().nonempty().max(256),
|
||||
openSearchInNewTab: z.boolean(),
|
||||
autoFocusSearch: z.boolean(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user