@charcoal-ui/react-sandbox 1.0.0-alpha.1
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 +201 -0
- package/README.md +18 -0
- package/package.json +72 -0
- package/src/_lib/compat.ts +15 -0
- package/src/components/Carousel/index.story.tsx +86 -0
- package/src/components/Carousel/index.tsx +382 -0
- package/src/components/CarouselButton/index.story.tsx +44 -0
- package/src/components/CarouselButton/index.tsx +162 -0
- package/src/components/Filter/index.story.tsx +80 -0
- package/src/components/Filter/index.tsx +182 -0
- package/src/components/HintText/index.story.tsx +19 -0
- package/src/components/HintText/index.tsx +95 -0
- package/src/components/Layout/index.story.tsx +121 -0
- package/src/components/Layout/index.tsx +363 -0
- package/src/components/LeftMenu/index.tsx +68 -0
- package/src/components/MenuListItem/index.story.tsx +143 -0
- package/src/components/MenuListItem/index.tsx +226 -0
- package/src/components/Pager/index.story.tsx +102 -0
- package/src/components/Pager/index.tsx +255 -0
- package/src/components/Spinner/index.story.tsx +47 -0
- package/src/components/Spinner/index.tsx +86 -0
- package/src/components/SwitchCheckbox/index.story.tsx +32 -0
- package/src/components/SwitchCheckbox/index.tsx +147 -0
- package/src/components/TextEllipsis/helper.ts +57 -0
- package/src/components/TextEllipsis/index.story.tsx +41 -0
- package/src/components/TextEllipsis/index.tsx +35 -0
- package/src/components/WithIcon/index.story.tsx +145 -0
- package/src/components/WithIcon/index.tsx +158 -0
- package/src/components/icons/Base.tsx +75 -0
- package/src/components/icons/DotsIcon.tsx +33 -0
- package/src/components/icons/InfoIcon.tsx +30 -0
- package/src/components/icons/NextIcon.tsx +47 -0
- package/src/components/icons/WedgeIcon.tsx +57 -0
- package/src/foundation/contants.ts +6 -0
- package/src/foundation/hooks.ts +195 -0
- package/src/foundation/support.ts +29 -0
- package/src/foundation/utils.ts +31 -0
- package/src/index.ts +45 -0
- package/src/misc/storybook-helper.ts +17 -0
- package/src/styled.ts +3 -0
- package/src/type.d.ts +12 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions'
|
|
2
|
+
import { boolean } from '@storybook/addon-knobs'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import SwitchCheckbox from '.'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Sandbox/Selection Control/SwitchCheckbox',
|
|
8
|
+
component: SwitchCheckbox,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Default = () => {
|
|
12
|
+
const checked = boolean('checked', false)
|
|
13
|
+
const disabled = boolean('disabled', false)
|
|
14
|
+
const flex = boolean('flex', false)
|
|
15
|
+
const rowReverse = boolean('rowReverse', false)
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<SwitchCheckbox
|
|
19
|
+
defaultChecked={checked}
|
|
20
|
+
flex={flex}
|
|
21
|
+
disabled={disabled}
|
|
22
|
+
rowReverse={rowReverse}
|
|
23
|
+
onChange={action('onChange')}
|
|
24
|
+
>
|
|
25
|
+
label
|
|
26
|
+
</SwitchCheckbox>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const On = () => <SwitchCheckbox checked />
|
|
31
|
+
export const Off = () => <SwitchCheckbox checked={false} />
|
|
32
|
+
export const Disabled = () => <SwitchCheckbox checked disabled />
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import styled, { css } from 'styled-components'
|
|
4
|
+
import { applyEffect } from '@charcoal-ui/utils'
|
|
5
|
+
|
|
6
|
+
export interface Props extends React.ComponentPropsWithoutRef<'input'> {
|
|
7
|
+
gtmClass?: string
|
|
8
|
+
flex?: boolean
|
|
9
|
+
rowReverse?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default React.forwardRef(function SwitchCheckbox(
|
|
13
|
+
{
|
|
14
|
+
gtmClass,
|
|
15
|
+
flex = false,
|
|
16
|
+
rowReverse = false,
|
|
17
|
+
children,
|
|
18
|
+
disabled,
|
|
19
|
+
...props
|
|
20
|
+
}: Props,
|
|
21
|
+
ref: React.Ref<HTMLInputElement>
|
|
22
|
+
) {
|
|
23
|
+
return (
|
|
24
|
+
<Label
|
|
25
|
+
className={gtmClass !== undefined ? `gtm-${gtmClass}` : ''}
|
|
26
|
+
flex={flex}
|
|
27
|
+
rowReverse={rowReverse}
|
|
28
|
+
aria-disabled={disabled}
|
|
29
|
+
>
|
|
30
|
+
<SwitchOuter>
|
|
31
|
+
<SwitchInput {...props} disabled={disabled} ref={ref} />
|
|
32
|
+
<SwitchInner>
|
|
33
|
+
<SwitchInnerKnob />
|
|
34
|
+
</SwitchInner>
|
|
35
|
+
</SwitchOuter>
|
|
36
|
+
{children != null && (
|
|
37
|
+
<Children rowReverse={rowReverse}>{children}</Children>
|
|
38
|
+
)}
|
|
39
|
+
</Label>
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const Children = styled.span<{ rowReverse: boolean }>`
|
|
44
|
+
${(p) =>
|
|
45
|
+
p.rowReverse
|
|
46
|
+
? css`
|
|
47
|
+
margin-right: 8px;
|
|
48
|
+
`
|
|
49
|
+
: css`
|
|
50
|
+
margin-left: 8px;
|
|
51
|
+
`}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const Label = styled.label<{ flex: boolean; rowReverse: boolean }>`
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
${({ flex }) =>
|
|
58
|
+
flex &&
|
|
59
|
+
css`
|
|
60
|
+
display: flex;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
`}
|
|
63
|
+
${({ rowReverse }) =>
|
|
64
|
+
css`
|
|
65
|
+
flex-direction: ${rowReverse ? 'row-reverse' : 'row'};
|
|
66
|
+
`}
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
outline: 0;
|
|
69
|
+
|
|
70
|
+
&[aria-disabled='true'] {
|
|
71
|
+
cursor: auto;
|
|
72
|
+
}
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
const SwitchOuter = styled.span`
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
position: relative;
|
|
78
|
+
z-index: 0;
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
const SwitchInner = styled.div`
|
|
82
|
+
position: relative;
|
|
83
|
+
box-sizing: border-box;
|
|
84
|
+
width: 28px;
|
|
85
|
+
height: 16px;
|
|
86
|
+
border-radius: 16px;
|
|
87
|
+
border: 2px solid transparent;
|
|
88
|
+
background: ${({ theme }) => theme.color.text4};
|
|
89
|
+
transition: box-shadow 0.2s, background-color 0.2s;
|
|
90
|
+
`
|
|
91
|
+
|
|
92
|
+
const SwitchInnerKnob = styled.div`
|
|
93
|
+
position: absolute;
|
|
94
|
+
display: block;
|
|
95
|
+
top: 0;
|
|
96
|
+
left: 0;
|
|
97
|
+
width: 12px;
|
|
98
|
+
height: 12px;
|
|
99
|
+
background-color: ${({ theme }) => theme.color.text5};
|
|
100
|
+
border-radius: 50%;
|
|
101
|
+
transform: translateX(0);
|
|
102
|
+
transition: transform 0.2s;
|
|
103
|
+
`
|
|
104
|
+
|
|
105
|
+
const SwitchInput = styled.input.attrs({
|
|
106
|
+
type: 'checkbox' as string,
|
|
107
|
+
})`
|
|
108
|
+
position: absolute;
|
|
109
|
+
/* NOTE: this is contained by the GraphicCheckboxOuter */
|
|
110
|
+
z-index: 1;
|
|
111
|
+
top: 0;
|
|
112
|
+
left: 0;
|
|
113
|
+
width: 100%;
|
|
114
|
+
height: 100%;
|
|
115
|
+
/* just to control the clickable area if used standalone */
|
|
116
|
+
border-radius: 16px;
|
|
117
|
+
opacity: 0;
|
|
118
|
+
appearance: none;
|
|
119
|
+
outline: none;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
|
|
122
|
+
&:checked {
|
|
123
|
+
~ ${SwitchInner} {
|
|
124
|
+
background-color: ${({ theme }) => theme.color.brand};
|
|
125
|
+
|
|
126
|
+
${SwitchInnerKnob} {
|
|
127
|
+
transform: translateX(12px);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&:disabled {
|
|
133
|
+
cursor: auto;
|
|
134
|
+
|
|
135
|
+
~ ${SwitchInner} {
|
|
136
|
+
opacity: ${({ theme }) => theme.elementEffect.disabled.opacity};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&:not(:disabled):focus {
|
|
141
|
+
~ ${SwitchInner} {
|
|
142
|
+
box-shadow: 0 0 0 4px
|
|
143
|
+
${({ theme }) =>
|
|
144
|
+
applyEffect(theme.color.brand, theme.elementEffect.disabled)};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// https://github.com/fernandopasik/react-children-utilities/blob/971d8a0324e6183734d8d1af9a65dbad18ab3d00/src/lib/onlyText.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
isValidElement,
|
|
6
|
+
ReactElement,
|
|
7
|
+
ReactNode,
|
|
8
|
+
ReactText,
|
|
9
|
+
} from 'react'
|
|
10
|
+
|
|
11
|
+
const hasChildren = (
|
|
12
|
+
element: ReactNode
|
|
13
|
+
): element is ReactElement<{ children: ReactNode[] }> =>
|
|
14
|
+
isValidElement<{ children?: ReactNode[] }>(element) &&
|
|
15
|
+
Boolean(element.props.children)
|
|
16
|
+
|
|
17
|
+
export const childToString = (
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
19
|
+
child?: ReactText | boolean | {} | null
|
|
20
|
+
): string => {
|
|
21
|
+
if (
|
|
22
|
+
typeof child === 'undefined' ||
|
|
23
|
+
child === null ||
|
|
24
|
+
typeof child === 'boolean'
|
|
25
|
+
) {
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (JSON.stringify(child) === '{}') {
|
|
30
|
+
return ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (child as string | number).toString()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const onlyText = (children: ReactNode): string => {
|
|
37
|
+
if (!Array.isArray(children) && !isValidElement(children)) {
|
|
38
|
+
return childToString(children)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Children.toArray(children).reduce(
|
|
42
|
+
(text: string, child: ReactNode): string => {
|
|
43
|
+
let newText = ''
|
|
44
|
+
|
|
45
|
+
if (isValidElement(child) && hasChildren(child)) {
|
|
46
|
+
newText = onlyText(child.props.children)
|
|
47
|
+
} else if (isValidElement(child) && !hasChildren(child)) {
|
|
48
|
+
newText = ''
|
|
49
|
+
} else {
|
|
50
|
+
newText = childToString(child)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return text.concat(newText)
|
|
54
|
+
},
|
|
55
|
+
''
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { number, text } from '@storybook/addon-knobs'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import styled from 'styled-components'
|
|
4
|
+
import { TextEllipsis } from '.'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Sandbox/TextEllipsis',
|
|
8
|
+
component: TextEllipsis,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Default = () => {
|
|
12
|
+
const maxRows = number('maxRaws', 2)
|
|
13
|
+
const contentText = text(
|
|
14
|
+
'text',
|
|
15
|
+
'隴西の李徴は博学才穎、天宝の末年、若くして名を虎榜に連ね、ついで江南尉に補せられたが、性、狷介、自ら恃むところ頗る厚く、賤吏に甘んずるを潔しとしなかった。' +
|
|
16
|
+
'いくばくもなく官を退いた後は、故山、※(「埒のつくり+虎」、第3水準1-91-48)略に帰臥し、人と交を絶って、ひたすら詩作に耽った。' +
|
|
17
|
+
'下吏となって長く膝を俗悪な大官の前に屈するよりは、詩家としての名を死後百年に遺そうとしたのである。' +
|
|
18
|
+
'しかし、文名は容易に揚らず、生活は日を逐うて苦しくなる。' +
|
|
19
|
+
'李徴は漸く焦躁に駆られて来た。この頃からその容貌も峭刻となり、肉落ち骨秀で、眼光のみ徒らに炯々として、曾て進士に登第した頃の豊頬の美少年の俤は、何処に求めようもない。' +
|
|
20
|
+
'数年の後、貧窮に堪えず、妻子の衣食のために遂に節を屈して、再び東へ赴き、一地方官吏の職を奉ずることになった。' +
|
|
21
|
+
'一方、これは、己の詩業に半ば絶望したためでもある。' +
|
|
22
|
+
'曾ての同輩は既に遥か高位に進み、彼が昔、鈍物として歯牙にもかけなかったその連中の下命を拝さねばならぬことが、往年の儁才李徴の自尊心を如何に傷けたかは、想像に難くない。' +
|
|
23
|
+
'彼は怏々として楽しまず、狂悖の性は愈々抑え難くなった。' +
|
|
24
|
+
'一年の後、公用で旅に出、汝水のほとりに宿った時、遂に発狂した。' +
|
|
25
|
+
'或夜半、急に顔色を変えて寝床から起上ると、何か訳の分らぬことを叫びつつそのまま下にとび下りて、闇の中へ駈出した。' +
|
|
26
|
+
'彼は二度と戻って来なかった。附近の山野を捜索しても、何の手掛りもない。' +
|
|
27
|
+
'その後李徴がどうなったかを知る者は、誰もなかった。'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<FontSizeStyleProvider>
|
|
32
|
+
<TextEllipsis lineHeight={22} lineLimit={maxRows}>
|
|
33
|
+
{contentText}
|
|
34
|
+
</TextEllipsis>
|
|
35
|
+
</FontSizeStyleProvider>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FontSizeStyleProvider = styled.div`
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
`
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components'
|
|
2
|
+
import { onlyText } from './helper'
|
|
3
|
+
|
|
4
|
+
export interface Props {
|
|
5
|
+
lineHeight: number
|
|
6
|
+
lineLimit?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 複数行のテキストに表示行数制限を設けて`...`で省略する
|
|
11
|
+
*/
|
|
12
|
+
export const TextEllipsis = styled.div.attrs(
|
|
13
|
+
({ children, title = onlyText(children) }) => ({
|
|
14
|
+
title: title !== '' ? title : undefined,
|
|
15
|
+
})
|
|
16
|
+
)<Props>`
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
line-height: ${(props) => props.lineHeight}px;
|
|
19
|
+
/* For english */
|
|
20
|
+
overflow-wrap: break-word;
|
|
21
|
+
|
|
22
|
+
${({ lineLimit = 1, lineHeight }) =>
|
|
23
|
+
lineLimit === 1
|
|
24
|
+
? css`
|
|
25
|
+
text-overflow: ellipsis;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
`
|
|
28
|
+
: css`
|
|
29
|
+
display: box;
|
|
30
|
+
-webkit-box-orient: vertical;
|
|
31
|
+
-webkit-line-clamp: ${lineLimit};
|
|
32
|
+
/* Fallback for -webkit-line-clamp */
|
|
33
|
+
max-height: ${lineHeight * lineLimit}px;
|
|
34
|
+
`}
|
|
35
|
+
`
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { boolean } from '@storybook/addon-knobs'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import styled, { css } from 'styled-components'
|
|
4
|
+
import { theme } from '../../styled'
|
|
5
|
+
import WithIcon from '.'
|
|
6
|
+
import { applyEffect } from '@charcoal-ui/utils'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'Sandbox/WithIcon',
|
|
10
|
+
component: WithIcon,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Default = () => {
|
|
14
|
+
const fit = boolean('fit', true)
|
|
15
|
+
const bigger = boolean(
|
|
16
|
+
'Icon height bigger than line-height (`fit` is effective)',
|
|
17
|
+
false
|
|
18
|
+
)
|
|
19
|
+
const prefix = boolean('prefix', false)
|
|
20
|
+
const show = boolean('show', true)
|
|
21
|
+
return (
|
|
22
|
+
<Container>
|
|
23
|
+
<WithIcon
|
|
24
|
+
icon={<TestInlineIcon lineHeight={bigger ? 32 : 22} />}
|
|
25
|
+
show={show}
|
|
26
|
+
prefix={prefix}
|
|
27
|
+
fit={fit}
|
|
28
|
+
>
|
|
29
|
+
Menu
|
|
30
|
+
</WithIcon>
|
|
31
|
+
</Container>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Performance = () => (
|
|
36
|
+
<Container>
|
|
37
|
+
<WithIcon
|
|
38
|
+
icon={<TestInlineIcon lineHeight={22} />}
|
|
39
|
+
show
|
|
40
|
+
fit
|
|
41
|
+
// Hard-coded width (ResizeObserver free and NO measuring cost)
|
|
42
|
+
width={16}
|
|
43
|
+
>
|
|
44
|
+
Menu
|
|
45
|
+
</WithIcon>
|
|
46
|
+
</Container>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
export const Naive = () => (
|
|
50
|
+
// NO measuring cost
|
|
51
|
+
<Container>
|
|
52
|
+
<WithIcon icon={<TestInlineIcon lineHeight={22} />} show>
|
|
53
|
+
Menu
|
|
54
|
+
</WithIcon>
|
|
55
|
+
</Container>
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export const Prefix = () => (
|
|
59
|
+
<Container>
|
|
60
|
+
<WithIcon icon={<TestIcon />} show prefix fit>
|
|
61
|
+
Selection
|
|
62
|
+
</WithIcon>
|
|
63
|
+
</Container>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
function Container(props: { className?: string; children?: React.ReactNode }) {
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
css={css`
|
|
70
|
+
${theme((o) => [o.font.text1, o.typography(14).preserveHalfLeading])}
|
|
71
|
+
display: flex;
|
|
72
|
+
`}
|
|
73
|
+
>
|
|
74
|
+
<div
|
|
75
|
+
css={css`
|
|
76
|
+
background-color: ${({ theme }) =>
|
|
77
|
+
applyEffect(theme.color.brand, theme.elementEffect.disabled)};
|
|
78
|
+
`}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Hide = () => (
|
|
86
|
+
<Container>
|
|
87
|
+
<WithIcon icon={<TestIcon />} show={false} prefix>
|
|
88
|
+
Selection
|
|
89
|
+
</WithIcon>
|
|
90
|
+
</Container>
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
export const Collapse = () => (
|
|
94
|
+
<Container>
|
|
95
|
+
<WithIcon icon={<TestIcon />} show="collapse" prefix>
|
|
96
|
+
Selection
|
|
97
|
+
</WithIcon>
|
|
98
|
+
</Container>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
export const LongText = () => (
|
|
102
|
+
<Container
|
|
103
|
+
css={`
|
|
104
|
+
width: 200px;
|
|
105
|
+
`}
|
|
106
|
+
>
|
|
107
|
+
<WithIcon icon={<TestIcon />}>
|
|
108
|
+
Long Long Long Long Long Long Long Long Long Long Text
|
|
109
|
+
</WithIcon>
|
|
110
|
+
</Container>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
export const LongTextOverflow = () => (
|
|
114
|
+
<Container
|
|
115
|
+
css={`
|
|
116
|
+
width: 200px;
|
|
117
|
+
`}
|
|
118
|
+
>
|
|
119
|
+
<WithIcon icon={<TestIcon />} fixed>
|
|
120
|
+
Long Long Long Long Long Long Long Long Long Long Text
|
|
121
|
+
</WithIcon>
|
|
122
|
+
</Container>
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const TestIcon = styled.div`
|
|
126
|
+
display: inline-block;
|
|
127
|
+
width: 16px;
|
|
128
|
+
height: 16px;
|
|
129
|
+
background-color: currentColor;
|
|
130
|
+
`
|
|
131
|
+
|
|
132
|
+
const TestInlineIcon = styled.div<{ lineHeight: number }>`
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
vertical-align: top;
|
|
135
|
+
align-items: center;
|
|
136
|
+
line-height: ${(p) => p.lineHeight}px;
|
|
137
|
+
height: ${(p) => p.lineHeight}px;
|
|
138
|
+
&::before {
|
|
139
|
+
content: '';
|
|
140
|
+
display: inline-block;
|
|
141
|
+
height: 16px;
|
|
142
|
+
width: 16px;
|
|
143
|
+
background-color: currentColor;
|
|
144
|
+
}
|
|
145
|
+
`
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import styled, { css } from 'styled-components'
|
|
3
|
+
import { useElementSize } from '../../foundation/hooks'
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
children?: React.ReactNode
|
|
7
|
+
icon: React.ReactNode
|
|
8
|
+
/**
|
|
9
|
+
* アイコンを表示。デフォルトがtrueなので、非表示にするときに使います。 (アイコン自体の幅を維持します)
|
|
10
|
+
*/
|
|
11
|
+
show?: boolean | 'collapse'
|
|
12
|
+
/**
|
|
13
|
+
* アイコンを前にする
|
|
14
|
+
*/
|
|
15
|
+
prefix?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* アイコンの高さが文字の高さよりも大きいケースで有効。アイコンの高さをゼロにしてインラインの高さに関与させないようにします。
|
|
18
|
+
*/
|
|
19
|
+
fit?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* `fit`と併用した時にのみ有効な最適化オプション。アイコンの幅の自動計算を行わず指定した数値を利用します。
|
|
22
|
+
*/
|
|
23
|
+
width?: number
|
|
24
|
+
/**
|
|
25
|
+
* 親要素のサイズに合わせるのではなく、コンテンツのサイズを優先する
|
|
26
|
+
*/
|
|
27
|
+
fixed?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default React.memo(function WithIcon({
|
|
31
|
+
children,
|
|
32
|
+
icon,
|
|
33
|
+
show = true,
|
|
34
|
+
prefix: pre = false,
|
|
35
|
+
width,
|
|
36
|
+
fit = false,
|
|
37
|
+
fixed = false,
|
|
38
|
+
}: Props) {
|
|
39
|
+
const node = fit ? (
|
|
40
|
+
width === undefined ? (
|
|
41
|
+
<AutoWidthIconAnchor show={show} pre={pre}>
|
|
42
|
+
{icon}
|
|
43
|
+
</AutoWidthIconAnchor>
|
|
44
|
+
) : (
|
|
45
|
+
<IconAnchor width={width} show={show} pre={pre}>
|
|
46
|
+
<Icon>{icon}</Icon>
|
|
47
|
+
</IconAnchor>
|
|
48
|
+
)
|
|
49
|
+
) : (
|
|
50
|
+
<IconAnchorNaive show={show} pre={pre}>
|
|
51
|
+
<IconNaive>{icon}</IconNaive>
|
|
52
|
+
</IconAnchorNaive>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Root>
|
|
57
|
+
{pre && node}
|
|
58
|
+
<Text fixed={fixed}>{children}</Text>
|
|
59
|
+
{!pre && node}
|
|
60
|
+
</Root>
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const Root = styled.div`
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
`
|
|
68
|
+
|
|
69
|
+
const Text = styled.div<{ fixed: boolean }>`
|
|
70
|
+
${(p) =>
|
|
71
|
+
!p.fixed &&
|
|
72
|
+
css`
|
|
73
|
+
min-width: 0;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
`}
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
text-overflow: ellipsis;
|
|
78
|
+
`
|
|
79
|
+
|
|
80
|
+
function AutoWidthIconAnchor({
|
|
81
|
+
children,
|
|
82
|
+
show,
|
|
83
|
+
pre,
|
|
84
|
+
}: {
|
|
85
|
+
children: React.ReactNode
|
|
86
|
+
show: boolean | 'collapse'
|
|
87
|
+
pre: boolean
|
|
88
|
+
}) {
|
|
89
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
90
|
+
// depsを空配列にしないことで初回だけ同期で幅を計算させるテクニック
|
|
91
|
+
const width = useElementSize(ref, [null])?.width ?? 0
|
|
92
|
+
return (
|
|
93
|
+
<IconAnchor width={width} show={show} pre={pre}>
|
|
94
|
+
<Icon ref={ref}>{children}</Icon>
|
|
95
|
+
</IconAnchor>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const forceCenteringCss = css`
|
|
100
|
+
> svg {
|
|
101
|
+
display: block;
|
|
102
|
+
}
|
|
103
|
+
`
|
|
104
|
+
|
|
105
|
+
const iconAnchorCss = css`
|
|
106
|
+
${(p: { show: boolean | 'collapse'; pre: boolean }) =>
|
|
107
|
+
p.show === 'collapse'
|
|
108
|
+
? css`
|
|
109
|
+
display: none;
|
|
110
|
+
`
|
|
111
|
+
: css`
|
|
112
|
+
visibility: ${p.show ? 'visible' : 'hidden'};
|
|
113
|
+
`};
|
|
114
|
+
${(p) =>
|
|
115
|
+
p.pre
|
|
116
|
+
? css`
|
|
117
|
+
margin-right: 4px;
|
|
118
|
+
`
|
|
119
|
+
: css`
|
|
120
|
+
margin-left: 4px;
|
|
121
|
+
`}
|
|
122
|
+
`
|
|
123
|
+
|
|
124
|
+
const IconAnchorNaive = styled.div`
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
|
|
128
|
+
${iconAnchorCss}
|
|
129
|
+
`
|
|
130
|
+
|
|
131
|
+
const IconNaive = styled.div`
|
|
132
|
+
display: inline-flex;
|
|
133
|
+
|
|
134
|
+
${forceCenteringCss}
|
|
135
|
+
`
|
|
136
|
+
|
|
137
|
+
const IconAnchor = styled.div<{
|
|
138
|
+
width: number
|
|
139
|
+
show: boolean | 'collapse'
|
|
140
|
+
pre: boolean
|
|
141
|
+
}>`
|
|
142
|
+
display: flex;
|
|
143
|
+
position: relative;
|
|
144
|
+
/* Iconをline-heightに関与させない */
|
|
145
|
+
height: 0;
|
|
146
|
+
/* 横方向の領域は確保する */
|
|
147
|
+
width: ${(p) => p.width}px;
|
|
148
|
+
|
|
149
|
+
${iconAnchorCss}
|
|
150
|
+
`
|
|
151
|
+
|
|
152
|
+
const Icon = styled.div`
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
position: absolute;
|
|
155
|
+
transform: translateY(-50%);
|
|
156
|
+
|
|
157
|
+
${forceCenteringCss}
|
|
158
|
+
`
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
|
|
4
|
+
export type IconSizes = 16 | 24 | 32
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
path: string | React.ReactNode
|
|
8
|
+
viewBoxSize: number
|
|
9
|
+
size?: IconSizes | 40 | 48 | 64 | 72
|
|
10
|
+
transform?: string
|
|
11
|
+
currentColor?: boolean
|
|
12
|
+
fillRule?: 'nonzero' | 'evenodd'
|
|
13
|
+
clipRule?: 'nonzero' | 'evenodd' | 'inherit'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function IconBase({
|
|
17
|
+
size = 24,
|
|
18
|
+
viewBoxSize,
|
|
19
|
+
currentColor,
|
|
20
|
+
path,
|
|
21
|
+
transform,
|
|
22
|
+
fillRule,
|
|
23
|
+
clipRule,
|
|
24
|
+
}: Props) {
|
|
25
|
+
return (
|
|
26
|
+
<Icon
|
|
27
|
+
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
|
28
|
+
size={size}
|
|
29
|
+
currentColor={currentColor}
|
|
30
|
+
>
|
|
31
|
+
<IconBasePath
|
|
32
|
+
path={path}
|
|
33
|
+
transform={transform}
|
|
34
|
+
fillRule={fillRule}
|
|
35
|
+
clipRule={clipRule}
|
|
36
|
+
/>
|
|
37
|
+
</Icon>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Icon = styled.svg<{ size: number; currentColor?: boolean }>`
|
|
42
|
+
stroke: none;
|
|
43
|
+
fill: ${({ currentColor = false, theme }) =>
|
|
44
|
+
currentColor ? 'currentColor' : theme.color.text2};
|
|
45
|
+
width: ${(props) => props.size}px;
|
|
46
|
+
height: ${(props) => props.size}px;
|
|
47
|
+
line-height: 0;
|
|
48
|
+
font-size: 0;
|
|
49
|
+
vertical-align: middle;
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
type IconBasePathProps = Pick<
|
|
53
|
+
Props,
|
|
54
|
+
'path' | 'transform' | 'fillRule' | 'clipRule'
|
|
55
|
+
>
|
|
56
|
+
export const IconBasePath = ({
|
|
57
|
+
path,
|
|
58
|
+
transform,
|
|
59
|
+
fillRule,
|
|
60
|
+
clipRule,
|
|
61
|
+
}: IconBasePathProps) => {
|
|
62
|
+
if (typeof path === 'string') {
|
|
63
|
+
return (
|
|
64
|
+
<path
|
|
65
|
+
d={path}
|
|
66
|
+
transform={transform}
|
|
67
|
+
fillRule={fillRule}
|
|
68
|
+
clipRule={clipRule}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
} else {
|
|
72
|
+
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
73
|
+
return <>{path}</>
|
|
74
|
+
}
|
|
75
|
+
}
|