@dloizides/ui-forms 1.14.0 → 1.15.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/CHANGELOG.md +19 -0
- package/dist/index.d.mts +42 -16
- package/dist/index.d.ts +42 -16
- package/dist/index.js +193 -96
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +193 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.15.0
|
|
4
|
+
|
|
5
|
+
**`FormSwitch` now emits `aria-checked` / `aria-disabled` on web; `ChipSelector` gained an option icon slot and opt-in radio semantics.**
|
|
6
|
+
|
|
7
|
+
`FormSwitch` (A11Y-ARIA-CHECKED-1): react-native-web 0.21 emits nothing from `accessibilityState` on web, so the
|
|
8
|
+
switch wrapper rendered `role="switch"` with no `aria-checked` - an invalid switch that announced no state. The
|
|
9
|
+
state is now passed as literal `aria-checked` (always) and `aria-disabled` (only when disabled) web props;
|
|
10
|
+
`accessibilityState` also carries `disabled` for native.
|
|
11
|
+
|
|
12
|
+
`ChipSelector`:
|
|
13
|
+
- `ChipOption.icon?: React.ReactNode` - rendered before the label inside the chip, wrapped `aria-hidden`. Options
|
|
14
|
+
without an icon render the exact same tree as before.
|
|
15
|
+
- `singleSelectRole?: 'button' | 'radio'` (type `ChipSingleSelectRole`). Default `button` = unchanged. With
|
|
16
|
+
`radio` and `multiple` unset, the group is `role="radiogroup"` and each chip `role="radio"` + `aria-checked`.
|
|
17
|
+
Ignored for multi-select. Trade-off: RN-web renders a radio chip as a `<div>`, which activates on Enter but not
|
|
18
|
+
Space and has no arrow-key roving focus.
|
|
19
|
+
|
|
20
|
+
**Additive and backward-compatible.** No prop set = identical DOM for every existing consumer.
|
|
21
|
+
|
|
3
22
|
## 1.14.0
|
|
4
23
|
|
|
5
24
|
**`FormField` gained an opt-in `recentKey` — remember the last N submitted values and offer them on focus.**
|
package/dist/index.d.mts
CHANGED
|
@@ -525,27 +525,24 @@ declare function useRecentValues(key: string, max?: number): RecentValues;
|
|
|
525
525
|
declare const DEFAULT_MAX_RECENTS = 10;
|
|
526
526
|
|
|
527
527
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* Two visual variants (both fully theme-driven):
|
|
531
|
-
* - `solid` (default) — the original filled-pill look: selected = solid primary fill with
|
|
532
|
-
* white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
|
|
533
|
-
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
534
|
-
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
535
|
-
* Hover (web) lifts the border/ink to the primary colour.
|
|
536
|
-
*
|
|
537
|
-
* The label row, the error line and the container spacing come from the shared `Field` wrapper —
|
|
538
|
-
* NOT from a local `<Text>`. Its own label used to drift from `Field` on three of four axes
|
|
539
|
-
* (`marginBottom: 8` vs 4, no `fontSize` at all so ~14 vs 13, `colors.text` vs `textSecondary`),
|
|
540
|
-
* so a `ChipSelector` next to a `FormField` in a row/grid started a different distance down the
|
|
541
|
-
* column and read as a different weight of text. Now there is exactly one label implementation.
|
|
528
|
+
* Public + internal types for ChipSelector. Split out of `ChipSelector.tsx` so the component, its
|
|
529
|
+
* two chip variants and the radio keyboard hook can share them without a circular import.
|
|
542
530
|
*/
|
|
543
531
|
|
|
544
532
|
/** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
|
|
545
533
|
type ChipVariant = 'solid' | 'outline';
|
|
534
|
+
/**
|
|
535
|
+
* Role of a SINGLE-select chip. `button` (default) = today's semantics; `radio` = opt-in
|
|
536
|
+
* `radiogroup` of `radio` chips carrying `aria-checked`. Ignored when `multiple` is set.
|
|
537
|
+
* A string union rather than a `const enum` to match `ChipVariant`: a const enum does not
|
|
538
|
+
* survive the package boundary for consumers compiled with `isolatedModules`.
|
|
539
|
+
*/
|
|
540
|
+
type ChipSingleSelectRole = 'button' | 'radio';
|
|
546
541
|
interface ChipOption<T> {
|
|
547
542
|
value: T;
|
|
548
543
|
label: string;
|
|
544
|
+
/** Optional decorative icon rendered before the label; hidden from assistive tech. */
|
|
545
|
+
icon?: React.ReactNode;
|
|
549
546
|
}
|
|
550
547
|
interface ChipSelectorProps<T> {
|
|
551
548
|
/** Optional — with no label, no label row is rendered (no phantom gap above the chips). */
|
|
@@ -585,8 +582,37 @@ interface ChipSelectorProps<T> {
|
|
|
585
582
|
* consumers keep their current announcement.
|
|
586
583
|
*/
|
|
587
584
|
optionAccessibilityHint?: string;
|
|
585
|
+
/**
|
|
586
|
+
* Opt-in radio semantics for a SINGLE-select group: the group becomes `role="radiogroup"` and
|
|
587
|
+
* each chip `role="radio"` + `aria-checked`, with the WAI-ARIA radio group keyboard pattern
|
|
588
|
+
* (Space activates, arrow keys move focus + selection with wrap, one roving tab stop).
|
|
589
|
+
* Defaults to `button` (unchanged behaviour). Ignored when `multiple` is set — a multi chip
|
|
590
|
+
* stays a toggle button with `aria-pressed`.
|
|
591
|
+
*/
|
|
592
|
+
singleSelectRole?: ChipSingleSelectRole;
|
|
588
593
|
}
|
|
589
|
-
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Chip/pill selector for selecting one or multiple options.
|
|
597
|
+
*
|
|
598
|
+
* Two visual variants (both fully theme-driven):
|
|
599
|
+
* - `solid` (default) — the original filled-pill look: selected = solid primary fill with
|
|
600
|
+
* white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
|
|
601
|
+
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
602
|
+
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
603
|
+
* Hover (web) lifts the border/ink to the primary colour.
|
|
604
|
+
*
|
|
605
|
+
* The label row, the error line and the container spacing come from the shared `Field` wrapper —
|
|
606
|
+
* NOT from a local `<Text>`. Its own label used to drift from `Field` on three of four axes
|
|
607
|
+
* (`marginBottom: 8` vs 4, no `fontSize` at all so ~14 vs 13, `colors.text` vs `textSecondary`),
|
|
608
|
+
* so a `ChipSelector` next to a `FormField` in a row/grid started a different distance down the
|
|
609
|
+
* column and read as a different weight of text. Now there is exactly one label implementation.
|
|
610
|
+
*
|
|
611
|
+
* Structure: chip variants in `components/`, the radio keyboard wiring in `hooks/`, and the pure
|
|
612
|
+
* resolvers (`chipIdentity`, colours, radio key logic) in `utils/`.
|
|
613
|
+
*/
|
|
614
|
+
|
|
615
|
+
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, spacing, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, singleSelectRole, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
590
616
|
|
|
591
617
|
/**
|
|
592
618
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -944,4 +970,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
|
|
|
944
970
|
/** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
|
|
945
971
|
declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
|
|
946
972
|
|
|
947
|
-
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_MAX_RECENTS, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useRecentValues, useThemedInput };
|
|
973
|
+
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipSingleSelectRole, type ChipVariant, type ControlOption, DEFAULT_MAX_RECENTS, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useRecentValues, useThemedInput };
|
package/dist/index.d.ts
CHANGED
|
@@ -525,27 +525,24 @@ declare function useRecentValues(key: string, max?: number): RecentValues;
|
|
|
525
525
|
declare const DEFAULT_MAX_RECENTS = 10;
|
|
526
526
|
|
|
527
527
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* Two visual variants (both fully theme-driven):
|
|
531
|
-
* - `solid` (default) — the original filled-pill look: selected = solid primary fill with
|
|
532
|
-
* white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
|
|
533
|
-
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
534
|
-
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
535
|
-
* Hover (web) lifts the border/ink to the primary colour.
|
|
536
|
-
*
|
|
537
|
-
* The label row, the error line and the container spacing come from the shared `Field` wrapper —
|
|
538
|
-
* NOT from a local `<Text>`. Its own label used to drift from `Field` on three of four axes
|
|
539
|
-
* (`marginBottom: 8` vs 4, no `fontSize` at all so ~14 vs 13, `colors.text` vs `textSecondary`),
|
|
540
|
-
* so a `ChipSelector` next to a `FormField` in a row/grid started a different distance down the
|
|
541
|
-
* column and read as a different weight of text. Now there is exactly one label implementation.
|
|
528
|
+
* Public + internal types for ChipSelector. Split out of `ChipSelector.tsx` so the component, its
|
|
529
|
+
* two chip variants and the radio keyboard hook can share them without a circular import.
|
|
542
530
|
*/
|
|
543
531
|
|
|
544
532
|
/** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
|
|
545
533
|
type ChipVariant = 'solid' | 'outline';
|
|
534
|
+
/**
|
|
535
|
+
* Role of a SINGLE-select chip. `button` (default) = today's semantics; `radio` = opt-in
|
|
536
|
+
* `radiogroup` of `radio` chips carrying `aria-checked`. Ignored when `multiple` is set.
|
|
537
|
+
* A string union rather than a `const enum` to match `ChipVariant`: a const enum does not
|
|
538
|
+
* survive the package boundary for consumers compiled with `isolatedModules`.
|
|
539
|
+
*/
|
|
540
|
+
type ChipSingleSelectRole = 'button' | 'radio';
|
|
546
541
|
interface ChipOption<T> {
|
|
547
542
|
value: T;
|
|
548
543
|
label: string;
|
|
544
|
+
/** Optional decorative icon rendered before the label; hidden from assistive tech. */
|
|
545
|
+
icon?: React.ReactNode;
|
|
549
546
|
}
|
|
550
547
|
interface ChipSelectorProps<T> {
|
|
551
548
|
/** Optional — with no label, no label row is rendered (no phantom gap above the chips). */
|
|
@@ -585,8 +582,37 @@ interface ChipSelectorProps<T> {
|
|
|
585
582
|
* consumers keep their current announcement.
|
|
586
583
|
*/
|
|
587
584
|
optionAccessibilityHint?: string;
|
|
585
|
+
/**
|
|
586
|
+
* Opt-in radio semantics for a SINGLE-select group: the group becomes `role="radiogroup"` and
|
|
587
|
+
* each chip `role="radio"` + `aria-checked`, with the WAI-ARIA radio group keyboard pattern
|
|
588
|
+
* (Space activates, arrow keys move focus + selection with wrap, one roving tab stop).
|
|
589
|
+
* Defaults to `button` (unchanged behaviour). Ignored when `multiple` is set — a multi chip
|
|
590
|
+
* stays a toggle button with `aria-pressed`.
|
|
591
|
+
*/
|
|
592
|
+
singleSelectRole?: ChipSingleSelectRole;
|
|
588
593
|
}
|
|
589
|
-
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Chip/pill selector for selecting one or multiple options.
|
|
597
|
+
*
|
|
598
|
+
* Two visual variants (both fully theme-driven):
|
|
599
|
+
* - `solid` (default) — the original filled-pill look: selected = solid primary fill with
|
|
600
|
+
* white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
|
|
601
|
+
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
602
|
+
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
603
|
+
* Hover (web) lifts the border/ink to the primary colour.
|
|
604
|
+
*
|
|
605
|
+
* The label row, the error line and the container spacing come from the shared `Field` wrapper —
|
|
606
|
+
* NOT from a local `<Text>`. Its own label used to drift from `Field` on three of four axes
|
|
607
|
+
* (`marginBottom: 8` vs 4, no `fontSize` at all so ~14 vs 13, `colors.text` vs `textSecondary`),
|
|
608
|
+
* so a `ChipSelector` next to a `FormField` in a row/grid started a different distance down the
|
|
609
|
+
* column and read as a different weight of text. Now there is exactly one label implementation.
|
|
610
|
+
*
|
|
611
|
+
* Structure: chip variants in `components/`, the radio keyboard wiring in `hooks/`, and the pure
|
|
612
|
+
* resolvers (`chipIdentity`, colours, radio key logic) in `utils/`.
|
|
613
|
+
*/
|
|
614
|
+
|
|
615
|
+
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, spacing, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, singleSelectRole, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
590
616
|
|
|
591
617
|
/**
|
|
592
618
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -944,4 +970,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
|
|
|
944
970
|
/** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
|
|
945
971
|
declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
|
|
946
972
|
|
|
947
|
-
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_MAX_RECENTS, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useRecentValues, useThemedInput };
|
|
973
|
+
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipSingleSelectRole, type ChipVariant, type ControlOption, DEFAULT_MAX_RECENTS, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, type RecentValues, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useRecentValues, useThemedInput };
|
package/dist/index.js
CHANGED
|
@@ -952,56 +952,19 @@ var FormField = ({
|
|
|
952
952
|
}
|
|
953
953
|
}
|
|
954
954
|
);
|
|
955
|
+
|
|
956
|
+
// src/ChipSelector/constants.ts
|
|
955
957
|
var TRANSPARENT_COLOR = "transparent";
|
|
956
|
-
var
|
|
958
|
+
var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
|
|
959
|
+
var CHIP_GUTTER = 8;
|
|
960
|
+
var DEFAULT_CHIP_TESTID_PREFIX = "chip-selector-chip";
|
|
957
961
|
var SOLID_RADIUS = 20;
|
|
958
962
|
var OUTLINE_RADIUS = 999;
|
|
959
963
|
var OUTLINE_PAD_H = 12;
|
|
960
964
|
var OUTLINE_PAD_V = 5;
|
|
961
965
|
var OUTLINE_FONT_SIZE = 13;
|
|
962
|
-
var
|
|
963
|
-
var
|
|
964
|
-
var HEX_RADIX2 = 16;
|
|
965
|
-
var RGB_R_START = 0;
|
|
966
|
-
var RGB_R_END = 2;
|
|
967
|
-
var RGB_G_END = 4;
|
|
968
|
-
var RGB_B_END = 6;
|
|
969
|
-
var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
|
|
970
|
-
var CHIP_GUTTER = 8;
|
|
971
|
-
var DEFAULT_CHIP_TESTID_PREFIX = "chip-selector-chip";
|
|
972
|
-
var ON_BRAND_SCALE = "onBrand";
|
|
973
|
-
var MAIN_STEP = "500";
|
|
974
|
-
function onBrandInk(palette) {
|
|
975
|
-
const scales = palette;
|
|
976
|
-
return scales[ON_BRAND_SCALE]?.[MAIN_STEP] ?? WHITE_COLOR;
|
|
977
|
-
}
|
|
978
|
-
function withAlpha2(hex, alpha) {
|
|
979
|
-
const normalized = hex.replace("#", "");
|
|
980
|
-
const isShort = normalized.length === HEX_SHORT_LENGTH2;
|
|
981
|
-
const full = isShort ? normalized.split("").map((c) => c + c).join("") : normalized;
|
|
982
|
-
const r = parseInt(full.slice(RGB_R_START, RGB_R_END), HEX_RADIX2);
|
|
983
|
-
const g = parseInt(full.slice(RGB_R_END, RGB_G_END), HEX_RADIX2);
|
|
984
|
-
const b = parseInt(full.slice(RGB_G_END, RGB_B_END), HEX_RADIX2);
|
|
985
|
-
const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
|
|
986
|
-
return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
|
|
987
|
-
}
|
|
988
|
-
var styles5 = reactNative.StyleSheet.create({
|
|
989
|
-
chipContainer: {
|
|
990
|
-
flexDirection: "row",
|
|
991
|
-
flexWrap: "wrap",
|
|
992
|
-
// `gap`, NOT per-chip margins plus a compensating negative container margin.
|
|
993
|
-
//
|
|
994
|
-
// The old form was `chipWrapper { marginRight: 8, marginBottom: 8 }` with
|
|
995
|
-
// `chipContainer { marginBottom: -8 }`. Its comment blamed `Field`'s 16px container margin —
|
|
996
|
-
// that was WRONG, and worth recording: F3 was briefed on the belief that the -8 compensated for
|
|
997
|
-
// `Field` and had to move in lockstep with it. It did not. -8 cancelled the CHIP's own gutter
|
|
998
|
-
// hanging off the last row, identically whether `Field` contributed 16, 0, or anything else.
|
|
999
|
-
//
|
|
1000
|
-
// The pair is still the wrong tool: it holds only while the two constants agree, and it leaves
|
|
1001
|
-
// a real negative margin any measuring parent inherits. `gap` produces the byte-identical box
|
|
1002
|
-
// and is correct under BOTH of `Field`'s spacing models, not just the one it was tuned against.
|
|
1003
|
-
gap: CHIP_GUTTER
|
|
1004
|
-
},
|
|
966
|
+
var CHIP_ICON_GAP = 6;
|
|
967
|
+
var chipStyles = reactNative.StyleSheet.create({
|
|
1005
968
|
chip: {
|
|
1006
969
|
paddingHorizontal: 12,
|
|
1007
970
|
paddingVertical: 6,
|
|
@@ -1015,28 +978,21 @@ var styles5 = reactNative.StyleSheet.create({
|
|
|
1015
978
|
borderRadius: OUTLINE_RADIUS,
|
|
1016
979
|
borderWidth: 1
|
|
1017
980
|
},
|
|
981
|
+
chipContent: { flexDirection: "row", alignItems: "center", gap: CHIP_ICON_GAP },
|
|
1018
982
|
chipText: {},
|
|
1019
983
|
chipTextOutline: { fontSize: OUTLINE_FONT_SIZE }
|
|
1020
984
|
});
|
|
1021
|
-
function
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
};
|
|
985
|
+
function ChipContent({ icon, label, textStyle }) {
|
|
986
|
+
const text = /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: textStyle, children: label });
|
|
987
|
+
if (icon === void 0 || icon === null)
|
|
988
|
+
return text;
|
|
989
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: chipStyles.chipContent, children: [
|
|
990
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { "aria-hidden": true, children: icon }),
|
|
991
|
+
text
|
|
992
|
+
] });
|
|
1029
993
|
}
|
|
1030
|
-
function OutlineChip({
|
|
1031
|
-
option,
|
|
1032
|
-
selected,
|
|
1033
|
-
disabled,
|
|
1034
|
-
colors,
|
|
1035
|
-
testID,
|
|
1036
|
-
accessibilityHint,
|
|
1037
|
-
ariaPressed,
|
|
1038
|
-
onPress
|
|
1039
|
-
}) {
|
|
994
|
+
function OutlineChip(props) {
|
|
995
|
+
const { option, selected, disabled, colors, testID, accessibilityHint, onPress } = props;
|
|
1040
996
|
const [hovered, setHovered] = React2__default.default.useState(false);
|
|
1041
997
|
const active = selected || hovered;
|
|
1042
998
|
const borderColor = active ? colors.primary : colors.border;
|
|
@@ -1045,49 +1001,179 @@ function OutlineChip({
|
|
|
1045
1001
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1046
1002
|
reactNative.Pressable,
|
|
1047
1003
|
{
|
|
1004
|
+
ref: props.hostRef,
|
|
1048
1005
|
accessibilityHint,
|
|
1049
1006
|
accessibilityLabel: option.label,
|
|
1050
|
-
accessibilityRole:
|
|
1007
|
+
accessibilityRole: props.accessibilityRole,
|
|
1051
1008
|
accessibilityState: { selected, disabled },
|
|
1052
|
-
"aria-
|
|
1009
|
+
"aria-checked": props.ariaChecked,
|
|
1010
|
+
"aria-pressed": props.ariaPressed,
|
|
1053
1011
|
disabled,
|
|
1054
1012
|
hitSlop: CHIP_HIT_SLOP,
|
|
1013
|
+
tabIndex: props.tabIndex,
|
|
1055
1014
|
testID,
|
|
1056
1015
|
onHoverIn: () => setHovered(true),
|
|
1057
1016
|
onHoverOut: () => setHovered(false),
|
|
1058
1017
|
onPress,
|
|
1059
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [
|
|
1018
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [chipStyles.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1019
|
+
ChipContent,
|
|
1020
|
+
{
|
|
1021
|
+
icon: option.icon,
|
|
1022
|
+
label: option.label,
|
|
1023
|
+
textStyle: [chipStyles.chipText, chipStyles.chipTextOutline, { color: textColor }]
|
|
1024
|
+
}
|
|
1025
|
+
) })
|
|
1060
1026
|
}
|
|
1061
1027
|
);
|
|
1062
1028
|
}
|
|
1063
|
-
function SolidChip({
|
|
1064
|
-
option,
|
|
1065
|
-
selected,
|
|
1066
|
-
disabled,
|
|
1067
|
-
colors,
|
|
1068
|
-
testID,
|
|
1069
|
-
accessibilityHint,
|
|
1070
|
-
ariaPressed,
|
|
1071
|
-
onPress
|
|
1072
|
-
}) {
|
|
1029
|
+
function SolidChip(props) {
|
|
1030
|
+
const { option, selected, disabled, colors, testID, accessibilityHint, onPress } = props;
|
|
1073
1031
|
const backgroundColor = selected ? colors.primary : TRANSPARENT_COLOR;
|
|
1074
1032
|
const textColor = selected ? colors.onPrimary : colors.textStrong;
|
|
1033
|
+
const rovingTabStop = { tabIndex: props.tabIndex };
|
|
1075
1034
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1076
1035
|
reactNative.TouchableOpacity,
|
|
1077
1036
|
{
|
|
1037
|
+
ref: props.hostRef,
|
|
1078
1038
|
accessibilityHint,
|
|
1079
1039
|
accessibilityLabel: option.label,
|
|
1080
|
-
accessibilityRole:
|
|
1040
|
+
accessibilityRole: props.accessibilityRole,
|
|
1081
1041
|
accessibilityState: { selected, disabled },
|
|
1082
|
-
"aria-
|
|
1042
|
+
"aria-checked": props.ariaChecked,
|
|
1043
|
+
"aria-pressed": props.ariaPressed,
|
|
1083
1044
|
disabled,
|
|
1084
1045
|
hitSlop: CHIP_HIT_SLOP,
|
|
1085
1046
|
testID,
|
|
1086
1047
|
onPress,
|
|
1087
|
-
|
|
1048
|
+
...rovingTabStop,
|
|
1049
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [chipStyles.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(ChipContent, { icon: option.icon, label: option.label, textStyle: [chipStyles.chipText, { color: textColor }] }) })
|
|
1088
1050
|
}
|
|
1089
1051
|
);
|
|
1090
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
// src/ChipSelector/utils/radioKeyboard.ts
|
|
1055
|
+
var SPACE_KEYS = [" ", "Spacebar"];
|
|
1056
|
+
var NEXT_KEYS = ["ArrowRight", "ArrowDown"];
|
|
1057
|
+
var PREVIOUS_KEYS = ["ArrowLeft", "ArrowUp"];
|
|
1058
|
+
var ROVING_TAB_STOP = 0;
|
|
1059
|
+
var ROVING_SKIPPED = -1;
|
|
1060
|
+
function radioKeyTarget(key, index, count) {
|
|
1061
|
+
const isOutOfRange = index < 0 || index >= count;
|
|
1062
|
+
if (isOutOfRange)
|
|
1063
|
+
return null;
|
|
1064
|
+
if (SPACE_KEYS.includes(key))
|
|
1065
|
+
return index;
|
|
1066
|
+
const isArrow = NEXT_KEYS.includes(key) || PREVIOUS_KEYS.includes(key);
|
|
1067
|
+
if (!isArrow || count === 1)
|
|
1068
|
+
return null;
|
|
1069
|
+
const step = NEXT_KEYS.includes(key) ? 1 : -1;
|
|
1070
|
+
return (index + step + count) % count;
|
|
1071
|
+
}
|
|
1072
|
+
function radioTabIndex(index, selectedIndex, disabled) {
|
|
1073
|
+
if (disabled)
|
|
1074
|
+
return ROVING_SKIPPED;
|
|
1075
|
+
const tabStop = selectedIndex >= 0 ? selectedIndex : 0;
|
|
1076
|
+
return index === tabStop ? ROVING_TAB_STOP : ROVING_SKIPPED;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/ChipSelector/hooks/useRadioKeyboard.ts
|
|
1080
|
+
var NO_GROUP_PROPS = {};
|
|
1081
|
+
function useRadioKeyboard(config) {
|
|
1082
|
+
const { enabled, disabled, values, selectedIndex, onChange } = config;
|
|
1083
|
+
const nodes = React2__default.default.useRef([]);
|
|
1084
|
+
const chipRef = React2__default.default.useCallback((index) => (node) => {
|
|
1085
|
+
nodes.current[index] = node;
|
|
1086
|
+
}, []);
|
|
1087
|
+
const onKeyDown = (event) => {
|
|
1088
|
+
if (disabled)
|
|
1089
|
+
return;
|
|
1090
|
+
const index = nodes.current.indexOf(event.target);
|
|
1091
|
+
const target = radioKeyTarget(event.key, index, values.length);
|
|
1092
|
+
const nextValue = target === null ? void 0 : values[target];
|
|
1093
|
+
if (target === null || nextValue === void 0)
|
|
1094
|
+
return;
|
|
1095
|
+
event.preventDefault();
|
|
1096
|
+
if (target !== index)
|
|
1097
|
+
nodes.current[target]?.focus?.();
|
|
1098
|
+
onChange(nextValue);
|
|
1099
|
+
};
|
|
1100
|
+
return {
|
|
1101
|
+
chipRef,
|
|
1102
|
+
tabIndexFor: (index) => enabled ? radioTabIndex(index, selectedIndex, disabled) : void 0,
|
|
1103
|
+
// `onKeyDown` is a web-only View prop RN's types do not declare; RNW forwards it to the DOM.
|
|
1104
|
+
groupProps: enabled ? { onKeyDown } : NO_GROUP_PROPS
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/ChipSelector/utils/chipColors.ts
|
|
1109
|
+
var WHITE_COLOR = "#fff";
|
|
1110
|
+
var OUTLINE_TINT_ALPHA = 0.1;
|
|
1111
|
+
var HEX_SHORT_LENGTH2 = 3;
|
|
1112
|
+
var HEX_RADIX2 = 16;
|
|
1113
|
+
var RGB_R_START = 0;
|
|
1114
|
+
var RGB_R_END = 2;
|
|
1115
|
+
var RGB_G_END = 4;
|
|
1116
|
+
var RGB_B_END = 6;
|
|
1117
|
+
var ON_BRAND_SCALE = "onBrand";
|
|
1118
|
+
var MAIN_STEP = "500";
|
|
1119
|
+
function onBrandInk(palette) {
|
|
1120
|
+
const scales = palette;
|
|
1121
|
+
return scales[ON_BRAND_SCALE]?.[MAIN_STEP] ?? WHITE_COLOR;
|
|
1122
|
+
}
|
|
1123
|
+
function withAlpha2(hex, alpha) {
|
|
1124
|
+
const normalized = hex.replace("#", "");
|
|
1125
|
+
const isShort = normalized.length === HEX_SHORT_LENGTH2;
|
|
1126
|
+
const full = isShort ? normalized.split("").map((c) => c + c).join("") : normalized;
|
|
1127
|
+
const r = parseInt(full.slice(RGB_R_START, RGB_R_END), HEX_RADIX2);
|
|
1128
|
+
const g = parseInt(full.slice(RGB_R_END, RGB_G_END), HEX_RADIX2);
|
|
1129
|
+
const b = parseInt(full.slice(RGB_G_END, RGB_B_END), HEX_RADIX2);
|
|
1130
|
+
const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
|
|
1131
|
+
return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
|
|
1132
|
+
}
|
|
1133
|
+
function buildChipColors(colors, palette) {
|
|
1134
|
+
const primary = palette.primary[500];
|
|
1135
|
+
return {
|
|
1136
|
+
border: colors.border,
|
|
1137
|
+
textStrong: colors.text,
|
|
1138
|
+
textMuted: colors.textSecondary,
|
|
1139
|
+
primary,
|
|
1140
|
+
onPrimary: onBrandInk(palette),
|
|
1141
|
+
muted: colors.background,
|
|
1142
|
+
tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/ChipSelector/utils/chipIdentity.ts
|
|
1147
|
+
function chipIdentity(option, selected, config) {
|
|
1148
|
+
const isRadio = config.radio === true && !config.multiple;
|
|
1149
|
+
return {
|
|
1150
|
+
testID: `${config.prefix}-${String(option.value)}`,
|
|
1151
|
+
// The English default is retained verbatim: erevna/katalogos assert the exact string
|
|
1152
|
+
// `Selects Red`. Consumers that localize pass `optionAccessibilityHint`.
|
|
1153
|
+
accessibilityHint: config.hint ?? `Selects ${option.label}`,
|
|
1154
|
+
ariaPressed: config.multiple ? selected : void 0,
|
|
1155
|
+
accessibilityRole: isRadio ? "radio" : "button",
|
|
1156
|
+
ariaChecked: isRadio ? selected : void 0
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
var styles5 = reactNative.StyleSheet.create({
|
|
1160
|
+
chipContainer: {
|
|
1161
|
+
flexDirection: "row",
|
|
1162
|
+
flexWrap: "wrap",
|
|
1163
|
+
// `gap`, NOT per-chip margins plus a compensating negative container margin.
|
|
1164
|
+
//
|
|
1165
|
+
// The old form was `chipWrapper { marginRight: 8, marginBottom: 8 }` with
|
|
1166
|
+
// `chipContainer { marginBottom: -8 }`. Its comment blamed `Field`'s 16px container margin —
|
|
1167
|
+
// that was WRONG, and worth recording: F3 was briefed on the belief that the -8 compensated for
|
|
1168
|
+
// `Field` and had to move in lockstep with it. It did not. -8 cancelled the CHIP's own gutter
|
|
1169
|
+
// hanging off the last row, identically whether `Field` contributed 16, 0, or anything else.
|
|
1170
|
+
//
|
|
1171
|
+
// The pair is still the wrong tool: it holds only while the two constants agree, and it leaves
|
|
1172
|
+
// a real negative margin any measuring parent inherits. `gap` produces the byte-identical box
|
|
1173
|
+
// and is correct under BOTH of `Field`'s spacing models, not just the one it was tuned against.
|
|
1174
|
+
gap: CHIP_GUTTER
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1091
1177
|
var ChipSelector = ({
|
|
1092
1178
|
label,
|
|
1093
1179
|
options,
|
|
@@ -1102,28 +1188,26 @@ var ChipSelector = ({
|
|
|
1102
1188
|
error,
|
|
1103
1189
|
testID,
|
|
1104
1190
|
chipTestIDPrefix = DEFAULT_CHIP_TESTID_PREFIX,
|
|
1105
|
-
optionAccessibilityHint
|
|
1191
|
+
optionAccessibilityHint,
|
|
1192
|
+
singleSelectRole = "button"
|
|
1106
1193
|
}) => {
|
|
1194
|
+
const isRadioGroup = !multiple && singleSelectRole === "radio";
|
|
1107
1195
|
const { theme } = uiFeedback.useUi();
|
|
1108
1196
|
const { colors, palette } = theme;
|
|
1109
|
-
const chipColors = React2__default.default.useMemo(() =>
|
|
1110
|
-
const primary = palette.primary["500"];
|
|
1111
|
-
return {
|
|
1112
|
-
border: colors.border,
|
|
1113
|
-
textStrong: colors.text,
|
|
1114
|
-
textMuted: colors.textSecondary,
|
|
1115
|
-
primary,
|
|
1116
|
-
onPrimary: onBrandInk(palette),
|
|
1117
|
-
muted: colors.background,
|
|
1118
|
-
tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
|
|
1119
|
-
};
|
|
1120
|
-
}, [colors.border, colors.text, colors.textSecondary, colors.background, palette]);
|
|
1197
|
+
const chipColors = React2__default.default.useMemo(() => buildChipColors(colors, palette), [colors, palette]);
|
|
1121
1198
|
function isSelected(optionValue) {
|
|
1122
1199
|
if (multiple && Array.isArray(value))
|
|
1123
1200
|
return value.includes(optionValue);
|
|
1124
1201
|
return value === optionValue;
|
|
1125
1202
|
}
|
|
1126
|
-
|
|
1203
|
+
const radio = useRadioKeyboard({
|
|
1204
|
+
enabled: isRadioGroup,
|
|
1205
|
+
disabled,
|
|
1206
|
+
values: options.map((option) => option.value),
|
|
1207
|
+
selectedIndex: options.findIndex((option) => isSelected(option.value)),
|
|
1208
|
+
onChange
|
|
1209
|
+
});
|
|
1210
|
+
function renderChip(option, index) {
|
|
1127
1211
|
const selected = isSelected(option.value);
|
|
1128
1212
|
const chipProps = {
|
|
1129
1213
|
colors: chipColors,
|
|
@@ -1133,8 +1217,13 @@ var ChipSelector = ({
|
|
|
1133
1217
|
...chipIdentity(option, selected, {
|
|
1134
1218
|
prefix: chipTestIDPrefix,
|
|
1135
1219
|
hint: optionAccessibilityHint,
|
|
1136
|
-
multiple
|
|
1220
|
+
multiple,
|
|
1221
|
+
radio: isRadioGroup
|
|
1137
1222
|
}),
|
|
1223
|
+
// Radio mode only: roving tab stop + host node for arrow-key focus moves. Both stay
|
|
1224
|
+
// `undefined` otherwise, so button / multi chips keep their existing tab order.
|
|
1225
|
+
tabIndex: radio.tabIndexFor(index),
|
|
1226
|
+
hostRef: isRadioGroup ? radio.chipRef(index) : void 0,
|
|
1138
1227
|
onPress: () => onChange(option.value)
|
|
1139
1228
|
};
|
|
1140
1229
|
const key = String(option.value);
|
|
@@ -1149,7 +1238,16 @@ var ChipSelector = ({
|
|
|
1149
1238
|
required,
|
|
1150
1239
|
spacing,
|
|
1151
1240
|
testID,
|
|
1152
|
-
children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1241
|
+
children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1242
|
+
reactNative.View,
|
|
1243
|
+
{
|
|
1244
|
+
accessibilityRole: isRadioGroup ? "radiogroup" : void 0,
|
|
1245
|
+
style: styles5.chipContainer,
|
|
1246
|
+
...webFieldA11y({ describedById, hasError }),
|
|
1247
|
+
...radio.groupProps,
|
|
1248
|
+
children: options.map(renderChip)
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
1153
1251
|
}
|
|
1154
1252
|
);
|
|
1155
1253
|
};
|
|
@@ -1207,8 +1305,7 @@ var FormSwitch = ({
|
|
|
1207
1305
|
{
|
|
1208
1306
|
accessibilityHint: accessibilityHint ?? label,
|
|
1209
1307
|
accessibilityLabel: label,
|
|
1210
|
-
|
|
1211
|
-
accessibilityState: { checked: value },
|
|
1308
|
+
accessibilityState: { checked: value, disabled },
|
|
1212
1309
|
disabled,
|
|
1213
1310
|
testID,
|
|
1214
1311
|
value,
|