asma-ui-core 3.112.0 → 3.113.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asma-ui-core.css +1 -1
- package/dist/components/inputs/dynamic-select/components/DynamicSelectAutocomplete.js +95 -93
- package/dist/components/inputs/full-screen-picker/FullScreenPicker.js +93 -0
- package/dist/components/inputs/full-screen-picker/PickerLiveRegion.js +34 -0
- package/dist/components/inputs/full-screen-picker/pickerCopy.js +34 -0
- package/dist/components/inputs/full-screen-picker/useVisualViewportHeight.js +10 -0
- package/dist/components/inputs/phone-field/CountryCodeOptions.js +28 -24
- package/dist/components/inputs/phone-field/CountryCodeSelect.js +82 -75
- package/dist/components/inputs/select-autocomplete/SelectAutocompletePicker.js +217 -0
- package/dist/components/inputs/select-autocomplete/StyledSelectAutocomplete.js +270 -220
- package/dist/components/inputs/select-autocomplete/draftChanges.js +24 -0
- package/dist/components/inputs/select-autocomplete/listboxCursor.js +7 -0
- package/dist/components/inputs/select-autocomplete/readFieldName.js +18 -0
- package/dist/hooks/useMediaQuery.hook.js +12 -8
- package/dist/index.js +137 -133
- package/dist/src/components/inputs/dynamic-select/StyledDynamicSelect.d.ts +4 -1
- package/dist/src/components/inputs/full-screen-picker/FullScreenPicker.d.ts +62 -0
- package/dist/src/components/inputs/full-screen-picker/PickerLiveRegion.d.ts +31 -0
- package/dist/src/components/inputs/full-screen-picker/index.d.ts +2 -0
- package/dist/src/components/inputs/full-screen-picker/pickerCopy.d.ts +29 -0
- package/dist/src/components/inputs/full-screen-picker/useVisualViewportHeight.d.ts +8 -0
- package/dist/src/components/inputs/phone-field/CountryCodeOptions.d.ts +9 -1
- package/dist/src/components/inputs/phone-field/CountryCodeSelect.d.ts +4 -3
- package/dist/src/components/inputs/phone-field/useCountryPicker.d.ts +2 -1
- package/dist/src/components/inputs/select-autocomplete/SelectAutocompletePicker.d.ts +52 -0
- package/dist/src/components/inputs/select-autocomplete/StyledSelectAutocomplete.d.ts +21 -2
- package/dist/src/components/inputs/select-autocomplete/draftChanges.d.ts +17 -0
- package/dist/src/components/inputs/select-autocomplete/listboxCursor.d.ts +9 -0
- package/dist/src/components/inputs/select-autocomplete/readFieldName.d.ts +13 -0
- package/dist/src/hooks/useMediaQuery.hook.d.ts +6 -0
- package/dist/src/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The visible height in CSS px — the part of the screen the on-screen keyboard does not cover.
|
|
3
|
+
*
|
|
4
|
+
* iOS Safari keeps `100%`/`dvh` at the full layout height while the keyboard is up, so a list sized
|
|
5
|
+
* by CSS alone ends behind the keyboard. The picker sizes its column with this instead (ASMA-8468
|
|
6
|
+
* REQ-008, RISK-004 — verify on a real device).
|
|
7
|
+
*/
|
|
8
|
+
export declare const useVisualViewportHeight: () => number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KeyboardEvent } from 'react';
|
|
1
2
|
import { PhoneCountryChoice, RenderCountryFlag } from './types';
|
|
2
3
|
export interface CountryCodeOptionsProps {
|
|
3
4
|
dataTest: string;
|
|
@@ -12,9 +13,16 @@ export interface CountryCodeOptionsProps {
|
|
|
12
13
|
label: string;
|
|
13
14
|
renderFlag?: RenderCountryFlag;
|
|
14
15
|
className?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Makes the list a Tab stop of its own (the mobile sheet, ASMA-8468), driven by these keys and
|
|
18
|
+
* the 3 px active-row marker. Without it the list is only driven from the combobox (desktop).
|
|
19
|
+
*/
|
|
20
|
+
onKeyDown?: (event: KeyboardEvent<HTMLElement>) => void;
|
|
21
|
+
/** Tabbing into the list: put the active-row marker on a row. */
|
|
22
|
+
onFocus?: () => void;
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
25
|
* The country listbox. Presentation only — filtering, the active row and the keyboard live in
|
|
18
26
|
* [[useCountryPicker]], so the desktop trigger and the mobile sheet render the same list.
|
|
19
27
|
*/
|
|
20
|
-
export declare const CountryCodeOptions: ({ dataTest, visible, selectedIso2, activeIndex, optionId, onSelect, listId, label, renderFlag, className, }: CountryCodeOptionsProps) => JSX.Element;
|
|
28
|
+
export declare const CountryCodeOptions: ({ dataTest, visible, selectedIso2, activeIndex, optionId, onSelect, listId, label, renderFlag, className, onKeyDown, onFocus, }: CountryCodeOptionsProps) => JSX.Element;
|
|
@@ -23,8 +23,9 @@ export interface CountryCodeSelectProps {
|
|
|
23
23
|
* - **Desktop** — the trigger *is* the combobox. Opening it turns the calling-code slot into an
|
|
24
24
|
* input and typing filters the list below, which is what Figma draws (the open trigger carries a
|
|
25
25
|
* text cursor, and the menu holds rows only — node 8565:284950 has no search field).
|
|
26
|
-
* - **Mobile** — a full-screen sheet with its own search box, per node 8699:112423
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
26
|
+
* - **Mobile** — a full-screen sheet with its own search box, per node 8699:112423, drawn by the
|
|
27
|
+
* shared `FullScreenPicker` (ASMA-8468). The break is the selects' picker break — below 744 px
|
|
28
|
+
* wide **or** at most 480 px high (D-002) — so a landscape phone gets the sheet too, not a
|
|
29
|
+
* popover squeezed against the keyboard.
|
|
29
30
|
*/
|
|
30
31
|
export declare const CountryCodeSelect: ({ dataTest, countries, value, onChange, disabled, selectCountryLabel, searchPlaceholder, renderFlag, labelledBy, className, }: CountryCodeSelectProps) => JSX.Element;
|
|
@@ -18,7 +18,8 @@ export interface CountryPicker {
|
|
|
18
18
|
activeIndex: number | null;
|
|
19
19
|
optionId: (index: number) => string;
|
|
20
20
|
openWithKeyboard: (direction: -1 | 1) => void;
|
|
21
|
-
|
|
21
|
+
/** Chooses a country; `close = false` keeps the list open (Space). */
|
|
22
|
+
select: (iso2: string, close?: boolean) => void;
|
|
22
23
|
dismiss: () => void;
|
|
23
24
|
handleKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
24
25
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ReactNode, SyntheticEvent } from 'react';
|
|
2
|
+
import { PickerText } from '../full-screen-picker/pickerCopy';
|
|
3
|
+
import { AutocompleteRenderOptionState, OptionLiProps } from './StyledSelectAutocomplete';
|
|
4
|
+
export interface SelectAutocompletePickerProps<T> {
|
|
5
|
+
open: boolean;
|
|
6
|
+
dataTest: string;
|
|
7
|
+
listboxId: string;
|
|
8
|
+
title?: ReactNode;
|
|
9
|
+
/** Plain-text field name: names the listbox, and the dialog when there is no `title`. */
|
|
10
|
+
name: string;
|
|
11
|
+
text: PickerText;
|
|
12
|
+
options: readonly T[];
|
|
13
|
+
multiple: boolean;
|
|
14
|
+
/** Current value: the option (or `null`) with one choice, the chosen options with several. */
|
|
15
|
+
value: T | null | readonly T[];
|
|
16
|
+
/** The field may be emptied: not `disableClearable`, not read-only. */
|
|
17
|
+
clearable: boolean;
|
|
18
|
+
isEqual: (a: T, b: T) => boolean;
|
|
19
|
+
isOptionDisabled: (option: T) => boolean;
|
|
20
|
+
getLabel: (option: T) => string;
|
|
21
|
+
getOptionKey?: (option: T) => string | number;
|
|
22
|
+
filterOptions?: (options: T[], state: {
|
|
23
|
+
inputValue: string;
|
|
24
|
+
}) => T[];
|
|
25
|
+
renderOptionRow: (props: OptionLiProps, option: T, state: AutocompleteRenderOptionState) => ReactNode;
|
|
26
|
+
optionRowClassName: string;
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
loadingText: ReactNode;
|
|
29
|
+
noOptionsText: ReactNode;
|
|
30
|
+
allowSelectAll?: boolean;
|
|
31
|
+
selectAllLabel: string;
|
|
32
|
+
/**
|
|
33
|
+
* One choice: an option was chosen. The caller applies it, and closes when `close` is set — a tap
|
|
34
|
+
* or Enter; Space chooses and keeps the picker open, as in the attached list.
|
|
35
|
+
*/
|
|
36
|
+
onSelect: (event: SyntheticEvent, option: T, close: boolean) => void;
|
|
37
|
+
/** One choice: *Fjern valget*. The caller empties the field, closes and announces. */
|
|
38
|
+
onClear: (event: SyntheticEvent) => void;
|
|
39
|
+
/** Several choices: *Ferdig*. The caller applies the draft (see `getDraftChanges`) and closes. */
|
|
40
|
+
onApply: (event: SyntheticEvent, draft: T[]) => void;
|
|
41
|
+
/** Close button, Escape, Back: a cancel — nothing is applied. */
|
|
42
|
+
onCancel: () => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The body of `StyledSelectAutocomplete` on a small viewport (ASMA-8468): its own search query
|
|
46
|
+
* (the field's text is left alone), filtering, the 50-row cap, the keyboard, and the choice rules —
|
|
47
|
+
* one choice closes on tap; several choices tick a draft that only *Ferdig* applies. The draft and
|
|
48
|
+
* the clear-row visibility are taken at the moment it opens (REQ-011).
|
|
49
|
+
*
|
|
50
|
+
* @see asma-modules/_docs/frontend/plans/2026-10-19-11-04-plan-full-screen-select-picker-phones.md:218 — TASK-012..018
|
|
51
|
+
*/
|
|
52
|
+
export declare function SelectAutocompletePicker<T>({ open, dataTest, listboxId, title, name, text, options, multiple, value, clearable, isEqual, isOptionDisabled, getLabel, getOptionKey, filterOptions, renderOptionRow, optionRowClassName, loading, loadingText, noOptionsText, allowSelectAll, selectAllLabel, onSelect, onClear, onApply, onCancel, }: SelectAutocompletePickerProps<T>): JSX.Element;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { CSSProperties, HTMLAttributes, KeyboardEvent, ReactNode, SyntheticEvent } from 'react';
|
|
2
2
|
import { FieldWidth } from '../field-styles';
|
|
3
|
+
import { PickerText } from '../full-screen-picker/pickerCopy';
|
|
3
4
|
export type AutocompleteChangeReason = 'createOption' | 'selectOption' | 'removeOption' | 'clear' | 'blur';
|
|
4
5
|
export type AutocompleteInputChangeReason = 'input' | 'reset' | 'clear';
|
|
5
6
|
export interface AutocompleteRenderOptionState {
|
|
6
7
|
selected: boolean;
|
|
7
8
|
index: number;
|
|
8
9
|
inputValue: string;
|
|
10
|
+
/** The row is drawn in the full-screen picker (ASMA-8468): show the full label, no line clamp. */
|
|
11
|
+
inPicker?: boolean;
|
|
9
12
|
}
|
|
10
13
|
/** Props the combobox hands to `renderInput`; spread straight onto `StyledInputField`. */
|
|
11
14
|
export interface AutocompleteRenderInputParams {
|
|
@@ -29,7 +32,7 @@ export interface AutocompleteRenderInputParams {
|
|
|
29
32
|
};
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
|
-
type OptionLiProps = HTMLAttributes<HTMLLIElement> & {
|
|
35
|
+
export type OptionLiProps = HTMLAttributes<HTMLLIElement> & {
|
|
33
36
|
key?: string;
|
|
34
37
|
ref?: React.Ref<HTMLLIElement>;
|
|
35
38
|
[dataAttr: `data-${string}`]: unknown;
|
|
@@ -128,6 +131,22 @@ export interface StyledSelectAutocompleteProps<T, Multiple extends boolean | und
|
|
|
128
131
|
};
|
|
129
132
|
};
|
|
130
133
|
sx?: unknown;
|
|
134
|
+
/**
|
|
135
|
+
* Title of the full-screen picker on a small viewport (ASMA-8468). Defaults to the field's label,
|
|
136
|
+
* read when the picker opens from the input's `<label>`, `aria-labelledby` or `aria-label`. With
|
|
137
|
+
* none, there is no title and the dialog and list are named by the placeholder, else by
|
|
138
|
+
* `pickerText.search`.
|
|
139
|
+
*/
|
|
140
|
+
pickerTitle?: ReactNode;
|
|
141
|
+
/** Overrides for any picker string (*Ferdig*, *Fjern valget*, counts, …). */
|
|
142
|
+
pickerText?: Partial<PickerText>;
|
|
143
|
+
/** Language of the default picker copy. Defaults to English, like the rest of ui-core. */
|
|
144
|
+
locale?: 'no' | 'en';
|
|
145
|
+
/**
|
|
146
|
+
* Keep the attached list on every viewport. By default the list opens as a full-screen picker
|
|
147
|
+
* below 744 CSS px wide or at most 480 CSS px high (ASMA-8468).
|
|
148
|
+
*/
|
|
149
|
+
disableFullScreenPicker?: boolean;
|
|
131
150
|
}
|
|
132
151
|
/**
|
|
133
152
|
* single + multiple selection (with tag chips + optional select-all), type-ahead filtering,
|
|
@@ -136,5 +155,5 @@ export interface StyledSelectAutocompleteProps<T, Multiple extends boolean | und
|
|
|
136
155
|
* ponytail: `freeSolo` is accepted (arbitrary input is emitted via onInputChange) but not turned
|
|
137
156
|
* into option values; exact keyboard-parity edge cases are Chromatic/axe-gated.
|
|
138
157
|
*/
|
|
139
|
-
export declare function StyledSelectAutocomplete<T, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false>({ dataTest, options, renderInput, value, onChange, getOptionLabel, getOptionKey, isOptionEqualToValue, renderOption, renderValue, filterOptions, loading, loadingText, noOptionsText, disabled, readOnly, error, helperText, size, rowSize, disableClearable, disableCloseOnSelect, popupIcon, autoHeight, multiple, allowSelectAll, selectAllLabel, width, fullWidth: _fullWidth, classes, inputValue: controlledInput, onInputChange, open: controlledOpen, onOpen, onClose, className, wrapperClassName, getOptionDisabled, slotProps, }: StyledSelectAutocompleteProps<T, Multiple, DisableClearable, FreeSolo>): JSX.Element;
|
|
158
|
+
export declare function StyledSelectAutocomplete<T, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false>({ dataTest, options, renderInput, value, onChange, getOptionLabel, getOptionKey, isOptionEqualToValue, renderOption, renderValue, filterOptions, loading, loadingText, noOptionsText, disabled, readOnly, error, helperText, size, rowSize, disableClearable, disableCloseOnSelect, popupIcon, autoHeight, multiple, allowSelectAll, selectAllLabel, width, fullWidth: _fullWidth, classes, inputValue: controlledInput, onInputChange, open: controlledOpen, onOpen, onClose, className, wrapperClassName, getOptionDisabled, slotProps, pickerTitle, pickerText, locale, disableFullScreenPicker, }: StyledSelectAutocompleteProps<T, Multiple, DisableClearable, FreeSolo>): JSX.Element;
|
|
140
159
|
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type DraftChange<T> = {
|
|
2
|
+
value: T[];
|
|
3
|
+
reason: 'removeOption' | 'selectOption';
|
|
4
|
+
option: T;
|
|
5
|
+
} | {
|
|
6
|
+
value: T[];
|
|
7
|
+
reason: 'clear';
|
|
8
|
+
option?: undefined;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The `onChange` calls that turn `applied` into `draft` the way ticking in the attached list would
|
|
12
|
+
* have (ASMA-8468 D-007): one `removeOption`/`selectOption` per changed option, carrying
|
|
13
|
+
* `details.option`, so consumers that act per tick keep working. Removals come first, additions in
|
|
14
|
+
* the order they were ticked. Emptying a non-empty value is one `clear`, as the field's × does. No
|
|
15
|
+
* change, no calls.
|
|
16
|
+
*/
|
|
17
|
+
export declare const getDraftChanges: <T>(applied: readonly T[], draft: readonly T[], isEqual: (a: T, b: T) => boolean) => DraftChange<T>[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where ArrowUp/ArrowDown moves the keyboard cursor of a select listbox. Shared by the attached list
|
|
3
|
+
* and the full-screen picker (ASMA-8468), which must step the same way.
|
|
4
|
+
*
|
|
5
|
+
* `stops` are the cursor positions in order (`-1` is the *Select all* row). Without a cursor the
|
|
6
|
+
* move starts from `fallback` (the chosen option); from outside the stops it lands on the first or
|
|
7
|
+
* the last one. Clamped at both ends, no wrap-around. `null` when there is nowhere to go.
|
|
8
|
+
*/
|
|
9
|
+
export declare const nextActiveIndex: (stops: readonly number[], current: number | null, fallback: number, direction: -1 | 1) => number | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface FieldName {
|
|
2
|
+
/** The field's label: shown as the picker title. */
|
|
3
|
+
label: string;
|
|
4
|
+
/** What names the dialog and list when there is no label: the label, else the placeholder. */
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The field's name for the full-screen picker (ASMA-8468 TASK-015), in the order a browser names an
|
|
9
|
+
* input: its first `<label>`, else the elements named by `aria-labelledby`, else `aria-label` — a
|
|
10
|
+
* visible label that is not linked to the input usually reaches it that way (RISK-002). A
|
|
11
|
+
* placeholder only names the dialog and list, it is no title. Empty strings when there is nothing.
|
|
12
|
+
*/
|
|
13
|
+
export declare const readFieldName: (input: HTMLInputElement | null) => FieldName;
|
|
@@ -2,3 +2,9 @@ export declare function makeMediaQueryStore(mediaQuery: string): () => boolean;
|
|
|
2
2
|
export declare const useMobileMediaQuery: () => boolean;
|
|
3
3
|
/** A phone in landscape: too short for a sheet capped at 90vh to leave a usable body. */
|
|
4
4
|
export declare const useShortViewportMediaQuery: () => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* ASMA-8468 REQ-001: a searchable select opens as a full-screen picker below 744 CSS px wide or at
|
|
7
|
+
* most 480 CSS px high. The height half is the same boundary as {@link useShortViewportMediaQuery}
|
|
8
|
+
* (D-003), so a landscape phone counts as "small" for every overlay.
|
|
9
|
+
*/
|
|
10
|
+
export declare const useFullScreenPickerMediaQuery: () => boolean;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './components/inputs/search-field';
|
|
|
21
21
|
export * from './components/inputs/textarea';
|
|
22
22
|
export * from './components/inputs/select';
|
|
23
23
|
export * from './components/inputs/select-autocomplete';
|
|
24
|
+
export * from './components/inputs/full-screen-picker';
|
|
24
25
|
export * from './components/inputs/slider';
|
|
25
26
|
export * from './components/inputs/switch';
|
|
26
27
|
export * from './components/inputs/radio-button';
|