@peassoft/mnr-web-ui-kit 0.1.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/LICENSE.md +21 -0
- package/README.md +1 -0
- package/dist/button/index.d.ts +24 -0
- package/dist/button/index.js +36 -0
- package/dist/button/process-click.d.ts +1 -0
- package/dist/button/process-click.js +3 -0
- package/dist/button/process-key-down.d.ts +2 -0
- package/dist/button/process-key-down.js +12 -0
- package/dist/button/styles.css +76 -0
- package/dist/button-radio-group/index.d.ts +46 -0
- package/dist/button-radio-group/index.js +76 -0
- package/dist/button-radio-group/styles.css +39 -0
- package/dist/error-message/index.d.ts +15 -0
- package/dist/error-message/index.js +13 -0
- package/dist/error-message/styles.css +10 -0
- package/dist/input-field/index.d.ts +28 -0
- package/dist/input-field/index.js +85 -0
- package/dist/input-field/process-key-down.d.ts +2 -0
- package/dist/input-field/process-key-down.js +14 -0
- package/dist/input-field/styles.css +48 -0
- package/dist/plain-select/icon.d.ts +6 -0
- package/dist/plain-select/icon.js +17 -0
- package/dist/plain-select/index.d.ts +61 -0
- package/dist/plain-select/index.js +195 -0
- package/dist/plain-select/styles.css +41 -0
- package/dist/text-button/index.d.ts +21 -0
- package/dist/text-button/index.js +25 -0
- package/dist/text-button/process-key-down.d.ts +2 -0
- package/dist/text-button/process-key-down.js +12 -0
- package/dist/text-button/styles.css +14 -0
- package/package.json +78 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## The MIT License (MIT) ##
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alexey Gorokhov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Memorize'n'Revise Web UI Kit
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
type Props = {
|
|
4
|
+
/** Button label */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Kind of the button */
|
|
7
|
+
variant?: 'primary' | 'secondary';
|
|
8
|
+
/** Size of the button */
|
|
9
|
+
size?: 'normal' | 'small';
|
|
10
|
+
/** Whether the button is stretched to the size of its container */
|
|
11
|
+
stretched?: boolean;
|
|
12
|
+
/** Whether action initiated by click on the button is in progress */
|
|
13
|
+
isInProgress?: boolean;
|
|
14
|
+
/** Ref */
|
|
15
|
+
ref?: React.RefObject<HTMLButtonElement | null>;
|
|
16
|
+
/** onClick event handler; is ignored if props.isInProgress === true */
|
|
17
|
+
onClick: () => unknown;
|
|
18
|
+
/** Optional handler of Tab key press */
|
|
19
|
+
onTab?: () => unknown;
|
|
20
|
+
/** Optional handler of Shift + Tab key press */
|
|
21
|
+
onShiftTab?: () => unknown;
|
|
22
|
+
};
|
|
23
|
+
export declare function Button(props: Props): JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import './styles.css';
|
|
4
|
+
import processClick from './process-click.js';
|
|
5
|
+
import processKeyDown from './process-key-down.js';
|
|
6
|
+
export function Button(props) {
|
|
7
|
+
const {
|
|
8
|
+
label,
|
|
9
|
+
variant = 'primary',
|
|
10
|
+
size = 'normal',
|
|
11
|
+
stretched = false,
|
|
12
|
+
isInProgress = false,
|
|
13
|
+
ref,
|
|
14
|
+
onClick,
|
|
15
|
+
onTab,
|
|
16
|
+
onShiftTab
|
|
17
|
+
} = props;
|
|
18
|
+
const handleClick = useCallback(() => processClick(isInProgress, onClick), [isInProgress, onClick]);
|
|
19
|
+
const handleKeyDown = useCallback(e => processKeyDown(e, onTab, onShiftTab), [onTab, onShiftTab]);
|
|
20
|
+
const btnCn = 'uikit_Button_btn' + (variant === 'secondary' ? ' uikit_Button_btn--secondary' : '') + (stretched ? ' uikit_Button_btn--stretched uikit_Button_btn--withTopMargin' : ' uikit_Button_btn--withLeftMargin') + (size === 'small' ? ' uikit_Button_btn--small' : '');
|
|
21
|
+
const labelCn = isInProgress ? 'uikit_Button_label--hidden' : '';
|
|
22
|
+
return _jsxs("button", {
|
|
23
|
+
ref: ref,
|
|
24
|
+
type: 'button',
|
|
25
|
+
className: btnCn,
|
|
26
|
+
onClick: handleClick,
|
|
27
|
+
onKeyDown: handleKeyDown,
|
|
28
|
+
children: [_jsx("span", {
|
|
29
|
+
className: labelCn,
|
|
30
|
+
children: label
|
|
31
|
+
}), !!isInProgress && _jsx("div", {
|
|
32
|
+
className: 'uikit_Button_spinner',
|
|
33
|
+
"data-test-id": 'spinner'
|
|
34
|
+
})]
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function processClick(isInProgress: boolean, onClick: () => void): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const TAB_KEYCODE = 9;
|
|
2
|
+
export default function processKeyDown(e, onTab, onShiftTab) {
|
|
3
|
+
if (e.which === TAB_KEYCODE && onTab && !e.shiftKey) {
|
|
4
|
+
e.preventDefault();
|
|
5
|
+
onTab();
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (e.which === TAB_KEYCODE && onShiftTab && e.shiftKey) {
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
onShiftTab();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
.uikit_Button_btn {
|
|
2
|
+
display: block;
|
|
3
|
+
appearance: none;
|
|
4
|
+
border: 0;
|
|
5
|
+
color: #f0e9d2;
|
|
6
|
+
background-color: #678983;
|
|
7
|
+
border-radius: 5px;
|
|
8
|
+
font-family: inherit;
|
|
9
|
+
font-size: 1em;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 1em 2em;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
position: relative;
|
|
14
|
+
outline-offset: 1px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.uikit_Button_btn--secondary {
|
|
18
|
+
color: #678983;
|
|
19
|
+
background-color: transparent;
|
|
20
|
+
border: 1px solid currentColor;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.uikit_Button_btn--stretched {
|
|
24
|
+
width: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.uikit_Button_btn--small {
|
|
28
|
+
padding: .8em 1.4em;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.uikit_Button_btn--withLeftMargin:not(:first-child) {
|
|
32
|
+
margin-left: 1em;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.uikit_Button_btn--withTopMargin:not(:first-child) {
|
|
36
|
+
margin-top: 1em;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.uikit_Button_label--hidden {
|
|
40
|
+
opacity: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.uikit_Button_spinner {
|
|
44
|
+
width: 6px;
|
|
45
|
+
height: 6px;
|
|
46
|
+
border-radius: 50%;
|
|
47
|
+
background-color: currentColor;
|
|
48
|
+
animation: uikit_Button_spinner_animation 1s linear infinite alternate;
|
|
49
|
+
position: absolute;
|
|
50
|
+
left: 50%;
|
|
51
|
+
top: 50%;
|
|
52
|
+
transform: translate(calc(-50% - 12px), -50%);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
.uikit_Button_spinner {
|
|
57
|
+
box-shadow: 12px 0px 0px 0px currentColor, 24px 0px 0px 0px currentColor;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@keyframes uikit_Button_spinner_animation{
|
|
62
|
+
0% {
|
|
63
|
+
opacity: 1;
|
|
64
|
+
box-shadow: 12px 0px 0px 0px currentColor, 24px 0px 0px 0px currentColor;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
25% {
|
|
68
|
+
opacity: 0.4;
|
|
69
|
+
box-shadow: 12px 0px 0px 0px currentColor, 24px 0px 0px 0px currentColor;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
75% {
|
|
73
|
+
opacity: 0.4;
|
|
74
|
+
box-shadow: 12px 0px 0px 0px currentColor, 24px 0px 0px 0px currentColor;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
export type ButtonRadioGroupItem = {
|
|
4
|
+
/** Unique item ID */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Contents of the button */
|
|
7
|
+
contents: React.ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Optional description label.
|
|
10
|
+
* Should be provided if button contents does not contain descriptive text.
|
|
11
|
+
*/
|
|
12
|
+
a11yLabel?: string;
|
|
13
|
+
};
|
|
14
|
+
type Props = {
|
|
15
|
+
/**
|
|
16
|
+
* Optional. Visible label for the entire group of radio buttons.
|
|
17
|
+
*
|
|
18
|
+
* If `label` is provided, `groupA11yLabel` is ignored.
|
|
19
|
+
* If `label` is NOT provided, you should provide `groupA11yLabel`.
|
|
20
|
+
*/
|
|
21
|
+
label?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional. Accessability label for the entire group of radio buttons.
|
|
24
|
+
*
|
|
25
|
+
* If `label` is provided, `groupA11yLabel` is ignored.
|
|
26
|
+
*/
|
|
27
|
+
groupA11yLabel?: string;
|
|
28
|
+
/** Items of the group */
|
|
29
|
+
items: ButtonRadioGroupItem[];
|
|
30
|
+
/** ID of the currently checked item */
|
|
31
|
+
selectedItemId: string;
|
|
32
|
+
/** Handler for change event */
|
|
33
|
+
onChange: (itemId: string) => unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Exposes react ref object that might be used for setting focus on the currently
|
|
36
|
+
* checked item from outside the component
|
|
37
|
+
*/
|
|
38
|
+
focusHandle?: React.RefObject<HTMLButtonElement | null>;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Standalone radio group widget styled as a set of toggle buttons.
|
|
42
|
+
*
|
|
43
|
+
* Note: Not for use within a toolbar.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ButtonRadioGroup(props: Props): JSX.Element;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useId, createRef } from 'react';
|
|
3
|
+
import './styles.css';
|
|
4
|
+
/**
|
|
5
|
+
* Standalone radio group widget styled as a set of toggle buttons.
|
|
6
|
+
*
|
|
7
|
+
* Note: Not for use within a toolbar.
|
|
8
|
+
*/
|
|
9
|
+
export function ButtonRadioGroup(props) {
|
|
10
|
+
const {
|
|
11
|
+
label,
|
|
12
|
+
groupA11yLabel,
|
|
13
|
+
items,
|
|
14
|
+
selectedItemId,
|
|
15
|
+
onChange,
|
|
16
|
+
focusHandle
|
|
17
|
+
} = props;
|
|
18
|
+
const labelDomId = useId();
|
|
19
|
+
const buttonRefs = useMemo(() => items.map(item => {
|
|
20
|
+
return item.id === selectedItemId ? focusHandle : createRef();
|
|
21
|
+
}), [items, selectedItemId, focusHandle]);
|
|
22
|
+
const handleKeyDown = useCallback(event => {
|
|
23
|
+
const currIdx = items.findIndex(item => item.id === selectedItemId);
|
|
24
|
+
let nextIdx = null;
|
|
25
|
+
switch (event.key) {
|
|
26
|
+
case 'ArrowRight':
|
|
27
|
+
case 'ArrowDown':
|
|
28
|
+
nextIdx = currIdx + 1;
|
|
29
|
+
if (nextIdx > items.length - 1) nextIdx = 0;
|
|
30
|
+
break;
|
|
31
|
+
case 'ArrowLeft':
|
|
32
|
+
case 'ArrowUp':
|
|
33
|
+
nextIdx = currIdx - 1;
|
|
34
|
+
if (nextIdx < 0) nextIdx = items.length - 1;
|
|
35
|
+
}
|
|
36
|
+
if (nextIdx != null) {
|
|
37
|
+
const nextItem = items[nextIdx];
|
|
38
|
+
if (nextItem) {
|
|
39
|
+
onChange(nextItem.id);
|
|
40
|
+
}
|
|
41
|
+
const nextRef = buttonRefs[nextIdx];
|
|
42
|
+
if (nextRef) {
|
|
43
|
+
nextRef.current?.focus();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}, [items, selectedItemId, onChange, buttonRefs]);
|
|
47
|
+
return _jsxs(_Fragment, {
|
|
48
|
+
children: [label && _jsx("p", {
|
|
49
|
+
id: labelDomId,
|
|
50
|
+
className: 'uikit_ButtonRadioGroup_label',
|
|
51
|
+
children: label
|
|
52
|
+
}), _jsx("div", {
|
|
53
|
+
className: 'uikit_ButtonRadioGroup_cont',
|
|
54
|
+
role: 'radiogroup',
|
|
55
|
+
"aria-labelledby": label ? labelDomId : undefined,
|
|
56
|
+
"aria-label": !label && groupA11yLabel ? groupA11yLabel : undefined,
|
|
57
|
+
children: items.map((item, idx) => _jsx("button", {
|
|
58
|
+
ref: buttonRefs[idx],
|
|
59
|
+
type: 'button',
|
|
60
|
+
className: 'uikit_ButtonRadioGroup_btn' + (item.id === selectedItemId ? ' uikit_ButtonRadioGroup_btn--checked' : '') + (idx === items.length - 1 ? ' uikit_ButtonRadioGroup_btn--last' : '') + (idx > 0 && idx < items.length - 1 ? ' uikit_ButtonRadioGroup_btn--mid' : ''),
|
|
61
|
+
tabIndex: item.id === selectedItemId ? 0 : -1,
|
|
62
|
+
role: 'radio',
|
|
63
|
+
"aria-label": item.a11yLabel || undefined,
|
|
64
|
+
"aria-checked": item.id === selectedItemId,
|
|
65
|
+
title: item.a11yLabel,
|
|
66
|
+
onClick: () => {
|
|
67
|
+
if (item.id !== selectedItemId) {
|
|
68
|
+
onChange(item.id);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onKeyDown: handleKeyDown,
|
|
72
|
+
children: item.contents
|
|
73
|
+
}, item.id))
|
|
74
|
+
})]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.uikit_ButtonRadioGroup_label {
|
|
2
|
+
margin: 0 0 0.5em;
|
|
3
|
+
color: #848484;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.uikit_ButtonRadioGroup_cont {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.uikit_ButtonRadioGroup_btn {
|
|
12
|
+
display: block;
|
|
13
|
+
appearance: none;
|
|
14
|
+
border: 1px solid #bcbcbc;
|
|
15
|
+
border-right: 0;
|
|
16
|
+
color: inherit;
|
|
17
|
+
background-color: transparent;
|
|
18
|
+
border-radius: 5px 0 0 5px;
|
|
19
|
+
font-family: inherit;
|
|
20
|
+
font-size: 1em;
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0.6em;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
position: relative;
|
|
25
|
+
outline-offset: 1px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.uikit_ButtonRadioGroup_btn--checked {
|
|
29
|
+
background-color: #ddd;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.uikit_ButtonRadioGroup_btn--last {
|
|
33
|
+
border-radius: 0 5px 5px 0;
|
|
34
|
+
border-right: 1px solid #bcbcbc;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.uikit_ButtonRadioGroup_btn--mid {
|
|
38
|
+
border-radius: 0;
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
type Props = {
|
|
4
|
+
/**
|
|
5
|
+
* Variant of error message.
|
|
6
|
+
*
|
|
7
|
+
* "standalone": a message which is not a part of any other widget.
|
|
8
|
+
* "attached": a message which is a part of another widget.
|
|
9
|
+
*/
|
|
10
|
+
variant: 'standalone' | 'attached';
|
|
11
|
+
/** Text of error message */
|
|
12
|
+
children: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function ErrorMessage(props: Props): JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import './styles.css';
|
|
3
|
+
export function ErrorMessage(props) {
|
|
4
|
+
const {
|
|
5
|
+
variant,
|
|
6
|
+
children
|
|
7
|
+
} = props;
|
|
8
|
+
const className = 'uikit_ErrorMessage' + (variant === 'standalone' ? ' uikit_ErrorMessage--standalone' : '');
|
|
9
|
+
return _jsx("p", {
|
|
10
|
+
className: className,
|
|
11
|
+
children: children
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
type Props = {
|
|
4
|
+
/** Type of the inner input element */
|
|
5
|
+
type?: 'text' | 'email' | 'password';
|
|
6
|
+
/** Label */
|
|
7
|
+
label: string;
|
|
8
|
+
/** autocomplete attribute */
|
|
9
|
+
autocompleteAttribute?: string;
|
|
10
|
+
/** Width in any CSS length value */
|
|
11
|
+
width?: string;
|
|
12
|
+
/** Error message */
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
/** Controlled value */
|
|
15
|
+
value: string;
|
|
16
|
+
/** Ref */
|
|
17
|
+
ref?: React.RefObject<HTMLInputElement | null>;
|
|
18
|
+
/** onChange callback */
|
|
19
|
+
onChange: (newValue: string) => unknown;
|
|
20
|
+
/** Optional handler of Tab key press */
|
|
21
|
+
onTab?: () => unknown;
|
|
22
|
+
/** Optional handler of Shift + Tab key press */
|
|
23
|
+
onShiftTab?: () => unknown;
|
|
24
|
+
/** Optional handler of the input onBlur event */
|
|
25
|
+
onBlur?: () => unknown;
|
|
26
|
+
};
|
|
27
|
+
export declare function InputField(props: Props): JSX.Element;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
3
|
+
import './styles.css';
|
|
4
|
+
import processKeyDown from './process-key-down.js';
|
|
5
|
+
export function InputField(props) {
|
|
6
|
+
const {
|
|
7
|
+
type = 'text',
|
|
8
|
+
label,
|
|
9
|
+
autocompleteAttribute,
|
|
10
|
+
width = '100%',
|
|
11
|
+
errorMessage,
|
|
12
|
+
value,
|
|
13
|
+
ref,
|
|
14
|
+
onChange,
|
|
15
|
+
onTab,
|
|
16
|
+
onShiftTab,
|
|
17
|
+
onBlur
|
|
18
|
+
} = props;
|
|
19
|
+
const [isCapslockOn, setIsCapslockOn] = useState(false);
|
|
20
|
+
const handleChange = useCallback(e => onChange(e.target.value), [onChange]);
|
|
21
|
+
const handleKeyDown = useCallback(e => processKeyDown(e, onTab, onShiftTab, onBlur), [onTab, onShiftTab, onBlur]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
function detectCapsLock(e) {
|
|
24
|
+
if (e.getModifierState('CapsLock')) {
|
|
25
|
+
if (!isCapslockOn) setIsCapslockOn(true);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (isCapslockOn) setIsCapslockOn(false);
|
|
29
|
+
}
|
|
30
|
+
// We need to listen for both keydown and keyup events so that we can detect presses
|
|
31
|
+
// of the cap lock key itself. Usually browsers dispatch a keydown event when a
|
|
32
|
+
// key is pressed down and a keyup event when it’s released, but for the caps
|
|
33
|
+
// lock key only one of these events is dispatched on each press.
|
|
34
|
+
//
|
|
35
|
+
// All browsers dispatch only a keydown event when the caps lock key is pressed to
|
|
36
|
+
// turn caps lock on. Firefox also dispatches only a keydown event when the caps lock
|
|
37
|
+
// key is pressed to turn caps lock off, but Chrome, Safari, and Edge dispatch only
|
|
38
|
+
// a keyup event instead (in effect, they act as if caps lock is another shift key
|
|
39
|
+
// that’s being pressed down for the duration of caps lock being turned on).
|
|
40
|
+
window.addEventListener('keydown', detectCapsLock);
|
|
41
|
+
window.addEventListener('keyup', detectCapsLock);
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener('keydown', detectCapsLock);
|
|
44
|
+
window.removeEventListener('keyup', detectCapsLock);
|
|
45
|
+
};
|
|
46
|
+
}, [isCapslockOn]);
|
|
47
|
+
return _jsxs("div", {
|
|
48
|
+
className: 'uikit_InputField_cont',
|
|
49
|
+
style: {
|
|
50
|
+
width
|
|
51
|
+
},
|
|
52
|
+
children: [_jsxs("label", {
|
|
53
|
+
className: 'uikit_InputField_innerCont',
|
|
54
|
+
children: [_jsx("span", {
|
|
55
|
+
className: 'uikit_InputField_label',
|
|
56
|
+
children: label
|
|
57
|
+
}), _jsx("input", {
|
|
58
|
+
ref: ref,
|
|
59
|
+
className: 'uikit_InputField_input',
|
|
60
|
+
type: type,
|
|
61
|
+
autoComplete: autocompleteAttribute,
|
|
62
|
+
value: value,
|
|
63
|
+
onChange: handleChange,
|
|
64
|
+
onKeyDown: handleKeyDown,
|
|
65
|
+
onBlur: onBlur || undefined
|
|
66
|
+
})]
|
|
67
|
+
}), type === 'password' && !!isCapslockOn && _jsxs("div", {
|
|
68
|
+
className: 'uikit_InputField_capslockMsg',
|
|
69
|
+
children: [_jsx("svg", {
|
|
70
|
+
width: '24',
|
|
71
|
+
height: '24',
|
|
72
|
+
viewBox: '0 0 24 24',
|
|
73
|
+
className: 'uikit_InputField_capslockIcon',
|
|
74
|
+
children: _jsx("path", {
|
|
75
|
+
d: 'M12 20.016q3.281 0 5.648-2.367t2.367-5.648-2.367-5.648-5.648-2.367-5.648\n 2.367-2.367 5.648 2.367 5.648 5.648 2.367zM12 2.016q4.125 0 7.055 2.93t2.93\n 7.055-2.93 7.055-7.055 2.93-7.055-2.93-2.93-7.055 2.93-7.055\n 7.055-2.93zM11.016 6.984h1.969v6h-1.969v-6zM11.016 15h1.969v2.016h-1.969v-2.016z'
|
|
76
|
+
})
|
|
77
|
+
}), _jsx("span", {
|
|
78
|
+
children: "CapsLock"
|
|
79
|
+
})]
|
|
80
|
+
}), !!errorMessage && _jsx("div", {
|
|
81
|
+
className: 'uikit_InputField_errorMsg',
|
|
82
|
+
children: errorMessage
|
|
83
|
+
})]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const TAB_KEYCODE = 9;
|
|
2
|
+
export default function processKeyDown(e, onTab, onShiftTab, onBlur) {
|
|
3
|
+
if (e.which === TAB_KEYCODE && onTab && !e.shiftKey) {
|
|
4
|
+
e.preventDefault();
|
|
5
|
+
onTab();
|
|
6
|
+
if (onBlur) onBlur();
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (e.which === TAB_KEYCODE && onShiftTab && e.shiftKey) {
|
|
10
|
+
e.preventDefault();
|
|
11
|
+
onShiftTab();
|
|
12
|
+
if (onBlur) onBlur();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.uikit_InputField_cont:not(:first-child) {
|
|
2
|
+
margin-top: 1.5em;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.uikit_InputField_innerCont {
|
|
6
|
+
display: block;
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.uikit_InputField_label {
|
|
12
|
+
display: block;
|
|
13
|
+
margin-bottom: 0.25em;
|
|
14
|
+
color: #848484;
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.uikit_InputField_input {
|
|
19
|
+
display: block;
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
width: 100%;
|
|
22
|
+
border: 1px solid #bcbcbc;
|
|
23
|
+
border-radius: 5px;
|
|
24
|
+
font-size: 1.2em;
|
|
25
|
+
padding: 0.5em 1em;
|
|
26
|
+
color: #242629;
|
|
27
|
+
background-color: #fff;
|
|
28
|
+
font-family: inherit;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.uikit_InputField_capslockMsg {
|
|
32
|
+
display: flex;
|
|
33
|
+
gap: .5em;
|
|
34
|
+
align-items: center;
|
|
35
|
+
margin-top: 0.2em;
|
|
36
|
+
font-size: 0.85rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.uikit_InputField_capslockIcon {
|
|
40
|
+
color: #a7241b;
|
|
41
|
+
fill: currentColor;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.uikit_InputField_errorMsg {
|
|
45
|
+
color: #a7241b;
|
|
46
|
+
font-size: 0.85rem;
|
|
47
|
+
margin-top: 0.2em;
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export default function Icon(props) {
|
|
3
|
+
const {
|
|
4
|
+
selected
|
|
5
|
+
} = props;
|
|
6
|
+
const className = 'uikit_PlainSelect_icon' + (selected ? '' : ' uikit_PlainSelect_icon--hidden');
|
|
7
|
+
return _jsx("svg", {
|
|
8
|
+
className: className,
|
|
9
|
+
width: '24',
|
|
10
|
+
height: '24',
|
|
11
|
+
viewBox: '0 0 24 24',
|
|
12
|
+
children: _jsx("path", {
|
|
13
|
+
d: 'M9 16.172l10.594-10.594 1.406 1.406-12 12-5.578-5.578 1.406-1.406z',
|
|
14
|
+
fill: '#24b23e'
|
|
15
|
+
})
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
type Props = {
|
|
4
|
+
/**
|
|
5
|
+
* Label
|
|
6
|
+
*/
|
|
7
|
+
label: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional. Flag to switch the multi-select mode.
|
|
10
|
+
* If provided and set to true, allows multiple selections.
|
|
11
|
+
* Defaults to false.
|
|
12
|
+
*/
|
|
13
|
+
multiple?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* List items
|
|
16
|
+
*/
|
|
17
|
+
items: PlainSelectItem[];
|
|
18
|
+
/**
|
|
19
|
+
* Currently selected item's ID (for single-select mode)
|
|
20
|
+
* or array of items' IDs (for multi-select mode).
|
|
21
|
+
*
|
|
22
|
+
* The component throws an Error if:
|
|
23
|
+
* - a single string value is provided in multi-select mode;
|
|
24
|
+
* - an array of string values is provided in single-select mode.
|
|
25
|
+
*
|
|
26
|
+
* In order to indicate that no item/items are selected:
|
|
27
|
+
* - for single-select mode: pass null;
|
|
28
|
+
* - for multi-select mode: pass an empty array or null.
|
|
29
|
+
*/
|
|
30
|
+
selectedItemId: string | string[] | null;
|
|
31
|
+
/** Maximum height of list container set in any CSS length value (e.g. px, em, etc).
|
|
32
|
+
* If not provided, list container is as high as all its list items' height.
|
|
33
|
+
*/
|
|
34
|
+
maxHeight?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Callback function invoked when selected item/items change.
|
|
37
|
+
*
|
|
38
|
+
* @param payload In single-select mode: ID of newely selected item.
|
|
39
|
+
* In multi-select mode: Array of IDs of selected items after change.
|
|
40
|
+
* Might be an empty array as multi-select mode allows deselecting
|
|
41
|
+
* all items.
|
|
42
|
+
*/
|
|
43
|
+
onChange: (payload: string | string[]) => unknown;
|
|
44
|
+
};
|
|
45
|
+
/** Item descriptor */
|
|
46
|
+
export type PlainSelectItem = {
|
|
47
|
+
/** Unique ID */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Item's main title */
|
|
50
|
+
title: string;
|
|
51
|
+
/** Optional additional description */
|
|
52
|
+
description?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Select element implementing Listbox pattern. Presents a list of options and allows
|
|
56
|
+
* a user to select one or more of them.
|
|
57
|
+
*
|
|
58
|
+
* Note: Can only be used in controlled mode.
|
|
59
|
+
*/
|
|
60
|
+
export declare function PlainSelect(props: Props): JSX.Element;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useCallback, useState, useMemo, useLayoutEffect, createRef } from 'react';
|
|
3
|
+
import './styles.css';
|
|
4
|
+
import Icon from './icon.js';
|
|
5
|
+
/**
|
|
6
|
+
* Select element implementing Listbox pattern. Presents a list of options and allows
|
|
7
|
+
* a user to select one or more of them.
|
|
8
|
+
*
|
|
9
|
+
* Note: Can only be used in controlled mode.
|
|
10
|
+
*/
|
|
11
|
+
export function PlainSelect(props) {
|
|
12
|
+
const {
|
|
13
|
+
label,
|
|
14
|
+
multiple = false,
|
|
15
|
+
items,
|
|
16
|
+
selectedItemId,
|
|
17
|
+
maxHeight,
|
|
18
|
+
onChange
|
|
19
|
+
} = props;
|
|
20
|
+
const labelDomId = useId();
|
|
21
|
+
const [currFocusedIdx, setCurrFocusedIdx] = useState(() => {
|
|
22
|
+
if (multiple) {
|
|
23
|
+
if (Array.isArray(selectedItemId)) {
|
|
24
|
+
const firstId = selectedItemId[0];
|
|
25
|
+
if (firstId) {
|
|
26
|
+
const idx = items.findIndex(item => item.id === firstId);
|
|
27
|
+
if (idx >= 0) {
|
|
28
|
+
return idx;
|
|
29
|
+
} else {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const itemRefs = useMemo(() => items.map(() => createRef()), [items]);
|
|
43
|
+
const handleSingleSelectKeyDown = useCallback(event => {
|
|
44
|
+
const currSelectedItemIdx = items.findIndex(item => item.id === selectedItemId);
|
|
45
|
+
let nextSelectedItemIdx = currSelectedItemIdx;
|
|
46
|
+
switch (event.key) {
|
|
47
|
+
case 'ArrowDown':
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
if (currSelectedItemIdx < items.length - 1) {
|
|
50
|
+
nextSelectedItemIdx = currSelectedItemIdx + 1;
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case 'ArrowUp':
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
if (currSelectedItemIdx > 0) {
|
|
56
|
+
nextSelectedItemIdx = currSelectedItemIdx - 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (currSelectedItemIdx !== nextSelectedItemIdx) {
|
|
60
|
+
const nextSelectedItem = items[nextSelectedItemIdx];
|
|
61
|
+
if (nextSelectedItem != null) {
|
|
62
|
+
onChange(nextSelectedItem.id);
|
|
63
|
+
itemRefs[nextSelectedItemIdx]?.current?.scrollIntoView({
|
|
64
|
+
behavior: 'smooth',
|
|
65
|
+
block: 'nearest'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, [items, selectedItemId, itemRefs, onChange]);
|
|
70
|
+
const handleMultiSelectKeyDown = useCallback(event => {
|
|
71
|
+
switch (event.key) {
|
|
72
|
+
case 'ArrowDown':
|
|
73
|
+
{
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
const nextFocusedIdx = currFocusedIdx + 1;
|
|
76
|
+
if (nextFocusedIdx <= items.length - 1) {
|
|
77
|
+
setCurrFocusedIdx(nextFocusedIdx);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'ArrowUp':
|
|
82
|
+
{
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
const nextFocusedIdx = currFocusedIdx - 1;
|
|
85
|
+
if (nextFocusedIdx >= 0) {
|
|
86
|
+
setCurrFocusedIdx(nextFocusedIdx);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case ' ':
|
|
91
|
+
{
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
const item = items[currFocusedIdx];
|
|
94
|
+
if (item) {
|
|
95
|
+
if (Array.isArray(selectedItemId)) {
|
|
96
|
+
if (selectedItemId.includes(item.id)) {
|
|
97
|
+
onChange(selectedItemId.filter(id => id !== item.id));
|
|
98
|
+
} else {
|
|
99
|
+
onChange(selectedItemId.concat(item.id));
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
onChange([item.id]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'a':
|
|
108
|
+
{
|
|
109
|
+
if (event.ctrlKey || event.metaKey) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
if (Array.isArray(selectedItemId) && selectedItemId.length === items.length) {
|
|
112
|
+
onChange([]);
|
|
113
|
+
} else {
|
|
114
|
+
onChange(items.map(item => item.id));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, [currFocusedIdx, items, selectedItemId, onChange]);
|
|
120
|
+
const handleItemClick = useCallback(id => {
|
|
121
|
+
if (multiple) {
|
|
122
|
+
const itemIdx = items.findIndex(item => item.id === id);
|
|
123
|
+
if (itemIdx >= 0) {
|
|
124
|
+
setCurrFocusedIdx(itemIdx);
|
|
125
|
+
if (Array.isArray(selectedItemId)) {
|
|
126
|
+
if (selectedItemId.includes(id)) {
|
|
127
|
+
onChange(selectedItemId.filter(selectedId => selectedId !== id));
|
|
128
|
+
} else {
|
|
129
|
+
onChange(selectedItemId.concat(id));
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
onChange([id]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else if (id !== selectedItemId) {
|
|
136
|
+
onChange(id);
|
|
137
|
+
}
|
|
138
|
+
}, [multiple, selectedItemId, onChange, items]);
|
|
139
|
+
useLayoutEffect(() => {
|
|
140
|
+
if (multiple && itemRefs.some(ref => ref.current === document.activeElement)) {
|
|
141
|
+
itemRefs[currFocusedIdx]?.current?.focus();
|
|
142
|
+
}
|
|
143
|
+
}, [multiple, itemRefs, currFocusedIdx]);
|
|
144
|
+
if (multiple && !Array.isArray(selectedItemId) && selectedItemId != null) {
|
|
145
|
+
const msg = 'single string value in selectedItemId prop is not ' + 'acceptable for multi-select mode';
|
|
146
|
+
throw new Error(msg);
|
|
147
|
+
}
|
|
148
|
+
if (!multiple && Array.isArray(selectedItemId)) {
|
|
149
|
+
const msg = 'array in selectedItemId prop is not ' + 'acceptable for single-select mode';
|
|
150
|
+
throw new Error(msg);
|
|
151
|
+
}
|
|
152
|
+
return _jsxs(_Fragment, {
|
|
153
|
+
children: [_jsx("p", {
|
|
154
|
+
id: labelDomId,
|
|
155
|
+
className: 'uikit_PlainSelect_label',
|
|
156
|
+
children: label
|
|
157
|
+
}), _jsx("ul", {
|
|
158
|
+
className: 'uikit_PlainSelect_cont',
|
|
159
|
+
style: {
|
|
160
|
+
...(maxHeight ? {
|
|
161
|
+
maxHeight,
|
|
162
|
+
overflow: 'auto'
|
|
163
|
+
} : {})
|
|
164
|
+
},
|
|
165
|
+
role: 'listbox',
|
|
166
|
+
"aria-labelledby": labelDomId,
|
|
167
|
+
"aria-multiselectable": multiple,
|
|
168
|
+
tabIndex: multiple ? -1 : 0,
|
|
169
|
+
onKeyDown: multiple ? undefined : handleSingleSelectKeyDown,
|
|
170
|
+
children: items.map((item, idx) => {
|
|
171
|
+
const isSelected = multiple ? selectedItemId != null && selectedItemId.includes(item.id) : selectedItemId != null && item.id === selectedItemId;
|
|
172
|
+
return _jsxs("li", {
|
|
173
|
+
ref: itemRefs[idx],
|
|
174
|
+
className: 'uikit_PlainSelect_itemCont',
|
|
175
|
+
role: 'option',
|
|
176
|
+
"aria-selected": isSelected,
|
|
177
|
+
tabIndex: multiple && currFocusedIdx === idx ? 0 : -1,
|
|
178
|
+
onClick: () => handleItemClick(item.id),
|
|
179
|
+
onKeyDown: multiple ? handleMultiSelectKeyDown : undefined,
|
|
180
|
+
children: [_jsx(Icon, {
|
|
181
|
+
selected: isSelected
|
|
182
|
+
}), _jsxs("div", {
|
|
183
|
+
className: 'uikit_PlainSelect_itemContent',
|
|
184
|
+
children: [_jsx("div", {
|
|
185
|
+
children: item.title
|
|
186
|
+
}), item.description && _jsx("div", {
|
|
187
|
+
className: 'uikit_PlainSelect_description',
|
|
188
|
+
children: item.description
|
|
189
|
+
})]
|
|
190
|
+
})]
|
|
191
|
+
}, item.id);
|
|
192
|
+
})
|
|
193
|
+
})]
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.uikit_PlainSelect_label {
|
|
2
|
+
margin: 0;
|
|
3
|
+
color: #848484;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.uikit_PlainSelect_cont {
|
|
7
|
+
margin: 0.5em 0 0;
|
|
8
|
+
padding: 0.5em;
|
|
9
|
+
border: 1px solid #bcbcbc;
|
|
10
|
+
list-style-type: none;
|
|
11
|
+
border-radius: 0.25em;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.uikit_PlainSelect_itemCont {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: flex-start;
|
|
17
|
+
gap: 0.5rem;
|
|
18
|
+
padding: 0.25em;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.uikit_PlainSelect_icon {
|
|
23
|
+
flex-shrink: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.uikit_PlainSelect_icon--hidden {
|
|
27
|
+
visibility: hidden;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.uikit_PlainSelect_itemContent {
|
|
31
|
+
border-bottom: 1px solid #ddd;
|
|
32
|
+
flex-basis: 100%;
|
|
33
|
+
flex-shrink: 1;
|
|
34
|
+
padding-bottom: 0.25em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.uikit_PlainSelect_description {
|
|
38
|
+
margin-top: 0.5em;
|
|
39
|
+
color: #848484;
|
|
40
|
+
font-size: 87.5%;
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
type Props = {
|
|
4
|
+
/** Button label */
|
|
5
|
+
label: string;
|
|
6
|
+
/**
|
|
7
|
+
* Tab index
|
|
8
|
+
* @default 0
|
|
9
|
+
*/
|
|
10
|
+
tabIndex?: number;
|
|
11
|
+
/** Ref */
|
|
12
|
+
ref?: React.RefObject<HTMLButtonElement | null>;
|
|
13
|
+
/** onClick event handler */
|
|
14
|
+
onClick: (e: React.MouseEvent) => unknown;
|
|
15
|
+
/** Optional handler of Tab key press */
|
|
16
|
+
onTab?: () => unknown;
|
|
17
|
+
/** Optional handler of Shift + Tab key press */
|
|
18
|
+
onShiftTab?: () => unknown;
|
|
19
|
+
};
|
|
20
|
+
export declare function TextButton(props: Props): JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import './styles.css';
|
|
4
|
+
import processKeyDown from './process-key-down.js';
|
|
5
|
+
export function TextButton(props) {
|
|
6
|
+
const {
|
|
7
|
+
label,
|
|
8
|
+
tabIndex = 0,
|
|
9
|
+
ref,
|
|
10
|
+
onClick,
|
|
11
|
+
onTab,
|
|
12
|
+
onShiftTab
|
|
13
|
+
} = props;
|
|
14
|
+
const handleClick = useCallback(e => onClick(e), [onClick]);
|
|
15
|
+
const handleKeyDown = useCallback(e => processKeyDown(e, onTab, onShiftTab), [onTab, onShiftTab]);
|
|
16
|
+
return _jsx("button", {
|
|
17
|
+
ref: ref,
|
|
18
|
+
type: 'button',
|
|
19
|
+
className: 'uikit_TextButton_btn',
|
|
20
|
+
tabIndex: tabIndex,
|
|
21
|
+
onClick: handleClick,
|
|
22
|
+
onKeyDown: handleKeyDown,
|
|
23
|
+
children: label
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const TAB_KEYCODE = 9;
|
|
2
|
+
export default function processKeyDown(e, onTab, onShiftTab) {
|
|
3
|
+
if (e.which === TAB_KEYCODE && onTab && !e.shiftKey) {
|
|
4
|
+
e.preventDefault();
|
|
5
|
+
onTab();
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (e.which === TAB_KEYCODE && onShiftTab && e.shiftKey) {
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
onShiftTab();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.uikit_TextButton_btn {
|
|
2
|
+
display: inline;
|
|
3
|
+
appearance: none;
|
|
4
|
+
border: 0;
|
|
5
|
+
border-bottom: 1px solid #678983;
|
|
6
|
+
color: #678983;
|
|
7
|
+
background-color: transparent;
|
|
8
|
+
font-family: inherit;
|
|
9
|
+
font-size: 1em;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
outline-offset: 3px;
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peassoft/mnr-web-ui-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Peassoft web UI kit for MNR web applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./*": "./dist/*"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "npm run storybook",
|
|
14
|
+
"test": "stylelint './src/**/*.css' && eslint ./ && npm run tsc && jest",
|
|
15
|
+
"tsc": "tsc --noEmit",
|
|
16
|
+
"build": "del-cli ./compiled && del-cli ./dist && tsc -p tsconfig-prod.json && babel ./compiled --out-dir ./dist && cpy './src/**/*.css' './dist' && cpy './compiled/**/*.d.ts' './dist'",
|
|
17
|
+
"storybook": "storybook dev -p 9001 --no-open",
|
|
18
|
+
"build-storybook": "storybook build"
|
|
19
|
+
},
|
|
20
|
+
"browserslist": [
|
|
21
|
+
"extends @memnrev/browserslist-config"
|
|
22
|
+
],
|
|
23
|
+
"jest": {
|
|
24
|
+
"verbose": false,
|
|
25
|
+
"coverageDirectory": "coverage",
|
|
26
|
+
"transform": {
|
|
27
|
+
"^.+\\.(ts|js)$": [
|
|
28
|
+
"<rootDir>/node_modules/ts-jest",
|
|
29
|
+
{
|
|
30
|
+
"useESM": true
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"transformIgnorePatterns": [
|
|
35
|
+
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$"
|
|
36
|
+
],
|
|
37
|
+
"extensionsToTreatAsEsm": [
|
|
38
|
+
".ts"
|
|
39
|
+
],
|
|
40
|
+
"moduleNameMapper": {
|
|
41
|
+
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@babel/cli": "^7.22.15",
|
|
46
|
+
"@babel/core": "^7.22.17",
|
|
47
|
+
"@babel/preset-env": "^7.22.15",
|
|
48
|
+
"@memnrev/browserslist-config": "^0.1.0",
|
|
49
|
+
"@memnrev/eslint-v9-config": "^0.1.1",
|
|
50
|
+
"@storybook/addon-essentials": "^8.0.5",
|
|
51
|
+
"@storybook/addon-interactions": "^8.0.5",
|
|
52
|
+
"@storybook/addon-links": "^8.0.5",
|
|
53
|
+
"@storybook/addon-onboarding": "^8.0.5",
|
|
54
|
+
"@storybook/addon-webpack5-compiler-babel": "^3.0.3",
|
|
55
|
+
"@storybook/blocks": "^8.0.5",
|
|
56
|
+
"@storybook/react": "^8.0.5",
|
|
57
|
+
"@storybook/react-webpack5": "^8.0.5",
|
|
58
|
+
"@storybook/test": "^8.0.5",
|
|
59
|
+
"@types/jest": "^29.5.4",
|
|
60
|
+
"@types/react": "^19.0.2",
|
|
61
|
+
"cpy-cli": "^5.0.0",
|
|
62
|
+
"del-cli": "^6.0.0",
|
|
63
|
+
"eslint": "^9.8.0",
|
|
64
|
+
"eslint-plugin-storybook": "^0.11.0",
|
|
65
|
+
"jest": "^29.4.1",
|
|
66
|
+
"react": "^19.0.0",
|
|
67
|
+
"react-dom": "^19.0.0",
|
|
68
|
+
"storybook": "^8.0.5",
|
|
69
|
+
"stylelint": "^16.2.1",
|
|
70
|
+
"ts-jest": "^29.1.1",
|
|
71
|
+
"ts-loader": "^9.4.4",
|
|
72
|
+
"typescript": "^5.2.2"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"react": ">= 19 < 20",
|
|
76
|
+
"react-dom": ">= 19 < 20"
|
|
77
|
+
}
|
|
78
|
+
}
|