@pyreon/elements 0.11.1 → 0.11.3
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/package.json +8 -7
- package/src/Element/component.tsx +211 -0
- package/src/Element/constants.ts +96 -0
- package/src/Element/index.ts +6 -0
- package/src/Element/types.ts +168 -0
- package/src/Element/utils.ts +15 -0
- package/src/List/component.tsx +57 -0
- package/src/List/index.ts +5 -0
- package/src/Overlay/component.tsx +131 -0
- package/src/Overlay/context.tsx +37 -0
- package/src/Overlay/index.ts +7 -0
- package/src/Overlay/useOverlay.tsx +616 -0
- package/src/Portal/component.tsx +41 -0
- package/src/Portal/index.ts +5 -0
- package/src/Text/component.tsx +65 -0
- package/src/Text/index.ts +5 -0
- package/src/Text/styled.ts +30 -0
- package/src/Util/component.tsx +43 -0
- package/src/Util/index.ts +5 -0
- package/src/__tests__/Content.test.tsx +115 -0
- package/src/__tests__/Element.test.ts +604 -0
- package/src/__tests__/Iterator.test.ts +483 -0
- package/src/__tests__/List.test.ts +199 -0
- package/src/__tests__/Overlay.test.ts +485 -0
- package/src/__tests__/Portal.test.ts +82 -0
- package/src/__tests__/Text.test.ts +274 -0
- package/src/__tests__/Util.test.ts +63 -0
- package/src/__tests__/Wrapper.test.tsx +152 -0
- package/src/__tests__/equalBeforeAfter.test.ts +122 -0
- package/src/__tests__/helpers.test.ts +65 -0
- package/src/__tests__/overlayContext.test.tsx +78 -0
- package/src/__tests__/responsiveProps.test.ts +298 -0
- package/src/__tests__/useOverlay.test.ts +1330 -0
- package/src/__tests__/utils.test.ts +69 -0
- package/src/constants.ts +1 -0
- package/src/helpers/Content/component.tsx +51 -0
- package/src/helpers/Content/index.ts +3 -0
- package/src/helpers/Content/styled.ts +105 -0
- package/src/helpers/Content/types.ts +49 -0
- package/src/helpers/Iterator/component.tsx +252 -0
- package/src/helpers/Iterator/index.ts +13 -0
- package/src/helpers/Iterator/types.ts +79 -0
- package/src/helpers/Wrapper/component.tsx +78 -0
- package/src/helpers/Wrapper/constants.ts +10 -0
- package/src/helpers/Wrapper/index.ts +3 -0
- package/src/helpers/Wrapper/styled.ts +69 -0
- package/src/helpers/Wrapper/types.ts +56 -0
- package/src/helpers/Wrapper/utils.ts +7 -0
- package/src/helpers/index.ts +4 -0
- package/src/index.ts +37 -0
- package/src/types.ts +81 -0
- package/src/utils.ts +1 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { getShouldBeEmpty, isInlineElement } from "../Element/utils"
|
|
3
|
+
import { isWebFixNeeded } from "../helpers/Wrapper/utils"
|
|
4
|
+
|
|
5
|
+
describe("isInlineElement", () => {
|
|
6
|
+
it("returns true for inline elements", () => {
|
|
7
|
+
expect(isInlineElement("span")).toBe(true)
|
|
8
|
+
expect(isInlineElement("a")).toBe(true)
|
|
9
|
+
expect(isInlineElement("button")).toBe(true)
|
|
10
|
+
expect(isInlineElement("input")).toBe(true)
|
|
11
|
+
expect(isInlineElement("label")).toBe(true)
|
|
12
|
+
expect(isInlineElement("strong")).toBe(true)
|
|
13
|
+
expect(isInlineElement("em")).toBe(true)
|
|
14
|
+
expect(isInlineElement("img")).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("returns false for block elements", () => {
|
|
18
|
+
expect(isInlineElement("div")).toBe(false)
|
|
19
|
+
expect(isInlineElement("p")).toBe(false)
|
|
20
|
+
expect(isInlineElement("section")).toBe(false)
|
|
21
|
+
expect(isInlineElement("header")).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("returns false for undefined", () => {
|
|
25
|
+
expect(isInlineElement(undefined)).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("returns false for empty string", () => {
|
|
29
|
+
expect(isInlineElement("")).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("getShouldBeEmpty", () => {
|
|
34
|
+
it("returns true for void elements", () => {
|
|
35
|
+
expect(getShouldBeEmpty("br")).toBe(true)
|
|
36
|
+
expect(getShouldBeEmpty("img")).toBe(true)
|
|
37
|
+
expect(getShouldBeEmpty("input")).toBe(true)
|
|
38
|
+
expect(getShouldBeEmpty("hr")).toBe(true)
|
|
39
|
+
expect(getShouldBeEmpty("embed")).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("returns false for non-void elements", () => {
|
|
43
|
+
expect(getShouldBeEmpty("div")).toBe(false)
|
|
44
|
+
expect(getShouldBeEmpty("span")).toBe(false)
|
|
45
|
+
expect(getShouldBeEmpty("p")).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("returns false for undefined", () => {
|
|
49
|
+
expect(getShouldBeEmpty(undefined)).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe("isWebFixNeeded", () => {
|
|
54
|
+
it("returns true for button, fieldset, legend", () => {
|
|
55
|
+
expect(isWebFixNeeded("button")).toBe(true)
|
|
56
|
+
expect(isWebFixNeeded("fieldset")).toBe(true)
|
|
57
|
+
expect(isWebFixNeeded("legend")).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("returns false for other elements", () => {
|
|
61
|
+
expect(isWebFixNeeded("div")).toBe(false)
|
|
62
|
+
expect(isWebFixNeeded("span")).toBe(false)
|
|
63
|
+
expect(isWebFixNeeded("input")).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("returns false for undefined", () => {
|
|
67
|
+
expect(isWebFixNeeded(undefined)).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
})
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PKG_NAME = "@pyreon/elements" as const
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content area used inside Element to render one of the three
|
|
3
|
+
* layout slots (before, content, after). Passes alignment, direction,
|
|
4
|
+
* gap, and equalCols styling props to the underlying styled component.
|
|
5
|
+
* Adds a `data-pyr-element` attribute in development for debugging.
|
|
6
|
+
*
|
|
7
|
+
* Children are rendered via core `render()`.
|
|
8
|
+
*/
|
|
9
|
+
import { render } from "@pyreon/ui-core"
|
|
10
|
+
import { IS_DEVELOPMENT } from "../../utils"
|
|
11
|
+
import Styled from "./styled"
|
|
12
|
+
import type { Props } from "./types"
|
|
13
|
+
|
|
14
|
+
const Component = ({
|
|
15
|
+
contentType,
|
|
16
|
+
tag,
|
|
17
|
+
parentDirection,
|
|
18
|
+
direction,
|
|
19
|
+
alignX,
|
|
20
|
+
alignY,
|
|
21
|
+
equalCols,
|
|
22
|
+
gap,
|
|
23
|
+
extendCss,
|
|
24
|
+
children,
|
|
25
|
+
...props
|
|
26
|
+
}: Partial<Props>) => {
|
|
27
|
+
const debugProps = IS_DEVELOPMENT
|
|
28
|
+
? {
|
|
29
|
+
"data-pyr-element": contentType,
|
|
30
|
+
}
|
|
31
|
+
: {}
|
|
32
|
+
|
|
33
|
+
const stylingProps = {
|
|
34
|
+
contentType,
|
|
35
|
+
parentDirection,
|
|
36
|
+
direction,
|
|
37
|
+
alignX,
|
|
38
|
+
alignY,
|
|
39
|
+
equalCols,
|
|
40
|
+
gap,
|
|
41
|
+
extraStyles: extendCss,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Styled as={tag} $contentType={contentType} $element={stylingProps} {...debugProps} {...props}>
|
|
46
|
+
{render(children)}
|
|
47
|
+
</Styled>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default Component
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styled component for content areas (before/content/after). Applies
|
|
3
|
+
* responsive flex alignment, gap spacing between slots based on parent
|
|
4
|
+
* direction (margin-right for inline, margin-bottom for rows), and
|
|
5
|
+
* equalCols flex distribution. The "content" slot gets `flex: 1` to
|
|
6
|
+
* fill remaining space between before and after.
|
|
7
|
+
*/
|
|
8
|
+
import { config } from "@pyreon/ui-core"
|
|
9
|
+
import { alignContent, extendCss, makeItResponsive, value } from "@pyreon/unistyle"
|
|
10
|
+
import type { ResponsiveStylesCallback } from "../../types"
|
|
11
|
+
import type { StyledProps, ThemeProps } from "./types"
|
|
12
|
+
|
|
13
|
+
const { styled, css, component } = config
|
|
14
|
+
|
|
15
|
+
const equalColsCSS = `
|
|
16
|
+
flex: 1;
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
const typeContentCSS = `
|
|
20
|
+
flex: 1;
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
// --------------------------------------------------------
|
|
24
|
+
// calculate spacing between before / content / after
|
|
25
|
+
// --------------------------------------------------------
|
|
26
|
+
const gapDimensions = {
|
|
27
|
+
inline: {
|
|
28
|
+
before: "margin-right",
|
|
29
|
+
after: "margin-left",
|
|
30
|
+
},
|
|
31
|
+
reverseInline: {
|
|
32
|
+
before: "margin-right",
|
|
33
|
+
after: "margin-left",
|
|
34
|
+
},
|
|
35
|
+
rows: {
|
|
36
|
+
before: "margin-bottom",
|
|
37
|
+
after: "margin-top",
|
|
38
|
+
},
|
|
39
|
+
reverseRows: {
|
|
40
|
+
before: "margin-bottom",
|
|
41
|
+
after: "margin-top",
|
|
42
|
+
},
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
const calculateGap = ({
|
|
46
|
+
direction,
|
|
47
|
+
type,
|
|
48
|
+
value: gapValue,
|
|
49
|
+
}: {
|
|
50
|
+
direction: keyof typeof gapDimensions
|
|
51
|
+
type: ThemeProps["contentType"]
|
|
52
|
+
value: string | number | null | undefined
|
|
53
|
+
}) => {
|
|
54
|
+
if (!direction || !type || type === "content") return undefined
|
|
55
|
+
|
|
56
|
+
const finalStyles = `${gapDimensions[direction][type]}: ${gapValue};`
|
|
57
|
+
|
|
58
|
+
return finalStyles
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --------------------------------------------------------
|
|
62
|
+
// calculations of styles to be rendered
|
|
63
|
+
// --------------------------------------------------------
|
|
64
|
+
const styles: ResponsiveStylesCallback = ({ css: cssFn, theme: t, rootSize }) => cssFn`
|
|
65
|
+
${alignContent({
|
|
66
|
+
direction: t.direction,
|
|
67
|
+
alignX: t.alignX,
|
|
68
|
+
alignY: t.alignY,
|
|
69
|
+
})};
|
|
70
|
+
|
|
71
|
+
${t.equalCols && equalColsCSS};
|
|
72
|
+
|
|
73
|
+
${
|
|
74
|
+
t.gap &&
|
|
75
|
+
t.contentType &&
|
|
76
|
+
calculateGap({
|
|
77
|
+
direction: t.parentDirection,
|
|
78
|
+
type: t.contentType,
|
|
79
|
+
value: value(t.gap, rootSize),
|
|
80
|
+
})
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
${t.extraStyles && extendCss(t.extraStyles as Parameters<typeof extendCss>[0])};
|
|
84
|
+
`
|
|
85
|
+
|
|
86
|
+
const platformCSS = `box-sizing: border-box;`
|
|
87
|
+
|
|
88
|
+
const StyledComponent = styled(component)`
|
|
89
|
+
${platformCSS};
|
|
90
|
+
|
|
91
|
+
display: flex;
|
|
92
|
+
align-self: stretch;
|
|
93
|
+
flex-wrap: wrap;
|
|
94
|
+
|
|
95
|
+
${(({ $contentType }: StyledProps) => $contentType === "content" && typeContentCSS) as any};
|
|
96
|
+
|
|
97
|
+
${makeItResponsive({
|
|
98
|
+
key: "$element",
|
|
99
|
+
styles,
|
|
100
|
+
css,
|
|
101
|
+
normalize: true,
|
|
102
|
+
})};
|
|
103
|
+
`
|
|
104
|
+
|
|
105
|
+
export default StyledComponent
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HTMLTags } from "@pyreon/ui-core"
|
|
2
|
+
import type {
|
|
3
|
+
AlignX,
|
|
4
|
+
AlignY,
|
|
5
|
+
Content,
|
|
6
|
+
ContentAlignX,
|
|
7
|
+
ContentAlignY,
|
|
8
|
+
ContentBoolean,
|
|
9
|
+
ContentDirection,
|
|
10
|
+
ContentSimpleValue,
|
|
11
|
+
Css,
|
|
12
|
+
Direction,
|
|
13
|
+
ExtendCss,
|
|
14
|
+
Responsive,
|
|
15
|
+
ResponsiveBoolType,
|
|
16
|
+
} from "../../types"
|
|
17
|
+
|
|
18
|
+
export interface Props {
|
|
19
|
+
parentDirection: Direction | undefined
|
|
20
|
+
gap: Responsive | undefined
|
|
21
|
+
contentType: "before" | "content" | "after" | undefined
|
|
22
|
+
children: Content
|
|
23
|
+
tag: HTMLTags | undefined
|
|
24
|
+
direction: Direction | undefined
|
|
25
|
+
alignX: AlignX | undefined
|
|
26
|
+
alignY: AlignY | undefined
|
|
27
|
+
equalCols: ResponsiveBoolType | undefined
|
|
28
|
+
extendCss: ExtendCss | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface StyledProps {
|
|
32
|
+
$element: Pick<
|
|
33
|
+
Props,
|
|
34
|
+
"contentType" | "parentDirection" | "direction" | "alignX" | "alignY" | "equalCols" | "gap"
|
|
35
|
+
> & {
|
|
36
|
+
extraStyles: Props["extendCss"]
|
|
37
|
+
}
|
|
38
|
+
$contentType: Props["contentType"]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ThemeProps = Pick<Props, "contentType"> & {
|
|
42
|
+
parentDirection: ContentDirection
|
|
43
|
+
direction: ContentDirection
|
|
44
|
+
alignX: ContentAlignX
|
|
45
|
+
alignY: ContentAlignY
|
|
46
|
+
equalCols?: ContentBoolean
|
|
47
|
+
gap?: ContentSimpleValue
|
|
48
|
+
extraStyles?: Css
|
|
49
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-driven list renderer that supports three input modes: children,
|
|
3
|
+
* an array of primitives, or an array of objects.
|
|
4
|
+
* Each item receives positional metadata (first, last, odd, even, position)
|
|
5
|
+
* and optional injected props via `itemProps`. Items can be individually
|
|
6
|
+
* wrapped with `wrapComponent`. Children always take priority over the
|
|
7
|
+
* component+data prop pattern.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { VNode, VNodeChild } from "@pyreon/core"
|
|
11
|
+
import { Fragment } from "@pyreon/core"
|
|
12
|
+
import { isEmpty, render } from "@pyreon/ui-core"
|
|
13
|
+
import type { ExtendedProps, ObjectValue, Props, SimpleValue } from "./types"
|
|
14
|
+
|
|
15
|
+
type ClassifiedData =
|
|
16
|
+
| { type: "simple"; data: SimpleValue[] }
|
|
17
|
+
| { type: "complex"; data: ObjectValue[] }
|
|
18
|
+
| null
|
|
19
|
+
|
|
20
|
+
const classifyData = (data: unknown[]): ClassifiedData => {
|
|
21
|
+
const items = data.filter(
|
|
22
|
+
(item) =>
|
|
23
|
+
item != null && !(typeof item === "object" && isEmpty(item as Record<string, unknown>)),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if (items.length === 0) return null
|
|
27
|
+
|
|
28
|
+
let isSimple = true
|
|
29
|
+
let isComplex = true
|
|
30
|
+
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
33
|
+
isComplex = false
|
|
34
|
+
} else if (typeof item === "object") {
|
|
35
|
+
isSimple = false
|
|
36
|
+
} else {
|
|
37
|
+
isSimple = false
|
|
38
|
+
isComplex = false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isSimple) return { type: "simple", data: items as SimpleValue[] }
|
|
43
|
+
if (isComplex) return { type: "complex", data: items as ObjectValue[] }
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const RESERVED_PROPS = [
|
|
48
|
+
"children",
|
|
49
|
+
"component",
|
|
50
|
+
"wrapComponent",
|
|
51
|
+
"data",
|
|
52
|
+
"itemKey",
|
|
53
|
+
"valueName",
|
|
54
|
+
"itemProps",
|
|
55
|
+
"wrapProps",
|
|
56
|
+
] as const
|
|
57
|
+
|
|
58
|
+
type AttachItemProps = ({ i, length }: { i: number; length: number }) => ExtendedProps
|
|
59
|
+
|
|
60
|
+
const attachItemProps: AttachItemProps = ({ i, length }: { i: number; length: number }) => {
|
|
61
|
+
const position = i + 1
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
index: i,
|
|
65
|
+
first: position === 1,
|
|
66
|
+
last: position === length,
|
|
67
|
+
odd: position % 2 === 1,
|
|
68
|
+
even: position % 2 === 0,
|
|
69
|
+
position,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const Component = (props: Props) => {
|
|
74
|
+
const {
|
|
75
|
+
itemKey,
|
|
76
|
+
valueName,
|
|
77
|
+
children,
|
|
78
|
+
component,
|
|
79
|
+
data,
|
|
80
|
+
wrapComponent: Wrapper,
|
|
81
|
+
wrapProps,
|
|
82
|
+
itemProps,
|
|
83
|
+
} = props
|
|
84
|
+
|
|
85
|
+
const injectItemProps = typeof itemProps === "function" ? itemProps : () => itemProps
|
|
86
|
+
|
|
87
|
+
const injectWrapItemProps = typeof wrapProps === "function" ? wrapProps : () => wrapProps
|
|
88
|
+
|
|
89
|
+
const getKey = (item: string | number, index: number) => {
|
|
90
|
+
if (typeof itemKey === "function") return itemKey(item, index)
|
|
91
|
+
return index
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const renderChild = (child: VNodeChild, total = 1, i = 0) => {
|
|
95
|
+
if (!itemProps && !Wrapper) return child
|
|
96
|
+
|
|
97
|
+
const extendedProps = attachItemProps({
|
|
98
|
+
i,
|
|
99
|
+
length: total,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const finalItemProps = itemProps ? injectItemProps({}, extendedProps) : {}
|
|
103
|
+
|
|
104
|
+
if (Wrapper) {
|
|
105
|
+
const finalWrapProps = wrapProps ? injectWrapItemProps({}, extendedProps) : {}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Wrapper key={i} {...finalWrapProps}>
|
|
109
|
+
{render(child, finalItemProps)}
|
|
110
|
+
</Wrapper>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return render(child, {
|
|
115
|
+
key: i,
|
|
116
|
+
...finalItemProps,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --------------------------------------------------------
|
|
121
|
+
// render children
|
|
122
|
+
// --------------------------------------------------------
|
|
123
|
+
const renderChildren = () => {
|
|
124
|
+
if (!children) return null
|
|
125
|
+
|
|
126
|
+
// if children is Array
|
|
127
|
+
if (Array.isArray(children)) {
|
|
128
|
+
return children.map((item, i) => renderChild(item, children.length, i))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// if children is Fragment — check VNode type
|
|
132
|
+
if (
|
|
133
|
+
typeof children === "object" &&
|
|
134
|
+
"type" in (children as VNode) &&
|
|
135
|
+
(children as VNode).type === Fragment
|
|
136
|
+
) {
|
|
137
|
+
const fragmentChildren = (children as VNode).children as VNodeChild[]
|
|
138
|
+
const childrenLength = fragmentChildren.length
|
|
139
|
+
|
|
140
|
+
return fragmentChildren.map((item, i) => renderChild(item, childrenLength, i))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// if single child
|
|
144
|
+
return renderChild(children)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --------------------------------------------------------
|
|
148
|
+
// render array of strings or numbers
|
|
149
|
+
// --------------------------------------------------------
|
|
150
|
+
const renderSimpleArray = (simpleData: SimpleValue[]) => {
|
|
151
|
+
const { length } = simpleData
|
|
152
|
+
|
|
153
|
+
if (length === 0) return null
|
|
154
|
+
|
|
155
|
+
return simpleData.map((item, i) => {
|
|
156
|
+
const key = getKey(item, i)
|
|
157
|
+
const keyName = valueName ?? "children"
|
|
158
|
+
const extendedProps = attachItemProps({
|
|
159
|
+
i,
|
|
160
|
+
length,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const finalItemProps = {
|
|
164
|
+
...(itemProps ? injectItemProps({ [keyName]: item }, extendedProps) : {}),
|
|
165
|
+
[keyName]: item,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (Wrapper) {
|
|
169
|
+
const finalWrapProps = wrapProps
|
|
170
|
+
? injectWrapItemProps({ [keyName]: item }, extendedProps)
|
|
171
|
+
: {}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Wrapper key={key} {...finalWrapProps}>
|
|
175
|
+
{render(component, finalItemProps)}
|
|
176
|
+
</Wrapper>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return render(component, { key, ...finalItemProps })
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --------------------------------------------------------
|
|
185
|
+
// render array of objects
|
|
186
|
+
// --------------------------------------------------------
|
|
187
|
+
const getObjectKey = (item: ObjectValue, index: number) => {
|
|
188
|
+
if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index
|
|
189
|
+
if (typeof itemKey === "function") return itemKey(item, index)
|
|
190
|
+
if (typeof itemKey === "string") return item[itemKey]
|
|
191
|
+
|
|
192
|
+
return index
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const renderComplexArray = (complexData: ObjectValue[]) => {
|
|
196
|
+
const { length } = complexData
|
|
197
|
+
|
|
198
|
+
if (length === 0) return null
|
|
199
|
+
|
|
200
|
+
return complexData.map((item, i) => {
|
|
201
|
+
const { component: itemComponent, ...restItem } = item
|
|
202
|
+
const renderItem = itemComponent ?? component
|
|
203
|
+
const key = getObjectKey(restItem, i)
|
|
204
|
+
const extendedProps = attachItemProps({
|
|
205
|
+
i,
|
|
206
|
+
length,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const finalItemProps = {
|
|
210
|
+
...(itemProps ? injectItemProps(item, extendedProps) : {}),
|
|
211
|
+
...restItem,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (Wrapper && !itemComponent) {
|
|
215
|
+
const finalWrapProps = wrapProps ? injectWrapItemProps(item, extendedProps) : {}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Wrapper key={key} {...finalWrapProps}>
|
|
219
|
+
{render(renderItem, finalItemProps)}
|
|
220
|
+
</Wrapper>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return render(renderItem, { key, ...finalItemProps })
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --------------------------------------------------------
|
|
229
|
+
// render list items
|
|
230
|
+
// --------------------------------------------------------
|
|
231
|
+
const renderItems = (): VNodeChild => {
|
|
232
|
+
// children have priority over props component + data
|
|
233
|
+
if (children) return renderChildren() as VNodeChild
|
|
234
|
+
|
|
235
|
+
// render props component + data
|
|
236
|
+
if (component && Array.isArray(data)) {
|
|
237
|
+
const classified = classifyData(data)
|
|
238
|
+
if (!classified) return null
|
|
239
|
+
if (classified.type === "simple") return renderSimpleArray(classified.data) as VNodeChild
|
|
240
|
+
return renderComplexArray(classified.data) as VNodeChild
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return renderItems()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export default Object.assign(Component, {
|
|
250
|
+
isIterator: true as const,
|
|
251
|
+
RESERVED_PROPS,
|
|
252
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import component from "./component"
|
|
2
|
+
import type {
|
|
3
|
+
ElementType,
|
|
4
|
+
ExtendedProps,
|
|
5
|
+
ObjectValue,
|
|
6
|
+
Props,
|
|
7
|
+
PropsCallback,
|
|
8
|
+
SimpleValue,
|
|
9
|
+
} from "./types"
|
|
10
|
+
|
|
11
|
+
export type { ElementType, ExtendedProps, ObjectValue, Props, PropsCallback, SimpleValue }
|
|
12
|
+
|
|
13
|
+
export default component
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ComponentFn, VNodeChild } from "@pyreon/core"
|
|
2
|
+
import type { HTMLTags } from "@pyreon/ui-core"
|
|
3
|
+
|
|
4
|
+
export type MaybeNull = undefined | null
|
|
5
|
+
export type TObj = Record<string, unknown>
|
|
6
|
+
export type SimpleValue = string | number
|
|
7
|
+
export type ObjectValue = Partial<{
|
|
8
|
+
id: SimpleValue
|
|
9
|
+
key: SimpleValue
|
|
10
|
+
itemId: SimpleValue
|
|
11
|
+
component: ElementType
|
|
12
|
+
}> &
|
|
13
|
+
Record<string, unknown>
|
|
14
|
+
|
|
15
|
+
export type ElementType<T extends Record<string, unknown> = any> = ComponentFn<T> | HTMLTags
|
|
16
|
+
|
|
17
|
+
export type ExtendedProps = {
|
|
18
|
+
index: number
|
|
19
|
+
first: boolean
|
|
20
|
+
last: boolean
|
|
21
|
+
odd: boolean
|
|
22
|
+
even: boolean
|
|
23
|
+
position: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PropsCallback =
|
|
27
|
+
| TObj
|
|
28
|
+
| ((
|
|
29
|
+
itemProps: Record<string, never> | Record<string, SimpleValue> | ObjectValue,
|
|
30
|
+
extendedProps: ExtendedProps,
|
|
31
|
+
) => TObj)
|
|
32
|
+
|
|
33
|
+
export type Props = Partial<{
|
|
34
|
+
/**
|
|
35
|
+
* Valid children
|
|
36
|
+
*/
|
|
37
|
+
children: VNodeChild
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Array of data passed to `component` prop
|
|
41
|
+
*/
|
|
42
|
+
data: Array<SimpleValue | ObjectValue | MaybeNull>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A component to be rendered within list
|
|
46
|
+
*/
|
|
47
|
+
component: ElementType
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Defines name of the prop to be passed to the iteration component
|
|
51
|
+
* when **data** prop is type of `string[]`, `number[]` or combination
|
|
52
|
+
* of both. Otherwise ignored.
|
|
53
|
+
*/
|
|
54
|
+
valueName: string
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A component to be rendered within list. `wrapComponent`
|
|
58
|
+
* wraps `component`. Therefore it can be used to enhance the behavior
|
|
59
|
+
* of the list component
|
|
60
|
+
*/
|
|
61
|
+
wrapComponent: ElementType
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extension of **item** `component` props to be passed
|
|
65
|
+
*/
|
|
66
|
+
itemProps: PropsCallback
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extension of **item** `wrapComponent` props to be passed
|
|
70
|
+
*/
|
|
71
|
+
wrapProps?: PropsCallback
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extension of **item** `wrapComponent` props to be passed
|
|
75
|
+
*/
|
|
76
|
+
itemKey?:
|
|
77
|
+
| keyof ObjectValue
|
|
78
|
+
| ((item: SimpleValue | Omit<ObjectValue, "component">, index: number) => SimpleValue)
|
|
79
|
+
}>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper component that serves as the outermost styled container for Element.
|
|
3
|
+
* On web, it detects button/fieldset/legend tags and applies a two-layer flex
|
|
4
|
+
* fix (parent + child Styled) because these HTML elements do not natively
|
|
5
|
+
* support `display: flex` consistently across browsers.
|
|
6
|
+
*/
|
|
7
|
+
import { IS_DEVELOPMENT } from "../../utils"
|
|
8
|
+
import Styled from "./styled"
|
|
9
|
+
import type { Props } from "./types"
|
|
10
|
+
import { isWebFixNeeded } from "./utils"
|
|
11
|
+
|
|
12
|
+
const DEV_PROPS: Record<string, string> = IS_DEVELOPMENT ? { "data-pyr-element": "Element" } : {}
|
|
13
|
+
|
|
14
|
+
const Component = ({
|
|
15
|
+
children,
|
|
16
|
+
tag,
|
|
17
|
+
block,
|
|
18
|
+
extendCss,
|
|
19
|
+
direction,
|
|
20
|
+
alignX,
|
|
21
|
+
alignY,
|
|
22
|
+
equalCols,
|
|
23
|
+
isInline,
|
|
24
|
+
ref,
|
|
25
|
+
...props
|
|
26
|
+
}: Partial<Props> & { ref?: any }) => {
|
|
27
|
+
const COMMON_PROPS = {
|
|
28
|
+
...props,
|
|
29
|
+
...DEV_PROPS,
|
|
30
|
+
ref,
|
|
31
|
+
as: tag,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const needsFix = !props.dangerouslySetInnerHTML && isWebFixNeeded(tag)
|
|
35
|
+
|
|
36
|
+
const normalElement = {
|
|
37
|
+
block,
|
|
38
|
+
direction,
|
|
39
|
+
alignX,
|
|
40
|
+
alignY,
|
|
41
|
+
equalCols,
|
|
42
|
+
extraStyles: extendCss,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parentFixElement = {
|
|
46
|
+
parentFix: true as const,
|
|
47
|
+
block,
|
|
48
|
+
extraStyles: extendCss,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const childFixElement = {
|
|
52
|
+
childFix: true as const,
|
|
53
|
+
direction,
|
|
54
|
+
alignX,
|
|
55
|
+
alignY,
|
|
56
|
+
equalCols,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!needsFix) {
|
|
60
|
+
return (
|
|
61
|
+
<Styled {...COMMON_PROPS} $element={normalElement}>
|
|
62
|
+
{children}
|
|
63
|
+
</Styled>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const asTag = isInline ? "span" : "div"
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Styled {...COMMON_PROPS} $element={parentFixElement}>
|
|
71
|
+
<Styled as={asTag} $childFix $element={childFixElement}>
|
|
72
|
+
{children}
|
|
73
|
+
</Styled>
|
|
74
|
+
</Styled>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default Component
|