[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からなる自己識別要素であることがわかる.
しかし、このように子供として加入する場合、FilterItemoverflow-x:scrollを追加する必要がある場合、ドロップダウンメニューは隠されます.
だからDropDownListFilterItemの子供ではなく兄弟とすることにした.ということで.

上の赤色矩形を容器ブロックと識別し,位置決めが不正確であり,この目標を達成するためにまず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:yleft:xの値をスケーリングして、位置が以前と同じようにFilterItemの真下でプルダウンメニューを開くことができます.
しかし、そのためには、まずコンテナブロックをビューポートとして認識させる必要があります.現在のコードは
///Filter
position:relative;

///DropDownItem(Filter의 직계자식)
position:absolute;
したがって、コンテナブロックは、ビューポートではなくFilterと見なされる.したがって、ビューポートから座標値を求めても、Filterのビューポートから座標値を減算することで、top,leftのパーセントに座標値を加算する必要がある複雑さが生じる.
そこで,このような無駄な構造を変えるために,視区を親とするposition:fixedを用いた.DropDownListposition: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で包む必要はなく造形も可能ですが、ラベルで包むのは明確です