@linzjs/lui 24.2.0 → 24.3.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 CHANGED
@@ -5,6 +5,13 @@
5
5
 
6
6
  * **LuiComboSelect:** restore interface to support styling of the input element. ([#1270](https://github.com/linz/lui/issues/1270)) ([7ce6a13](https://github.com/linz/lui/commit/7ce6a137c7f16ffa3acbe4357569a6eb59ef47db))
7
7
 
8
+ ## [24.3.0](https://github.com/linz/Lui/compare/lui-v24.2.0...lui-v24.3.0) (2025-11-20)
9
+
10
+
11
+ ### Features
12
+
13
+ * CS-8474 - filter characters in LuiInput ([#1272](https://github.com/linz/Lui/issues/1272)) ([2b58207](https://github.com/linz/Lui/commit/2b58207f2844eb769d9c274f55a5b1b8e2336dcf))
14
+
8
15
  ## [24.2.0](https://github.com/linz/Lui/compare/lui-v24.1.2...lui-v24.2.0) (2025-11-19)
9
16
 
10
17
 
package/README.md CHANGED
@@ -50,4 +50,4 @@ There are lots of things to do in this project, often things will be left until
50
50
 
51
51
  Storybook is the main source of documentation.
52
52
 
53
- - [Lui Storybook](https://linz.github.io/Lui/)
53
+ - [Lui Storybook](https://linz.github.io/Lui/)
@@ -0,0 +1,11 @@
1
+ import React, { ChangeEvent, PropsWithChildren } from 'react';
2
+ export interface LuiFilterCharactersContext {
3
+ filterCharacters: (input: string | undefined) => string | undefined;
4
+ filterInputEvent: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
5
+ }
6
+ export declare const LuiFilterCharactersContext: React.Context<LuiFilterCharactersContext>;
7
+ export declare function useFilterCharacters(): LuiFilterCharactersContext;
8
+ export type LuiFilterCharactersProviderProps = {
9
+ filterFunction?: (input: string) => string;
10
+ };
11
+ export declare const LuiFilterCharactersProvider: ({ children, filterFunction, }: PropsWithChildren<LuiFilterCharactersProviderProps>) => React.JSX.Element;
@@ -0,0 +1,14 @@
1
+ export type LinzFreeTextFilterType = (value?: string | null) => string;
2
+ /**
3
+ * Default LINZ free text filter function.
4
+ * Silently converts some invalid characters with supported alternatives.
5
+ * Removes all other invalid characters.
6
+ */
7
+ export declare const LinzFreeTextFilter: LinzFreeTextFilterType;
8
+ /**
9
+ * LINZ free text filter function that leaves invalid characters intact.
10
+ * Silently converts some invalid characters with supported alternatives.
11
+ * Leaves all other invalid characters as is.
12
+ * Should be paired with LinzFreeTextValidator to warn users about invalid characters.
13
+ */
14
+ export declare const LinzFreeTextFilterConvertOnly: LinzFreeTextFilterType;
@@ -0,0 +1,2 @@
1
+ export declare const LinzInvalidCharMatcher: RegExp;
2
+ export declare const LinzFreeTextValidator: (value?: string | null, invalidCharMatcher?: RegExp, maxReportedChars?: number) => string | undefined;
package/dist/index.d.ts CHANGED
@@ -85,3 +85,6 @@ export { LuiErrorPanel } from './components/LuiErrorPanel/LuiErrorPanel';
85
85
  export { DrawerGlobalProvider, useDrawerGlobalContext, DrawerGlobalContext, } from './components/Drawer/DrawerGlobalContext';
86
86
  export { Drawer, DrawerProps } from './components/Drawer/Drawer';
87
87
  export { DrawerGlobal, DrawerGlobalProps, } from './components/Drawer/DrawerGlobal';
88
+ export { LuiFilterCharactersProvider, useFilterCharacters, } from './contexts/LuiFilterCharactersProvider';
89
+ export { LinzFreeTextValidator } from './functions/LinzFreeTextValidator';
90
+ export { LinzFreeTextFilter, LinzFreeTextFilterConvertOnly, } from './functions/LinzFreeTextFilter';
package/dist/index.js CHANGED
@@ -7018,6 +7018,113 @@ var DateInput = React.forwardRef(function (props, ref) {
7018
7018
  } })));
7019
7019
  });
7020
7020
 
7021
+ // eslint-disable-next-line no-control-regex
7022
+ var LinzInvalidCharMatcher = /([^\u0000-\u017f])/gu;
7023
+ var LinzFreeTextValidator = function (value, invalidCharMatcher, maxReportedChars) {
7024
+ if (invalidCharMatcher === void 0) { invalidCharMatcher = LinzInvalidCharMatcher; }
7025
+ if (maxReportedChars === void 0) { maxReportedChars = 10; }
7026
+ if (!value) {
7027
+ return undefined;
7028
+ }
7029
+ var matchedItems = value.match(invalidCharMatcher);
7030
+ if (matchedItems === null) {
7031
+ return undefined;
7032
+ }
7033
+ var invalidCharacters = Array.from(new Set(matchedItems)).slice(0, maxReportedChars);
7034
+ return "Invalid character".concat(invalidCharacters.length === 1 ? '' : 's', " ").concat(invalidCharacters.join(', '));
7035
+ };
7036
+
7037
+ function CreateLinzFreeTextFilter(removeInvalidChars) {
7038
+ if (removeInvalidChars === void 0) { removeInvalidChars = true; }
7039
+ return function (value) {
7040
+ if (!value) {
7041
+ return '';
7042
+ }
7043
+ var strLength = value.length;
7044
+ if (!strLength) {
7045
+ return '';
7046
+ }
7047
+ return Array.prototype.flatMap
7048
+ .call(value, function (char) {
7049
+ // Supported character range
7050
+ if (char.match(LinzInvalidCharMatcher) === null) {
7051
+ return char;
7052
+ }
7053
+ var charCode = char.charCodeAt(0);
7054
+ // Silent conversion characters
7055
+ // why: https://toitutewhenua.atlassian.net/wiki/spaces/STEP/pages/124322198/Minimum+Text+Validation#Silent-conversion-characters
7056
+ switch (charCode) {
7057
+ case 0x2018:
7058
+ case 0x2019:
7059
+ return "'";
7060
+ case 0x201c:
7061
+ case 0x201d:
7062
+ return '"';
7063
+ case 0x2022:
7064
+ return '*';
7065
+ case 0x2013:
7066
+ case 0x2014:
7067
+ return '-';
7068
+ }
7069
+ return removeInvalidChars ? '' : char;
7070
+ })
7071
+ .join('');
7072
+ };
7073
+ }
7074
+ /**
7075
+ * Default LINZ free text filter function.
7076
+ * Silently converts some invalid characters with supported alternatives.
7077
+ * Removes all other invalid characters.
7078
+ */
7079
+ var LinzFreeTextFilter = CreateLinzFreeTextFilter(true);
7080
+ /**
7081
+ * LINZ free text filter function that leaves invalid characters intact.
7082
+ * Silently converts some invalid characters with supported alternatives.
7083
+ * Leaves all other invalid characters as is.
7084
+ * Should be paired with LinzFreeTextValidator to warn users about invalid characters.
7085
+ */
7086
+ var LinzFreeTextFilterConvertOnly = CreateLinzFreeTextFilter(false);
7087
+
7088
+ var LuiFilterCharactersContext = React.createContext({
7089
+ filterCharacters: function (input) {
7090
+ // Default implementation: return input as is.
7091
+ return input;
7092
+ },
7093
+ filterInputEvent: function () {
7094
+ // Default implementation: do nothing.
7095
+ return;
7096
+ }
7097
+ });
7098
+ function useFilterCharacters() {
7099
+ return React.useContext(LuiFilterCharactersContext);
7100
+ }
7101
+ var LuiFilterCharactersProvider = function (_a) {
7102
+ var children = _a.children, filterFunction = _a.filterFunction;
7103
+ var filterCharacters = React.useCallback(function (input) {
7104
+ if (input === undefined) {
7105
+ return input;
7106
+ }
7107
+ var filter = typeof filterFunction === 'function'
7108
+ ? filterFunction
7109
+ : LinzFreeTextFilter;
7110
+ return filter(input);
7111
+ }, [filterFunction]);
7112
+ var filterInputEvent = React.useCallback(function (event) {
7113
+ var value = event.target.value;
7114
+ var filteredValue = filterCharacters(value);
7115
+ if (filteredValue !== value) {
7116
+ var caretPosition = event.target.selectionStart;
7117
+ event.target.value = filteredValue;
7118
+ if (caretPosition !== null) {
7119
+ var positionOffset = value.length - filteredValue.length;
7120
+ var newCaretPosition = Math.max(caretPosition - positionOffset, 0);
7121
+ event.target.setSelectionRange(newCaretPosition, newCaretPosition);
7122
+ }
7123
+ }
7124
+ }, [filterFunction]);
7125
+ return (React__default["default"].createElement(LuiFilterCharactersContext.Provider, { value: { filterCharacters: filterCharacters, filterInputEvent: filterInputEvent } }, children));
7126
+ };
7127
+
7021
7128
  function useGenerateOrDefaultId(idFromProps) {
7022
7129
  var id = React.useState(idFromProps ? idFromProps : v4())[0];
7023
7130
  return id;
@@ -7026,6 +7133,7 @@ var BareInput = React.forwardRef(function (props, ref) { return React__default["
7026
7133
  var LuiTextInput = React.forwardRef(function (props, ref) {
7027
7134
  var _a, _b, _c;
7028
7135
  var id = useGenerateOrDefaultId((_a = props.inputProps) === null || _a === void 0 ? void 0 : _a.id);
7136
+ var filterInputEvent = useFilterCharacters().filterInputEvent;
7029
7137
  var LuiInput = ((_b = props.inputProps) === null || _b === void 0 ? void 0 : _b.type) === 'date' ? DateInput : BareInput;
7030
7138
  return (React__default["default"].createElement("div", { className: clsx$1('LuiTextInput', props.error && 'hasError', props.warning && 'hasWarning', props.className) },
7031
7139
  React__default["default"].createElement("label", { className: 'LuiTextInput-label', htmlFor: id },
@@ -7033,7 +7141,15 @@ var LuiTextInput = React.forwardRef(function (props, ref) {
7033
7141
  React__default["default"].createElement("span", { className: 'LuiTextInput-label-text ' +
7034
7142
  clsx$1(props.hideLabel ? 'LuiScreenReadersOnly' : '') }, props.label),
7035
7143
  React__default["default"].createElement("span", { className: "LuiTextInput-inputWrapper" },
7036
- React__default["default"].createElement(LuiInput, __assign({ ref: ref, className: clsx$1('LuiTextInput-input', props.showPadlockIcon ? 'LuiTextInput-padlock-icon ' : '', props.size ? "LuiTextInput-".concat(props.size) : ''), min: ((_c = props.inputProps) === null || _c === void 0 ? void 0 : _c.type) === 'date' ? undefined : '0', value: props.value, onChange: props.onChange }, props.inputProps, { id: id })),
7144
+ React__default["default"].createElement(LuiInput, __assign({ ref: ref, className: clsx$1('LuiTextInput-input', props.showPadlockIcon ? 'LuiTextInput-padlock-icon ' : '', props.size ? "LuiTextInput-".concat(props.size) : ''), min: ((_c = props.inputProps) === null || _c === void 0 ? void 0 : _c.type) === 'date' ? undefined : '0', value: props.value, onChange: function (event) {
7145
+ var _a;
7146
+ if (((_a = props.inputProps) === null || _a === void 0 ? void 0 : _a.type) !== 'date') {
7147
+ filterInputEvent(event);
7148
+ }
7149
+ if (props.onChange) {
7150
+ props.onChange(event);
7151
+ }
7152
+ } }, props.inputProps, { id: id })),
7037
7153
  props.icon),
7038
7154
  props.error && (React__default["default"].createElement("span", { className: "LuiTextInput-error" },
7039
7155
  React__default["default"].createElement(LuiIcon, { alt: "error", name: "ic_error", className: "LuiTextInput-error-icon", size: "sm", status: "error" }),
@@ -7178,13 +7294,22 @@ var LuiSelectInput = function (props) {
7178
7294
  var LuiTextAreaInput = React__default["default"].forwardRef(function (props, ref) {
7179
7295
  var _a, _b;
7180
7296
  var id = useGenerateOrDefaultId((_a = props.inputProps) === null || _a === void 0 ? void 0 : _a.id);
7297
+ var filterInputEvent = useFilterCharacters().filterInputEvent;
7181
7298
  var rows = props.rows !== undefined ? props.rows : 5;
7182
7299
  return (React__default["default"].createElement("div", { className: clsx$1('LuiTextAreaInput', ((_b = props.inputProps) === null || _b === void 0 ? void 0 : _b.disabled) ? 'isDisabled' : '', props.error ? 'hasError' : '', props.warning ? 'hasWarning' : '') },
7183
7300
  React__default["default"].createElement("label", { htmlFor: id },
7184
7301
  props.mandatory && (React__default["default"].createElement("span", { className: "LuiTextAreaInput-mandatory" }, "*")),
7185
7302
  React__default["default"].createElement("span", { className: "LuiTextAreaInput-label" }, props.label),
7186
7303
  React__default["default"].createElement("div", { className: "LuiTextAreaInput-wrapper" },
7187
- React__default["default"].createElement("textarea", __assign({ id: id, value: props.value, onChange: props.onChange, ref: ref, rows: rows }, props.inputProps)))),
7304
+ React__default["default"].createElement("textarea", __assign({ id: id, value: props.value, onChange: function (event) {
7305
+ var _a;
7306
+ if (((_a = props.inputProps) === null || _a === void 0 ? void 0 : _a.type) !== 'date') {
7307
+ filterInputEvent(event);
7308
+ }
7309
+ if (props.onChange) {
7310
+ props.onChange(event);
7311
+ }
7312
+ }, ref: ref, rows: rows }, props.inputProps)))),
7188
7313
  props.error && (React__default["default"].createElement("span", { className: "LuiTextAreaInput-error" },
7189
7314
  React__default["default"].createElement(LuiIcon, { alt: "error", name: "ic_error", className: "LuiTextAreaInput-error-icon", size: "sm", status: "error" }),
7190
7315
  props.error)),
@@ -41344,6 +41469,9 @@ exports.Drawer = Drawer;
41344
41469
  exports.DrawerGlobal = DrawerGlobal;
41345
41470
  exports.DrawerGlobalContext = DrawerGlobalContext;
41346
41471
  exports.DrawerGlobalProvider = DrawerGlobalProvider;
41472
+ exports.LinzFreeTextFilter = LinzFreeTextFilter;
41473
+ exports.LinzFreeTextFilterConvertOnly = LinzFreeTextFilterConvertOnly;
41474
+ exports.LinzFreeTextValidator = LinzFreeTextValidator;
41347
41475
  exports.LuiAccordicard = LuiAccordicard;
41348
41476
  exports.LuiAccordicardStatic = LuiAccordicardStatic;
41349
41477
  exports.LuiAccordion = LuiAccordion;
@@ -41386,6 +41514,7 @@ exports.LuiErrorPage = LuiErrorPage;
41386
41514
  exports.LuiErrorPanel = LuiErrorPanel;
41387
41515
  exports.LuiExpandableBanner = LuiExpandableBanner;
41388
41516
  exports.LuiFileInputBox = LuiFileInputBox;
41517
+ exports.LuiFilterCharactersProvider = LuiFilterCharactersProvider;
41389
41518
  exports.LuiFilterContainer = LuiFilterContainer;
41390
41519
  exports.LuiFilterMenu = LuiFilterMenu;
41391
41520
  exports.LuiFooter = LuiFooter;
@@ -41456,6 +41585,7 @@ exports.isChromatic = isChromatic;
41456
41585
  exports.useClickedOutsideElement = useClickedOutsideElement;
41457
41586
  exports.useDrawerGlobalContext = useDrawerGlobalContext;
41458
41587
  exports.useEscapeFunction = useEscapeFunction;
41588
+ exports.useFilterCharacters = useFilterCharacters;
41459
41589
  exports.useLuiCloseableHeaderMenuContextV2 = useLuiCloseableHeaderMenuContextV2;
41460
41590
  exports.useMediaQuery = useMediaQuery;
41461
41591
  exports.usePageClickFunction = usePageClickFunction;