diff --git a/README.md b/README.md
index f6db064..f4a87f4 100644
--- a/README.md
+++ b/README.md
@@ -130,7 +130,7 @@ Every picker comes three ways, with the same props.
```
-Sheets open as a bottom sheet, or as a centered modal on wide web windows (`presentation="bottomSheet" | "modal"`). Fields and `SelectPicker` also open as a dropdown under the field, their default on the web (`presentation="dropdown"`). `PickerField` wraps any sheet of your own in a field.
+Sheets open as a bottom sheet, or as a centered modal on wide web windows (`presentation="bottomSheet" | "modal"`). Fields and `SelectPicker` also open as a dropdown under the field, their default on the web (`presentation="dropdown"`), or expand inline under it, pushing the content below, like the iOS Settings app (`presentation="inline"`). `PickerField` wraps any sheet of your own in a field.
Button text is yours too: `confirmLabel`, `cancelLabel` and `showCancel` on any sheet or field.
@@ -348,7 +348,7 @@ A length of time, such as a timer or a workout, rather than a time of day. The v
| `searchable` | `boolean` | more than 8 options |
| `loadOptions` | `(query) => Promise`: search a server | |
| `clearable` / `onClear` | `boolean` / `() => void` | `false` |
-| `presentation` | `'auto' \| 'bottomSheet' \| 'modal' \| 'dropdown'` | `'auto'`: dropdown on the web |
+| `presentation` | `'auto' \| 'bottomSheet' \| 'modal' \| 'dropdown' \| 'inline'` | `'auto'`: dropdown on the web |
`loadOptions` waits for a pause in typing, shows a loader and ignores answers to older queries:
diff --git a/package.json b/package.json
index bc1cf8e..a60369c 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"range-picker",
"select",
"dropdown",
+ "inline-picker",
"emoji-picker",
"country-picker",
"country-code-picker",
diff --git a/src/color/ColorPickerSheet.tsx b/src/color/ColorPickerSheet.tsx
index e9bf7b1..dfd3b6e 100644
--- a/src/color/ColorPickerSheet.tsx
+++ b/src/color/ColorPickerSheet.tsx
@@ -22,7 +22,7 @@ export const ColorPickerSheet = ({
...rest
}: ColorPickerSheetProps) => {
const theme = useDesignOverride(themeProp, design);
- const [draft, setDraft] = useDraft((value && normalizeHex(value)) ?? DEFAULT_COLOR, visible);
+ const [draft, setDraft] = useDraft((value && normalizeHex(value)) ?? DEFAULT_COLOR, visible, { commit: onConfirm });
return (
void;
onHidden?: () => void;
}
diff --git a/src/core/hooks/usePresence.ts b/src/core/hooks/usePresence.ts
index ae2e328..9d8e327 100644
--- a/src/core/hooks/usePresence.ts
+++ b/src/core/hooks/usePresence.ts
@@ -6,7 +6,7 @@ import { timing } from '../utils/animation.utils';
import { useAnimatedValue } from './useAnimatedValue';
/** Mounts when `visible` turns on and animates `progress` in; on hide it animates out first and unmounts after. */
-export const usePresence = ({ visible, duration, enterDuration = duration, onShown, onHidden }: PresenceOptions): Presence => {
+export const usePresence = ({ visible, duration, enterDuration = duration, native, onShown, onHidden }: PresenceOptions): Presence => {
const [mounted, setMounted] = useState(visible);
const progress = useAnimatedValue(0);
const wasVisible = useRef(false);
@@ -16,18 +16,18 @@ export const usePresence = ({ visible, duration, enterDuration = duration, onSho
useEffect(() => {
if (visible) {
setMounted(true);
- timing(progress, 1, { duration: enterDuration }).start(({ finished }) => {
+ timing(progress, 1, { duration: enterDuration, native }).start(({ finished }) => {
if (finished) callbacks.current.onShown?.();
});
} else if (wasVisible.current) {
- timing(progress, 0, { duration, easing: EASE_IN }).start(({ finished }) => {
+ timing(progress, 0, { duration, easing: EASE_IN, native }).start(({ finished }) => {
if (!finished) return;
setMounted(false);
callbacks.current.onHidden?.();
});
}
wasVisible.current = visible;
- }, [duration, enterDuration, progress, visible]);
+ }, [duration, enterDuration, native, progress, visible]);
return {
mounted,
diff --git a/src/date/DatePickerSheet.tsx b/src/date/DatePickerSheet.tsx
index 7911376..d3533c4 100644
--- a/src/date/DatePickerSheet.tsx
+++ b/src/date/DatePickerSheet.tsx
@@ -35,12 +35,19 @@ export const DatePickerSheet = (props: DatePickerSheetProps) => {
const display = getSheetDisplay(mode, displayProp);
// The sheet holds whichever value shape the mode uses; `mode` ties value, draft and onConfirm together, so a new mode starts a new draft.
const confirmDraft = onConfirm as (draft: DateDraft) => void;
- const [draft, setDraft] = useDraft(value ?? getEmptyDraft(mode, display), visible, mode);
+ const confirmsOnSelect = !!confirmOnSelect && mode === CALENDAR_MODE.single && display === PICKER_DISPLAY.calendar;
+ // Inline, a range commits once both ends are picked; confirmOnSelect commits on its own.
+ const commit = (next: DateDraft) => {
+ if (isDraftReady(mode, next)) confirmDraft(next);
+ };
+ const [draft, setDraft] = useDraft(value ?? getEmptyDraft(mode, display), visible, {
+ resetKey: mode,
+ commit: confirmsOnSelect ? undefined : commit,
+ });
const ready = isDraftReady(mode, draft);
const resolved = usePickerTheme(theme);
const headline =
DATE_HEADLINE[resolved.design] && display === PICKER_DISPLAY.calendar ? formatDateHeadline(mode, draft, calendarProps.locale) : null;
- const confirmsOnSelect = !!confirmOnSelect && mode === CALENDAR_MODE.single && display === PICKER_DISPLAY.calendar;
const confirm = () => {
if (!ready) return;
diff --git a/src/datetime/DateTimePickerSheet.tsx b/src/datetime/DateTimePickerSheet.tsx
index 72d424a..63266b2 100644
--- a/src/datetime/DateTimePickerSheet.tsx
+++ b/src/datetime/DateTimePickerSheet.tsx
@@ -23,14 +23,15 @@ export const DateTimePickerSheet = ({
...rest
}: DateTimePickerSheetProps) => {
const theme = useDesignOverride(themeProp, design);
- const [draft, setDraft] = useDraft(value ?? new Date(), visible);
+ const commit = (next: Date) => onConfirm(snapTime(next, rest));
+ const [draft, setDraft] = useDraft(value ?? new Date(), visible, { commit });
const { design: resolvedDesign } = usePickerTheme(theme);
const display = rest.display ?? DATETIME_DESIGN[resolvedDesign].display;
return (
{
- onConfirm(snapTime(draft, rest));
+ commit(draft);
onClose();
}}
>
diff --git a/src/duration/DurationPickerSheet.tsx b/src/duration/DurationPickerSheet.tsx
index 0afb8c1..fae4c36 100644
--- a/src/duration/DurationPickerSheet.tsx
+++ b/src/duration/DurationPickerSheet.tsx
@@ -21,7 +21,7 @@ export const DurationPickerSheet = ({
...rest
}: DurationPickerSheetProps) => {
const theme = useDesignOverride(themeProp, design);
- const [draft, setDraft] = useDraft(snapDuration(value ?? 0, rest), visible);
+ const [draft, setDraft] = useDraft(snapDuration(value ?? 0, rest), visible, { commit: onConfirm });
return (
{
await fireEvent.press(screen.getByRole('button', { name: 'Done' }));
expect(onConfirm).toHaveBeenCalledWith(new Date(2026, 11, 1));
});
+
+ it('expands inline, commits each change without a Done button, and collapses on a second tap', async () => {
+ const onConfirm = jest.fn();
+ const onClose = jest.fn();
+ await render(
+
+ );
+ await fireEvent.press(screen.getByRole('button', { name: 'Report' }));
+ expect(screen.queryByRole('button', { name: 'Done' })).toBeNull();
+ await fireEvent.press(screen.getByText('Dec'));
+ expect(onConfirm).toHaveBeenCalledWith(new Date(2026, 11, 1));
+ await fireEvent.press(screen.getByRole('button', { name: 'Report' }));
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/src/month/MonthPickerSheet.tsx b/src/month/MonthPickerSheet.tsx
index 6a465d4..5fe0e61 100644
--- a/src/month/MonthPickerSheet.tsx
+++ b/src/month/MonthPickerSheet.tsx
@@ -21,7 +21,9 @@ export const MonthPickerSheet = ({
...rest
}: MonthPickerSheetProps) => {
const theme = useDesignOverride(themeProp, design);
- const [draft, setDraft] = useDraft(startOfMonth(clampDate(value ?? new Date(), rest.minDate, rest.maxDate)), visible);
+ const [draft, setDraft] = useDraft(startOfMonth(clampDate(value ?? new Date(), rest.minDate, rest.maxDate)), visible, {
+ commit: onConfirm,
+ });
return (
(props: SelectPickerProps) => {
@@ -51,7 +51,7 @@ export const SelectPicker = (props: SelectPickerProps) => {
isEqual={isEqual}
theme={themeOverride}
styles={slots}
- style={picker.presentation === SELECT_PRESENTATION.dropdown ? styles.dropdownList : styles.sheetList}
+ style={LIST_STYLE[picker.presentation]}
/>
);
@@ -60,7 +60,7 @@ export const SelectPicker = (props: SelectPickerProps) => {
{label ? : null}
{renderTrigger ? (
- renderTrigger({ open: picker.show, selected: picker.selected, disabled })
+ renderTrigger({ open: picker.toggle, selected: picker.selected, disabled })
) : (
(props: SelectPickerProps) => {
open={picker.open}
disabled={disabled}
clearable={clearable}
- onOpen={picker.show}
+ onOpen={picker.toggle}
onRemove={picker.removeOption}
onClear={picker.clear}
renderChevron={renderChevron}
diff --git a/src/select/hooks/useSelectPicker.ts b/src/select/hooks/useSelectPicker.ts
index 7406aef..8bfe96d 100644
--- a/src/select/hooks/useSelectPicker.ts
+++ b/src/select/hooks/useSelectPicker.ts
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react';
import { usePickerPlatform } from '../../core/theme/ThemeProvider';
import { useAnchor } from '../../sheet/hooks/useAnchor';
-import { resolveFieldPresentation } from '../../sheet/sheet.utils';
+import { resolveFieldPresentation, shouldCollapseOnPress } from '../../sheet/sheet.utils';
import { NO_OPTIONS, SELECT_PRESENTATION } from '../select.constants';
import type { SelectOption, SelectPickerProps, SelectValueProps } from '../select.types';
import { findOptions, shouldSearchInField, toValueList } from '../select.utils';
@@ -38,6 +38,8 @@ export const useSelectPicker = (props: SelectPickerProps, isEqual: (a: T,
onClose?.();
}, [onClose]);
+ const toggle = shouldCollapseOnPress(presentation, open) ? hide : show;
+
const valueProps: SelectValueProps = props.multiple
? {
multiple: true,
@@ -80,6 +82,7 @@ export const useSelectPicker = (props: SelectPickerProps, isEqual: (a: T,
anchor,
show,
hide,
+ toggle,
selected,
valueProps,
removeOption,
diff --git a/src/select/select.styles.ts b/src/select/select.styles.ts
index 8e44033..8ea0285 100644
--- a/src/select/select.styles.ts
+++ b/src/select/select.styles.ts
@@ -1,4 +1,8 @@
-import { StyleSheet } from 'react-native';
+import { StyleSheet, type ViewStyle } from 'react-native';
+
+import { DROPDOWN_MAX_HEIGHT } from '../sheet/sheet.constants';
+import { SELECT_PRESENTATION } from './select.constants';
+import type { SelectPresentation } from './select.types';
export const styles = StyleSheet.create({
flex: { flex: 1 },
@@ -17,4 +21,14 @@ export const styles = StyleSheet.create({
chip: { paddingHorizontal: 10, paddingVertical: 4 },
dropdownList: { flexShrink: 1 },
sheetList: { flexGrow: 1, flexShrink: 1 },
+ /** Inline, the list scrolls on its own instead of stretching the page. */
+ inlineList: { maxHeight: DROPDOWN_MAX_HEIGHT },
});
+
+export const LIST_STYLE: Record = {
+ [SELECT_PRESENTATION.auto]: styles.sheetList,
+ [SELECT_PRESENTATION.bottomSheet]: styles.sheetList,
+ [SELECT_PRESENTATION.modal]: styles.sheetList,
+ [SELECT_PRESENTATION.dropdown]: styles.dropdownList,
+ [SELECT_PRESENTATION.inline]: styles.inlineList,
+};
diff --git a/src/sheet/PickerField.tsx b/src/sheet/PickerField.tsx
index d0ac8f9..809a653 100644
--- a/src/sheet/PickerField.tsx
+++ b/src/sheet/PickerField.tsx
@@ -39,7 +39,7 @@ export const PickerField = ({
{label ? : null}
{renderTrigger ? (
- renderTrigger({ open: field.show, displayValue, disabled })
+ renderTrigger({ open: field.toggle, displayValue, disabled })
) : (
{children};
+ if (presentation === FIELD_PRESENTATION.inline) {
+ return (
+
+ {content}
+
+ );
+ }
+
if (presentation === FIELD_PRESENTATION.dropdown) {
return (
{
+ const expand = useInlineExpand(visible, theme.animation.duration);
+ if (!expand.mounted) return null;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/src/sheet/hooks/useDraft.test.ts b/src/sheet/hooks/useDraft.test.ts
index 432d885..f22012d 100644
--- a/src/sheet/hooks/useDraft.test.ts
+++ b/src/sheet/hooks/useDraft.test.ts
@@ -1,6 +1,9 @@
+import { createElement, type ReactNode } from 'react';
+
import { act, renderHook } from '@testing-library/react-native';
import { useDraft } from './useDraft';
+import { FieldSheetContext } from './useFieldSheet';
describe('useDraft', () => {
it('keeps edits while open and resets to the value on reopen', async () => {
@@ -18,13 +21,35 @@ describe('useDraft', () => {
});
it('resets to the value when the reset key changes', async () => {
- const { result, rerender } = await renderHook(({ value, mode }: { value: number; mode: string }) => useDraft(value, true, mode), {
- initialProps: { value: 1, mode: 'single' },
- });
+ const { result, rerender } = await renderHook(
+ ({ value, mode }: { value: number; mode: string }) => useDraft(value, true, { resetKey: mode }),
+ {
+ initialProps: { value: 1, mode: 'single' },
+ }
+ );
await act(() => result.current[1](2));
await rerender({ value: 7, mode: 'single' });
expect(result.current[0]).toBe(2);
await rerender({ value: 7, mode: 'multiple' });
expect(result.current[0]).toBe(7);
});
+
+ it('commits each change only inside an inline field', async () => {
+ const commit = jest.fn();
+ const inline = ({ children }: { children: ReactNode }) =>
+ createElement(FieldSheetContext.Provider, {
+ value: {
+ presentation: 'inline',
+ anchor: null,
+ },
+ children,
+ });
+ const sheet = await renderHook(() => useDraft(1, true, { commit }));
+ await act(() => sheet.result.current[1](2));
+ expect(commit).not.toHaveBeenCalled();
+ const field = await renderHook(() => useDraft(1, true, { commit }), { wrapper: inline });
+ await act(() => field.result.current[1](3));
+ expect(field.result.current[0]).toBe(3);
+ expect(commit).toHaveBeenCalledWith(3);
+ });
});
diff --git a/src/sheet/hooks/useDraft.ts b/src/sheet/hooks/useDraft.ts
index 8b0b1fe..d5b777e 100644
--- a/src/sheet/hooks/useDraft.ts
+++ b/src/sheet/hooks/useDraft.ts
@@ -1,10 +1,24 @@
-import { useState } from 'react';
+import { useCallback, useRef, useState } from 'react';
-/** Draft state that resets to `value` every time the sheet opens, and whenever `resetKey` changes (e.g. a new mode), open or not. */
-export const useDraft = (value: V, visible: boolean, resetKey?: unknown): [V, (v: V) => void] => {
+import { FIELD_PRESENTATION } from '../sheet.constants';
+import type { DraftOptions } from '../sheet.types';
+import { useFieldSheet } from './useFieldSheet';
+
+/**
+ * Draft state that resets to `value` every time the sheet opens, and whenever `resetKey` changes (e.g. a new mode), open or not.
+ * In an `inline` field there's no Done button, so each change also goes to `commit`.
+ */
+export const useDraft = (value: V, visible: boolean, { resetKey, commit }: DraftOptions = {}): [V, (v: V) => void] => {
const [draft, setDraft] = useState(value);
const [wasVisible, setWasVisible] = useState(visible);
const [key, setKey] = useState(resetKey);
+ const inline = useFieldSheet()?.presentation === FIELD_PRESENTATION.inline;
+ const liveCommit = useRef(commit);
+ liveCommit.current = inline ? commit : undefined;
+ const update = useCallback((next: V) => {
+ setDraft(next);
+ liveCommit.current?.(next);
+ }, []);
const opening = visible && !wasVisible;
const rekeyed = resetKey !== key;
if (visible !== wasVisible || rekeyed) {
@@ -14,7 +28,7 @@ export const useDraft = (value: V, visible: boolean, resetKey?: unknown): [V,
if (opening || rekeyed) {
setDraft(value);
// A field keeps its sheet mounted while closed, so the rest of this render must already see the reset draft.
- return [value, setDraft];
+ return [value, update];
}
- return [draft, setDraft];
+ return [draft, update];
};
diff --git a/src/sheet/hooks/useInlineExpand.test.ts b/src/sheet/hooks/useInlineExpand.test.ts
new file mode 100644
index 0000000..6b13996
--- /dev/null
+++ b/src/sheet/hooks/useInlineExpand.test.ts
@@ -0,0 +1,38 @@
+import type { Animated, LayoutChangeEvent } from 'react-native';
+
+import { act, renderHook } from '@testing-library/react-native';
+
+import { useInlineExpand } from './useInlineExpand';
+
+const layout = (height: number) => ({ nativeEvent: { layout: { x: 0, y: 0, width: 320, height } } }) as LayoutChangeEvent;
+const heightOf = (height: Animated.AnimatedInterpolation) => (height as unknown as { __getValue: () => number }).__getValue();
+
+describe('useInlineExpand', () => {
+ beforeEach(() => jest.useFakeTimers());
+ afterEach(() => jest.useRealTimers());
+
+ it('grows to the measured content height on open and unmounts once folded back up', async () => {
+ const { result, rerender } = await renderHook(({ visible }: { visible: boolean }) => useInlineExpand(visible, 200), {
+ initialProps: { visible: false },
+ });
+ expect(result.current.mounted).toBe(false);
+ await rerender({ visible: true });
+ expect(result.current.mounted).toBe(true);
+ await act(() => result.current.onLayout(layout(180)));
+ await act(() => jest.advanceTimersByTime(500));
+ expect(heightOf(result.current.style.height)).toBe(180);
+ await rerender({ visible: false });
+ expect(result.current.mounted).toBe(true);
+ await act(() => jest.advanceTimersByTime(500));
+ expect(heightOf(result.current.style.height)).toBe(0);
+ expect(result.current.mounted).toBe(false);
+ });
+
+ it('follows the content when it changes height while open', async () => {
+ const { result } = await renderHook(() => useInlineExpand(true, 200));
+ await act(() => result.current.onLayout(layout(180)));
+ await act(() => jest.advanceTimersByTime(500));
+ await act(() => result.current.onLayout(layout(240)));
+ expect(heightOf(result.current.style.height)).toBe(240);
+ });
+});
diff --git a/src/sheet/hooks/useInlineExpand.ts b/src/sheet/hooks/useInlineExpand.ts
new file mode 100644
index 0000000..2a85479
--- /dev/null
+++ b/src/sheet/hooks/useInlineExpand.ts
@@ -0,0 +1,25 @@
+import { useCallback, useMemo, useState } from 'react';
+import type { LayoutChangeEvent } from 'react-native';
+
+import { usePresence } from '../../core/hooks/usePresence';
+import type { InlineExpand } from '../sheet.types';
+
+/** Grows down to the content's height on open and folds back up before unmounting on close; the height animates in JS, as the native driver can't animate layout. */
+export const useInlineExpand = (visible: boolean, duration: number): InlineExpand => {
+ const { mounted, progress } = usePresence({ visible, duration, native: false });
+ const [height, setHeight] = useState(0);
+ const onLayout = useCallback((e: LayoutChangeEvent) => setHeight(e.nativeEvent.layout.height), []);
+ const style = useMemo(
+ () => ({
+ height: progress.interpolate({ inputRange: [0, 1], outputRange: [0, height] }),
+ opacity: progress,
+ }),
+ [height, progress]
+ );
+
+ return {
+ mounted,
+ style,
+ onLayout,
+ };
+};
diff --git a/src/sheet/hooks/usePickerField.test.ts b/src/sheet/hooks/usePickerField.test.ts
index d28c314..da7f42d 100644
--- a/src/sheet/hooks/usePickerField.test.ts
+++ b/src/sheet/hooks/usePickerField.test.ts
@@ -50,6 +50,18 @@ describe('usePickerField', () => {
expect(result.current.sheet.presentation).toBe('dropdown');
});
+ it('toggles an inline picker, and only opens the others', async () => {
+ const inline = await renderHook(() => usePickerField({ presentation: 'inline' }));
+ await act(() => inline.result.current.toggle());
+ expect(inline.result.current.open).toBe(true);
+ await act(() => inline.result.current.toggle());
+ expect(inline.result.current.open).toBe(false);
+ const sheet = await renderHook(() => usePickerField({ presentation: 'bottomSheet' }));
+ await act(() => sheet.result.current.toggle());
+ await act(() => sheet.result.current.toggle());
+ expect(sheet.result.current.open).toBe(true);
+ });
+
it('stays closed while disabled', async () => {
const onOpen = jest.fn();
const { result } = await renderHook(() => usePickerField({ disabled: true, onOpen }));
diff --git a/src/sheet/hooks/usePickerField.ts b/src/sheet/hooks/usePickerField.ts
index d0913e5..22bba27 100644
--- a/src/sheet/hooks/usePickerField.ts
+++ b/src/sheet/hooks/usePickerField.ts
@@ -3,10 +3,10 @@ import { useCallback, useMemo, useState } from 'react';
import { usePickerPlatform } from '../../core/theme/ThemeProvider';
import { FIELD_PRESENTATION } from '../sheet.constants';
import type { FieldPresentation, FieldSheetContextValue, PickerFieldBaseProps } from '../sheet.types';
-import { resolveFieldPresentation } from '../sheet.utils';
+import { resolveFieldPresentation, shouldCollapseOnPress } from '../sheet.utils';
import { useAnchor } from './useAnchor';
-/** Whether the field's picker is open, and where; opening does nothing while `disabled`. */
+/** Whether the field's picker is open, and where; opening does nothing while `disabled`. `toggle` is the field's tap. */
export const usePickerField = ({
disabled,
presentation: presentationProp = FIELD_PRESENTATION.auto,
@@ -30,6 +30,8 @@ export const usePickerField = ({
onClose?.();
}, [onClose]);
+ const toggle = shouldCollapseOnPress(presentation, open) ? hide : show;
+
const sheet = useMemo(
() => ({
presentation,
@@ -42,6 +44,7 @@ export const usePickerField = ({
open,
show,
hide,
+ toggle,
anchor,
sheet,
};
diff --git a/src/sheet/sheet.constants.ts b/src/sheet/sheet.constants.ts
index 5f6c2bf..c2335d4 100644
--- a/src/sheet/sheet.constants.ts
+++ b/src/sheet/sheet.constants.ts
@@ -10,10 +10,11 @@ export const SHEET_PRESENTATION = {
modal: 'modal',
} as const;
-/** A field opens its picker in one of the sheet presentations, or in a dropdown under the field. */
+/** A field opens its picker in one of the sheet presentations, in a dropdown under the field, or inline: expanded under it. */
export const FIELD_PRESENTATION = {
...SHEET_PRESENTATION,
dropdown: 'dropdown',
+ inline: 'inline',
} as const;
export const SHEET_SNAP = {
diff --git a/src/sheet/sheet.styles.ts b/src/sheet/sheet.styles.ts
index b6bf75e..f04dbdf 100644
--- a/src/sheet/sheet.styles.ts
+++ b/src/sheet/sheet.styles.ts
@@ -22,6 +22,8 @@ export const styles = StyleSheet.create({
dropdownOverlay: { position: 'absolute' },
dropdown: { position: 'absolute', borderWidth: StyleSheet.hairlineWidth, paddingVertical: DROPDOWN_PADDING, overflow: 'hidden' },
dropdownScroll: { flexGrow: 0, flexShrink: 1 },
+ inline: { overflow: 'hidden' },
+ inlineContent: { position: 'absolute', top: 0, left: 0, right: 0 },
actions: { flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', gap: 24 },
dropdownFooter: {
paddingTop: 8,
diff --git a/src/sheet/sheet.types.ts b/src/sheet/sheet.types.ts
index ed9a1bd..114f1d4 100644
--- a/src/sheet/sheet.types.ts
+++ b/src/sheet/sheet.types.ts
@@ -17,7 +17,7 @@ import type {
/** `auto`: a centered modal on web windows wider than the sheet, a bottom sheet everywhere else. */
export type SheetPresentation = ValueOf;
-/** A sheet presentation, or `dropdown`: a popover under the field. */
+/** A sheet presentation, `dropdown`: a popover under the field, or `inline`: expanded in place under the field. */
export type FieldPresentation = ValueOf;
export interface FieldSheetContextValue {
@@ -77,12 +77,21 @@ export interface SheetChromeProps extends ThemableProps {
sheetProps?: Partial>;
}
-export interface PickerSheetProps extends SheetChromeProps {
+export interface PickerSheetProps extends Omit {
+ /** Default: the field's presentation, else 'auto' */
+ presentation?: FieldPresentation;
onConfirm?: () => void;
confirmDisabled?: boolean;
children: React.ReactNode;
}
+export interface DraftOptions {
+ /** Resets the draft to `value` when it changes, open or not */
+ resetKey?: unknown;
+ /** Called with each change in an `inline` field, which has no Done button */
+ commit?: (v: V) => void;
+}
+
export interface SheetContentProps {
children: React.ReactNode;
/** Narrower side padding, for content with its own inner spacing (e.g. a calendar). */
@@ -200,6 +209,19 @@ export interface AnchorRect {
height: number;
}
+/** An inline picker's animated height and fade, and the `onLayout` that measures its content. */
+export interface InlineExpand {
+ mounted: boolean;
+ style: { height: Animated.AnimatedInterpolation; opacity: Animated.Value };
+ onLayout: (e: LayoutChangeEvent) => void;
+}
+
+export interface InlinePanelProps {
+ visible: boolean;
+ children: React.ReactNode;
+ theme: PickerTheme;
+}
+
export interface DropdownFrame {
left: number;
width: number;
@@ -239,6 +261,7 @@ export interface ChevronProps {
export type PickerFieldSlot = 'label' | 'trigger' | 'triggerText' | 'placeholder' | 'chevron' | 'clear';
export interface PickerFieldTriggerInfo {
+ /** With the `inline` presentation, a second call collapses the picker */
open: () => void;
displayValue: string;
disabled: boolean;
@@ -262,7 +285,10 @@ export interface PickerFieldBaseProps extends Themable
renderChevron?: (info: ChevronInfo) => React.ReactNode;
onOpen?: () => void;
onClose?: () => void;
- /** Where the picker opens: `dropdown` under the field, `bottomSheet`, `modal`, or `auto`: a dropdown on the web, a bottom sheet on native. Default: 'auto' */
+ /**
+ * Where the picker opens: `dropdown` under the field, `bottomSheet`, `modal`, `inline`: expanded in place under the field, committing each
+ * change (no Cancel / Done) and collapsing on a second tap, or `auto`: a dropdown on the web, a bottom sheet on native. Default: 'auto'
+ */
presentation?: FieldPresentation;
style?: StyleProp;
styles?: SlotStyles;
diff --git a/src/sheet/sheet.utils.test.ts b/src/sheet/sheet.utils.test.ts
index 7b8aa97..dc1dcf5 100644
--- a/src/sheet/sheet.utils.test.ts
+++ b/src/sheet/sheet.utils.test.ts
@@ -11,6 +11,7 @@ import {
resolveFieldPresentation,
resolvePresentation,
resolveSheetHeight,
+ shouldCollapseOnPress,
shouldDismiss,
splitFieldProps,
} from './sheet.utils';
@@ -220,6 +221,15 @@ describe('resolveFieldPresentation', () => {
});
});
+describe('shouldCollapseOnPress', () => {
+ it('collapses only an open inline field', () => {
+ expect(shouldCollapseOnPress('inline', true)).toBe(true);
+ expect(shouldCollapseOnPress('inline', false)).toBe(false);
+ expect(shouldCollapseOnPress('dropdown', true)).toBe(false);
+ expect(shouldCollapseOnPress('bottomSheet', true)).toBe(false);
+ });
+});
+
describe('getSheetDefaults', () => {
it('leaves room for the iOS home indicator and status bar, and drags by the header on native only', () => {
expect(getSheetDefaults('ios')).toEqual({
diff --git a/src/sheet/sheet.utils.ts b/src/sheet/sheet.utils.ts
index c459f8b..9795dd6 100644
--- a/src/sheet/sheet.utils.ts
+++ b/src/sheet/sheet.utils.ts
@@ -95,6 +95,9 @@ export const resolvePresentation = (
export const resolveFieldPresentation = (presentation: FieldPresentation, platform: PlatformOSType): FieldPresentation =>
presentation === FIELD_PRESENTATION.auto && platform === PLATFORM.web ? FIELD_PRESENTATION.dropdown : presentation;
+/** A tap on an open inline field collapses it; any other tap opens the picker. */
+export const shouldCollapseOnPress = (presentation: FieldPresentation, open: boolean) => open && presentation === FIELD_PRESENTATION.inline;
+
/** Platform defaults for a bottom sheet. Mouse drags on the header would select its text on the web, so there only the handle drags. */
export const getSheetDefaults = (platform: PlatformOSType) => ({
bottomInset: platform === PLATFORM.ios ? IOS_BOTTOM_INSET : DEFAULT_BOTTOM_INSET,
diff --git a/src/time/TimePickerSheet.tsx b/src/time/TimePickerSheet.tsx
index 560b7b2..059e6fc 100644
--- a/src/time/TimePickerSheet.tsx
+++ b/src/time/TimePickerSheet.tsx
@@ -21,12 +21,13 @@ export const TimePickerSheet = ({
...rest
}: TimePickerSheetProps) => {
const theme = useDesignOverride(themeProp, design);
- const [draft, setDraft] = useDraft(value ?? new Date(), visible);
+ const commit = (next: Date) => onConfirm(snapTime(next, rest));
+ const [draft, setDraft] = useDraft(value ?? new Date(), visible, { commit });
return (
{
- onConfirm(snapTime(draft, rest));
+ commit(draft);
onClose();
}}
>
diff --git a/src/wheel/WheelPickerSheet.tsx b/src/wheel/WheelPickerSheet.tsx
index 5697552..548e485 100644
--- a/src/wheel/WheelPickerSheet.tsx
+++ b/src/wheel/WheelPickerSheet.tsx
@@ -21,7 +21,7 @@ export const WheelPickerSheet = ({
...rest
}: WheelPickerSheetProps) => {
const theme = useDesignOverride(themeProp, design);
- const [draft, setDraft] = useDraft(value ?? (firstItemValue(rest.items) as T), visible);
+ const [draft, setDraft] = useDraft(value ?? (firstItemValue(rest.items) as T), visible, { commit: onConfirm });
return (
`](../components/picker-sheets#wheelpickersheet) | WheelPicker in a sheet |
| [`EmojiPickerSheet`](../components/picker-sheets#emojipickersheet) | EmojiPicker in a sheet |
| [`ColorPickerSheet`](../components/color-picker#in-a-sheet) | ColorPicker in a sheet |
-| [`useDraft(value, visible)`](../components/picker-sheets#usedraft) | Draft state that resets on open |
+| [`useDraft(value, visible, { resetKey?, commit? })`](../components/picker-sheets#usedraft) | Draft state that resets on open; commits each change inline |
## Fields
@@ -129,8 +129,8 @@ type SkinTone = 0 | 1 | 2 | 3 | 4 | 5;
/** Every sheet's `presentation`. */
type SheetPresentation = 'auto' | 'bottomSheet' | 'modal';
-/** A field's `presentation`: a sheet presentation, or a dropdown under the field. Its `auto` (the default) is a dropdown on the web. */
-type FieldPresentation = SheetPresentation | 'dropdown';
+/** A field's `presentation`: a sheet presentation, a dropdown under the field, or inline: expanded in place under it. Its `auto` (the default) is a dropdown on the web. */
+type FieldPresentation = SheetPresentation | 'dropdown' | 'inline';
/** SelectPicker's `presentation`; its `auto` is a dropdown on the web. */
type SelectPresentation = FieldPresentation;
diff --git a/website/docs/components/phone-field.mdx b/website/docs/components/phone-field.mdx
index da6e630..2d72636 100644
--- a/website/docs/components/phone-field.mdx
+++ b/website/docs/components/phone-field.mdx
@@ -106,7 +106,7 @@ const shown = country ? formatNationalNumber(nationalNumber, country) : user.pho
| `countries` / `exclude` / `preferred` | `CountryCode[]` | — | Which countries the list offers, and which come first. See [CountryPicker](./country-picker#a-subset-of-countries). |
| `preferredLabel` / `allCountriesLabel` | `string` | `'Suggested'` / `'All countries'` | Headers of the list. |
| `locale` | `string` | device locale | Language of the country names. |
-| `presentation` | `'auto' \| 'dropdown' \| 'bottomSheet' \| 'modal'` | `'auto'` | Where the country list opens: a dropdown on the web, a bottom sheet on native. |
+| `presentation` | `'auto' \| 'dropdown' \| 'bottomSheet' \| 'modal' \| 'inline'` | `'auto'` | Where the country list opens: a dropdown on the web, a bottom sheet on native. |
| `countrySheetTitle` | `string` | `'Country code'` | Title of the country sheet. |
| `sheet` | `SheetChromeProps` | `sheetProps.height: 0.8` | Options for the country sheet, as [`CountryPicker`](./country-picker#props)'s. |
| `theme` / `design` | | | See [Theming](../guides/theming) and [Designs](../guides/designs). |
diff --git a/website/docs/components/picker-fields.mdx b/website/docs/components/picker-fields.mdx
index 1b6eeab..2f68918 100644
--- a/website/docs/components/picker-fields.mdx
+++ b/website/docs/components/picker-fields.mdx
@@ -1,6 +1,6 @@
---
title: React Native Date, Time & Wheel Picker Fields
-description: "Form fields for React Native that open a picker in a dropdown, bottom sheet or modal and show the picked value: DatePickerField, TimePickerField, DateTimePickerField, DurationPickerField, MonthPickerField, ColorPickerField, WheelPickerField, EmojiPickerField and PickerField."
+description: "Form fields for React Native that open a picker in a dropdown, bottom sheet or modal, or expand it inline under the field, and show the picked value: DatePickerField, TimePickerField, DateTimePickerField, DurationPickerField, MonthPickerField, ColorPickerField, WheelPickerField, EmojiPickerField and PickerField."
---
import PropPlayground from '@site/src/components/PropPlayground';
@@ -105,13 +105,13 @@ export default function Example() {
}
```
-Each field takes the props of the sheet it opens, without `visible`, plus the [field props](#props). The field owns the open state; you keep the value. As with the sheets, `onConfirm` fires only on **Done**, so your state changes only when the user confirms. On the web they open as dropdowns; switch `presentation` in the panel to see the same form with bottom sheets or modals.
+Each field takes the props of the sheet it opens, without `visible`, plus the [field props](#props). The field owns the open state; you keep the value. As with the sheets, `onConfirm` fires only on **Done**, so your state changes only when the user confirms (except `inline`, which has no **Done** and commits each change). On the web they open as dropdowns; switch `presentation` in the panel to see the same form with bottom sheets or modals.
## Try the props
-## Dropdown, bottom sheet or modal
+## Dropdown, bottom sheet, modal or inline
`presentation` sets where the picker opens:
@@ -121,13 +121,32 @@ Each field takes the props of the sheet it opens, without `visible`, plus the [f
| `'dropdown'` | Under the field, like a menu, or above it when there's more room there. **Cancel** and **Done** sit below the picker; a tap outside cancels. It's at least 320 px wide, so a calendar fits under a narrow field. It opens on the side with room for the whole picker; in a window too short for it, the dropdown scrolls. |
| `'bottomSheet'` | A bottom sheet with the title and buttons in its header. |
| `'modal'` | A centered dialog. |
+| `'inline'` | In place, under the field: the picker expands down, pushing the content below it, and folds back up on a second tap on the field. There's no **Cancel** or **Done**: each change goes to `onConfirm` (or `onSelect`) right away, like the iOS Settings and Clock apps. A date range commits once both ends are picked. |
```tsx
+
```
-`sheetProps` and `title` apply to the bottom sheet and the modal; the dropdown has no header. [`SelectPicker`](./select-picker#presentation) takes the same values and the same `'auto'` default.
+```tsx live
+import { useState } from 'react';
+import { View } from 'react-native';
+import { DurationPickerField, TimePickerField } from '@elik56a/react-native-pickers';
+
+export default function Example() {
+ const [alarm, setAlarm] = useState(new Date(2026, 0, 1, 7, 30));
+ const [snooze, setSnooze] = useState(9 * 60);
+ return (
+
+
+
+
+ );
+}
+```
+
+`sheetProps` and `title` apply to the bottom sheet and the modal; the dropdown and the inline picker have no header. With `renderTrigger`, `open()` toggles an inline picker, so your trigger collapses it too. [`SelectPicker`](./select-picker#presentation) takes the same values and the same `'auto'` default.
## What the field shows
@@ -322,7 +341,7 @@ Every field takes these, plus its sheet's props without `visible`: `value`, `onC
| `onClear` | `() => void` | — | Called by ×. Reset your value here. |
| `renderTrigger` | `(info: { open, displayValue, disabled }) => ReactNode` | — | Replace the field box. |
| `renderChevron` | `(info: { open, theme }) => ReactNode` | — | Custom chevron icon. It still rotates on open and close. |
-| `presentation` | `'auto' \| 'dropdown' \| 'bottomSheet' \| 'modal'` | `'auto'` | Where the picker opens: a dropdown on the web, a bottom sheet on native. See [Dropdown, bottom sheet or modal](#dropdown-bottom-sheet-or-modal). |
+| `presentation` | `'auto' \| 'dropdown' \| 'bottomSheet' \| 'modal' \| 'inline'` | `'auto'` | Where the picker opens: a dropdown on the web, a bottom sheet on native. See [Dropdown, bottom sheet, modal or inline](#dropdown-bottom-sheet-modal-or-inline). |
| `onOpen` / `onClose` | `() => void` | — | The picker opened / closed (Done, Cancel or dismiss). |
| `style` | `StyleProp` | — | Wrapper style (label + field). |
| `styles` | `SlotStyles` | — | Field slots plus the picker's slots. |
diff --git a/website/docs/components/picker-sheets.mdx b/website/docs/components/picker-sheets.mdx
index 74fe49c..2b02743 100644
--- a/website/docs/components/picker-sheets.mdx
+++ b/website/docs/components/picker-sheets.mdx
@@ -30,7 +30,7 @@ The sheets take `presentation`: `'auto'` (default), `'bottomSheet'` or `'modal'`
```
-[`SelectPicker`](./select-picker#presentation) also supports `'dropdown'`, which opens under the field. It's `SelectPicker`'s default on the web.
+[`SelectPicker`](./select-picker#presentation) and the [fields](./picker-fields#dropdown-bottom-sheet-modal-or-inline) also support `'dropdown'`, which opens under the field, and `'inline'`, which expands under it. It's `SelectPicker`'s default on the web.
```tsx
import { DatePickerSheet, TimePickerSheet, WheelPickerSheet, EmojiPickerSheet, PickerSheet, useDraft } from '@elik56a/react-native-pickers';
@@ -415,7 +415,13 @@ export default function Example() {
## `useDraft`
```ts
-function useDraft(value: V, visible: boolean): [draft: V, setDraft: (v: V) => void];
+function useDraft(
+ value: V,
+ visible: boolean,
+ options?: { resetKey?: unknown; commit?: (v: V) => void }
+): [draft: V, setDraft: (v: V) => void];
```
-Returns a draft copy of `value` that **resets to `value` each time `visible` turns true**. It's the hook the built-in sheets use; use it for your own `PickerSheet`s as shown above.
+Returns a draft copy of `value` that **resets to `value` each time `visible` turns true** (and whenever `resetKey` changes). It's the hook the built-in sheets use; use it for your own `PickerSheet`s as shown above.
+
+Inside a [`PickerField`](./picker-fields#dropdown-bottom-sheet-modal-or-inline) set to `presentation="inline"` there's no **Done** button, so `setDraft` also calls `commit` with each change. Pass your save function there to support inline: `useDraft(prefs, visible, { commit: onSave })`.
diff --git a/website/docs/components/select-picker.mdx b/website/docs/components/select-picker.mdx
index ca05fca..c70bbbc 100644
--- a/website/docs/components/select-picker.mdx
+++ b/website/docs/components/select-picker.mdx
@@ -1,6 +1,6 @@
---
title: React Native Select & Dropdown Picker
-description: "React Native select and dropdown picker: a searchable bottom sheet, modal or dropdown. Single or multi-select with chips, groups, icons, a clear button and server search (autocomplete)."
+description: "React Native select and dropdown picker: a searchable bottom sheet, modal, dropdown or inline list. Single or multi-select with chips, groups, icons, a clear button and server search (autocomplete)."
---
import PropPlayground from '@site/src/components/PropPlayground';
@@ -499,6 +499,7 @@ export default function Example() {
- `'bottomSheet'`: always a bottom sheet.
- `'modal'`: always a centered modal.
- `'dropdown'`: a popover under the field, as wide as the field. It flips above the field when there's more room there, and a tap outside closes it.
+- `'inline'`: the list expands under the field, pushing the content below it, and folds back up on a second tap or once an option is picked (with `closeOnSelect`). It's at most 320 px tall and scrolls past that.
```tsx live
import { useState } from 'react';
@@ -625,7 +626,7 @@ export default function Example() {
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
-| `presentation` | `'auto' \| 'bottomSheet' \| 'modal' \| 'dropdown'` | `'auto'` | How the options open. `auto`: dropdown on the web, bottom sheet on native. |
+| `presentation` | `'auto' \| 'bottomSheet' \| 'modal' \| 'dropdown' \| 'inline'` | `'auto'` | How the options open. `auto`: dropdown on the web, bottom sheet on native. |
| `sheetTitle` | `string` | `label` | Sheet header title. |
| `closeOnSelect` | `boolean` | `true` | Single: close on tap. `false` shows a Done button. |
| `sheet` | `Partial` | — | `confirmLabel`, `cancelLabel`, `showCancel`, `title`, `sheetProps`. |
diff --git a/website/docs/getting-started/core-concepts.mdx b/website/docs/getting-started/core-concepts.mdx
index 4b3e18b..b15476f 100644
--- a/website/docs/getting-started/core-concepts.mdx
+++ b/website/docs/getting-started/core-concepts.mdx
@@ -72,7 +72,7 @@ Wheel-based pickers (`WheelPicker`, `MultiWheelPicker` columns, `TimePicker`, `D
## Fields
-In a form, the picker hides behind a field. `SelectPicker` is one, and every sheet has a field version that takes the same props without `visible` / `onClose`: [`DatePickerField`, `TimePickerField`, `DateTimePickerField`, `WheelPickerField`, `EmojiPickerField`](../components/picker-fields). The field owns the open state, shows the confirmed value formatted for `locale`, and looks the same as `SelectPicker`'s field. By default it opens a bottom sheet on iOS and Android and a dropdown under the field on the web; `presentation` picks `'bottomSheet'`, `'modal'` or `'dropdown'`.
+In a form, the picker hides behind a field. `SelectPicker` is one, and every sheet has a field version that takes the same props without `visible` / `onClose`: [`DatePickerField`, `TimePickerField`, `DateTimePickerField`, `WheelPickerField`, `EmojiPickerField`](../components/picker-fields). The field owns the open state, shows the confirmed value formatted for `locale`, and looks the same as `SelectPicker`'s field. By default it opens a bottom sheet on iOS and Android and a dropdown under the field on the web; `presentation` picks `'bottomSheet'`, `'modal'`, `'dropdown'` or `'inline'` (expanded in place under the field).
```tsx
diff --git a/website/docs/resources/ai.mdx b/website/docs/resources/ai.mdx
index 62b32a7..8fb0c6f 100644
--- a/website/docs/resources/ai.mdx
+++ b/website/docs/resources/ai.mdx
@@ -60,7 +60,7 @@ WeekStrip one week of days in a row: value: Date|null onChange(date)
SelectPicker options: {label,value,description?,icon?,group?,disabled?,keywords?}[] value onChange(value, option)
multiple → value T[], onChange(values, options), maxSelected, chips
label placeholder searchable(dropdown: type in field, default on; sheet: >8 auto) clearable+onClear (single: reset value in onClear) renderTrigger({open,selected}) renderChevron({open,theme})
- presentation 'auto'(dropdown on web, bottom sheet on native)|'bottomSheet'|'modal'|'dropdown'
+ presentation 'auto'(dropdown on web, bottom sheet on native)|'bottomSheet'|'modal'|'dropdown'|'inline'(expands under the field)
SelectList same list inline: options value onChange multiple listHeight
server search: loadOptions(query) => Promise (debounced by searchDebounce=300ms, loader until results, stale answers ignored; options = shown before typing; searchPrompt='Type to search' when none, emptyText after an empty search)
or manual: onSearchChange(query) + loading + onEndReached; loadingText renderLoading(theme); no local filtering with either
@@ -93,13 +93,13 @@ Sheets (draft; commit on Done):
DateTimePickerSheet visible onClose value onConfirm + DateTimePicker props
DurationPickerSheet visible onClose value onConfirm(seconds) + DurationPicker props
MonthPickerSheet visible onClose value onConfirm(month) + MonthPicker props
- PickerSheet visible onClose onConfirm children + useDraft(value, visible)
+ PickerSheet visible onClose onConfirm children + useDraft(value, visible, { resetKey?, commit? }) (commit: each change in an inline field)
Fields (a SelectPicker-style field that opens the sheet and shows the confirmed value; field owns open state):
DatePickerField / TimePickerField / DateTimePickerField / DurationPickerField / MonthPickerField / ColorPickerField / WheelPickerField / EmojiPickerField
= the matching *Sheet props WITHOUT visible (value, onConfirm | onSelect, mode, locale, title...)
+ label placeholder='Select…' displayValue(default: value formatted for locale) clearable+onClear(reset value there)
disabled renderTrigger({open,displayValue,disabled}) renderChevron({open,theme}) onOpen onClose style testID
- presentation 'auto'(dropdown on web, bottom sheet on native)|'bottomSheet'|'modal'|'dropdown'; design is passed on to the sheet
+ presentation 'auto'(dropdown on web, bottom sheet on native)|'bottomSheet'|'modal'|'dropdown'|'inline'(expands under the field, no Done: each change fires onConfirm, second tap collapses); design is passed on to the sheet
styles = field slots (label trigger triggerText placeholder chevron clear) + the picker's slots
EmojiPickerField also takes value: string (the emoji shown). Sheet title defaults to label.
PickerField same field props, children: ({visible, onClose}) =>
diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts
index 914c846..bec51da 100644
--- a/website/docusaurus.config.ts
+++ b/website/docusaurus.config.ts
@@ -70,7 +70,7 @@ const structuredData = {
version,
author: { '@type': 'Person', name: AUTHOR, url: 'https://github.com/elik56a', sameAs: [LINKEDIN] },
keywords:
- 'react native date picker, time picker, duration picker, month picker, week strip, date time picker, wheel picker, calendar, date range picker, select, dropdown, multi-select, autocomplete, segmented control, country picker, phone code picker, phone number input, time zone picker, emoji picker, color picker, bottom sheet, material design, ios picker, expo, react native web, keyboard accessible',
+ 'react native date picker, time picker, duration picker, month picker, week strip, date time picker, wheel picker, calendar, date range picker, select, dropdown, inline picker, multi-select, autocomplete, segmented control, country picker, phone code picker, phone number input, time zone picker, emoji picker, color picker, bottom sheet, material design, ios picker, expo, react native web, keyboard accessible',
},
],
};
diff --git a/website/src/catalog/playgrounds.tsx b/website/src/catalog/playgrounds.tsx
index 783da4d..e3f8fe0 100644
--- a/website/src/catalog/playgrounds.tsx
+++ b/website/src/catalog/playgrounds.tsx
@@ -32,7 +32,7 @@ import {
import { styles } from './catalog.styles';
import type { PlaygroundConfig, PlaygroundKey, PlaygroundSelection } from './catalog.types';
-const PRESENTATION = { prop: 'presentation', options: ['auto', 'dropdown', 'bottomSheet', 'modal'] } as const;
+const PRESENTATION = { prop: 'presentation', options: ['auto', 'dropdown', 'bottomSheet', 'modal', 'inline'] } as const;
const CLEARABLE = { prop: 'clearable', options: [false, true] } as const;
const toOptions = (labels: string[]) =>
diff --git a/website/src/home/home.constants.ts b/website/src/home/home.constants.ts
index 194bb20..38f878c 100644
--- a/website/src/home/home.constants.ts
+++ b/website/src/home/home.constants.ts
@@ -70,7 +70,7 @@ export const COMPONENTS = [
{ doc: 'components/color-picker', name: 'ColorPicker', body: "iOS's grid, Google's circles, a web spectrum" },
{ doc: 'components/multi-wheel-picker', name: 'MultiWheelPicker', body: 'Units, compound values, dependent columns' },
{ doc: 'components/picker-sheets', name: 'Sheets', body: 'Every picker in a bottom sheet or modal' },
- { doc: 'components/picker-fields', name: 'Fields', body: 'Tap a field, pick in a sheet or dropdown' },
+ { doc: 'components/picker-fields', name: 'Fields', body: 'Tap a field, pick in a sheet, a dropdown or inline' },
];
export const HERO_CHIPS = [
diff --git a/website/src/live/controls.test.ts b/website/src/live/controls.test.ts
index 07a0454..c3d8146 100644
--- a/website/src/live/controls.test.ts
+++ b/website/src/live/controls.test.ts
@@ -20,7 +20,7 @@ describe('findControls', () => {
const props = findControls(fields).map((c) => c.prop);
expect(props).toEqual(['presentation', 'clearable', 'disabled']);
expect(control(fields, 'presentation')).toMatchObject({
- options: ['auto', 'dropdown', 'bottomSheet', 'modal'],
+ options: ['auto', 'dropdown', 'bottomSheet', 'modal', 'inline'],
components: ['DatePickerField', 'SelectPicker'],
});
});
diff --git a/website/src/live/live.constants.ts b/website/src/live/live.constants.ts
index cf60950..6e27fd2 100644
--- a/website/src/live/live.constants.ts
+++ b/website/src/live/live.constants.ts
@@ -40,7 +40,12 @@ const SELECT = ['SelectPicker', 'CountryPicker'];
* A prop listed twice (e.g. `presentation`) becomes one control; the first entry an example matches sets its options.
*/
export const LIVE_CONTROLS: readonly LiveControl[] = [
- { prop: 'presentation', components: [...FIELDS, ...SELECT], options: ['auto', 'dropdown', 'bottomSheet', 'modal'], default: 'auto' },
+ {
+ prop: 'presentation',
+ components: [...FIELDS, ...SELECT],
+ options: ['auto', 'dropdown', 'bottomSheet', 'modal', 'inline'],
+ default: 'auto',
+ },
{ prop: 'presentation', components: SHEETS, options: ['auto', 'bottomSheet', 'modal'], default: 'auto' },
{ prop: 'display', components: DATE_TIME, options: ['wheel', 'calendar'], default: 'wheel' },
{ prop: 'format', components: [...TIME, ...DATE_TIME], options: ['auto', '12h', '24h'], default: 'auto' },