Skip to content

Commit 90df2c0

Browse files
authored
docs(patterns): add SelectionAssitant pattern (#7420)
1 parent 704a312 commit 90df2c0

File tree

8 files changed

+600
-5
lines changed

8 files changed

+600
-5
lines changed

.storybook/components/Import.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ interface ImportStatementPropTypes {
1010
* Package name (e.g. "@ui5/webcomponents-react")
1111
*/
1212
packageName: string;
13+
/**
14+
* Defines if it's a named or default import.
15+
*
16+
* __Note:__ If `true`, only a single `moduleName` is supported.
17+
*/
18+
defaultImport?: boolean;
1319
}
1420
interface DeepPath {
1521
path: string;
@@ -37,7 +43,7 @@ function FromPath({ packageName, deepPath }: FromPathPropTypes) {
3743

3844
FromPath.displayName = 'FromPath';
3945

40-
export const ImportStatement = ({ moduleNames, packageName }: ImportStatementPropTypes) => {
46+
export const ImportStatement = ({ moduleNames, packageName, defaultImport }: ImportStatementPropTypes) => {
4147
if (!moduleNames) {
4248
return null;
4349
}
@@ -68,8 +74,7 @@ export const ImportStatement = ({ moduleNames, packageName }: ImportStatementPro
6874
if (!deepPath) {
6975
return (
7076
<span style={{ fontSize: '14px' }} key="0">
71-
{' '}
72-
{'{'}
77+
{!defaultImport && ' {'}
7378
{moduleNames.length > 2 ? (
7479
<>
7580
{moduleNames.map((item) => {
@@ -86,7 +91,7 @@ export const ImportStatement = ({ moduleNames, packageName }: ImportStatementPro
8691
) : (
8792
<>&nbsp;{moduleNames.join(', ')}&nbsp;</>
8893
)}
89-
{'}'}{' '}
94+
{!defaultImport && '} '}
9095
</span>
9196
);
9297
} else {

.storybook/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ const config: StorybookConfig = {
7070
files: '**/*.@(mdx|stories.@(mdx|js|jsx|mjs|ts|tsx))',
7171
titlePrefix: 'Legacy Components',
7272
},
73+
{
74+
directory: '../patterns',
75+
files: '**/*.@(mdx|stories.@(mdx|tsx))',
76+
titlePrefix: 'Patterns',
77+
},
7378
],
7479
addons,
7580
typescript: {

.storybook/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"compilerOptions": {
44
"allowJs": true
55
},
6-
"include": ["./**/*.js", "./**/*.tsx", "./**/*.ts"]
6+
"include": ["./**/*.js", "./**/*.tsx", "./**/*.ts", "../patterns/selection-assistant"]
77
}

patterns/Docs.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {Footer} from '@sb/components';
2+
import {Meta} from '@storybook/blocks';
3+
4+
<Meta title="Docs"/>
5+
6+
# Patterns
7+
8+
This section describes (design) patterns you can use in your application.
9+
10+
- [UXC Integration: Navigation Layout pattern](/docs/patterns-uxc-integration--docs)
11+
- [Selection Assistant](/patterns-selectionassistant-experimental--docs)
12+
13+
## UI5 Web Components Patterns
14+
15+
Discover additional patterns built with standard UI5 Web Components: https://sap.github.io/ui5-webcomponents/components/patterns/
16+
17+
<Footer/>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import getElementSelection from '@ui5/webcomponents-base/dist/util/SelectionAssistant.js';
2+
import ai from '@ui5/webcomponents-icons/dist/ai.js';
3+
import type { ButtonDomRef, InputPropTypes } from '@ui5/webcomponents-react';
4+
import { Button, Input, Label, Toast } from '@ui5/webcomponents-react';
5+
import type { CSSProperties } from 'react';
6+
import { useRef, useState } from 'react';
7+
import { SelectionAssistantContainer } from '@/patterns/selection-assistant/SelectionAssistantContainer.js';
8+
9+
type ElementSelection = ReturnType<typeof getElementSelection>;
10+
11+
export function InputSelectionAssistant() {
12+
const containerRef = useRef<HTMLDivElement>(null); // Needed to calculate container-relative coordinates
13+
const buttonRef = useRef<ButtonDomRef>(null);
14+
const [showBtn, setShowBtn] = useState(false);
15+
const [btnStyle, setBtnStyle] = useState<CSSProperties>({});
16+
const [toastText, setToastText] = useState('');
17+
const [toastOpen, setToastOpen] = useState(false);
18+
19+
const repositionButtonAtInput = (inputRect: DOMRect, containerRect: DOMRect) => {
20+
setBtnStyle({
21+
// Subtract containerRect to make coordinates relative to the container
22+
// This subtraction is only needed if the container is not the viewport
23+
// If positioned relative to body (i.e., viewport), you can use inputRect.left/top directly
24+
left: `${inputRect.left - containerRect.left + inputRect.width + 4}px`,
25+
top: `${inputRect.top - containerRect.top}px`,
26+
});
27+
setShowBtn(true);
28+
};
29+
30+
const repositionButtonAtSelection = (selectionRect: ElementSelection, containerRect: DOMRect) => {
31+
setBtnStyle({
32+
// If positioned relative to body (i.e., viewport), you can use selectionRect.left/top directly
33+
left: `${selectionRect.left - containerRect.left + selectionRect.width}px`,
34+
top: `${selectionRect.top - containerRect.top + selectionRect.height}px`,
35+
});
36+
setShowBtn(true);
37+
};
38+
39+
const handleSelect: InputPropTypes['onSelect'] = (e) => {
40+
const target = e.currentTarget;
41+
const selectionRect = getElementSelection(target);
42+
const inputRect = target.getBoundingClientRect();
43+
const containerRect = containerRef.current?.getBoundingClientRect();
44+
45+
if (!containerRect) return;
46+
47+
// If the selected text overflows the visible input, position near full input instead
48+
if (selectionRect.bottom > inputRect.bottom || selectionRect.right > inputRect.right) {
49+
repositionButtonAtInput(inputRect, containerRect);
50+
} else {
51+
repositionButtonAtSelection(selectionRect, containerRect);
52+
}
53+
};
54+
55+
const handleNativeSelect: InputPropTypes['onSelect'] = (e) => {
56+
const target = e.currentTarget;
57+
const inputRect = target.getBoundingClientRect();
58+
const containerRect = containerRef.current?.getBoundingClientRect();
59+
60+
repositionButtonAtInput(inputRect, containerRect);
61+
};
62+
63+
const handleBtnClick = () => {
64+
const selectedText = document.getSelection().toString();
65+
const message = `The selected text equals to: "${selectedText}"`;
66+
67+
setToastText(message);
68+
setToastOpen(true);
69+
};
70+
71+
return (
72+
<SelectionAssistantContainer ref={containerRef}>
73+
<Label for="ai-input" showColon>
74+
Input with Selection Assistant
75+
</Label>
76+
<Input
77+
id="ai-input"
78+
value="Ipsum enim esse ipsum cupidatat ex veniam labore quis irure. Eiusmod labore anim anim nulla aute ut."
79+
onSelect={handleSelect}
80+
onMouseDown={() => {
81+
setShowBtn(false);
82+
}}
83+
onScroll={() => {
84+
setShowBtn(false);
85+
}}
86+
onBlur={(e) => {
87+
if (e.relatedTarget !== buttonRef.current) {
88+
setShowBtn(false);
89+
}
90+
}}
91+
/>
92+
<br />
93+
<Label for="ai-native-input" showColon>
94+
Input with native API
95+
</Label>
96+
<Input
97+
id="ai-native-input"
98+
value="Ipsum enim esse ipsum cupidatat ex veniam labore quis irure. Eiusmod labore anim anim nulla aute ut."
99+
onSelect={handleNativeSelect}
100+
/>
101+
{showBtn && (
102+
<Button
103+
// This button is positioned absolute inside a relative container
104+
// If instead it were in `body`, containerRect adjustments wouldn't be needed
105+
style={{ position: 'absolute', zIndex: 2, ...btnStyle }}
106+
ref={buttonRef}
107+
icon={ai}
108+
onClick={handleBtnClick}
109+
onBlur={() => {
110+
setShowBtn(false);
111+
}}
112+
/>
113+
)}
114+
<Toast
115+
open={toastOpen}
116+
onClose={() => {
117+
setToastOpen(false);
118+
}}
119+
>
120+
{toastText}
121+
</Toast>
122+
</SelectionAssistantContainer>
123+
);
124+
}
125+
126+
InputSelectionAssistant.displayName = 'InputSelectionAssistant';

0 commit comments

Comments
 (0)