[21/07/19]企業探索説明モダリ工場
ドロップダウンリスト
ドロップダウン・リストの配置の変更
return (
<>
{type === 'quantity' ? (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{list.map((l) => (
<DropDownItem key={l} onClick={(e) => onClick({ e, value: l })}>
{l}
</DropDownItem>
))}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
) : (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{type === 'hashtags' && list && '*중복 선택 가능'}
{list ? (
list.map((l) => (
<DropDownItem
key={l.name}
onClick={(e) => onClick({ e, value: l })}
>
{l.name}
</DropDownItem>
))
) : (
<None select={select}></None>
)}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
)}
</>
);
非常に毒性の低いコードです.DropDownList
は、プルダウンの位置を特定するために行われたFilterItem
からなる自己識別要素であることがわかる.
しかし、このように子供として加入する場合、FilterItem
にoverflow-x:scroll
を追加する必要がある場合、ドロップダウンメニューは隠されます.
だからDropDownList
をFilterItem
の子供ではなく兄弟とすることにした.ということで.
上の赤色矩形を容器ブロックと識別し,位置決めが不正確であり,この目標を達成するためにまずgetBoundingClientRect
法を用いた. const onClickFilterItem = (e) => {
const { x, y } = e.currentTarget.getBoundingClientRect();
setPosition({ x, y });
setFocus((prev) => {
Object.keys(prev).forEach((key) => {
if (key !== type) prev[key] = false;
});
return {
...prev,
[type]: !prev[type],
};
});
};
getClientBoundingRect
ビューポートからx,y
座標値を取得します.position
がビューポート基準であれば、top:y
、left:x
の値をスケーリングして、位置が以前と同じようにFilterItem
の真下でプルダウンメニューを開くことができます.
しかし、そのためには、まずコンテナブロックをビューポートとして認識させる必要があります.現在のコードは///Filter
position:relative;
///DropDownItem(Filter의 직계자식)
position:absolute;
したがって、コンテナブロックは、ビューポートではなくFilter
と見なされる.したがって、ビューポートから座標値を求めても、Filter
のビューポートから座標値を減算することで、top,left
のパーセントに座標値を加算する必要がある複雑さが生じる.
そこで,このような無駄な構造を変えるために,視区を親とするposition:fixed
を用いた.DropDownList
position:fixed
をあげましたが、正しく動作しませんでした.理由は以下の通り
ただし、要素の祖先がtransform、perspective、filterプロパティのいずれかでない場合、その祖先はビューポートではなくコンテンツブロックとして機能します.const Wrapper = styled.div`
// ExplorePick Component
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
//...
Filter
の真上成分ExplorePick
は、transform
の属性を使用しているからである.ExplorePick
を使用して、コンテナブロックとしてビューポートの代わりに使用します.上のコードは、モードを真ん中にするためのコードです.このコードを次のように変更します.// ExplorePick Component
const Wrapper = styled.div`
position: fixed;
// auto auto를 하게되면 위아래좌우가 같은 비율로 마진값을 갖게됨.
margin: auto auto;
// 위치를 0 0 0 0 으로 잡고
left: 0;
right: 0;
top: 0;
bottom: 0;
// 명시된 너비와 높이값을 갖고있다면
// 정중앙에 오게된다.
width: 80%;
height: 307px;
min-width: 266px;
min-height: 307px;
これにより、transform
の属性がない場合、modal内部のcontent
を真中に置き、手前のDropDownList
度コンテナブロックをビューポートとして置くことができる.次は完成したFilterItem
です.// ...
// FilterItem
function FilterItem({ select, current, list, onClick, type, focus, setFocus }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const onClickFilterItem = (e) => {
// 뷰포트로 부터 좌표를 구한후
const { x, y } = e.currentTarget.getBoundingClientRect();
// 이를 상태에 집어넣는다.
setPosition({ x, y });
setFocus((prev) => {
Object.keys(prev).forEach((key) => {
if (key !== type) prev[key] = false;
});
return {
...prev,
[type]: !prev[type],
};
});
};
return (
<>
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
<Icon icon="Arrow" size={10} />
</Wrapper>
{focus[type] && (
// 그 상태값을 켜지는 드랍다운리스트에 전달해준다.
<DropDownList
position={position}
list={list}
onClick={onClick}
select={select}
type={type}
></DropDownList>
)}
</>
);
}
ドロップダウンリストを分離(再パッケージ)
// 종류나 해시태그 필터를 선택하였을때 품목이나 종류가 선택되지 않았으면
// 보여주는 컴포넌트
function None({ select }) {
// useRef는 재 렌더링 시 새롭게 계산한다.
const needType = useRef(select.item ? '종류' : '품목');
return (
<NoneWrapper>
<NoneText>
{needType.current}을 먼저 선택해야 해시태그를 선택할 수 있어요!
</NoneText>
<Button
variant="primary"
fontWeight="400"
borderRadius="10px"
width="70px"
height="38px"
fontSize={'14px'}
>
{needType.current} 선택
</Button>
</NoneWrapper>
);
}
None.propTypes = {
select: PropTypes.object,
};
function DropDownList({ position, list, onClick, type, select }) {
// list가 없다면 None컴포넌트를 보여준다. DropDown 컴포넌트는 위치를 잡게 해주는 컴포넌트
if (!list) {
return (
<DropDown top={position.y} left={position.x}>
<None select={select}></None>
</DropDown>
);
}
return (
// list도 있는 경우 다음의 DropDownList를 렌더링한다.
<DropDown top={position.y} left={position.x}>
{type === 'hashtags' && list && <span>*중복 선택 가능</span>}
{list.map((l) => (
<DropDownItem key={l} onClick={(e) => onClick({ e, value: l })}>
{type === 'quantity' ? l : l.name}
</DropDownItem>
))}
</DropDown>
);
}
DropDownList.propTypes = {
position: PropTypes.object,
list: PropTypes.array,
onClick: PropTypes.func,
type: PropTypes.string,
select: PropTypes.object,
};
export default DropDownList;
ドロップダウンリストの外部をクリックして閉じる
まず、ドロップダウンリストはフィルタの下にあるフィルタ項目コンポーネントに属します.すなわち,フィルタの下では直系子項である.外部をクリックして閉じる機能はイベントbundlingで実現されます.// Filter 컴포넌트
useEffect(() => {
// outside click => off dropdownlist
const pageClickEvent = (e) => {
if (filterRef.current !== null && !filterRef.current.contains(e.target)) {
setFocus({
item: false,
kind: false,
hashtags: false,
quantity: false,
});
}
};
if (Object.values(focus).includes(true)) {
window.addEventListener('click', pageClickEvent);
return () => {
window.removeEventListener('click', pageClickEvent);
};
}
}, [focus, filterRef]);
アクティビティはターゲットから始まり、window
までbundlingで行われます.focus[type]
をクリックしてtrue
、すなわちドロップダウンリストを開いた後、window
でイベントハンドラをクリックする.このハンドルは、私が押した要素がFilter
要素のサブ要素であるかどうかをチェックします.Filter
要素の外部では、Filter
要素の子ではないので、focus
のすべてのproperty値をfalse
に変更し、ドロップダウンリストを閉じます.
最初はwindow
にイベントハンドラを掛けるとは思わなかったが、ExplorePick
にイベントハンドラを掛けて上記の論理を実行しようとした.しかし、このようにすると、focus
においてExplorePick
プルダウンをオンにした状態値を管理する必要があり、これは素子の意味では合致せず、原子設計モードにも違反している.(拡張には向いていないと思います)
したがって、ExplorePick
にハンドルを打つよりも、window
にハンドルを掛けて処理したほうがいい.
チップ
次の選択肢としてstyle
を使用し、span
を追加 & > span:first-child + ${DropDownItem} {
/* span이 첫번째 자식인 경우 그 DropDownItem은 다음 스타일을 입힌다.*/
border-top: 1px solid ${(props) => props.theme.whiteColor_3};
}
span
で包む必要はなく造形も可能ですが、ラベルで包むのは明確です
Reference
この問題について([21/07/19]企業探索説明モダリ工場), 我々は、より多くの情報をここで見つけました
https://velog.io/@rat8397/210719-업체-탐색-설명-모달-리팩토링
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
return (
<>
{type === 'quantity' ? (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{list.map((l) => (
<DropDownItem key={l} onClick={(e) => onClick({ e, value: l })}>
{l}
</DropDownItem>
))}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
) : (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{type === 'hashtags' && list && '*중복 선택 가능'}
{list ? (
list.map((l) => (
<DropDownItem
key={l.name}
onClick={(e) => onClick({ e, value: l })}
>
{l.name}
</DropDownItem>
))
) : (
<None select={select}></None>
)}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
)}
</>
);
const onClickFilterItem = (e) => {
const { x, y } = e.currentTarget.getBoundingClientRect();
setPosition({ x, y });
setFocus((prev) => {
Object.keys(prev).forEach((key) => {
if (key !== type) prev[key] = false;
});
return {
...prev,
[type]: !prev[type],
};
});
};
///Filter
position:relative;
///DropDownItem(Filter의 직계자식)
position:absolute;
const Wrapper = styled.div`
// ExplorePick Component
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
//...
// ExplorePick Component
const Wrapper = styled.div`
position: fixed;
// auto auto를 하게되면 위아래좌우가 같은 비율로 마진값을 갖게됨.
margin: auto auto;
// 위치를 0 0 0 0 으로 잡고
left: 0;
right: 0;
top: 0;
bottom: 0;
// 명시된 너비와 높이값을 갖고있다면
// 정중앙에 오게된다.
width: 80%;
height: 307px;
min-width: 266px;
min-height: 307px;
// ...
// FilterItem
function FilterItem({ select, current, list, onClick, type, focus, setFocus }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const onClickFilterItem = (e) => {
// 뷰포트로 부터 좌표를 구한후
const { x, y } = e.currentTarget.getBoundingClientRect();
// 이를 상태에 집어넣는다.
setPosition({ x, y });
setFocus((prev) => {
Object.keys(prev).forEach((key) => {
if (key !== type) prev[key] = false;
});
return {
...prev,
[type]: !prev[type],
};
});
};
return (
<>
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
<Icon icon="Arrow" size={10} />
</Wrapper>
{focus[type] && (
// 그 상태값을 켜지는 드랍다운리스트에 전달해준다.
<DropDownList
position={position}
list={list}
onClick={onClick}
select={select}
type={type}
></DropDownList>
)}
</>
);
}
// 종류나 해시태그 필터를 선택하였을때 품목이나 종류가 선택되지 않았으면
// 보여주는 컴포넌트
function None({ select }) {
// useRef는 재 렌더링 시 새롭게 계산한다.
const needType = useRef(select.item ? '종류' : '품목');
return (
<NoneWrapper>
<NoneText>
{needType.current}을 먼저 선택해야 해시태그를 선택할 수 있어요!
</NoneText>
<Button
variant="primary"
fontWeight="400"
borderRadius="10px"
width="70px"
height="38px"
fontSize={'14px'}
>
{needType.current} 선택
</Button>
</NoneWrapper>
);
}
None.propTypes = {
select: PropTypes.object,
};
function DropDownList({ position, list, onClick, type, select }) {
// list가 없다면 None컴포넌트를 보여준다. DropDown 컴포넌트는 위치를 잡게 해주는 컴포넌트
if (!list) {
return (
<DropDown top={position.y} left={position.x}>
<None select={select}></None>
</DropDown>
);
}
return (
// list도 있는 경우 다음의 DropDownList를 렌더링한다.
<DropDown top={position.y} left={position.x}>
{type === 'hashtags' && list && <span>*중복 선택 가능</span>}
{list.map((l) => (
<DropDownItem key={l} onClick={(e) => onClick({ e, value: l })}>
{type === 'quantity' ? l : l.name}
</DropDownItem>
))}
</DropDown>
);
}
DropDownList.propTypes = {
position: PropTypes.object,
list: PropTypes.array,
onClick: PropTypes.func,
type: PropTypes.string,
select: PropTypes.object,
};
export default DropDownList;
// Filter 컴포넌트
useEffect(() => {
// outside click => off dropdownlist
const pageClickEvent = (e) => {
if (filterRef.current !== null && !filterRef.current.contains(e.target)) {
setFocus({
item: false,
kind: false,
hashtags: false,
quantity: false,
});
}
};
if (Object.values(focus).includes(true)) {
window.addEventListener('click', pageClickEvent);
return () => {
window.removeEventListener('click', pageClickEvent);
};
}
}, [focus, filterRef]);
& > span:first-child + ${DropDownItem} {
/* span이 첫번째 자식인 경우 그 DropDownItem은 다음 스타일을 입힌다.*/
border-top: 1px solid ${(props) => props.theme.whiteColor_3};
}
Reference
この問題について([21/07/19]企業探索説明モダリ工場), 我々は、より多くの情報をここで見つけました https://velog.io/@rat8397/210719-업체-탐색-설명-모달-리팩토링テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol